Implement Custom Emojis (#1969)
This commit is contained in:
parent
63fac1f6be
commit
f88ddafe14
@ -1,4 +1,7 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
import {
|
||||||
|
ApiMessageEntityTypes,
|
||||||
|
} from '../../types';
|
||||||
import type {
|
import type {
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageForwardInfo,
|
ApiMessageForwardInfo,
|
||||||
@ -32,6 +35,7 @@ import type {
|
|||||||
ApiGame,
|
ApiGame,
|
||||||
PhoneCallAction,
|
PhoneCallAction,
|
||||||
ApiWebDocument,
|
ApiWebDocument,
|
||||||
|
ApiMessageEntityDefault,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -271,7 +275,7 @@ export function buildMessageContent(
|
|||||||
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
||||||
|
|
||||||
if (mtpMessage.message && !hasUnsupportedMedia
|
if (mtpMessage.message && !hasUnsupportedMedia
|
||||||
&& !content.sticker && !content.poll && !content.contact && !(content.video?.isRound)) {
|
&& !content.sticker && !content.poll && !content.contact && !(content.video?.isRound)) {
|
||||||
content = {
|
content = {
|
||||||
...content,
|
...content,
|
||||||
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
|
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
|
||||||
@ -1211,8 +1215,9 @@ export function buildLocalForwardedMessage(
|
|||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
serverTimeOffset: number,
|
serverTimeOffset: number,
|
||||||
scheduledAt?: number,
|
scheduledAt?: number,
|
||||||
noAuthor?: boolean,
|
noAuthors?: boolean,
|
||||||
noCaption?: boolean,
|
noCaptions?: boolean,
|
||||||
|
isCurrentUserPremium?: boolean,
|
||||||
): ApiMessage {
|
): ApiMessage {
|
||||||
const localId = getNextLocalMessageId();
|
const localId = getNextLocalMessageId();
|
||||||
const {
|
const {
|
||||||
@ -1228,10 +1233,16 @@ export function buildLocalForwardedMessage(
|
|||||||
const asIncomingInChatWithSelf = (
|
const asIncomingInChatWithSelf = (
|
||||||
toChat.id === currentUserId && (fromChatId !== toChat.id || message.forwardInfo) && !isAudio
|
toChat.id === currentUserId && (fromChatId !== toChat.id || message.forwardInfo) && !isAudio
|
||||||
);
|
);
|
||||||
const shouldHideText = Object.keys(content).length > 1 && content.text && noCaption;
|
const shouldHideText = Object.keys(content).length > 1 && content.text && noCaptions;
|
||||||
|
const shouldDropCustomEmoji = !isCurrentUserPremium;
|
||||||
|
const strippedText = content.text?.entities && shouldDropCustomEmoji ? {
|
||||||
|
text: content.text.text,
|
||||||
|
entities: content.text.entities?.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji),
|
||||||
|
} : content.text;
|
||||||
|
|
||||||
const updatedContent = {
|
const updatedContent = {
|
||||||
...content,
|
...content,
|
||||||
text: !shouldHideText ? content.text : undefined,
|
text: !shouldHideText ? strippedText : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -1245,7 +1256,7 @@ export function buildLocalForwardedMessage(
|
|||||||
groupedId,
|
groupedId,
|
||||||
isInAlbum,
|
isInAlbum,
|
||||||
// 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 && !noAuthor && {
|
...(senderId !== currentUserId && !isAudio && !noAuthors && {
|
||||||
forwardInfo: {
|
forwardInfo: {
|
||||||
date: message.date,
|
date: message.date,
|
||||||
isChannelPost: false,
|
isChannelPost: false,
|
||||||
@ -1365,14 +1376,50 @@ function buildNewPoll(poll: ApiNewPoll, localId: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMessageEntity {
|
export function buildApiMessageEntity(entity: GramJs.TypeMessageEntity): ApiMessageEntity {
|
||||||
const { className: type, offset, length } = entity;
|
const {
|
||||||
|
className: type, offset, length,
|
||||||
|
} = entity;
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityMentionName) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.MentionName,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
userId: buildApiPeerId(entity.userId, 'user'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityTextUrl) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.TextUrl,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
url: entity.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityPre) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.Pre,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
language: entity.language,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity instanceof GramJs.MessageEntityCustomEmoji) {
|
||||||
|
return {
|
||||||
|
type: ApiMessageEntityTypes.CustomEmoji,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
documentId: entity.documentId.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type: type as `${ApiMessageEntityDefault['type']}`,
|
||||||
offset,
|
offset,
|
||||||
length,
|
length,
|
||||||
...(entity instanceof GramJs.MessageEntityMentionName && { userId: buildApiPeerId(entity.userId, 'user') }),
|
|
||||||
...('url' in entity && { url: entity.url }),
|
|
||||||
...('language' in entity && { language: entity.language }),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import type {
|
import type {
|
||||||
ApiEmojiInteraction, ApiSticker, ApiStickerSet, GramJsEmojiInteraction,
|
ApiEmojiInteraction, ApiStickerSetInfo, ApiSticker, ApiStickerSet, GramJsEmojiInteraction,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { NO_STICKER_SET_ID } from '../../../config';
|
|
||||||
|
|
||||||
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
import { buildApiThumbnailFromCached, buildApiThumbnailFromPath } from './common';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
@ -20,6 +19,8 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPrem
|
|||||||
.find((attr: any): attr is GramJs.DocumentAttributeSticker => (
|
.find((attr: any): attr is GramJs.DocumentAttributeSticker => (
|
||||||
attr instanceof GramJs.DocumentAttributeSticker
|
attr instanceof GramJs.DocumentAttributeSticker
|
||||||
));
|
));
|
||||||
|
const customEmojiAttribute = document.attributes
|
||||||
|
.find((attr): attr is GramJs.DocumentAttributeCustomEmoji => attr instanceof GramJs.DocumentAttributeCustomEmoji);
|
||||||
|
|
||||||
const fileAttribute = (mimeType === LOTTIE_STICKER_MIME_TYPE || mimeType === VIDEO_STICKER_MIME_TYPE)
|
const fileAttribute = (mimeType === LOTTIE_STICKER_MIME_TYPE || mimeType === VIDEO_STICKER_MIME_TYPE)
|
||||||
&& document.attributes
|
&& document.attributes
|
||||||
@ -27,12 +28,13 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPrem
|
|||||||
attr instanceof GramJs.DocumentAttributeFilename
|
attr instanceof GramJs.DocumentAttributeFilename
|
||||||
));
|
));
|
||||||
|
|
||||||
if (!stickerAttribute && !fileAttribute) {
|
if (!(stickerAttribute || customEmojiAttribute) && !fileAttribute) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLottie = mimeType === LOTTIE_STICKER_MIME_TYPE;
|
const isLottie = mimeType === LOTTIE_STICKER_MIME_TYPE;
|
||||||
const isVideo = mimeType === VIDEO_STICKER_MIME_TYPE;
|
const isVideo = mimeType === VIDEO_STICKER_MIME_TYPE;
|
||||||
|
const isCustomEmoji = Boolean(customEmojiAttribute);
|
||||||
|
|
||||||
const imageSizeAttribute = document.attributes
|
const imageSizeAttribute = document.attributes
|
||||||
.find((attr: any): attr is GramJs.DocumentAttributeImageSize => (
|
.find((attr: any): attr is GramJs.DocumentAttributeImageSize => (
|
||||||
@ -46,10 +48,10 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPrem
|
|||||||
|
|
||||||
const sizeAttribute = imageSizeAttribute || videoSizeAttribute;
|
const sizeAttribute = imageSizeAttribute || videoSizeAttribute;
|
||||||
|
|
||||||
const stickerSetInfo = stickerAttribute && stickerAttribute.stickerset instanceof GramJs.InputStickerSetID
|
const stickerOrEmojiAttribute = (stickerAttribute || customEmojiAttribute)!;
|
||||||
? stickerAttribute.stickerset
|
const stickerSetInfo = buildApiStickerSetInfo(stickerOrEmojiAttribute?.stickerset);
|
||||||
: undefined;
|
const emoji = stickerOrEmojiAttribute?.alt;
|
||||||
const emoji = stickerAttribute?.alt;
|
const isFree = Boolean(customEmojiAttribute?.free ?? true);
|
||||||
|
|
||||||
const cachedThumb = document.thumbs && document.thumbs.find(
|
const cachedThumb = document.thumbs && document.thumbs.find(
|
||||||
(s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize,
|
(s): s is GramJs.PhotoCachedSize => s instanceof GramJs.PhotoCachedSize,
|
||||||
@ -82,15 +84,16 @@ export function buildStickerFromDocument(document: GramJs.TypeDocument, isNoPrem
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: String(document.id),
|
id: String(document.id),
|
||||||
stickerSetId: stickerSetInfo ? String(stickerSetInfo.id) : NO_STICKER_SET_ID,
|
stickerSetInfo,
|
||||||
stickerSetAccessHash: stickerSetInfo && String(stickerSetInfo.accessHash),
|
|
||||||
emoji,
|
emoji,
|
||||||
|
isCustomEmoji,
|
||||||
isLottie,
|
isLottie,
|
||||||
isVideo,
|
isVideo,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
hasEffect,
|
hasEffect,
|
||||||
|
isFree,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,6 +127,24 @@ export function buildStickerSet(set: GramJs.StickerSet): ApiStickerSet {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildApiStickerSetInfo(inputSet?: GramJs.TypeInputStickerSet): ApiStickerSetInfo {
|
||||||
|
if (inputSet instanceof GramJs.InputStickerSetID) {
|
||||||
|
return {
|
||||||
|
id: String(inputSet.id),
|
||||||
|
accessHash: String(inputSet.accessHash),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (inputSet instanceof GramJs.InputStickerSetShortName) {
|
||||||
|
return {
|
||||||
|
shortName: inputSet.shortName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isMissing: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetCovered): ApiStickerSet {
|
export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetCovered): ApiStickerSet {
|
||||||
const stickerSet = buildStickerSet(coveredStickerSet.set);
|
const stickerSet = buildStickerSet(coveredStickerSet.set);
|
||||||
|
|
||||||
@ -131,18 +152,20 @@ export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetC
|
|||||||
: (coveredStickerSet instanceof GramJs.StickerSetMultiCovered) ? coveredStickerSet.covers
|
: (coveredStickerSet instanceof GramJs.StickerSetMultiCovered) ? coveredStickerSet.covers
|
||||||
: coveredStickerSet.documents;
|
: coveredStickerSet.documents;
|
||||||
|
|
||||||
stickerSet.covers = [];
|
const stickers = processStickerResult(stickerSetCovers);
|
||||||
stickerSetCovers.forEach((cover) => {
|
|
||||||
if (cover instanceof GramJs.Document) {
|
|
||||||
const coverSticker = buildStickerFromDocument(cover);
|
|
||||||
if (coverSticker) {
|
|
||||||
stickerSet.covers!.push(coverSticker);
|
|
||||||
localDb.documents[String(cover.id)] = cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return stickerSet;
|
if (coveredStickerSet instanceof GramJs.StickerSetFullCovered) {
|
||||||
|
return {
|
||||||
|
...stickerSet,
|
||||||
|
stickers,
|
||||||
|
packs: processStickerPackResult(coveredStickerSet.packs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...stickerSet,
|
||||||
|
covers: stickers,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmojiInteraction {
|
export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmojiInteraction {
|
||||||
@ -150,3 +173,29 @@ export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmoji
|
|||||||
timestamps: json.a.map((l) => l.t),
|
timestamps: json.a.map((l) => l.t),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function processStickerPackResult(packs: GramJs.StickerPack[]) {
|
||||||
|
return packs.reduce((acc, { emoticon, documents }) => {
|
||||||
|
acc[emoticon] = documents.map((documentId) => buildStickerFromDocument(
|
||||||
|
localDb.documents[String(documentId)],
|
||||||
|
)).filter<ApiSticker>(Boolean as any);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, ApiSticker[]>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function processStickerResult(stickers: GramJs.TypeDocument[]) {
|
||||||
|
return stickers
|
||||||
|
.map((document) => {
|
||||||
|
if (document instanceof GramJs.Document) {
|
||||||
|
const sticker = buildStickerFromDocument(document);
|
||||||
|
if (sticker) {
|
||||||
|
localDb.documents[String(document.id)] = document;
|
||||||
|
|
||||||
|
return sticker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|||||||
@ -290,10 +290,10 @@ export function buildMessageFromUpdate(
|
|||||||
|
|
||||||
export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMessageEntity {
|
export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMessageEntity {
|
||||||
const {
|
const {
|
||||||
type, offset, length, url, userId, language,
|
type, offset, length,
|
||||||
} = entity;
|
} = entity;
|
||||||
|
|
||||||
const user = userId ? localDb.users[userId] : undefined;
|
const user = 'userId' in entity ? localDb.users[entity.userId] : undefined;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ApiMessageEntityTypes.Bold:
|
case ApiMessageEntityTypes.Bold:
|
||||||
@ -307,11 +307,11 @@ export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMess
|
|||||||
case ApiMessageEntityTypes.Code:
|
case ApiMessageEntityTypes.Code:
|
||||||
return new GramJs.MessageEntityCode({ offset, length });
|
return new GramJs.MessageEntityCode({ offset, length });
|
||||||
case ApiMessageEntityTypes.Pre:
|
case ApiMessageEntityTypes.Pre:
|
||||||
return new GramJs.MessageEntityPre({ offset, length, language: language || '' });
|
return new GramJs.MessageEntityPre({ offset, length, language: entity.language || '' });
|
||||||
case ApiMessageEntityTypes.Blockquote:
|
case ApiMessageEntityTypes.Blockquote:
|
||||||
return new GramJs.MessageEntityBlockquote({ offset, length });
|
return new GramJs.MessageEntityBlockquote({ offset, length });
|
||||||
case ApiMessageEntityTypes.TextUrl:
|
case ApiMessageEntityTypes.TextUrl:
|
||||||
return new GramJs.MessageEntityTextUrl({ offset, length, url: url! });
|
return new GramJs.MessageEntityTextUrl({ offset, length, url: entity.url });
|
||||||
case ApiMessageEntityTypes.Url:
|
case ApiMessageEntityTypes.Url:
|
||||||
return new GramJs.MessageEntityUrl({ offset, length });
|
return new GramJs.MessageEntityUrl({ offset, length });
|
||||||
case ApiMessageEntityTypes.Hashtag:
|
case ApiMessageEntityTypes.Hashtag:
|
||||||
@ -320,10 +320,12 @@ export function buildMtpMessageEntity(entity: ApiMessageEntity): GramJs.TypeMess
|
|||||||
return new GramJs.InputMessageEntityMentionName({
|
return new GramJs.InputMessageEntityMentionName({
|
||||||
offset,
|
offset,
|
||||||
length,
|
length,
|
||||||
userId: new GramJs.InputUser({ userId: BigInt(userId!), accessHash: user!.accessHash! }),
|
userId: new GramJs.InputUser({ userId: BigInt(user!.id), accessHash: user!.accessHash! }),
|
||||||
});
|
});
|
||||||
case ApiMessageEntityTypes.Spoiler:
|
case ApiMessageEntityTypes.Spoiler:
|
||||||
return new GramJs.MessageEntitySpoiler({ offset, length });
|
return new GramJs.MessageEntitySpoiler({ offset, length });
|
||||||
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
|
return new GramJs.MessageEntityCustomEmoji({ offset, length, documentId: BigInt(entity.documentId) });
|
||||||
default:
|
default:
|
||||||
return new GramJs.MessageEntityUnknown({ offset, length });
|
return new GramJs.MessageEntityUnknown({ offset, length });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export {
|
|||||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
||||||
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||||
removeRecentSticker, clearRecentStickers, fetchPremiumGifts,
|
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
||||||
} from './symbols';
|
} from './symbols';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -1128,6 +1128,7 @@ export async function forwardMessages({
|
|||||||
withMyScore,
|
withMyScore,
|
||||||
noAuthors,
|
noAuthors,
|
||||||
noCaptions,
|
noCaptions,
|
||||||
|
isCurrentUserPremium,
|
||||||
}: {
|
}: {
|
||||||
fromChat: ApiChat;
|
fromChat: ApiChat;
|
||||||
toChat: ApiChat;
|
toChat: ApiChat;
|
||||||
@ -1139,13 +1140,14 @@ export async function forwardMessages({
|
|||||||
withMyScore?: boolean;
|
withMyScore?: boolean;
|
||||||
noAuthors?: boolean;
|
noAuthors?: boolean;
|
||||||
noCaptions?: boolean;
|
noCaptions?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const messageIds = messages.map(({ id }) => id);
|
const messageIds = messages.map(({ id }) => id);
|
||||||
const randomIds = messages.map(generateRandomBigInt);
|
const randomIds = messages.map(generateRandomBigInt);
|
||||||
|
|
||||||
messages.forEach((message, index) => {
|
messages.forEach((message, index) => {
|
||||||
const localMessage = buildLocalForwardedMessage(
|
const localMessage = buildLocalForwardedMessage(
|
||||||
toChat, message, serverTimeOffset, scheduledAt, noAuthors, noCaptions,
|
toChat, message, serverTimeOffset, scheduledAt, noAuthors, noCaptions, isCurrentUserPremium,
|
||||||
);
|
);
|
||||||
localDb.localMessages[String(randomIds[index])] = localMessage;
|
localDb.localMessages[String(randomIds[index])] = localMessage;
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import BigInt from 'big-integer';
|
import BigInt from 'big-integer';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import type { ApiSticker, ApiVideo, OnApiUpdate } from '../../types';
|
import type {
|
||||||
|
ApiStickerSetInfo, ApiSticker, ApiVideo, OnApiUpdate,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import { buildStickerFromDocument, buildStickerSet, buildStickerSetCovered } from '../apiBuilders/symbols';
|
import {
|
||||||
|
buildStickerSet, buildStickerSetCovered, processStickerPackResult, processStickerResult,
|
||||||
|
} from '../apiBuilders/symbols';
|
||||||
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
||||||
import { buildVideoFromDocument } from '../apiBuilders/messages';
|
import { buildVideoFromDocument } from '../apiBuilders/messages';
|
||||||
import { RECENT_STICKERS_LIMIT } from '../../../config';
|
import { RECENT_STICKERS_LIMIT } from '../../../config';
|
||||||
@ -16,6 +20,25 @@ export function init(_onUpdate: OnApiUpdate) {
|
|||||||
onUpdate = _onUpdate;
|
onUpdate = _onUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCustomEmojiSets({ hash = '0' }: { hash?: string }) {
|
||||||
|
const allStickers = await invokeRequest(new GramJs.messages.GetEmojiStickers({ hash: BigInt(hash) }));
|
||||||
|
|
||||||
|
if (!allStickers || allStickers instanceof GramJs.messages.AllStickersNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
allStickers.sets.forEach((stickerSet) => {
|
||||||
|
if (stickerSet.thumbs?.length) {
|
||||||
|
localDb.stickerSets[String(stickerSet.id)] = stickerSet;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: String(allStickers.hash),
|
||||||
|
sets: allStickers.sets.map(buildStickerSet),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchStickerSets({ hash = '0' }: { hash?: string }) {
|
export async function fetchStickerSets({ hash = '0' }: { hash?: string }) {
|
||||||
const allStickers = await invokeRequest(new GramJs.messages.GetAllStickers({ hash: BigInt(hash) }));
|
const allStickers = await invokeRequest(new GramJs.messages.GetAllStickers({ hash: BigInt(hash) }));
|
||||||
|
|
||||||
@ -113,13 +136,14 @@ export function clearRecentStickers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchStickers(
|
export async function fetchStickers(
|
||||||
{ stickerSetShortName, stickerSetId, accessHash }:
|
{ stickerSetInfo }:
|
||||||
{ stickerSetShortName?: string; stickerSetId?: string; accessHash: string },
|
{ stickerSetInfo: ApiStickerSetInfo },
|
||||||
) {
|
) {
|
||||||
|
if ('isMissing' in stickerSetInfo) return undefined;
|
||||||
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||||
stickerset: stickerSetId
|
stickerset: 'id' in stickerSetInfo
|
||||||
? buildInputStickerSet(stickerSetId, accessHash)
|
? buildInputStickerSet(stickerSetInfo.id, stickerSetInfo.accessHash)
|
||||||
: buildInputStickerSetShortName(stickerSetShortName!),
|
: buildInputStickerSetShortName(stickerSetInfo.shortName),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!(result instanceof GramJs.messages.StickerSet)) {
|
if (!(result instanceof GramJs.messages.StickerSet)) {
|
||||||
@ -133,6 +157,16 @@ export async function fetchStickers(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchCustomEmoji({ documentId }: { documentId: string[] }) {
|
||||||
|
if (!documentId.length) return undefined;
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetCustomEmojiDocuments({
|
||||||
|
documentId: documentId.map((id) => BigInt(id)),
|
||||||
|
}));
|
||||||
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
return processStickerResult(result);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAnimatedEmojis() {
|
export async function fetchAnimatedEmojis() {
|
||||||
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||||
stickerset: new GramJs.InputStickerSetAnimatedEmoji(),
|
stickerset: new GramJs.InputStickerSetAnimatedEmoji(),
|
||||||
@ -334,32 +368,6 @@ export async function fetchEmojiKeywords({ language, fromVersion }: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function processStickerResult(stickers: GramJs.TypeDocument[]) {
|
|
||||||
return stickers
|
|
||||||
.map((document) => {
|
|
||||||
if (document instanceof GramJs.Document) {
|
|
||||||
const sticker = buildStickerFromDocument(document);
|
|
||||||
if (sticker) {
|
|
||||||
localDb.documents[String(document.id)] = document;
|
|
||||||
|
|
||||||
return sticker;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})
|
|
||||||
.filter<ApiSticker>(Boolean as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
function processStickerPackResult(packs: GramJs.StickerPack[]) {
|
|
||||||
return packs.reduce((acc, { emoticon, documents }) => {
|
|
||||||
acc[emoticon] = documents.map((documentId) => buildStickerFromDocument(
|
|
||||||
localDb.documents[String(documentId)],
|
|
||||||
)).filter<ApiSticker>(Boolean as any);
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, ApiSticker[]>);
|
|
||||||
}
|
|
||||||
|
|
||||||
function processGifResult(gifs: GramJs.TypeDocument[]) {
|
function processGifResult(gifs: GramJs.TypeDocument[]) {
|
||||||
return gifs
|
return gifs
|
||||||
.map((document) => {
|
.map((document) => {
|
||||||
|
|||||||
@ -866,7 +866,11 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
} else if (update instanceof GramJs.UpdateStickerSets) {
|
} else if (update instanceof GramJs.UpdateStickerSets) {
|
||||||
onUpdate({ '@type': 'updateStickerSets' });
|
onUpdate({ '@type': 'updateStickerSets' });
|
||||||
} else if (update instanceof GramJs.UpdateStickerSetsOrder) {
|
} else if (update instanceof GramJs.UpdateStickerSetsOrder) {
|
||||||
onUpdate({ '@type': 'updateStickerSetsOrder', order: update.order.map((n) => n.toString()) });
|
onUpdate({
|
||||||
|
'@type': 'updateStickerSetsOrder',
|
||||||
|
order: update.order.map((n) => n.toString()),
|
||||||
|
isCustomEmoji: update.emojis,
|
||||||
|
});
|
||||||
} else if (update instanceof GramJs.UpdateNewStickerSet) {
|
} else if (update instanceof GramJs.UpdateNewStickerSet) {
|
||||||
if (update.stickerset instanceof GramJs.messages.StickerSet) {
|
if (update.stickerset instanceof GramJs.messages.StickerSet) {
|
||||||
const stickerSet = buildStickerSet(update.stickerset.set);
|
const stickerSet = buildStickerSet(update.stickerset.set);
|
||||||
|
|||||||
@ -32,9 +32,9 @@ export interface ApiPhoto {
|
|||||||
|
|
||||||
export interface ApiSticker {
|
export interface ApiSticker {
|
||||||
id: string;
|
id: string;
|
||||||
stickerSetId: string;
|
stickerSetInfo: ApiStickerSetInfo;
|
||||||
stickerSetAccessHash?: string;
|
|
||||||
emoji?: string;
|
emoji?: string;
|
||||||
|
isCustomEmoji?: boolean;
|
||||||
isLottie: boolean;
|
isLottie: boolean;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
@ -42,6 +42,7 @@ export interface ApiSticker {
|
|||||||
thumbnail?: ApiThumbnail;
|
thumbnail?: ApiThumbnail;
|
||||||
isPreloadedGlobally?: boolean;
|
isPreloadedGlobally?: boolean;
|
||||||
hasEffect?: boolean;
|
hasEffect?: boolean;
|
||||||
|
isFree?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiStickerSet {
|
export interface ApiStickerSet {
|
||||||
@ -61,6 +62,21 @@ export interface ApiStickerSet {
|
|||||||
shortName: string;
|
shortName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiStickerSetInfoShortName = {
|
||||||
|
shortName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiStickerSetInfoId = {
|
||||||
|
id: string;
|
||||||
|
accessHash: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ApiStickerSetInfoMissing = {
|
||||||
|
isMissing: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiStickerSetInfo = ApiStickerSetInfoShortName | ApiStickerSetInfoId | ApiStickerSetInfoMissing;
|
||||||
|
|
||||||
export interface ApiVideo {
|
export interface ApiVideo {
|
||||||
id: string;
|
id: string;
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
@ -264,14 +280,46 @@ export interface ApiMessageForwardInfo {
|
|||||||
adminTitle?: string;
|
adminTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiMessageEntity {
|
export type ApiMessageEntityDefault = {
|
||||||
type: string;
|
type: Exclude<
|
||||||
|
`${ApiMessageEntityTypes}`,
|
||||||
|
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
|
||||||
|
`${ApiMessageEntityTypes.CustomEmoji}`
|
||||||
|
>;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityPre = {
|
||||||
|
type: ApiMessageEntityTypes.Pre;
|
||||||
offset: number;
|
offset: number;
|
||||||
length: number;
|
length: number;
|
||||||
userId?: string;
|
|
||||||
url?: string;
|
|
||||||
language?: string;
|
language?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityTextUrl = {
|
||||||
|
type: ApiMessageEntityTypes.TextUrl;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityMentionName = {
|
||||||
|
type: ApiMessageEntityTypes.MentionName;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntityCustomEmoji = {
|
||||||
|
type: ApiMessageEntityTypes.CustomEmoji;
|
||||||
|
offset: number;
|
||||||
|
length: number;
|
||||||
|
documentId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
||||||
|
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji;
|
||||||
|
|
||||||
export enum ApiMessageEntityTypes {
|
export enum ApiMessageEntityTypes {
|
||||||
Bold = 'MessageEntityBold',
|
Bold = 'MessageEntityBold',
|
||||||
@ -291,6 +339,7 @@ export enum ApiMessageEntityTypes {
|
|||||||
Url = 'MessageEntityUrl',
|
Url = 'MessageEntityUrl',
|
||||||
Underline = 'MessageEntityUnderline',
|
Underline = 'MessageEntityUnderline',
|
||||||
Spoiler = 'MessageEntitySpoiler',
|
Spoiler = 'MessageEntitySpoiler',
|
||||||
|
CustomEmoji = 'MessageEntityCustomEmoji',
|
||||||
Unknown = 'MessageEntityUnknown',
|
Unknown = 'MessageEntityUnknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -366,6 +366,7 @@ export type ApiUpdateStickerSets = {
|
|||||||
export type ApiUpdateStickerSetsOrder = {
|
export type ApiUpdateStickerSetsOrder = {
|
||||||
'@type': 'updateStickerSetsOrder';
|
'@type': 'updateStickerSetsOrder';
|
||||||
order: string[];
|
order: string[];
|
||||||
|
isCustomEmoji?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiUpdateStickerSet = {
|
export type ApiUpdateStickerSet = {
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export { default as SponsoredMessageContextMenuContainer }
|
|||||||
from '../components/middle/message/SponsoredMessageContextMenuContainer';
|
from '../components/middle/message/SponsoredMessageContextMenuContainer';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
||||||
|
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
|
||||||
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
||||||
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
||||||
|
|
||||||
|
|||||||
@ -124,15 +124,13 @@ const MicrophoneButton: FC<StateProps> = ({
|
|||||||
muteMouseDownState.current = 'up';
|
muteMouseDownState.current = 'up';
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonText = useMemo(() => {
|
const buttonText = lang(
|
||||||
return lang(
|
hasRequestedToSpeak ? 'VoipMutedTapedForSpeak' : (
|
||||||
hasRequestedToSpeak ? 'VoipMutedTapedForSpeak' : (
|
shouldRaiseHand ? 'VoipMutedByAdmin' : (
|
||||||
shouldRaiseHand ? 'VoipMutedByAdmin' : (
|
noAudioStream ? 'VoipUnmute' : 'VoipTapToMute'
|
||||||
noAudioStream ? 'VoipUnmute' : 'VoipTapToMute'
|
)
|
||||||
)
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
}, [hasRequestedToSpeak, noAudioStream, lang, shouldRaiseHand]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="button-wrapper microphone-wrapper">
|
<div className="button-wrapper microphone-wrapper">
|
||||||
|
|||||||
99
src/components/common/CustomEmoji.tsx
Normal file
99
src/components/common/CustomEmoji.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useEffect, useMemo, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
|
import { IS_WEBM_SUPPORTED } from '../../util/environment';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
import safePlay from '../../util/safePlay';
|
||||||
|
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
import useEnsureCustomEmoji from '../../hooks/useEnsureCustomEmoji';
|
||||||
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
|
import useCustomEmoji from './hooks/useCustomEmoji';
|
||||||
|
|
||||||
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
documentId: string;
|
||||||
|
children?: TeactNode;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const STICKER_SIZE = 24;
|
||||||
|
|
||||||
|
const CustomEmojiInner: FC<OwnProps> = ({
|
||||||
|
documentId,
|
||||||
|
children,
|
||||||
|
observeIntersection,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
// An alternative to `withGlobal` to avoid adding numerous global containers
|
||||||
|
const customEmoji = useCustomEmoji(documentId);
|
||||||
|
const mediaHash = customEmoji && `sticker${customEmoji.id}`;
|
||||||
|
const mediaData = useMedia(mediaHash);
|
||||||
|
const thumbDataUri = useThumbnail(customEmoji);
|
||||||
|
|
||||||
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
|
useEnsureCustomEmoji(documentId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!customEmoji?.isVideo) return;
|
||||||
|
const video = ref.current?.querySelector('video');
|
||||||
|
if (!video || isIntersecting === !video.paused) return;
|
||||||
|
|
||||||
|
if (isIntersecting) {
|
||||||
|
safePlay(video);
|
||||||
|
} else {
|
||||||
|
video.pause();
|
||||||
|
}
|
||||||
|
}, [customEmoji, isIntersecting]);
|
||||||
|
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (!customEmoji || (!thumbDataUri && !mediaData)) {
|
||||||
|
return (children && renderText(children, ['emoji']));
|
||||||
|
}
|
||||||
|
if (!mediaData || (customEmoji.isVideo && !IS_WEBM_SUPPORTED)) {
|
||||||
|
return (
|
||||||
|
<img src={thumbDataUri} alt={customEmoji.emoji} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!customEmoji.isVideo && !customEmoji.isLottie) {
|
||||||
|
return (
|
||||||
|
<img src={mediaData} alt={customEmoji.emoji} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (customEmoji.isVideo) {
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
autoPlay={isIntersecting}
|
||||||
|
loop
|
||||||
|
src={mediaData}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AnimatedSticker
|
||||||
|
size={STICKER_SIZE}
|
||||||
|
tgsUrl={mediaData}
|
||||||
|
play={isIntersecting}
|
||||||
|
isLowPriority
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [children, customEmoji, isIntersecting, mediaData, thumbDataUri]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="text-entity-custom-emoji emoji">
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomEmojiInner);
|
||||||
16
src/components/common/CustomEmojiSetsModal.async.tsx
Normal file
16
src/components/common/CustomEmojiSetsModal.async.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
import type { OwnProps } from './CustomEmojiSetsModal';
|
||||||
|
import { Bundles } from '../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const CustomEmojiSetsModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { customEmojiSetIds } = props;
|
||||||
|
const CustomEmojiSetsModal = useModuleLoader(Bundles.Extra, 'CustomEmojiSetsModal', !customEmojiSetIds);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return CustomEmojiSetsModal ? <CustomEmojiSetsModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(CustomEmojiSetsModalAsync);
|
||||||
27
src/components/common/CustomEmojiSetsModal.module.scss
Normal file
27
src/components/common/CustomEmojiSetsModal.module.scss
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
.root {
|
||||||
|
:global {
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 26.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.multiline-menu-item {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sets {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 19rem;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
78
src/components/common/CustomEmojiSetsModal.tsx
Normal file
78
src/components/common/CustomEmojiSetsModal.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ApiSticker, ApiStickerSet } from '../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||||
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
import StickerSetCard from './StickerSetCard';
|
||||||
|
|
||||||
|
import styles from './CustomEmojiSetsModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
customEmojiSetIds?: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
customEmojiSets?: ApiStickerSet[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const CustomEmojiSetsModal: FC<OwnProps & StateProps> = ({
|
||||||
|
customEmojiSets,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { openStickerSet } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const customEmojiModalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: customEmojiModalRef });
|
||||||
|
|
||||||
|
const prevCustomEmojiSets = usePrevious(customEmojiSets);
|
||||||
|
const renderingCustomEmojiSets = customEmojiSets || prevCustomEmojiSets;
|
||||||
|
|
||||||
|
const handleSetClick = useCallback((sticker: ApiSticker) => {
|
||||||
|
openStickerSet({
|
||||||
|
stickerSetInfo: sticker.stickerSetInfo,
|
||||||
|
});
|
||||||
|
}, [openStickerSet]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={Boolean(customEmojiSets)}
|
||||||
|
className={styles.root}
|
||||||
|
onClose={onClose}
|
||||||
|
hasCloseButton
|
||||||
|
title="Sets of used emoji"
|
||||||
|
>
|
||||||
|
<div className={buildClassName(styles.sets, 'custom-scroll')} ref={customEmojiModalRef}>
|
||||||
|
{renderingCustomEmojiSets?.map((customEmojiSet) => (
|
||||||
|
<StickerSetCard
|
||||||
|
key={customEmojiSet.id}
|
||||||
|
className={styles.setCard}
|
||||||
|
stickerSet={customEmojiSet}
|
||||||
|
onClick={handleSetClick}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { customEmojiSetIds }): StateProps => {
|
||||||
|
const customEmojiSets = customEmojiSetIds?.map((id) => global.stickers.setsById[id]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customEmojiSets,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(CustomEmojiSetsModal));
|
||||||
@ -103,6 +103,16 @@
|
|||||||
height: calc(1.125 * var(--message-text-size, 1rem)) !important;
|
height: calc(1.125 * var(--message-text-size, 1rem)) !important;
|
||||||
vertical-align: text-bottom !important;
|
vertical-align: text-bottom !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-entity-custom-emoji {
|
||||||
|
// Custom emoji needs to be slightly bigger than normal emoji
|
||||||
|
--custom-emoji-size: calc(1.125 * var(--message-text-size, 1rem) + 1px);
|
||||||
|
margin-inline-end: 1px;
|
||||||
|
|
||||||
|
& > img {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.embedded-action-message {
|
.embedded-action-message {
|
||||||
|
|||||||
@ -13,12 +13,13 @@ import {
|
|||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getPictogramDimensions } from './helpers/mediaDimensions';
|
import { getPictogramDimensions } from './helpers/mediaDimensions';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { renderMessageSummary } from './helpers/renderMessageText';
|
||||||
|
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useWebpThumbnail from '../../hooks/useWebpThumbnail';
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import { renderMessageSummary } from './helpers/renderMessageText';
|
|
||||||
|
|
||||||
import ActionMessage from '../middle/ActionMessage';
|
import ActionMessage from '../middle/ActionMessage';
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting);
|
||||||
const mediaThumbnail = useWebpThumbnail(message);
|
const mediaThumbnail = useThumbnail(message);
|
||||||
const isRoundVideo = Boolean(message && getMessageRoundVideo(message));
|
const isRoundVideo = Boolean(message && getMessageRoundVideo(message));
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|||||||
@ -10,6 +10,17 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
||||||
|
|
||||||
|
&.custom-emoji {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin: 0.3125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.set-expand {
|
||||||
|
padding: 0;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.sticker-locked {
|
.sticker-locked {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -52,7 +63,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
margin: 0.25rem;
|
&, &.custom-emoji {
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.set-button {
|
&.set-button {
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import useFlag from '../../hooks/useFlag';
|
|||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||||
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
|
|
||||||
import AnimatedSticker from './AnimatedSticker';
|
import AnimatedSticker from './AnimatedSticker';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
@ -73,7 +74,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
const thumbDataUri = sticker.thumbnail ? sticker.thumbnail.dataUri : undefined;
|
const thumbDataUri = useThumbnail(sticker);
|
||||||
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !isIntersecting, ApiMediaFormat.BlobUrl);
|
const previewBlobUrl = useMedia(`${localMediaHash}?size=m`, !isIntersecting, ApiMediaFormat.BlobUrl);
|
||||||
|
|
||||||
const shouldPlay = isIntersecting && !noAnimate;
|
const shouldPlay = isIntersecting && !noAnimate;
|
||||||
@ -81,6 +82,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
const [isLottieLoaded, markLoaded, unmarkLoaded] = useFlag(Boolean(lottieData));
|
const [isLottieLoaded, markLoaded, unmarkLoaded] = useFlag(Boolean(lottieData));
|
||||||
const canLottiePlay = isLottieLoaded && shouldPlay;
|
const canLottiePlay = isLottieLoaded && shouldPlay;
|
||||||
const isVideo = sticker.isVideo && IS_WEBM_SUPPORTED;
|
const isVideo = sticker.isVideo && IS_WEBM_SUPPORTED;
|
||||||
|
const isCustomEmoji = sticker.isCustomEmoji;
|
||||||
const videoBlobUrl = useMedia(isVideo && localMediaHash, !shouldPlay, ApiMediaFormat.BlobUrl);
|
const videoBlobUrl = useMedia(isVideo && localMediaHash, !shouldPlay, ApiMediaFormat.BlobUrl);
|
||||||
const canVideoPlay = Boolean(isVideo && videoBlobUrl && shouldPlay);
|
const canVideoPlay = Boolean(isVideo && videoBlobUrl && shouldPlay);
|
||||||
const isPremiumSticker = sticker.hasEffect;
|
const isPremiumSticker = sticker.hasEffect;
|
||||||
@ -184,7 +186,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
}, [clickArg, onClick]);
|
}, [clickArg, onClick]);
|
||||||
|
|
||||||
const handleOpenSet = useCallback(() => {
|
const handleOpenSet = useCallback(() => {
|
||||||
openStickerSet({ sticker });
|
openStickerSet({ stickerSetInfo: sticker.stickerSetInfo });
|
||||||
}, [openStickerSet, sticker]);
|
}, [openStickerSet, sticker]);
|
||||||
|
|
||||||
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
||||||
@ -192,6 +194,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'StickerButton',
|
'StickerButton',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
|
isCustomEmoji && 'custom-emoji',
|
||||||
stickerSelector,
|
stickerSelector,
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
@ -200,7 +203,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
|
|
||||||
const contextMenuItems = useMemo(() => {
|
const contextMenuItems = useMemo(() => {
|
||||||
const items: ReactNode[] = [];
|
const items: ReactNode[] = [];
|
||||||
if (noContextMenu) return items;
|
if (noContextMenu || isCustomEmoji) return items;
|
||||||
|
|
||||||
if (onUnfaveClick) {
|
if (onUnfaveClick) {
|
||||||
items.push(
|
items.push(
|
||||||
@ -247,7 +250,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
}, [
|
}, [
|
||||||
canViewSet, handleContextFave, handleContextRemoveRecent, handleContextUnfave, handleOpenSet, handleSendQuiet,
|
canViewSet, handleContextFave, handleContextRemoveRecent, handleContextUnfave, handleOpenSet, handleSendQuiet,
|
||||||
handleSendScheduled, isLocked, isSavedMessages, lang, onFaveClick, onRemoveRecentClick, onUnfaveClick, onClick,
|
handleSendScheduled, isLocked, isSavedMessages, lang, onFaveClick, onRemoveRecentClick, onUnfaveClick, onClick,
|
||||||
noContextMenu,
|
noContextMenu, isCustomEmoji,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.SettingsStickerSet {
|
.StickerSetCard {
|
||||||
.settings-item &.ListItem {
|
.settings-item &.ListItem {
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
@ -12,6 +12,10 @@
|
|||||||
flex: 0 0 3rem;
|
flex: 0 0 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.install-button {
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
99
src/components/common/StickerSetCard.tsx
Normal file
99
src/components/common/StickerSetCard.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import React, { memo, useCallback, useMemo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiSticker, ApiStickerSet } from '../../api/types';
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { STICKER_SIZE_GENERAL_SETTINGS } from '../../config';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import ListItem from '../ui/ListItem';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
import StickerSetCoverAnimated from '../middle/composer/StickerSetCoverAnimated';
|
||||||
|
import StickerSetCover from '../middle/composer/StickerSetCover';
|
||||||
|
import StickerButton from './StickerButton';
|
||||||
|
|
||||||
|
import './StickerSetCard.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
stickerSet?: ApiStickerSet;
|
||||||
|
className?: string;
|
||||||
|
observeIntersection: ObserveFn;
|
||||||
|
onClick: (value: ApiSticker) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StickerSetCard: FC<OwnProps> = ({
|
||||||
|
stickerSet,
|
||||||
|
className,
|
||||||
|
observeIntersection,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const firstSticker = stickerSet?.stickers?.[0];
|
||||||
|
|
||||||
|
const handleCardClick = useCallback(() => {
|
||||||
|
if (firstSticker) onClick(firstSticker);
|
||||||
|
}, [firstSticker, onClick]);
|
||||||
|
|
||||||
|
const preview = useMemo(() => {
|
||||||
|
if (!stickerSet) return undefined;
|
||||||
|
if (stickerSet.hasThumbnail || !firstSticker) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ariaLabel={stickerSet.title}
|
||||||
|
color="translucent"
|
||||||
|
isRtl={lang.isRtl}
|
||||||
|
>
|
||||||
|
{stickerSet.isLottie ? (
|
||||||
|
<StickerSetCoverAnimated
|
||||||
|
size={STICKER_SIZE_GENERAL_SETTINGS}
|
||||||
|
stickerSet={stickerSet}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StickerSetCover
|
||||||
|
stickerSet={stickerSet}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<StickerButton
|
||||||
|
sticker={firstSticker}
|
||||||
|
size={STICKER_SIZE_GENERAL_SETTINGS}
|
||||||
|
title={stickerSet.title}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
clickArg={undefined}
|
||||||
|
noContextMenu
|
||||||
|
isCurrentUserPremium
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [firstSticker, lang.isRtl, observeIntersection, stickerSet]);
|
||||||
|
|
||||||
|
if (!stickerSet || !stickerSet.stickers) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
narrow
|
||||||
|
className={buildClassName('StickerSetCard', className)}
|
||||||
|
inactive={!firstSticker}
|
||||||
|
onClick={handleCardClick}
|
||||||
|
>
|
||||||
|
{preview}
|
||||||
|
<div className="multiline-menu-item">
|
||||||
|
<div className="title">{stickerSet.title}</div>
|
||||||
|
<div className="subtitle">{lang('StickerPack.StickerCount', stickerSet.count, 'i')}</div>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(StickerSetCard);
|
||||||
@ -1,26 +1,27 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import type { ApiSticker, ApiStickerSet } from '../../api/types';
|
import type { ApiSticker, ApiStickerSet } from '../../api/types';
|
||||||
|
|
||||||
import { STICKER_SIZE_MODAL } from '../../config';
|
import { EMOJI_SIZE_MODAL, STICKER_SIZE_MODAL } from '../../config';
|
||||||
import {
|
import {
|
||||||
selectCanScheduleUntilOnline,
|
selectCanScheduleUntilOnline,
|
||||||
selectChat,
|
selectChat,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectIsChatWithSelf, selectIsCurrentUserPremium,
|
selectIsChatWithSelf, selectIsCurrentUserPremium,
|
||||||
|
selectIsSetPremium,
|
||||||
selectShouldSchedule,
|
selectShouldSchedule,
|
||||||
selectStickerSet,
|
selectStickerSet,
|
||||||
selectStickerSetByShortName,
|
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
|
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
|
||||||
import useSchedule from '../../hooks/useSchedule';
|
import useSchedule from '../../hooks/useSchedule';
|
||||||
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
@ -42,6 +43,7 @@ type StateProps = {
|
|||||||
canScheduleUntilOnline?: boolean;
|
canScheduleUntilOnline?: boolean;
|
||||||
shouldSchedule?: boolean;
|
shouldSchedule?: boolean;
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
|
isSetPremium?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +58,7 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
canScheduleUntilOnline,
|
canScheduleUntilOnline,
|
||||||
shouldSchedule,
|
shouldSchedule,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
|
isSetPremium,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
@ -63,12 +66,19 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
loadStickers,
|
loadStickers,
|
||||||
toggleStickerSet,
|
toggleStickerSet,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
|
openPremiumModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const prevStickerSet = usePrevious(stickerSet);
|
||||||
|
const renderingStickerSet = stickerSet || prevStickerSet;
|
||||||
|
|
||||||
|
const isEmoji = renderingStickerSet?.isEmoji;
|
||||||
|
const isButtonLocked = !renderingStickerSet?.installedDate && isSetPremium && !isCurrentUserPremium;
|
||||||
|
|
||||||
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
|
const [requestCalendar, calendar] = useSchedule(canScheduleUntilOnline);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -76,20 +86,12 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
|
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, isDisabled: !isOpen });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && !stickerSet?.stickers) {
|
if (isOpen && !renderingStickerSet?.stickers) {
|
||||||
if (fromSticker) {
|
loadStickers({
|
||||||
const { stickerSetId, stickerSetAccessHash } = fromSticker;
|
stickerSetInfo: fromSticker ? fromSticker.stickerSetInfo : { shortName: stickerSetShortName! },
|
||||||
loadStickers({
|
});
|
||||||
stickerSetId,
|
|
||||||
stickerSetAccessHash,
|
|
||||||
});
|
|
||||||
} else if (stickerSetShortName) {
|
|
||||||
loadStickers({
|
|
||||||
stickerSetShortName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [isOpen, fromSticker, loadStickers, stickerSetShortName, stickerSet]);
|
}, [isOpen, fromSticker, loadStickers, stickerSetShortName, renderingStickerSet]);
|
||||||
|
|
||||||
const handleSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean) => {
|
const handleSelect = useCallback((sticker: ApiSticker, isSilent?: boolean, isScheduleRequested?: boolean) => {
|
||||||
sticker = {
|
sticker = {
|
||||||
@ -109,11 +111,30 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
}, [onClose, requestCalendar, sendMessage, shouldSchedule]);
|
}, [onClose, requestCalendar, sendMessage, shouldSchedule]);
|
||||||
|
|
||||||
const handleButtonClick = useCallback(() => {
|
const handleButtonClick = useCallback(() => {
|
||||||
if (stickerSet) {
|
if (renderingStickerSet) {
|
||||||
toggleStickerSet({ stickerSetId: stickerSet.id });
|
if (isButtonLocked) {
|
||||||
|
openPremiumModal({ initialSection: 'animated_emoji' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleStickerSet({ stickerSetId: renderingStickerSet.id });
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [onClose, stickerSet, toggleStickerSet]);
|
}, [isButtonLocked, onClose, openPremiumModal, renderingStickerSet, toggleStickerSet]);
|
||||||
|
|
||||||
|
const renderButtonText = () => {
|
||||||
|
if (!renderingStickerSet) return lang('Loading');
|
||||||
|
if (isButtonLocked) {
|
||||||
|
return lang('EmojiInput.UnlockPack', renderingStickerSet.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = isEmoji ? 'Emoji' : 'Sticker';
|
||||||
|
|
||||||
|
return lang(
|
||||||
|
renderingStickerSet.installedDate ? `StickerPack.Remove${suffix}Count` : `StickerPack.Add${suffix}Count`,
|
||||||
|
renderingStickerSet.count,
|
||||||
|
'i',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -121,17 +142,18 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
hasCloseButton
|
hasCloseButton
|
||||||
title={stickerSet ? renderText(stickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}
|
title={renderingStickerSet
|
||||||
|
? renderText(renderingStickerSet.title, ['emoji', 'links']) : lang('AccDescrStickerSet')}
|
||||||
>
|
>
|
||||||
{stickerSet?.stickers ? (
|
{renderingStickerSet?.stickers ? (
|
||||||
<>
|
<>
|
||||||
<div ref={containerRef} className="stickers custom-scroll">
|
<div ref={containerRef} className="stickers custom-scroll">
|
||||||
{stickerSet.stickers.map((sticker) => (
|
{renderingStickerSet.stickers.map((sticker) => (
|
||||||
<StickerButton
|
<StickerButton
|
||||||
sticker={sticker}
|
sticker={sticker}
|
||||||
size={STICKER_SIZE_MODAL}
|
size={isEmoji ? EMOJI_SIZE_MODAL : STICKER_SIZE_MODAL}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={canSendStickers ? handleSelect : undefined}
|
onClick={canSendStickers && !isEmoji ? handleSelect : undefined}
|
||||||
clickArg={sticker}
|
clickArg={sticker}
|
||||||
isSavedMessages={isSavedMessages}
|
isSavedMessages={isSavedMessages}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
@ -142,14 +164,12 @@ const StickerSetModal: FC<OwnProps & StateProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
size="smaller"
|
size="smaller"
|
||||||
fluid
|
fluid
|
||||||
color={stickerSet.installedDate ? 'danger' : 'primary'}
|
color={renderingStickerSet.installedDate ? 'danger' : 'primary'}
|
||||||
|
isShiny={isButtonLocked}
|
||||||
|
withPremiumGradient={isButtonLocked}
|
||||||
onClick={handleButtonClick}
|
onClick={handleButtonClick}
|
||||||
>
|
>
|
||||||
{lang(
|
{renderButtonText()}
|
||||||
stickerSet.installedDate ? 'StickerPack.RemoveStickerCount' : 'StickerPack.AddStickerCount',
|
|
||||||
stickerSet.count,
|
|
||||||
'i',
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -172,17 +192,20 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
);
|
);
|
||||||
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||||
|
|
||||||
|
const stickerSetInfo = fromSticker ? fromSticker.stickerSetInfo
|
||||||
|
: stickerSetShortName ? { shortName: stickerSetShortName } : undefined;
|
||||||
|
|
||||||
|
const stickerSet = stickerSetInfo ? selectStickerSet(global, stickerSetInfo) : undefined;
|
||||||
|
const isSetPremium = stickerSet && selectIsSetPremium(stickerSet);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
|
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
|
||||||
canSendStickers,
|
canSendStickers,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
shouldSchedule: selectShouldSchedule(global),
|
shouldSchedule: selectShouldSchedule(global),
|
||||||
stickerSet: fromSticker
|
stickerSet,
|
||||||
? selectStickerSet(global, fromSticker.stickerSetId)
|
|
||||||
: stickerSetShortName
|
|
||||||
? selectStickerSetByShortName(global, stickerSetShortName)
|
|
||||||
: undefined,
|
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
isSetPremium,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(StickerSetModal));
|
)(StickerSetModal));
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { ApiMessage } from '../../../api/types';
|
import type { ApiMessage } from '../../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
import type { TextPart } from '../../../types';
|
import type { TextPart } from '../../../types';
|
||||||
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import type { LangFn } from '../../../hooks/useLang';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMessageSummaryDescription,
|
getMessageSummaryDescription,
|
||||||
@ -9,7 +11,6 @@ import {
|
|||||||
getMessageText,
|
getMessageText,
|
||||||
TRUNCATED_SUMMARY_LENGTH,
|
TRUNCATED_SUMMARY_LENGTH,
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import type { LangFn } from '../../../hooks/useLang';
|
|
||||||
import renderText from './renderText';
|
import renderText from './renderText';
|
||||||
import { renderTextWithEntities } from './renderTextWithEntities';
|
import { renderTextWithEntities } from './renderTextWithEntities';
|
||||||
import trimText from '../../../util/trimText';
|
import trimText from '../../../util/trimText';
|
||||||
@ -21,6 +22,7 @@ export function renderMessageText(
|
|||||||
isSimple?: boolean,
|
isSimple?: boolean,
|
||||||
truncateLength?: number,
|
truncateLength?: number,
|
||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
|
observeIntersection?: ObserveFn,
|
||||||
) {
|
) {
|
||||||
const { text, entities } = message.content.text || {};
|
const { text, entities } = message.content.text || {};
|
||||||
|
|
||||||
@ -38,6 +40,7 @@ export function renderMessageText(
|
|||||||
message.id,
|
message.id,
|
||||||
isSimple,
|
isSimple,
|
||||||
isProtected,
|
isProtected,
|
||||||
|
observeIntersection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,11 +50,13 @@ export function renderMessageSummary(
|
|||||||
noEmoji = false,
|
noEmoji = false,
|
||||||
highlight?: string,
|
highlight?: string,
|
||||||
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||||
|
observeIntersection?: ObserveFn,
|
||||||
): TextPart[] {
|
): TextPart[] {
|
||||||
const { entities } = message.content.text || {};
|
const { entities } = message.content.text || {};
|
||||||
|
|
||||||
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||||
if (!hasSpoilers) {
|
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
||||||
|
if (!hasSpoilers && !hasCustomEmoji) {
|
||||||
const text = trimText(getMessageSummaryText(lang, message, noEmoji), truncateLength);
|
const text = trimText(getMessageSummaryText(lang, message, noEmoji), truncateLength);
|
||||||
|
|
||||||
if (highlight) {
|
if (highlight) {
|
||||||
@ -64,11 +69,13 @@ export function renderMessageSummary(
|
|||||||
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
const emoji = !noEmoji && getMessageSummaryEmoji(message);
|
||||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||||
|
|
||||||
const text = renderMessageText(message, highlight, undefined, true, truncateLength);
|
const text = renderMessageText(
|
||||||
|
message, highlight, undefined, true, truncateLength, undefined, observeIntersection,
|
||||||
|
);
|
||||||
const description = getMessageSummaryDescription(lang, message, text);
|
const description = getMessageSummaryDescription(lang, message, text);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
emojiWithSpace,
|
...renderText(emojiWithSpace),
|
||||||
...(Array.isArray(description) ? description : [description]),
|
...(Array.isArray(description) ? description : [description]),
|
||||||
].filter<TextPart>(Boolean);
|
].filter<TextPart>(Boolean);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import type { MouseEvent } from 'react';
|
|
||||||
import React from '../../../lib/teact/teact';
|
import React from '../../../lib/teact/teact';
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { TextPart } from '../../../types';
|
import type { TextPart } from '../../../types';
|
||||||
import type { ApiFormattedText, ApiMessageEntity } from '../../../api/types';
|
import type { ApiFormattedText, ApiMessageEntity } from '../../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import type { TextFilter } from './renderText';
|
import type { TextFilter } from './renderText';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import renderText from './renderText';
|
import renderText from './renderText';
|
||||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||||
import { getTranslation } from '../../../util/langProvider';
|
import { getTranslation } from '../../../util/langProvider';
|
||||||
@ -14,8 +15,8 @@ import { getTranslation } from '../../../util/langProvider';
|
|||||||
import MentionLink from '../../middle/message/MentionLink';
|
import MentionLink from '../../middle/message/MentionLink';
|
||||||
import SafeLink from '../SafeLink';
|
import SafeLink from '../SafeLink';
|
||||||
import Spoiler from '../spoiler/Spoiler';
|
import Spoiler from '../spoiler/Spoiler';
|
||||||
|
import CustomEmoji from '../CustomEmoji';
|
||||||
import CodeBlock from '../code/CodeBlock';
|
import CodeBlock from '../code/CodeBlock';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
|
||||||
|
|
||||||
interface IOrganizedEntity {
|
interface IOrganizedEntity {
|
||||||
entity: ApiMessageEntity;
|
entity: ApiMessageEntity;
|
||||||
@ -32,6 +33,7 @@ export function renderTextWithEntities(
|
|||||||
messageId?: number,
|
messageId?: number,
|
||||||
isSimple?: boolean,
|
isSimple?: boolean,
|
||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
|
observeIntersection?: ObserveFn,
|
||||||
) {
|
) {
|
||||||
if (!entities || !entities.length) {
|
if (!entities || !entities.length) {
|
||||||
return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple);
|
return renderMessagePart(text, highlight, shouldRenderHqEmoji, shouldRenderAsHtml, isSimple);
|
||||||
@ -107,7 +109,7 @@ 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,
|
entity, entityContent, nestedEntityContent, highlight, messageId, isSimple, isProtected, observeIntersection,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (Array.isArray(newEntity)) {
|
if (Array.isArray(newEntity)) {
|
||||||
@ -284,6 +286,7 @@ function processEntity(
|
|||||||
messageId?: number,
|
messageId?: number,
|
||||||
isSimple?: boolean,
|
isSimple?: boolean,
|
||||||
isProtected?: boolean,
|
isProtected?: boolean,
|
||||||
|
observeIntersection?: ObserveFn,
|
||||||
) {
|
) {
|
||||||
const entityText = typeof entityContent === 'string' && entityContent;
|
const entityText = typeof entityContent === 'string' && entityContent;
|
||||||
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
const renderedContent = nestedEntityContent.length ? nestedEntityContent : entityContent;
|
||||||
@ -303,6 +306,14 @@ function processEntity(
|
|||||||
if (entity.type === ApiMessageEntityTypes.Spoiler) {
|
if (entity.type === ApiMessageEntityTypes.Spoiler) {
|
||||||
return <Spoiler>{text}</Spoiler>;
|
return <Spoiler>{text}</Spoiler>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (entity.type === ApiMessageEntityTypes.CustomEmoji) {
|
||||||
|
return (
|
||||||
|
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection}>
|
||||||
|
{renderNestedMessagePart()}
|
||||||
|
</CustomEmoji>
|
||||||
|
);
|
||||||
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,6 +417,12 @@ function processEntity(
|
|||||||
return <ins>{renderNestedMessagePart()}</ins>;
|
return <ins>{renderNestedMessagePart()}</ins>;
|
||||||
case ApiMessageEntityTypes.Spoiler:
|
case ApiMessageEntityTypes.Spoiler:
|
||||||
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
return <Spoiler messageId={messageId}>{renderNestedMessagePart()}</Spoiler>;
|
||||||
|
case ApiMessageEntityTypes.CustomEmoji:
|
||||||
|
return (
|
||||||
|
<CustomEmoji documentId={entity.documentId} observeIntersection={observeIntersection}>
|
||||||
|
{renderNestedMessagePart()}
|
||||||
|
</CustomEmoji>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return renderNestedMessagePart();
|
return renderNestedMessagePart();
|
||||||
}
|
}
|
||||||
@ -469,20 +486,20 @@ function processEntityAsHtml(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getLinkUrl(entityContent: string, entity: ApiMessageEntity) {
|
function getLinkUrl(entityContent: string, entity: ApiMessageEntity) {
|
||||||
const { type, url } = entity;
|
const { type } = entity;
|
||||||
return type === ApiMessageEntityTypes.TextUrl && url ? url : entityContent;
|
return type === ApiMessageEntityTypes.TextUrl && entity.url ? entity.url : entityContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBotCommandClick(e: MouseEvent<HTMLAnchorElement>) {
|
function handleBotCommandClick(e: React.MouseEvent<HTMLAnchorElement>) {
|
||||||
getActions().sendBotCommand({ command: e.currentTarget.innerText });
|
getActions().sendBotCommand({ command: e.currentTarget.innerText });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleHashtagClick(e: MouseEvent<HTMLAnchorElement>) {
|
function handleHashtagClick(e: React.MouseEvent<HTMLAnchorElement>) {
|
||||||
getActions().setLocalTextSearchQuery({ query: e.currentTarget.innerText });
|
getActions().setLocalTextSearchQuery({ query: e.currentTarget.innerText });
|
||||||
getActions().searchTextMessagesLocal();
|
getActions().searchTextMessagesLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCodeClick(e: MouseEvent<HTMLElement>) {
|
function handleCodeClick(e: React.MouseEvent<HTMLElement>) {
|
||||||
copyTextToClipboard(e.currentTarget.innerText);
|
copyTextToClipboard(e.currentTarget.innerText);
|
||||||
getActions().showNotification({
|
getActions().showNotification({
|
||||||
message: getTranslation('TextCopied'),
|
message: getTranslation('TextCopied'),
|
||||||
|
|||||||
48
src/components/common/hooks/useCustomEmoji.ts
Normal file
48
src/components/common/hooks/useCustomEmoji.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { useCallback, useEffect, useState } from '../../../lib/teact/teact';
|
||||||
|
import { addCallback } from '../../../lib/teact/teactn';
|
||||||
|
import { getGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
|
const handlers = new Set<AnyToVoidFunction>();
|
||||||
|
|
||||||
|
let prevGlobal: GlobalState | undefined;
|
||||||
|
|
||||||
|
addCallback((global: GlobalState) => {
|
||||||
|
const customEmojiById = global.customEmojis.byId;
|
||||||
|
|
||||||
|
if (customEmojiById === prevGlobal?.customEmojis.byId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const handler of handlers) {
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevGlobal = global;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function useCustomEmoji(documentId: string) {
|
||||||
|
const [customEmoji, setCustomEmoji] = useState<ApiSticker | undefined>(getGlobal().customEmojis.byId[documentId]);
|
||||||
|
|
||||||
|
const handleGlobalChange = useCallback(() => {
|
||||||
|
setCustomEmoji(getGlobal().customEmojis.byId[documentId]);
|
||||||
|
}, [documentId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!documentId) return;
|
||||||
|
handleGlobalChange();
|
||||||
|
}, [documentId, handleGlobalChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customEmoji) return undefined;
|
||||||
|
handlers.add(handleGlobalChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
handlers.delete(handleGlobalChange);
|
||||||
|
};
|
||||||
|
}, [customEmoji, documentId, handleGlobalChange]);
|
||||||
|
|
||||||
|
return customEmoji;
|
||||||
|
}
|
||||||
@ -145,12 +145,12 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
case SettingsScreens.Privacy:
|
case SettingsScreens.Privacy:
|
||||||
case SettingsScreens.ActiveSessions:
|
case SettingsScreens.ActiveSessions:
|
||||||
case SettingsScreens.Language:
|
case SettingsScreens.Language:
|
||||||
|
case SettingsScreens.Stickers:
|
||||||
case SettingsScreens.Experimental:
|
case SettingsScreens.Experimental:
|
||||||
setSettingsScreen(SettingsScreens.Main);
|
setSettingsScreen(SettingsScreens.Main);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case SettingsScreens.GeneralChatBackground:
|
case SettingsScreens.GeneralChatBackground:
|
||||||
case SettingsScreens.QuickReaction:
|
|
||||||
setSettingsScreen(SettingsScreens.General);
|
setSettingsScreen(SettingsScreens.General);
|
||||||
return;
|
return;
|
||||||
case SettingsScreens.GeneralChatBackgroundColor:
|
case SettingsScreens.GeneralChatBackgroundColor:
|
||||||
@ -279,6 +279,11 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
setContent(LeftColumnContent.ChatList);
|
setContent(LeftColumnContent.ChatList);
|
||||||
setSettingsScreen(SettingsScreens.Main);
|
setSettingsScreen(SettingsScreens.Main);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.QuickReaction:
|
||||||
|
case SettingsScreens.CustomEmoji:
|
||||||
|
setSettingsScreen(SettingsScreens.Stickers);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -173,6 +173,10 @@
|
|||||||
vertical-align: -0.125rem;
|
vertical-align: -0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-entity-custom-emoji {
|
||||||
|
--custom-emoji-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-play {
|
.icon-play {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@ -287,7 +287,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
<span className="colon">:</span>
|
<span className="colon">:</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{renderSummary(lang, lastMessage!, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
{renderSummary(lang, lastMessage!, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -369,16 +369,18 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) {
|
function renderSummary(
|
||||||
|
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
||||||
|
) {
|
||||||
if (!blobUrl) {
|
if (!blobUrl) {
|
||||||
return renderMessageSummary(lang, message);
|
return renderMessageSummary(lang, message, undefined, undefined, undefined, observeIntersection);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="media-preview">
|
<span className="media-preview">
|
||||||
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
||||||
{getMessageVideo(message) && <i className="icon-play" />}
|
{getMessageVideo(message) && <i className="icon-play" />}
|
||||||
{renderMessageSummary(lang, message, true)}
|
{renderMessageSummary(lang, message, true, undefined, undefined, observeIntersection)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,6 +27,8 @@ import SettingsTwoFa from './twoFa/SettingsTwoFa';
|
|||||||
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
import SettingsPrivacyVisibilityExceptionList from './SettingsPrivacyVisibilityExceptionList';
|
||||||
import SettingsQuickReaction from './SettingsQuickReaction';
|
import SettingsQuickReaction from './SettingsQuickReaction';
|
||||||
import SettingsPasscode from './passcode/SettingsPasscode';
|
import SettingsPasscode from './passcode/SettingsPasscode';
|
||||||
|
import SettingsStickers from './SettingsStickers';
|
||||||
|
import SettingsCustomEmoji from './SettingsCustomEmoji';
|
||||||
import SettingsExperimental from './SettingsExperimental';
|
import SettingsExperimental from './SettingsExperimental';
|
||||||
|
|
||||||
import './Settings.scss';
|
import './Settings.scss';
|
||||||
@ -216,6 +218,7 @@ const Settings: FC<OwnProps> = ({
|
|||||||
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
|| screen === SettingsScreens.GeneralChatBackgroundColor
|
||||||
|| screen === SettingsScreens.GeneralChatBackground
|
|| screen === SettingsScreens.GeneralChatBackground
|
||||||
|| screen === SettingsScreens.QuickReaction
|
|| screen === SettingsScreens.QuickReaction
|
||||||
|
|| screen === SettingsScreens.CustomEmoji
|
||||||
|| isPrivacyScreen || isFoldersScreen}
|
|| isPrivacyScreen || isFoldersScreen}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
/>
|
/>
|
||||||
@ -224,6 +227,10 @@ const Settings: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SettingsQuickReaction isActive={isScreenActive} onReset={handleReset} />
|
<SettingsQuickReaction isActive={isScreenActive} onReset={handleReset} />
|
||||||
);
|
);
|
||||||
|
case SettingsScreens.CustomEmoji:
|
||||||
|
return (
|
||||||
|
<SettingsCustomEmoji isActive={isScreenActive} onReset={handleReset} />
|
||||||
|
);
|
||||||
case SettingsScreens.Notifications:
|
case SettingsScreens.Notifications:
|
||||||
return (
|
return (
|
||||||
<SettingsNotifications isActive={isScreenActive} onReset={handleReset} />
|
<SettingsNotifications isActive={isScreenActive} onReset={handleReset} />
|
||||||
@ -244,6 +251,10 @@ const Settings: FC<OwnProps> = ({
|
|||||||
return (
|
return (
|
||||||
<SettingsLanguage isActive={isScreenActive} onReset={handleReset} />
|
<SettingsLanguage isActive={isScreenActive} onReset={handleReset} />
|
||||||
);
|
);
|
||||||
|
case SettingsScreens.Stickers:
|
||||||
|
return (
|
||||||
|
<SettingsStickers isActive={isScreenActive} onReset={handleReset} onScreenSelect={onScreenSelect} />
|
||||||
|
);
|
||||||
case SettingsScreens.Experimental:
|
case SettingsScreens.Experimental:
|
||||||
return (
|
return (
|
||||||
<SettingsExperimental isActive={isScreenActive} onReset={handleReset} />
|
<SettingsExperimental isActive={isScreenActive} onReset={handleReset} />
|
||||||
|
|||||||
86
src/components/left/settings/SettingsCustomEmoji.tsx
Normal file
86
src/components/left/settings/SettingsCustomEmoji.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useMemo, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
||||||
|
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import { pick } from '../../../util/iteratees';
|
||||||
|
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import StickerSetCard from '../../common/StickerSetCard';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
customEmojiSetIds?: string[];
|
||||||
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsCustomEmoji: FC<OwnProps & StateProps> = ({
|
||||||
|
isActive,
|
||||||
|
customEmojiSetIds,
|
||||||
|
stickerSetsById,
|
||||||
|
onReset,
|
||||||
|
}) => {
|
||||||
|
const { openStickerSet } = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const stickerSettingsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: stickerSettingsRef });
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive,
|
||||||
|
onBack: onReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleStickerSetClick = useCallback((sticker: ApiSticker) => {
|
||||||
|
openStickerSet({
|
||||||
|
stickerSetInfo: sticker.stickerSetInfo,
|
||||||
|
});
|
||||||
|
}, [openStickerSet]);
|
||||||
|
|
||||||
|
const customEmojiSets = useMemo(() => (
|
||||||
|
customEmojiSetIds && Object.values(pick(stickerSetsById, customEmojiSetIds))
|
||||||
|
), [customEmojiSetIds, stickerSetsById]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content custom-scroll">
|
||||||
|
{customEmojiSets && (
|
||||||
|
<div className="settings-item">
|
||||||
|
<div ref={stickerSettingsRef}>
|
||||||
|
{customEmojiSets.map((stickerSet: ApiStickerSet) => (
|
||||||
|
<StickerSetCard
|
||||||
|
key={stickerSet.id}
|
||||||
|
stickerSet={stickerSet}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
onClick={handleStickerSetClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="settings-item-description mt-3" dir="auto">
|
||||||
|
{renderText(lang('EmojiBotInfo'), ['links'])}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global) => {
|
||||||
|
return {
|
||||||
|
customEmojiSetIds: global.customEmojis.added.setIds,
|
||||||
|
stickerSetsById: global.stickers.setsById,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(SettingsCustomEmoji));
|
||||||
@ -1,12 +1,11 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback, memo, useRef, useState,
|
useCallback, memo,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ISettings, TimeFormat } from '../../../types';
|
import type { ISettings, TimeFormat } from '../../../types';
|
||||||
import { SettingsScreens } from '../../../types';
|
import { SettingsScreens } from '../../../types';
|
||||||
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getSystemTheme, IS_IOS, IS_MAC_OS, IS_TOUCH_ENV,
|
getSystemTheme, IS_IOS, IS_MAC_OS, IS_TOUCH_ENV,
|
||||||
@ -14,18 +13,12 @@ import {
|
|||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import { setTimeFormat } from '../../../util/langProvider';
|
import { setTimeFormat } from '../../../util/langProvider';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import RangeSlider from '../../ui/RangeSlider';
|
import RangeSlider from '../../ui/RangeSlider';
|
||||||
import Checkbox from '../../ui/Checkbox';
|
|
||||||
import type { IRadioOption } from '../../ui/RadioGroup';
|
import type { IRadioOption } from '../../ui/RadioGroup';
|
||||||
import RadioGroup from '../../ui/RadioGroup';
|
import RadioGroup from '../../ui/RadioGroup';
|
||||||
import SettingsStickerSet from './SettingsStickerSet';
|
|
||||||
import StickerSetModal from '../../common/StickerSetModal.async';
|
|
||||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
|
||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||||
|
|
||||||
@ -40,13 +33,8 @@ type StateProps =
|
|||||||
'messageTextSize' |
|
'messageTextSize' |
|
||||||
'animationLevel' |
|
'animationLevel' |
|
||||||
'messageSendKeyCombo' |
|
'messageSendKeyCombo' |
|
||||||
'shouldSuggestStickers' |
|
|
||||||
'shouldLoopStickers' |
|
|
||||||
'timeFormat'
|
'timeFormat'
|
||||||
)> & {
|
)> & {
|
||||||
stickerSetIds?: string[];
|
|
||||||
stickerSetsById?: Record<string, ApiStickerSet>;
|
|
||||||
defaultReaction?: string;
|
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
shouldUseSystemTheme: boolean;
|
shouldUseSystemTheme: boolean;
|
||||||
};
|
};
|
||||||
@ -69,14 +57,9 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
isActive,
|
isActive,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onReset,
|
onReset,
|
||||||
stickerSetIds,
|
|
||||||
stickerSetsById,
|
|
||||||
defaultReaction,
|
|
||||||
messageTextSize,
|
messageTextSize,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
messageSendKeyCombo,
|
messageSendKeyCombo,
|
||||||
shouldSuggestStickers,
|
|
||||||
shouldLoopStickers,
|
|
||||||
timeFormat,
|
timeFormat,
|
||||||
theme,
|
theme,
|
||||||
shouldUseSystemTheme,
|
shouldUseSystemTheme,
|
||||||
@ -85,12 +68,6 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
setSettingOption,
|
setSettingOption,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const stickerSettingsRef = useRef<HTMLDivElement>(null);
|
|
||||||
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: stickerSettingsRef });
|
|
||||||
const [isModalOpen, openModal, closeModal] = useFlag();
|
|
||||||
const [sticker, setSticker] = useState<ApiSticker>();
|
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const APPEARANCE_THEME_OPTIONS: IRadioOption[] = [{
|
const APPEARANCE_THEME_OPTIONS: IRadioOption[] = [{
|
||||||
@ -149,27 +126,10 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
setTimeFormat(newTimeFormat as TimeFormat);
|
setTimeFormat(newTimeFormat as TimeFormat);
|
||||||
}, [setSettingOption]);
|
}, [setSettingOption]);
|
||||||
|
|
||||||
const handleStickerSetClick = useCallback((value: ApiSticker) => {
|
|
||||||
setSticker(value);
|
|
||||||
openModal();
|
|
||||||
}, [openModal]);
|
|
||||||
|
|
||||||
const handleMessageSendComboChange = useCallback((newCombo: string) => {
|
const handleMessageSendComboChange = useCallback((newCombo: string) => {
|
||||||
setSettingOption({ messageSendKeyCombo: newCombo });
|
setSettingOption({ messageSendKeyCombo: newCombo });
|
||||||
}, [setSettingOption]);
|
}, [setSettingOption]);
|
||||||
|
|
||||||
const handleSuggestStickersChange = useCallback((newValue: boolean) => {
|
|
||||||
setSettingOption({ shouldSuggestStickers: newValue });
|
|
||||||
}, [setSettingOption]);
|
|
||||||
|
|
||||||
const handleShouldLoopStickersChange = useCallback((newValue: boolean) => {
|
|
||||||
setSettingOption({ shouldLoopStickers: newValue });
|
|
||||||
}, [setSettingOption]);
|
|
||||||
|
|
||||||
const stickerSets = stickerSetIds && stickerSetIds.map((id: string) => {
|
|
||||||
return stickerSetsById?.[id]?.installedDate ? stickerSetsById[id] : false;
|
|
||||||
}).filter<ApiStickerSet>(Boolean as any);
|
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
onBack: onReset,
|
onBack: onReset,
|
||||||
@ -248,50 +208,6 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="settings-item">
|
|
||||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('AccDescrStickers')}</h4>
|
|
||||||
|
|
||||||
{defaultReaction && (
|
|
||||||
<ListItem
|
|
||||||
className="SettingsDefaultReaction"
|
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
|
||||||
onClick={() => onScreenSelect(SettingsScreens.QuickReaction)}
|
|
||||||
>
|
|
||||||
<ReactionStaticEmoji reaction={defaultReaction} />
|
|
||||||
<div className="title">{lang('DoubleTapSetting')}</div>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label={lang('SuggestStickers')}
|
|
||||||
checked={shouldSuggestStickers}
|
|
||||||
onCheck={handleSuggestStickersChange}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label={lang('LoopAnimatedStickers')}
|
|
||||||
checked={shouldLoopStickers}
|
|
||||||
onCheck={handleShouldLoopStickersChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-4" ref={stickerSettingsRef}>
|
|
||||||
{stickerSets && stickerSets.map((stickerSet: ApiStickerSet) => (
|
|
||||||
<SettingsStickerSet
|
|
||||||
key={stickerSet.id}
|
|
||||||
stickerSet={stickerSet}
|
|
||||||
observeIntersection={observeIntersectionForCovers}
|
|
||||||
onClick={handleStickerSetClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{sticker && (
|
|
||||||
<StickerSetModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
fromSticker={sticker}
|
|
||||||
onClose={closeModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -305,15 +221,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'messageTextSize',
|
'messageTextSize',
|
||||||
'animationLevel',
|
'animationLevel',
|
||||||
'messageSendKeyCombo',
|
'messageSendKeyCombo',
|
||||||
'shouldSuggestStickers',
|
|
||||||
'shouldLoopStickers',
|
|
||||||
'isSensitiveEnabled',
|
'isSensitiveEnabled',
|
||||||
'canChangeSensitive',
|
'canChangeSensitive',
|
||||||
'timeFormat',
|
'timeFormat',
|
||||||
]),
|
]),
|
||||||
stickerSetIds: global.stickers.added.setIds,
|
|
||||||
stickerSetsById: global.stickers.setsById,
|
|
||||||
defaultReaction: global.appConfig?.defaultReaction,
|
|
||||||
theme,
|
theme,
|
||||||
shouldUseSystemTheme,
|
shouldUseSystemTheme,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -86,6 +86,8 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
return <h3>{lang('General')}</h3>;
|
return <h3>{lang('General')}</h3>;
|
||||||
case SettingsScreens.QuickReaction:
|
case SettingsScreens.QuickReaction:
|
||||||
return <h3>{lang('DoubleTapSetting')}</h3>;
|
return <h3>{lang('DoubleTapSetting')}</h3>;
|
||||||
|
case SettingsScreens.CustomEmoji:
|
||||||
|
return <h3>{lang('Emoji')}</h3>;
|
||||||
case SettingsScreens.Notifications:
|
case SettingsScreens.Notifications:
|
||||||
return <h3>{lang('Notifications')}</h3>;
|
return <h3>{lang('Notifications')}</h3>;
|
||||||
case SettingsScreens.DataStorage:
|
case SettingsScreens.DataStorage:
|
||||||
@ -94,6 +96,8 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
return <h3>{lang('PrivacySettings')}</h3>;
|
return <h3>{lang('PrivacySettings')}</h3>;
|
||||||
case SettingsScreens.Language:
|
case SettingsScreens.Language:
|
||||||
return <h3>{lang('Language')}</h3>;
|
return <h3>{lang('Language')}</h3>;
|
||||||
|
case SettingsScreens.Stickers:
|
||||||
|
return <h3>{lang('StickersName')}</h3>;
|
||||||
case SettingsScreens.Experimental:
|
case SettingsScreens.Experimental:
|
||||||
return <h3>{lang('lng_settings_experimental')}</h3>;
|
return <h3>{lang('lng_settings_experimental')}</h3>;
|
||||||
|
|
||||||
|
|||||||
@ -128,6 +128,13 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
{lang('Language')}
|
{lang('Language')}
|
||||||
<span className="settings-item__current-value">{lang.langName}</span>
|
<span className="settings-item__current-value">{lang.langName}</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
<ListItem
|
||||||
|
icon="stickers"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onScreenSelect(SettingsScreens.Stickers)}
|
||||||
|
>
|
||||||
|
{lang('StickersName')}
|
||||||
|
</ListItem>
|
||||||
{canBuyPremium && (
|
{canBuyPremium && (
|
||||||
<ListItem
|
<ListItem
|
||||||
leftElement={<PremiumIcon withGradient big />}
|
leftElement={<PremiumIcon withGradient big />}
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, { memo } from '../../../lib/teact/teact';
|
|
||||||
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
|
||||||
|
|
||||||
import { STICKER_SIZE_GENERAL_SETTINGS } from '../../../config';
|
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
|
||||||
import useLang from '../../../hooks/useLang';
|
|
||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
|
||||||
import Button from '../../ui/Button';
|
|
||||||
import StickerSetCoverAnimated from '../../middle/composer/StickerSetCoverAnimated';
|
|
||||||
import StickerSetCover from '../../middle/composer/StickerSetCover';
|
|
||||||
import StickerButton from '../../common/StickerButton';
|
|
||||||
|
|
||||||
import './SettingsStickerSet.scss';
|
|
||||||
|
|
||||||
type OwnProps = {
|
|
||||||
stickerSet?: ApiStickerSet;
|
|
||||||
observeIntersection: ObserveFn;
|
|
||||||
onClick: (value: ApiSticker) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SettingsStickerSet: FC<OwnProps> = ({
|
|
||||||
stickerSet,
|
|
||||||
observeIntersection,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const lang = useLang();
|
|
||||||
|
|
||||||
if (!stickerSet || !stickerSet.stickers) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstSticker = stickerSet.stickers?.[0];
|
|
||||||
|
|
||||||
if (stickerSet.hasThumbnail || !firstSticker) {
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
narrow
|
|
||||||
className="SettingsStickerSet"
|
|
||||||
inactive={!firstSticker}
|
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
|
||||||
onClick={() => firstSticker && onClick(firstSticker)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
ariaLabel={stickerSet.title}
|
|
||||||
color="translucent"
|
|
||||||
isRtl={lang.isRtl}
|
|
||||||
>
|
|
||||||
{stickerSet.isLottie ? (
|
|
||||||
<StickerSetCoverAnimated
|
|
||||||
size={STICKER_SIZE_GENERAL_SETTINGS}
|
|
||||||
stickerSet={stickerSet}
|
|
||||||
observeIntersection={observeIntersection}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<StickerSetCover
|
|
||||||
stickerSet={stickerSet}
|
|
||||||
observeIntersection={observeIntersection}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<div className="multiline-menu-item">
|
|
||||||
<div className="title">{stickerSet.title}</div>
|
|
||||||
<div className="subtitle">{lang('StickerPack.StickerCount', stickerSet.count, 'i')}</div>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
narrow
|
|
||||||
className="SettingsStickerSet"
|
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
|
||||||
onClick={() => onClick(firstSticker)}
|
|
||||||
>
|
|
||||||
<StickerButton
|
|
||||||
sticker={firstSticker}
|
|
||||||
size={STICKER_SIZE_GENERAL_SETTINGS}
|
|
||||||
title={stickerSet.title}
|
|
||||||
observeIntersection={observeIntersection}
|
|
||||||
clickArg={undefined}
|
|
||||||
noContextMenu
|
|
||||||
isCurrentUserPremium
|
|
||||||
/>
|
|
||||||
<div className="multiline-menu-item">
|
|
||||||
<div className="title">{stickerSet.title}</div>
|
|
||||||
<div className="subtitle">{lang('StickerPack.StickerCount', stickerSet.count, 'i')}</div>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(SettingsStickerSet);
|
|
||||||
154
src/components/left/settings/SettingsStickers.tsx
Normal file
154
src/components/left/settings/SettingsStickers.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useMemo, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import { SettingsScreens } from '../../../types';
|
||||||
|
import type { ISettings } from '../../../types';
|
||||||
|
import type { ApiSticker, ApiStickerSet } from '../../../api/types';
|
||||||
|
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import { pick } from '../../../util/iteratees';
|
||||||
|
|
||||||
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
|
import Checkbox from '../../ui/Checkbox';
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import StickerSetCard from '../../common/StickerSetCard';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
onReset: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps =
|
||||||
|
Pick<ISettings, (
|
||||||
|
'shouldSuggestStickers' |
|
||||||
|
'shouldLoopStickers'
|
||||||
|
)> & {
|
||||||
|
addedSetIds?: string[];
|
||||||
|
customEmojiSetIds?: string[];
|
||||||
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
|
defaultReaction?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SettingsStickers: FC<OwnProps & StateProps> = ({
|
||||||
|
isActive,
|
||||||
|
addedSetIds,
|
||||||
|
customEmojiSetIds,
|
||||||
|
stickerSetsById,
|
||||||
|
defaultReaction,
|
||||||
|
shouldSuggestStickers,
|
||||||
|
shouldLoopStickers,
|
||||||
|
onReset,
|
||||||
|
onScreenSelect,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
setSettingOption,
|
||||||
|
openStickerSet,
|
||||||
|
} = getActions();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const stickerSettingsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: stickerSettingsRef });
|
||||||
|
|
||||||
|
const handleStickerSetClick = useCallback((sticker: ApiSticker) => {
|
||||||
|
openStickerSet({
|
||||||
|
stickerSetInfo: sticker.stickerSetInfo,
|
||||||
|
});
|
||||||
|
}, [openStickerSet]);
|
||||||
|
|
||||||
|
const handleSuggestStickersChange = useCallback((newValue: boolean) => {
|
||||||
|
setSettingOption({ shouldSuggestStickers: newValue });
|
||||||
|
}, [setSettingOption]);
|
||||||
|
|
||||||
|
const handleShouldLoopStickersChange = useCallback((newValue: boolean) => {
|
||||||
|
setSettingOption({ shouldLoopStickers: newValue });
|
||||||
|
}, [setSettingOption]);
|
||||||
|
|
||||||
|
const stickerSets = useMemo(() => (
|
||||||
|
addedSetIds && Object.values(pick(stickerSetsById, addedSetIds))
|
||||||
|
), [addedSetIds, stickerSetsById]);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive,
|
||||||
|
onBack: onReset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="settings-content custom-scroll">
|
||||||
|
<div className="settings-item">
|
||||||
|
<Checkbox
|
||||||
|
label={lang('SuggestStickers')}
|
||||||
|
checked={shouldSuggestStickers}
|
||||||
|
onCheck={handleSuggestStickersChange}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={lang('LoopAnimatedStickers')}
|
||||||
|
checked={shouldLoopStickers}
|
||||||
|
onCheck={handleShouldLoopStickersChange}
|
||||||
|
/>
|
||||||
|
<ListItem
|
||||||
|
className="mt-4"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onScreenSelect(SettingsScreens.CustomEmoji)}
|
||||||
|
icon="smile"
|
||||||
|
>
|
||||||
|
{lang('StickersList.EmojiItem')}
|
||||||
|
{customEmojiSetIds && <span className="settings-item__current-value">{customEmojiSetIds.length}</span>}
|
||||||
|
</ListItem>
|
||||||
|
{defaultReaction && (
|
||||||
|
<ListItem
|
||||||
|
className="SettingsDefaultReaction"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => onScreenSelect(SettingsScreens.QuickReaction)}
|
||||||
|
>
|
||||||
|
<ReactionStaticEmoji reaction={defaultReaction} />
|
||||||
|
<div className="title">{lang('DoubleTapSetting')}</div>
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{stickerSets && (
|
||||||
|
<div className="settings-item">
|
||||||
|
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
{lang('ChooseStickerMyStickerSets')}
|
||||||
|
</h4>
|
||||||
|
<div ref={stickerSettingsRef}>
|
||||||
|
{stickerSets.map((stickerSet: ApiStickerSet) => (
|
||||||
|
<StickerSetCard
|
||||||
|
key={stickerSet.id}
|
||||||
|
stickerSet={stickerSet}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
onClick={handleStickerSetClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="settings-item-description mt-3" dir="auto">
|
||||||
|
{renderText(lang('StickersBotInfo'), ['links'])}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
return {
|
||||||
|
...pick(global.settings.byKey, [
|
||||||
|
'shouldSuggestStickers',
|
||||||
|
'shouldLoopStickers',
|
||||||
|
]),
|
||||||
|
addedSetIds: global.stickers.added.setIds,
|
||||||
|
customEmojiSetIds: global.customEmojis.added.setIds,
|
||||||
|
stickerSetsById: global.stickers.setsById,
|
||||||
|
defaultReaction: global.appConfig?.defaultReaction,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(SettingsStickers));
|
||||||
@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
|||||||
import React, {
|
import React, {
|
||||||
useEffect, memo, useCallback, useState, useRef,
|
useEffect, memo, useCallback, useState, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { LangCode } from '../../types';
|
import type { LangCode } from '../../types';
|
||||||
import type {
|
import type {
|
||||||
@ -23,12 +23,14 @@ import {
|
|||||||
selectIsServiceChatReady,
|
selectIsServiceChatReady,
|
||||||
selectUser,
|
selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||||
import { processDeepLink } from '../../util/deeplink';
|
import { processDeepLink } from '../../util/deeplink';
|
||||||
import windowSize from '../../util/windowSize';
|
import windowSize from '../../util/windowSize';
|
||||||
import { getAllNotificationsCount } from '../../util/folderManager';
|
import { getAllNotificationsCount } from '../../util/folderManager';
|
||||||
|
import { fastRaf } from '../../util/schedulers';
|
||||||
|
|
||||||
|
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||||
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
||||||
import useBeforeUnload from '../../hooks/useBeforeUnload';
|
import useBeforeUnload from '../../hooks/useBeforeUnload';
|
||||||
import useOnChange from '../../hooks/useOnChange';
|
import useOnChange from '../../hooks/useOnChange';
|
||||||
@ -36,7 +38,7 @@ import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture';
|
|||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
import { LOCATION_HASH } from '../../hooks/useHistoryBack';
|
import { LOCATION_HASH } from '../../hooks/useHistoryBack';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
import { fastRaf } from '../../util/schedulers';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
|
|
||||||
import StickerSetModal from '../common/StickerSetModal.async';
|
import StickerSetModal from '../common/StickerSetModal.async';
|
||||||
import UnreadCount from '../common/UnreadCounter';
|
import UnreadCount from '../common/UnreadCounter';
|
||||||
@ -68,6 +70,7 @@ import PaymentModal from '../payment/PaymentModal.async';
|
|||||||
import ReceiptModal from '../payment/ReceiptModal.async';
|
import ReceiptModal from '../payment/ReceiptModal.async';
|
||||||
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
||||||
import DeleteFolderDialog from './DeleteFolderDialog.async';
|
import DeleteFolderDialog from './DeleteFolderDialog.async';
|
||||||
|
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -87,6 +90,7 @@ type StateProps = {
|
|||||||
isHistoryCalendarOpen: boolean;
|
isHistoryCalendarOpen: boolean;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
openedStickerSetShortName?: string;
|
openedStickerSetShortName?: string;
|
||||||
|
openedCustomEmojiSetIds?: string[];
|
||||||
activeGroupCallId?: string;
|
activeGroupCallId?: string;
|
||||||
isServiceChatReady?: boolean;
|
isServiceChatReady?: boolean;
|
||||||
animationLevel: number;
|
animationLevel: number;
|
||||||
@ -94,6 +98,7 @@ type StateProps = {
|
|||||||
wasTimeFormatSetManually?: boolean;
|
wasTimeFormatSetManually?: boolean;
|
||||||
isPhoneCallActive?: boolean;
|
isPhoneCallActive?: boolean;
|
||||||
addedSetIds?: string[];
|
addedSetIds?: string[];
|
||||||
|
addedCustomEmojiIds?: string[];
|
||||||
newContactUserId?: string;
|
newContactUserId?: string;
|
||||||
newContactByPhoneNumber?: boolean;
|
newContactByPhoneNumber?: boolean;
|
||||||
openedGame?: GlobalState['openedGame'];
|
openedGame?: GlobalState['openedGame'];
|
||||||
@ -136,11 +141,13 @@ const Main: FC<StateProps> = ({
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
limitReached,
|
limitReached,
|
||||||
openedStickerSetShortName,
|
openedStickerSetShortName,
|
||||||
|
openedCustomEmojiSetIds,
|
||||||
isServiceChatReady,
|
isServiceChatReady,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
language,
|
language,
|
||||||
wasTimeFormatSetManually,
|
wasTimeFormatSetManually,
|
||||||
addedSetIds,
|
addedSetIds,
|
||||||
|
addedCustomEmojiIds,
|
||||||
isPhoneCallActive,
|
isPhoneCallActive,
|
||||||
newContactUserId,
|
newContactUserId,
|
||||||
newContactByPhoneNumber,
|
newContactByPhoneNumber,
|
||||||
@ -173,11 +180,13 @@ const Main: FC<StateProps> = ({
|
|||||||
loadAddedStickers,
|
loadAddedStickers,
|
||||||
loadFavoriteStickers,
|
loadFavoriteStickers,
|
||||||
ensureTimeFormat,
|
ensureTimeFormat,
|
||||||
openStickerSetShortName,
|
closeStickerSetModal,
|
||||||
|
closeCustomEmojiSets,
|
||||||
checkVersionNotification,
|
checkVersionNotification,
|
||||||
loadAppConfig,
|
loadAppConfig,
|
||||||
loadAttachMenuBots,
|
loadAttachMenuBots,
|
||||||
loadContactList,
|
loadContactList,
|
||||||
|
loadCustomEmojis,
|
||||||
closePaymentModal,
|
closePaymentModal,
|
||||||
clearReceipt,
|
clearReceipt,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
@ -226,17 +235,29 @@ const Main: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [language, lastSyncTime, loadCountryList, loadEmojiKeywords]);
|
}, [language, lastSyncTime, loadCountryList, loadEmojiKeywords]);
|
||||||
|
|
||||||
|
// Re-fetch cached saved emoji for `localDb`
|
||||||
|
useEffectWithPrevDeps(([prevLastSyncTime]) => {
|
||||||
|
if (!prevLastSyncTime && lastSyncTime) {
|
||||||
|
loadCustomEmojis({
|
||||||
|
ids: Object.keys(getGlobal().customEmojis.byId),
|
||||||
|
ignoreCache: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [lastSyncTime] as const);
|
||||||
|
|
||||||
// Sticker sets
|
// Sticker sets
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastSyncTime) {
|
if (lastSyncTime) {
|
||||||
if (!addedSetIds) {
|
if (!addedSetIds || !addedCustomEmojiIds) {
|
||||||
loadStickerSets();
|
loadStickerSets();
|
||||||
loadFavoriteStickers();
|
loadFavoriteStickers();
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (addedSetIds && addedCustomEmojiIds) {
|
||||||
loadAddedStickers();
|
loadAddedStickers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers]);
|
}, [lastSyncTime, addedSetIds, loadStickerSets, loadFavoriteStickers, loadAddedStickers, addedCustomEmojiIds]);
|
||||||
|
|
||||||
// Check version when service chat is ready
|
// Check version when service chat is ready
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -378,8 +399,12 @@ const Main: FC<StateProps> = ({
|
|||||||
}, [updateIsOnline]);
|
}, [updateIsOnline]);
|
||||||
|
|
||||||
const handleStickerSetModalClose = useCallback(() => {
|
const handleStickerSetModalClose = useCallback(() => {
|
||||||
openStickerSetShortName({ stickerSetShortName: undefined });
|
closeStickerSetModal();
|
||||||
}, [openStickerSetShortName]);
|
}, [closeStickerSetModal]);
|
||||||
|
|
||||||
|
const handleCustomEmojiSetsModalClose = useCallback(() => {
|
||||||
|
closeCustomEmojiSets();
|
||||||
|
}, [closeCustomEmojiSets]);
|
||||||
|
|
||||||
// Online status and browser tab indicators
|
// Online status and browser tab indicators
|
||||||
useBackgroundMode(handleBlur, handleFocus);
|
useBackgroundMode(handleBlur, handleFocus);
|
||||||
@ -404,6 +429,10 @@ const Main: FC<StateProps> = ({
|
|||||||
onClose={handleStickerSetModalClose}
|
onClose={handleStickerSetModalClose}
|
||||||
stickerSetShortName={openedStickerSetShortName}
|
stickerSetShortName={openedStickerSetShortName}
|
||||||
/>
|
/>
|
||||||
|
<CustomEmojiSetsModal
|
||||||
|
customEmojiSetIds={openedCustomEmojiSetIds}
|
||||||
|
onClose={handleCustomEmojiSetsModalClose}
|
||||||
|
/>
|
||||||
{activeGroupCallId && <GroupCall groupCallId={activeGroupCallId} />}
|
{activeGroupCallId && <GroupCall groupCallId={activeGroupCallId} />}
|
||||||
<ActiveCallHeader isActive={Boolean(activeGroupCallId || isPhoneCallActive)} />
|
<ActiveCallHeader isActive={Boolean(activeGroupCallId || isPhoneCallActive)} />
|
||||||
<NewContactModal
|
<NewContactModal
|
||||||
@ -486,6 +515,7 @@ export default memo(withGlobal(
|
|||||||
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
|
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
|
||||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||||
openedStickerSetShortName: global.openedStickerSetShortName,
|
openedStickerSetShortName: global.openedStickerSetShortName,
|
||||||
|
openedCustomEmojiSetIds: global.openedCustomEmojiSetIds,
|
||||||
isServiceChatReady: selectIsServiceChatReady(global),
|
isServiceChatReady: selectIsServiceChatReady(global),
|
||||||
activeGroupCallId: global.groupCalls.activeGroupCallId,
|
activeGroupCallId: global.groupCalls.activeGroupCallId,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
@ -493,6 +523,7 @@ export default memo(withGlobal(
|
|||||||
wasTimeFormatSetManually,
|
wasTimeFormatSetManually,
|
||||||
isPhoneCallActive: Boolean(global.phoneCall),
|
isPhoneCallActive: Boolean(global.phoneCall),
|
||||||
addedSetIds: global.stickers.added.setIds,
|
addedSetIds: global.stickers.added.setIds,
|
||||||
|
addedCustomEmojiIds: global.customEmojis.added.setIds,
|
||||||
newContactUserId: global.newContact?.userId,
|
newContactUserId: global.newContact?.userId,
|
||||||
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
|
newContactByPhoneNumber: global.newContact?.isByPhoneNumber,
|
||||||
openedGame,
|
openedGame,
|
||||||
|
|||||||
@ -8,10 +8,6 @@
|
|||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-premium {
|
|
||||||
background: var(--premium-gradient);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-content {
|
.button-content {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -309,8 +309,9 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
|||||||
onSelectSlide={handleSelectSlide}
|
onSelectSlide={handleSelectSlide}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={buildClassName(styles.button, !isPremium && styles.buttonPremium)}
|
className={buildClassName(styles.button)}
|
||||||
isShiny={!isPremium}
|
isShiny={!isPremium}
|
||||||
|
withPremiumGradient={!isPremium}
|
||||||
onClick={isPremium ? onBack : handleClick}
|
onClick={isPremium ? onBack : handleClick}
|
||||||
>
|
>
|
||||||
{isPremium
|
{isPremium
|
||||||
|
|||||||
@ -25,7 +25,6 @@
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: var(--premium-gradient);
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -283,7 +283,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
|||||||
{!isPremium && (
|
{!isPremium && (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
{/* eslint-disable-next-line react/jsx-no-bind */}
|
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||||
<Button className={styles.button} isShiny onClick={handleClick}>
|
<Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}>
|
||||||
{lang('SubscribeToPremium', formatCurrency(Number(promo.monthlyAmount), promo.currency, lang.code))}
|
{lang('SubscribeToPremium', formatCurrency(Number(promo.monthlyAmount), promo.currency, lang.code))}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -67,12 +67,16 @@
|
|||||||
max-height: 2.75rem;
|
max-height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji:not(.text-entity-custom-emoji) {
|
||||||
width: 0.9375rem;
|
width: 0.9375rem;
|
||||||
height: 0.9375rem;
|
height: 0.9375rem;
|
||||||
vertical-align: -2px;
|
vertical-align: -2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-entity-custom-emoji {
|
||||||
|
--custom-emoji-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
&.multiline {
|
&.multiline {
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import buildClassName from '../../util/buildClassName';
|
|||||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
|
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useWebpThumbnail from '../../hooks/useWebpThumbnail';
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { clickBotInlineButton } = getActions();
|
const { clickBotInlineButton } = getActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const mediaThumbnail = useWebpThumbnail(message);
|
const mediaThumbnail = useThumbnail(message);
|
||||||
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
|
const mediaBlobUrl = useMedia(getMessageMediaHash(message, 'pictogram'));
|
||||||
|
|
||||||
const text = renderMessageSummary(lang, message, Boolean(mediaThumbnail));
|
const text = renderMessageSummary(lang, message, Boolean(mediaThumbnail));
|
||||||
|
|||||||
@ -132,7 +132,7 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
&& isActionMessage(senderGroup[0])
|
&& isActionMessage(senderGroup[0])
|
||||||
&& !senderGroup[0].content.action?.phoneCall
|
&& !senderGroup[0].content.action?.phoneCall
|
||||||
) {
|
) {
|
||||||
const message = senderGroup[0];
|
const message = senderGroup[0]!;
|
||||||
const isLastInList = (
|
const isLastInList = (
|
||||||
senderGroupIndex === senderGroupsArray.length - 1
|
senderGroupIndex === senderGroupsArray.length - 1
|
||||||
&& dateGroupIndex === dateGroupsArray.length - 1
|
&& dateGroupIndex === dateGroupsArray.length - 1
|
||||||
|
|||||||
@ -520,6 +520,10 @@
|
|||||||
body.is-ios & {
|
body.is-ios & {
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-entity-custom-emoji {
|
||||||
|
--custom-emoji-size: 1.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiAttachment, ApiChatMember } from '../../../api/types';
|
import type { ApiAttachment, ApiChatMember } from '../../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -48,6 +48,7 @@ export type OwnProps = {
|
|||||||
baseEmojiKeywords?: Record<string, string[]>;
|
baseEmojiKeywords?: Record<string, string[]>;
|
||||||
emojiKeywords?: Record<string, string[]>;
|
emojiKeywords?: Record<string, string[]>;
|
||||||
shouldSchedule?: boolean;
|
shouldSchedule?: boolean;
|
||||||
|
captionLimit: number;
|
||||||
addRecentEmoji: AnyToVoidFunction;
|
addRecentEmoji: AnyToVoidFunction;
|
||||||
onCaptionUpdate: (html: string) => void;
|
onCaptionUpdate: (html: string) => void;
|
||||||
onSend: () => void;
|
onSend: () => void;
|
||||||
@ -55,7 +56,6 @@ export type OwnProps = {
|
|||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
onSendSilent: () => void;
|
onSendSilent: () => void;
|
||||||
onSendScheduled: () => void;
|
onSendScheduled: () => void;
|
||||||
captionLimit: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DROP_LEAVE_TIMEOUT_MS = 150;
|
const DROP_LEAVE_TIMEOUT_MS = 150;
|
||||||
@ -105,6 +105,7 @@ const AttachmentModal: FC<OwnProps> = ({
|
|||||||
undefined,
|
undefined,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
|
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
|
||||||
} = useEmojiTooltip(
|
} = useEmojiTooltip(
|
||||||
|
|||||||
@ -68,7 +68,7 @@ import focusEditableElement from '../../../util/focusEditableElement';
|
|||||||
import parseMessageInput from '../../../util/parseMessageInput';
|
import parseMessageInput from '../../../util/parseMessageInput';
|
||||||
import buildAttachment from './helpers/buildAttachment';
|
import buildAttachment from './helpers/buildAttachment';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
import insertHtmlInSelection from '../../../util/insertHtmlInSelection';
|
import { insertHtmlInSelection } from '../../../util/selection';
|
||||||
import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection';
|
import deleteLastCharacterOutsideSelection from '../../../util/deleteLastCharacterOutsideSelection';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import windowSize from '../../../util/windowSize';
|
import windowSize from '../../../util/windowSize';
|
||||||
@ -448,7 +448,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
!isReady,
|
!isReady,
|
||||||
);
|
);
|
||||||
|
|
||||||
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
const insertHtmlAndUpdateCursor = useCallback((newHtml: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||||
const selection = window.getSelection()!;
|
const selection = window.getSelection()!;
|
||||||
let messageInput: HTMLDivElement;
|
let messageInput: HTMLDivElement;
|
||||||
if (inputId === EDITABLE_INPUT_ID) {
|
if (inputId === EDITABLE_INPUT_ID) {
|
||||||
@ -456,9 +456,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
messageInput = document.getElementById(inputId) as HTMLDivElement;
|
messageInput = document.getElementById(inputId) as HTMLDivElement;
|
||||||
}
|
}
|
||||||
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
|
||||||
.join('')
|
|
||||||
.replace(/\u200b+/g, '\u200b');
|
|
||||||
|
|
||||||
if (selection.rangeCount) {
|
if (selection.rangeCount) {
|
||||||
const selectionRange = selection.getRangeAt(0);
|
const selectionRange = selection.getRangeAt(0);
|
||||||
@ -477,6 +474,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
}, [htmlRef]);
|
}, [htmlRef]);
|
||||||
|
|
||||||
|
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||||
|
const newHtml = renderText(text, ['escape_html', 'emoji_html', 'br_html'])
|
||||||
|
.join('')
|
||||||
|
.replace(/\u200b+/g, '\u200b');
|
||||||
|
insertHtmlAndUpdateCursor(newHtml, inputId);
|
||||||
|
}, [insertHtmlAndUpdateCursor]);
|
||||||
|
|
||||||
const removeSymbol = useCallback(() => {
|
const removeSymbol = useCallback(() => {
|
||||||
const selection = window.getSelection()!;
|
const selection = window.getSelection()!;
|
||||||
|
|
||||||
@ -1094,8 +1098,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
isDisabled={Boolean(activeVoiceRecording)}
|
isDisabled={Boolean(activeVoiceRecording)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isChatWithBot && isBotMenuButtonCommands && botCommands !== false && !activeVoiceRecording
|
{(isChatWithBot && isBotMenuButtonCommands
|
||||||
&& !editingMessage && (
|
&& botCommands !== false && !activeVoiceRecording && !editingMessage) && (
|
||||||
<ResponsiveHoverButton
|
<ResponsiveHoverButton
|
||||||
className={buildClassName('bot-commands', isBotCommandMenuOpen && 'activated')}
|
className={buildClassName('bot-commands', isBotCommandMenuOpen && 'activated')}
|
||||||
round
|
round
|
||||||
@ -1318,8 +1322,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const requestedText = selectRequestedText(global, chatId);
|
const requestedText = selectRequestedText(global, chatId);
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
const isForCurrentMessageList = chatId === currentMessageList?.chatId
|
const isForCurrentMessageList = chatId === currentMessageList?.chatId
|
||||||
&& threadId === currentMessageList?.threadId
|
&& threadId === currentMessageList?.threadId
|
||||||
&& messageListType === currentMessageList?.type;
|
&& messageListType === currentMessageList?.type;
|
||||||
const user = selectUser(global, chatId);
|
const user = selectUser(global, chatId);
|
||||||
const canSendVoiceByPrivacy = (user && !user.fullInfo?.noVoiceMessages) ?? true;
|
const canSendVoiceByPrivacy = (user && !user.fullInfo?.noVoiceMessages) ?? true;
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
||||||
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
@ -18,6 +19,7 @@ import {
|
|||||||
selectEditingScheduledId,
|
selectEditingScheduledId,
|
||||||
selectEditingMessage,
|
selectEditingMessage,
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
|
selectIsCurrentUserPremium,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
@ -47,6 +49,7 @@ type StateProps = {
|
|||||||
noAuthors?: boolean;
|
noAuthors?: boolean;
|
||||||
noCaptions?: boolean;
|
noCaptions?: boolean;
|
||||||
forwardsHaveCaptions?: boolean;
|
forwardsHaveCaptions?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -65,6 +68,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
noAuthors,
|
noAuthors,
|
||||||
noCaptions,
|
noCaptions,
|
||||||
forwardsHaveCaptions,
|
forwardsHaveCaptions,
|
||||||
|
isCurrentUserPremium,
|
||||||
onClear,
|
onClear,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -159,6 +163,23 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
? lang('ForwardedMessageCount', forwardedMessagesCount)
|
? lang('ForwardedMessageCount', forwardedMessagesCount)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const strippedMessage = useMemo(() => {
|
||||||
|
const textEntities = message?.content.text?.entities;
|
||||||
|
if (!message || !isForwarding || !textEntities?.length || !noAuthors || isCurrentUserPremium) return message;
|
||||||
|
|
||||||
|
const filteredEntities = textEntities.filter((entity) => entity.type !== ApiMessageEntityTypes.CustomEmoji);
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
content: {
|
||||||
|
...message.content,
|
||||||
|
text: {
|
||||||
|
text: message.content.text!.text,
|
||||||
|
entities: filteredEntities,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [isCurrentUserPremium, isForwarding, message, noAuthors]);
|
||||||
|
|
||||||
if (!shouldRender) {
|
if (!shouldRender) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -171,7 +192,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<EmbeddedMessage
|
<EmbeddedMessage
|
||||||
className="inside-input"
|
className="inside-input"
|
||||||
message={message}
|
message={strippedMessage}
|
||||||
sender={!noAuthors ? sender : undefined}
|
sender={!noAuthors ? sender : undefined}
|
||||||
customText={customText}
|
customText={customText}
|
||||||
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
|
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
|
||||||
@ -315,6 +336,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
noAuthors,
|
noAuthors,
|
||||||
noCaptions,
|
noCaptions,
|
||||||
forwardsHaveCaptions,
|
forwardsHaveCaptions,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ComposerEmbeddedMessage));
|
)(ComposerEmbeddedMessage));
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
margin: 0.125rem;
|
margin: 0.3125rem;
|
||||||
border-radius: var(--border-radius-messages-small);
|
border-radius: var(--border-radius-messages-small);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
@ -13,6 +13,10 @@
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
transition: background-color 0.15s ease;
|
transition: background-color 0.15s ease;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
margin: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mac-os-fix & {
|
.mac-os-fix & {
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,8 +13,8 @@ import useLang from '../../../hooks/useLang';
|
|||||||
|
|
||||||
import EmojiButton from './EmojiButton';
|
import EmojiButton from './EmojiButton';
|
||||||
|
|
||||||
const EMOJIS_PER_ROW_ON_DESKTOP = 9;
|
const EMOJIS_PER_ROW_ON_DESKTOP = 8;
|
||||||
const EMOJI_MARGIN = 4;
|
const EMOJI_MARGIN = 10;
|
||||||
const MOBILE_CONTAINER_PADDING = 8;
|
const MOBILE_CONTAINER_PADDING = 8;
|
||||||
const EMOJI_SIZE = 40;
|
const EMOJI_SIZE = 40;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
&-main {
|
&-main {
|
||||||
height: calc(100% - 3rem);
|
height: calc(100% - 3rem);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0.5rem;
|
padding: 0.4375rem;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
padding: 0.5rem 0.25rem;
|
padding: 0.5rem 0.25rem;
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import {
|
|||||||
uncompressEmoji,
|
uncompressEmoji,
|
||||||
} from '../../../util/emoji';
|
} from '../../../util/emoji';
|
||||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
@ -38,6 +38,7 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = Pick<GlobalState, 'recentEmojis'>;
|
type StateProps = Pick<GlobalState, 'recentEmojis'>;
|
||||||
|
|
||||||
type EmojiCategoryData = { id: string; name: string; emojis: string[] };
|
type EmojiCategoryData = { id: string; name: string; emojis: string[] };
|
||||||
|
|
||||||
const ICONS_BY_CATEGORY: Record<string, string> = {
|
const ICONS_BY_CATEGORY: Record<string, string> = {
|
||||||
@ -66,7 +67,9 @@ let emojiRawData: EmojiRawData;
|
|||||||
let emojiData: EmojiData;
|
let emojiData: EmojiData;
|
||||||
|
|
||||||
const EmojiPicker: FC<OwnProps & StateProps> = ({
|
const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||||
className, onEmojiSelect, recentEmojis,
|
className,
|
||||||
|
recentEmojis,
|
||||||
|
onEmojiSelect,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
|||||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||||
import { pickTruthy } from '../../../util/iteratees';
|
import { pickTruthy, uniqueByField } from '../../../util/iteratees';
|
||||||
import { selectChat, selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
|
import { selectChat, selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||||
|
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||||
@ -53,6 +53,7 @@ type StateProps = {
|
|||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
recentStickers: ApiSticker[];
|
recentStickers: ApiSticker[];
|
||||||
favoriteStickers: ApiSticker[];
|
favoriteStickers: ApiSticker[];
|
||||||
|
premiumStickers: ApiSticker[];
|
||||||
stickerSetsById: Record<string, ApiStickerSet>;
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
addedSetIds?: string[];
|
addedSetIds?: string[];
|
||||||
shouldPlay?: boolean;
|
shouldPlay?: boolean;
|
||||||
@ -74,6 +75,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
canSendStickers,
|
canSendStickers,
|
||||||
recentStickers,
|
recentStickers,
|
||||||
favoriteStickers,
|
favoriteStickers,
|
||||||
|
premiumStickers,
|
||||||
addedSetIds,
|
addedSetIds,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
shouldPlay,
|
shouldPlay,
|
||||||
@ -155,17 +157,19 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCurrentUserPremium) {
|
if (isCurrentUserPremium) {
|
||||||
const premiumStickers = existingAddedSetIds
|
const addedPremiumStickers = existingAddedSetIds
|
||||||
.map((l) => l.stickers?.filter((sticker) => sticker.hasEffect))
|
.map((l) => l.stickers?.filter((sticker) => sticker.hasEffect))
|
||||||
.flat()
|
.flat()
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (premiumStickers.length) {
|
const totalPremiumStickers = uniqueByField([...addedPremiumStickers, ...premiumStickers], 'id');
|
||||||
|
|
||||||
|
if (totalPremiumStickers.length) {
|
||||||
defaultSets.push({
|
defaultSets.push({
|
||||||
id: PREMIUM_STICKER_SET_ID,
|
id: PREMIUM_STICKER_SET_ID,
|
||||||
title: lang('PremiumStickers'),
|
title: lang('PremiumStickers'),
|
||||||
stickers: premiumStickers,
|
stickers: totalPremiumStickers,
|
||||||
count: premiumStickers.length,
|
count: totalPremiumStickers.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -187,7 +191,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
...existingAddedSetIds,
|
...existingAddedSetIds,
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
addedSetIds, favoriteStickers, isCurrentUserPremium, recentStickers, chat, lang, stickerSetsById,
|
addedSetIds, stickerSetsById, favoriteStickers, recentStickers, isCurrentUserPremium, chat, lang, premiumStickers,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const noPopulatedSets = useMemo(() => (
|
const noPopulatedSets = useMemo(() => (
|
||||||
@ -274,7 +278,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
onClick={() => selectStickerSet(index)}
|
onClick={() => selectStickerSet(index)}
|
||||||
>
|
>
|
||||||
{stickerSet.id === PREMIUM_STICKER_SET_ID ? (
|
{stickerSet.id === PREMIUM_STICKER_SET_ID ? (
|
||||||
<PremiumIcon withGradient />
|
<PremiumIcon withGradient big />
|
||||||
) : stickerSet.id === RECENT_SYMBOL_SET_ID ? (
|
) : stickerSet.id === RECENT_SYMBOL_SET_ID ? (
|
||||||
<i className="icon-recent" />
|
<i className="icon-recent" />
|
||||||
) : stickerSet.id === FAVORITE_SYMBOL_SET_ID ? (
|
) : stickerSet.id === FAVORITE_SYMBOL_SET_ID ? (
|
||||||
@ -370,6 +374,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
added,
|
added,
|
||||||
recent,
|
recent,
|
||||||
favorite,
|
favorite,
|
||||||
|
premiumSet,
|
||||||
} = global.stickers;
|
} = global.stickers;
|
||||||
|
|
||||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||||
@ -379,6 +384,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
chat,
|
chat,
|
||||||
recentStickers: recent.stickers,
|
recentStickers: recent.stickers,
|
||||||
favoriteStickers: favorite.stickers,
|
favoriteStickers: favorite.stickers,
|
||||||
|
premiumStickers: premiumSet.stickers,
|
||||||
stickerSetsById: setsById,
|
stickerSetsById: setsById,
|
||||||
addedSetIds: added.setIds,
|
addedSetIds: added.setIds,
|
||||||
shouldPlay: global.settings.byKey.shouldLoopStickers,
|
shouldPlay: global.settings.byKey.shouldLoopStickers,
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useMemo, useRef,
|
memo, useCallback, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiSticker } from '../../../api/types';
|
import type { ApiSticker } from '../../../api/types';
|
||||||
import type { StickerSetOrRecent } from '../../../types';
|
import type { StickerSetOrRecent } from '../../../types';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import { FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER } from '../../../config';
|
import {
|
||||||
|
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
|
||||||
|
} from '../../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import windowSize from '../../../util/windowSize';
|
import windowSize from '../../../util/windowSize';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
@ -20,6 +22,7 @@ import useMediaTransition from '../../../hooks/useMediaTransition';
|
|||||||
|
|
||||||
import StickerButton from '../../common/StickerButton';
|
import StickerButton from '../../common/StickerButton';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
stickerSet: StickerSetOrRecent;
|
stickerSet: StickerSetOrRecent;
|
||||||
@ -29,15 +32,17 @@ type OwnProps = {
|
|||||||
favoriteStickers?: ApiSticker[];
|
favoriteStickers?: ApiSticker[];
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
onStickerUnfave: (sticker: ApiSticker) => void;
|
onStickerUnfave?: (sticker: ApiSticker) => void;
|
||||||
onStickerFave: (sticker: ApiSticker) => void;
|
onStickerFave?: (sticker: ApiSticker) => void;
|
||||||
onStickerRemoveRecent: (sticker: ApiSticker) => void;
|
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STICKERS_PER_ROW_ON_DESKTOP = 5;
|
const STICKERS_PER_ROW_ON_DESKTOP = 5;
|
||||||
|
const EMOJI_PER_ROW_ON_DESKTOP = 8;
|
||||||
const STICKER_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 16;
|
const STICKER_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 16;
|
||||||
|
const EMOJI_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 10;
|
||||||
const MOBILE_CONTAINER_PADDING = 8;
|
const MOBILE_CONTAINER_PADDING = 8;
|
||||||
|
|
||||||
const StickerSet: FC<OwnProps> = ({
|
const StickerSet: FC<OwnProps> = ({
|
||||||
@ -58,21 +63,35 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||||
|
const [isExpanded, expand] = useFlag(!stickerSet.isEmoji);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useOnIntersect(ref, observeIntersection);
|
useOnIntersect(ref, observeIntersection);
|
||||||
|
|
||||||
const transitionClassNames = useMediaTransition(shouldRender);
|
const transitionClassNames = useMediaTransition(shouldRender);
|
||||||
|
|
||||||
|
const isEmoji = stickerSet.isEmoji;
|
||||||
|
|
||||||
const handleClearRecent = useCallback(() => {
|
const handleClearRecent = useCallback(() => {
|
||||||
clearRecentStickers();
|
clearRecentStickers();
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
}, [clearRecentStickers, closeConfirmModal]);
|
}, [clearRecentStickers, closeConfirmModal]);
|
||||||
|
|
||||||
|
const isLocked = !isSavedMessages && isEmoji && !isCurrentUserPremium
|
||||||
|
&& stickerSet.stickers?.some((l) => !l.isFree);
|
||||||
|
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
|
||||||
|
const itemsPerRow = isEmoji ? EMOJI_PER_ROW_ON_DESKTOP : STICKERS_PER_ROW_ON_DESKTOP;
|
||||||
|
const margin = isEmoji ? EMOJI_MARGIN : STICKER_MARGIN;
|
||||||
|
|
||||||
const stickersPerRow = IS_SINGLE_COLUMN_LAYOUT
|
const stickersPerRow = IS_SINGLE_COLUMN_LAYOUT
|
||||||
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (STICKER_SIZE_PICKER + STICKER_MARGIN))
|
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (itemSize + margin))
|
||||||
: STICKERS_PER_ROW_ON_DESKTOP;
|
: itemsPerRow;
|
||||||
const height = Math.ceil(stickerSet.count / stickersPerRow) * (STICKER_SIZE_PICKER + STICKER_MARGIN);
|
|
||||||
|
const shouldCutSet = isEmoji && !isExpanded && !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID;
|
||||||
|
const itemsBeforeCutout = shouldCutSet ? stickersPerRow * 3 : Infinity;
|
||||||
|
const height = Math.ceil((
|
||||||
|
!shouldCutSet ? stickerSet.count : Math.min(itemsBeforeCutout, stickerSet.count))
|
||||||
|
/ stickersPerRow) * (itemSize + margin);
|
||||||
|
|
||||||
const favoriteStickerIdsSet = useMemo(() => (
|
const favoriteStickerIdsSet = useMemo(() => (
|
||||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||||
@ -85,10 +104,15 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
id={`sticker-set-${index}`}
|
id={`sticker-set-${index}`}
|
||||||
className="symbol-set"
|
className={
|
||||||
|
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="symbol-set-header">
|
<div className="symbol-set-header">
|
||||||
<p className="symbol-set-name">{stickerSet.title}</p>
|
<p className="symbol-set-name">
|
||||||
|
{isLocked && <i className="symbol-set-locked-icon icon-lock-badge" />}
|
||||||
|
{stickerSet.title}
|
||||||
|
</p>
|
||||||
{isRecent && (
|
{isRecent && (
|
||||||
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
||||||
)}
|
)}
|
||||||
@ -97,29 +121,36 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
className={buildClassName('symbol-set-container', transitionClassNames)}
|
className={buildClassName('symbol-set-container', transitionClassNames)}
|
||||||
style={`height: ${height}px;`}
|
style={`height: ${height}px;`}
|
||||||
>
|
>
|
||||||
{shouldRender && stickerSet.stickers && stickerSet.stickers.map((sticker) => (
|
{shouldRender && stickerSet.stickers && stickerSet.stickers
|
||||||
<StickerButton
|
.slice(0, !isExpanded ? (itemsBeforeCutout - 1) : stickerSet.stickers.length)
|
||||||
key={sticker.id}
|
.map((sticker) => (
|
||||||
sticker={sticker}
|
<StickerButton
|
||||||
size={STICKER_SIZE_PICKER}
|
key={sticker.id}
|
||||||
observeIntersection={observeIntersection}
|
sticker={sticker}
|
||||||
noAnimate={!loadAndPlay}
|
size={itemSize}
|
||||||
onClick={onStickerSelect}
|
observeIntersection={observeIntersection}
|
||||||
clickArg={sticker}
|
noAnimate={!loadAndPlay}
|
||||||
onUnfaveClick={stickerSet.id === FAVORITE_SYMBOL_SET_ID && favoriteStickerIdsSet?.has(sticker.id)
|
onClick={onStickerSelect}
|
||||||
? onStickerUnfave : undefined}
|
clickArg={sticker}
|
||||||
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
onUnfaveClick={stickerSet.id === FAVORITE_SYMBOL_SET_ID && favoriteStickerIdsSet?.has(sticker.id)
|
||||||
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
|
? onStickerUnfave : undefined}
|
||||||
isSavedMessages={isSavedMessages}
|
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
||||||
canViewSet
|
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isSavedMessages={isSavedMessages}
|
||||||
/>
|
canViewSet
|
||||||
))}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!isExpanded && stickerSet.count > itemsBeforeCutout - 1 && (
|
||||||
|
<Button className="StickerButton custom-emoji set-expand" round color="translucent" onClick={expand}>
|
||||||
|
+{stickerSet.count - itemsBeforeCutout + 1}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isRecent && (
|
{isRecent && (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
text={lang('ClearRecentEmoji')}
|
text={lang('ClearRecentStickersAlertMessage')}
|
||||||
isOpen={isConfirmModalOpen}
|
isOpen={isConfirmModalOpen}
|
||||||
onClose={closeConfirmModal}
|
onClose={closeConfirmModal}
|
||||||
confirmHandler={handleClearRecent}
|
confirmHandler={handleClearRecent}
|
||||||
|
|||||||
@ -80,7 +80,7 @@ const StickerTooltip: FC<OwnProps & StateProps> = ({
|
|||||||
sticker={sticker}
|
sticker={sticker}
|
||||||
size={STICKER_SIZE_PICKER}
|
size={STICKER_SIZE_PICKER}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
onClick={onStickerSelect}
|
onClick={isOpen ? onStickerSelect : undefined}
|
||||||
clickArg={sticker}
|
clickArg={sticker}
|
||||||
isSavedMessages={isSavedMessages}
|
isSavedMessages={isSavedMessages}
|
||||||
canViewSet
|
canViewSet
|
||||||
|
|||||||
@ -165,11 +165,24 @@
|
|||||||
|
|
||||||
.symbol-set {
|
.symbol-set {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.symbol-set-locked::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset: -0.25rem;
|
||||||
|
top: 0.75rem;
|
||||||
|
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><rect width='100%' height='100%' style='stroke: rgba(112, 117, 121, 0.7); width: calc(100% - 4px); height: calc(100% - 4px);' fill='none' stroke-dashoffset='5' stroke-width='2' stroke-dasharray='8' stroke-linecap='round' rx='8' ry='8' x='2' y='2' /></svg>");
|
||||||
|
}
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: rgba(var(--color-text-secondary-rgb), 0.75);
|
color: rgba(var(--color-text-secondary-rgb), 0.75);
|
||||||
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
@ -177,16 +190,24 @@
|
|||||||
line-height: 1.6875rem;
|
line-height: 1.6875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding-left: 0.5rem;
|
padding: 0 0.5rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
text-align: initial;
|
text-align: center;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-locked-icon {
|
||||||
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-remove {
|
&-remove {
|
||||||
|
right: 0;
|
||||||
|
position: absolute;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiSticker, ApiVideo } from '../../../api/types';
|
import type { ApiSticker, ApiVideo } from '../../../api/types';
|
||||||
|
import type { GlobalActions } from '../../../global/types';
|
||||||
|
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
import { fastRaf } from '../../../util/schedulers';
|
import { fastRaf } from '../../../util/schedulers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||||
|
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useMouseInside from '../../../hooks/useMouseInside';
|
import useMouseInside from '../../../hooks/useMouseInside';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
@ -41,11 +44,12 @@ export type OwnProps = {
|
|||||||
onGifSelect: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
onGifSelect: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
onRemoveSymbol: () => void;
|
onRemoveSymbol: () => void;
|
||||||
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
onSearchOpen: (type: 'stickers' | 'gifs') => void;
|
||||||
addRecentEmoji: AnyToVoidFunction;
|
addRecentEmoji: GlobalActions['addRecentEmoji'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
isLeftColumnShown: boolean;
|
isLeftColumnShown: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let isActivated = false;
|
let isActivated = false;
|
||||||
@ -57,6 +61,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
canSendStickers,
|
canSendStickers,
|
||||||
canSendGifs,
|
canSendGifs,
|
||||||
isLeftColumnShown,
|
isLeftColumnShown,
|
||||||
|
isCurrentUserPremium,
|
||||||
onLoad,
|
onLoad,
|
||||||
onClose,
|
onClose,
|
||||||
onEmojiSelect,
|
onEmojiSelect,
|
||||||
@ -66,6 +71,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
onSearchOpen,
|
onSearchOpen,
|
||||||
addRecentEmoji,
|
addRecentEmoji,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { loadPremiumSetStickers } = getActions();
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
|
||||||
@ -80,6 +86,12 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
onLoad();
|
onLoad();
|
||||||
}, [onLoad]);
|
}, [onLoad]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCurrentUserPremium) {
|
||||||
|
loadPremiumSetStickers();
|
||||||
|
}
|
||||||
|
}, [isCurrentUserPremium, loadPremiumSetStickers]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -105,7 +117,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
const recentEmojisRef = useRef(recentEmojis);
|
const recentEmojisRef = useRef(recentEmojis);
|
||||||
recentEmojisRef.current = recentEmojis;
|
recentEmojisRef.current = recentEmojis;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!recentEmojisRef.current.length) {
|
if (!recentEmojisRef.current.length || isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,12 +126,10 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setRecentEmojis([]);
|
setRecentEmojis([]);
|
||||||
}, [isOpen, activeTab, addRecentEmoji]);
|
}, [isOpen, addRecentEmoji]);
|
||||||
|
|
||||||
const handleEmojiSelect = useCallback((emoji: string, name: string) => {
|
const handleEmojiSelect = useCallback((emoji: string, name: string) => {
|
||||||
setRecentEmojis((emojis) => {
|
setRecentEmojis((emojis) => [...emojis, name]);
|
||||||
return [...emojis, name];
|
|
||||||
});
|
|
||||||
|
|
||||||
onEmojiSelect(emoji);
|
onEmojiSelect(emoji);
|
||||||
}, [onEmojiSelect]);
|
}, [onEmojiSelect]);
|
||||||
@ -177,7 +187,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
<>
|
<>
|
||||||
<div className="SymbolMenu-main" onClick={stopPropagation}>
|
<div className="SymbolMenu-main" onClick={stopPropagation}>
|
||||||
{isActivated && (
|
{isActivated && (
|
||||||
<Transition name="slide" activeKey={activeTab} renderCount={SYMBOL_MENU_TAB_TITLES.length}>
|
<Transition name="slide" activeKey={activeTab} renderCount={Object.values(SYMBOL_MENU_TAB_TITLES).length}>
|
||||||
{renderContent}
|
{renderContent}
|
||||||
</Transition>
|
</Transition>
|
||||||
)}
|
)}
|
||||||
@ -246,6 +256,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
return {
|
return {
|
||||||
isLeftColumnShown: global.isLeftColumnShown,
|
isLeftColumnShown: global.isLeftColumnShown,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SymbolMenu));
|
)(SymbolMenu));
|
||||||
|
|||||||
@ -18,10 +18,11 @@ export enum SymbolMenuTabs {
|
|||||||
'GIFs',
|
'GIFs',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getting enum string values for display in Tabs.
|
export const SYMBOL_MENU_TAB_TITLES: Record<SymbolMenuTabs, string> = {
|
||||||
// See: https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings
|
[SymbolMenuTabs.Emoji]: 'Emoji',
|
||||||
export const SYMBOL_MENU_TAB_TITLES = Object.values(SymbolMenuTabs)
|
[SymbolMenuTabs.Stickers]: 'AccDescrStickers',
|
||||||
.filter((value): value is string => typeof value === 'string');
|
[SymbolMenuTabs.GIFs]: 'GifsTab',
|
||||||
|
};
|
||||||
|
|
||||||
const SYMBOL_MENU_TAB_ICONS = {
|
const SYMBOL_MENU_TAB_ICONS = {
|
||||||
[SymbolMenuTabs.Emoji]: 'icon-smile',
|
[SymbolMenuTabs.Emoji]: 'icon-smile',
|
||||||
@ -40,7 +41,7 @@ const SymbolMenuFooter: FC<OwnProps> = ({
|
|||||||
className={`symbol-tab-button ${activeTab === tab ? 'activated' : ''}`}
|
className={`symbol-tab-button ${activeTab === tab ? 'activated' : ''}`}
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => onSwitchTab(tab)}
|
onClick={() => onSwitchTab(tab)}
|
||||||
ariaLabel={SYMBOL_MENU_TAB_TITLES[tab]}
|
ariaLabel={lang(SYMBOL_MENU_TAB_TITLES[tab])}
|
||||||
round
|
round
|
||||||
faded
|
faded
|
||||||
color="translucent"
|
color="translucent"
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
|
|||||||
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
|
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiMessage, ApiWebPage } from '../../../api/types';
|
import type { ApiMessage, ApiMessageEntityTextUrl, ApiWebPage } from '../../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||||
import type { ISettings } from '../../../types';
|
import type { ISettings } from '../../../types';
|
||||||
|
|
||||||
@ -54,7 +54,9 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
|||||||
const link = useDebouncedMemo(() => {
|
const link = useDebouncedMemo(() => {
|
||||||
const { text, entities } = parseMessageInput(messageText);
|
const { text, entities } = parseMessageInput(messageText);
|
||||||
|
|
||||||
const linkEntity = entities && entities.find(({ type }) => type === ApiMessageEntityTypes.TextUrl);
|
const linkEntity = entities?.find((entity): entity is ApiMessageEntityTextUrl => (
|
||||||
|
entity.type === ApiMessageEntityTypes.TextUrl
|
||||||
|
));
|
||||||
if (linkEntity) {
|
if (linkEntity) {
|
||||||
return linkEntity.url;
|
return linkEntity.url;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,14 +20,14 @@ export default function useStickerTooltip(
|
|||||||
(IS_EMOJI_SUPPORTED && parseEmojiOnlyString(cleanHtml) === 1)
|
(IS_EMOJI_SUPPORTED && parseEmojiOnlyString(cleanHtml) === 1)
|
||||||
|| (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^<img.[^>]*?>$/g)))
|
|| (!IS_EMOJI_SUPPORTED && Boolean(html.match(/^<img.[^>]*?>$/g)))
|
||||||
);
|
);
|
||||||
const hasStickers = Boolean(stickers) && isSingleEmoji;
|
const hasStickers = Boolean(stickers?.length) && isSingleEmoji;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDisabled) return;
|
if (isDisabled) return;
|
||||||
|
|
||||||
if (isAllowed && isSingleEmoji) {
|
if (isAllowed && isSingleEmoji) {
|
||||||
loadStickersForEmoji({
|
loadStickersForEmoji({
|
||||||
emoji: IS_EMOJI_SUPPORTED ? cleanHtml : cleanHtml.match(/alt="(.+)"/)?.[1],
|
emoji: IS_EMOJI_SUPPORTED ? cleanHtml : cleanHtml.match(/alt="(.+)"/)?.[1]!,
|
||||||
});
|
});
|
||||||
} else if (hasStickers || !isSingleEmoji) {
|
} else if (hasStickers || !isSingleEmoji) {
|
||||||
clearStickersForEmoji();
|
clearStickersForEmoji();
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import React, {
|
|||||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { MessageListType } from '../../../global/types';
|
import type { MessageListType } from '../../../global/types';
|
||||||
import type { ApiAvailableReaction, ApiMessage } from '../../../api/types';
|
import type {
|
||||||
|
ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet,
|
||||||
|
} from '../../../api/types';
|
||||||
import type { IAlbum, IAnchorPosition } from '../../../types';
|
import type { IAlbum, IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -15,6 +17,8 @@ import {
|
|||||||
selectCurrentMessageList, selectIsCurrentUserPremium,
|
selectCurrentMessageList, selectIsCurrentUserPremium,
|
||||||
selectIsMessageProtected,
|
selectIsMessageProtected,
|
||||||
selectIsPremiumPurchaseBlocked,
|
selectIsPremiumPurchaseBlocked,
|
||||||
|
selectMessageCustomEmojiSets,
|
||||||
|
selectStickerSet,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import {
|
import {
|
||||||
isActionMessage, isChatChannel,
|
isActionMessage, isChatChannel,
|
||||||
@ -52,6 +56,8 @@ export type OwnProps = {
|
|||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
availableReactions?: ApiAvailableReaction[];
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
customEmojiSetsInfo?: ApiStickerSetInfo[];
|
||||||
|
customEmojiSets?: ApiStickerSet[];
|
||||||
noOptions?: boolean;
|
noOptions?: boolean;
|
||||||
canSendNow?: boolean;
|
canSendNow?: boolean;
|
||||||
canReschedule?: boolean;
|
canReschedule?: boolean;
|
||||||
@ -89,6 +95,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
messageListType,
|
messageListType,
|
||||||
chatUsername,
|
chatUsername,
|
||||||
message,
|
message,
|
||||||
|
customEmojiSetsInfo,
|
||||||
|
customEmojiSets,
|
||||||
album,
|
album,
|
||||||
anchor,
|
anchor,
|
||||||
onClose,
|
onClose,
|
||||||
@ -143,6 +151,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
loadReactors,
|
loadReactors,
|
||||||
copyMessagesByIds,
|
copyMessagesByIds,
|
||||||
saveGif,
|
saveGif,
|
||||||
|
loadStickers,
|
||||||
cancelPollVote,
|
cancelPollVote,
|
||||||
closePoll,
|
closePoll,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
@ -156,6 +165,9 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
const [isCalendarOpen, openCalendar, closeCalendar] = useFlag();
|
||||||
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
const [isClosePollDialogOpen, openClosePollDialog, closeClosePollDialog] = useFlag();
|
||||||
|
|
||||||
|
// `undefined` indicates that emoji are present and loading
|
||||||
|
const hasCustomEmoji = customEmojiSetsInfo === undefined || Boolean(customEmojiSetsInfo.length);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canShowSeenBy && isOpen) {
|
if (canShowSeenBy && isOpen) {
|
||||||
loadSeenBy({ chatId: message.chatId, messageId: message.id });
|
loadSeenBy({ chatId: message.chatId, messageId: message.id });
|
||||||
@ -168,6 +180,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [canShowReactionsCount, isOpen, loadReactors, message.chatId, message.id]);
|
}, [canShowReactionsCount, isOpen, loadReactors, message.chatId, message.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (customEmojiSetsInfo?.length && customEmojiSets?.length !== customEmojiSetsInfo.length) {
|
||||||
|
customEmojiSetsInfo.forEach((set) => {
|
||||||
|
loadStickers({ stickerSetInfo: set });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [customEmojiSetsInfo, customEmojiSets, loadStickers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasFullInfo && !isPrivate && isOpen) {
|
if (!hasFullInfo && !isPrivate && isOpen) {
|
||||||
loadFullChat({ chatId: message.chatId });
|
loadFullChat({ chatId: message.chatId });
|
||||||
@ -403,6 +423,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canRevote={canRevote}
|
canRevote={canRevote}
|
||||||
canClosePoll={canClosePoll}
|
canClosePoll={canClosePoll}
|
||||||
canShowSeenBy={canShowSeenBy}
|
canShowSeenBy={canShowSeenBy}
|
||||||
|
hasCustomEmoji={hasCustomEmoji}
|
||||||
|
customEmojiSets={customEmojiSets}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
seenByRecentUsers={seenByRecentUsers}
|
seenByRecentUsers={seenByRecentUsers}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
@ -516,6 +538,11 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const canCopyNumber = Boolean(message.content.contact);
|
const canCopyNumber = Boolean(message.content.contact);
|
||||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||||
|
|
||||||
|
const customEmojiSetsInfo = selectMessageCustomEmojiSets(global, message);
|
||||||
|
const customEmojiSetsNotFiltered = customEmojiSetsInfo?.map((set) => selectStickerSet(global, set));
|
||||||
|
const customEmojiSets = customEmojiSetsNotFiltered?.every<ApiStickerSet>(Boolean)
|
||||||
|
? customEmojiSetsNotFiltered : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
availableReactions: global.availableReactions,
|
availableReactions: global.availableReactions,
|
||||||
noOptions,
|
noOptions,
|
||||||
@ -547,6 +574,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
canShowReactionList: !isLocal && !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID,
|
canShowReactionList: !isLocal && !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
canRemoveReaction,
|
canRemoveReaction,
|
||||||
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
|
canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global),
|
||||||
|
customEmojiSetsInfo,
|
||||||
|
customEmojiSets,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ContextMenuContainer));
|
)(ContextMenuContainer));
|
||||||
|
|||||||
@ -507,7 +507,13 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const withAppendix = contentClassName.includes('has-appendix');
|
const withAppendix = contentClassName.includes('has-appendix');
|
||||||
const textParts = renderMessageText(
|
const textParts = renderMessageText(
|
||||||
message, highlight, isEmojiOnlyMessage(customShape), undefined, undefined, isProtected,
|
message,
|
||||||
|
highlight,
|
||||||
|
isEmojiOnlyMessage(customShape),
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
isProtected,
|
||||||
|
observeIntersectionForAnimatedStickers,
|
||||||
);
|
);
|
||||||
|
|
||||||
let metaPosition!: MetaPosition;
|
let metaPosition!: MetaPosition;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import React, {
|
|||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiAvailableReaction, ApiMessage, ApiSponsoredMessage, ApiUser,
|
ApiAvailableReaction, ApiMessage, ApiSponsoredMessage, ApiStickerSet, ApiUser,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { IAnchorPosition } from '../../../types';
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
|
|||||||
import { getUserFullName } from '../../../global/helpers';
|
import { getUserFullName } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
||||||
@ -21,6 +22,8 @@ import useLang from '../../../hooks/useLang';
|
|||||||
|
|
||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
|
import MenuSeparator from '../../ui/MenuSeparator';
|
||||||
|
import Skeleton from '../../ui/Skeleton';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import ReactionSelector from './ReactionSelector';
|
import ReactionSelector from './ReactionSelector';
|
||||||
|
|
||||||
@ -59,6 +62,8 @@ type OwnProps = {
|
|||||||
isDownloading?: boolean;
|
isDownloading?: boolean;
|
||||||
canShowSeenBy?: boolean;
|
canShowSeenBy?: boolean;
|
||||||
seenByRecentUsers?: ApiUser[];
|
seenByRecentUsers?: ApiUser[];
|
||||||
|
hasCustomEmoji?: boolean;
|
||||||
|
customEmojiSets?: ApiStickerSet[];
|
||||||
onReply?: () => void;
|
onReply?: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onPin?: () => void;
|
onPin?: () => void;
|
||||||
@ -124,6 +129,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
canRemoveReaction,
|
canRemoveReaction,
|
||||||
canShowReactionList,
|
canShowReactionList,
|
||||||
seenByRecentUsers,
|
seenByRecentUsers,
|
||||||
|
hasCustomEmoji,
|
||||||
|
customEmojiSets,
|
||||||
onReply,
|
onReply,
|
||||||
onEdit,
|
onEdit,
|
||||||
onPin,
|
onPin,
|
||||||
@ -151,7 +158,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onAboutAds,
|
onAboutAds,
|
||||||
onSponsoredHide,
|
onSponsoredHide,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification } = getActions();
|
const { showNotification, openStickerSet, openCustomEmojiSets } = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -171,6 +178,22 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}, [lang, onClose, showNotification]);
|
}, [lang, onClose, showNotification]);
|
||||||
|
|
||||||
|
const handleOpenCustomEmojiSets = useCallback(() => {
|
||||||
|
if (!customEmojiSets) return;
|
||||||
|
if (customEmojiSets.length === 1) {
|
||||||
|
openStickerSet({
|
||||||
|
stickerSetInfo: {
|
||||||
|
shortName: customEmojiSets[0].shortName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openCustomEmojiSets({
|
||||||
|
setIds: customEmojiSets.map((set) => set.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
}, [customEmojiSets, onClose, openCustomEmojiSets, openStickerSet]);
|
||||||
|
|
||||||
const copyOptions = isSponsoredMessage
|
const copyOptions = isSponsoredMessage
|
||||||
? []
|
? []
|
||||||
: getMessageCopyOptions(
|
: getMessageCopyOptions(
|
||||||
@ -332,6 +355,27 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
||||||
|
{hasCustomEmoji && (
|
||||||
|
<>
|
||||||
|
<MenuSeparator />
|
||||||
|
{!customEmojiSets && (
|
||||||
|
<>
|
||||||
|
<Skeleton inline className="menu-loading-row" />
|
||||||
|
<Skeleton inline className="menu-loading-row" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{customEmojiSets && customEmojiSets.length === 1 && (
|
||||||
|
<MenuItem withWrap onClick={handleOpenCustomEmojiSets} className="menu-custom-emoji-sets">
|
||||||
|
{renderText(lang('MessageContainsEmojiPack', customEmojiSets[0].title), ['simple_markdown', 'emoji'])}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{customEmojiSets && customEmojiSets.length > 1 && (
|
||||||
|
<MenuItem withWrap onClick={handleOpenCustomEmojiSets} className="menu-custom-emoji-sets">
|
||||||
|
{renderText(lang('MessageContainsEmojiPacks', customEmojiSets.length), ['simple_markdown'])}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{isSponsoredMessage && <MenuItem icon="help" onClick={onAboutAds}>{lang('SponsoredMessageInfo')}</MenuItem>}
|
{isSponsoredMessage && <MenuItem icon="help" onClick={onAboutAds}>{lang('SponsoredMessageInfo')}</MenuItem>}
|
||||||
{isSponsoredMessage && onSponsoredHide && (
|
{isSponsoredMessage && onSponsoredHide && (
|
||||||
<MenuItem icon="stop" onClick={onSponsoredHide}>{lang('HideAd')}</MenuItem>
|
<MenuItem icon="stop" onClick={onSponsoredHide}>{lang('HideAd')}</MenuItem>
|
||||||
|
|||||||
@ -4,23 +4,22 @@ import React, { useCallback, useEffect, useRef } from '../../../lib/teact/teact'
|
|||||||
import type { ApiMessage } from '../../../api/types';
|
import type { ApiMessage } from '../../../api/types';
|
||||||
import { ApiMediaFormat } from '../../../api/types';
|
import { ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
import { NO_STICKER_SET_ID } from '../../../config';
|
|
||||||
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
import { getStickerDimensions } from '../../common/helpers/mediaDimensions';
|
||||||
import { getMessageMediaFormat, getMessageMediaHash } from '../../../global/helpers';
|
import { getMessageMediaFormat, getMessageMediaHash } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import safePlay from '../../../util/safePlay';
|
||||||
|
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
||||||
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useWebpThumbnail from '../../../hooks/useWebpThumbnail';
|
import useThumbnail from '../../../hooks/useThumbnail';
|
||||||
import safePlay from '../../../util/safePlay';
|
|
||||||
import { IS_WEBM_SUPPORTED } from '../../../util/environment';
|
|
||||||
import { getActions } from '../../../global';
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
import StickerSetModal from '../../common/StickerSetModal.async';
|
|
||||||
|
|
||||||
import './Sticker.scss';
|
import './Sticker.scss';
|
||||||
|
|
||||||
@ -43,20 +42,18 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
message, observeIntersection, observeIntersectionForPlaying, shouldLoop, lastSyncTime,
|
message, observeIntersection, observeIntersectionForPlaying, shouldLoop, lastSyncTime,
|
||||||
shouldPlayEffect, onPlayEffect, onStopEffect,
|
shouldPlayEffect, onPlayEffect, onStopEffect,
|
||||||
}) => {
|
}) => {
|
||||||
const { showNotification } = getActions();
|
const { showNotification, openStickerSet } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [isModalOpen, openModal, closeModal] = useFlag();
|
|
||||||
|
|
||||||
const sticker = message.content.sticker!;
|
const sticker = message.content.sticker!;
|
||||||
const {
|
const {
|
||||||
isLottie, stickerSetId, isVideo, hasEffect,
|
isLottie, stickerSetInfo, isVideo, hasEffect,
|
||||||
} = sticker;
|
} = sticker;
|
||||||
const canDisplayVideo = IS_WEBM_SUPPORTED;
|
const canDisplayVideo = IS_WEBM_SUPPORTED;
|
||||||
const isMemojiSticker = stickerSetId === NO_STICKER_SET_ID;
|
const isMemojiSticker = 'isMissing' in stickerSetInfo;
|
||||||
|
|
||||||
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
const [isPlayingEffect, startPlayingEffect, stopPlayingEffect] = useFlag();
|
||||||
const shouldLoad = useIsIntersecting(ref, observeIntersection);
|
const shouldLoad = useIsIntersecting(ref, observeIntersection);
|
||||||
@ -68,7 +65,7 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
const previewMediaHash = isVideo && !canDisplayVideo && (
|
const previewMediaHash = isVideo && !canDisplayVideo && (
|
||||||
sticker.isPreloadedGlobally ? `sticker${sticker.id}?size=m` : getMessageMediaHash(message, 'pictogram'));
|
sticker.isPreloadedGlobally ? `sticker${sticker.id}?size=m` : getMessageMediaHash(message, 'pictogram'));
|
||||||
const previewBlobUrl = useMedia(previewMediaHash);
|
const previewBlobUrl = useMedia(previewMediaHash);
|
||||||
const thumbDataUri = useWebpThumbnail(message);
|
const thumbDataUri = useThumbnail(sticker);
|
||||||
const previewUrl = previewBlobUrl || thumbDataUri;
|
const previewUrl = previewBlobUrl || thumbDataUri;
|
||||||
|
|
||||||
const mediaData = useMedia(
|
const mediaData = useMedia(
|
||||||
@ -122,6 +119,12 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [hasEffect, shouldPlayEffect, onPlayEffect, shouldPlay, startPlayingEffect]);
|
}, [hasEffect, shouldPlayEffect, onPlayEffect, shouldPlay, startPlayingEffect]);
|
||||||
|
|
||||||
|
const openModal = useCallback(() => {
|
||||||
|
openStickerSet({
|
||||||
|
stickerSetInfo: sticker.stickerSetInfo,
|
||||||
|
});
|
||||||
|
}, [openStickerSet, sticker]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (hasEffect) {
|
if (hasEffect) {
|
||||||
if (isPlayingEffect) {
|
if (isPlayingEffect) {
|
||||||
@ -195,11 +198,6 @@ const Sticker: FC<OwnProps> = ({
|
|||||||
onEnded={handleEffectEnded}
|
onEnded={handleEffectEnded}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StickerSetModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
fromSticker={sticker}
|
|
||||||
onClose={closeModal}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -406,10 +406,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.custom-shape) .text-content .emoji {
|
&:not(.custom-shape) .text-content {
|
||||||
width: calc(1.25 * var(--message-text-size, 1rem));
|
.emoji {
|
||||||
height: calc(1.25 * var(--message-text-size, 1rem));
|
width: calc(1.25 * var(--message-text-size, 1rem));
|
||||||
background-size: calc(1.25 * var(--message-text-size, 1rem));
|
height: calc(1.25 * var(--message-text-size, 1rem));
|
||||||
|
background-size: calc(1.25 * var(--message-text-size, 1rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-entity-custom-emoji {
|
||||||
|
--custom-emoji-size: calc(1.25 * var(--message-text-size, 1rem));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-media-corners {
|
.no-media-corners {
|
||||||
@ -823,6 +829,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-entity-custom-emoji {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
--custom-emoji-size: 1.5rem;
|
||||||
|
width: var(--custom-emoji-size);
|
||||||
|
height: var(--custom-emoji-size);
|
||||||
|
|
||||||
|
& > video, & > img {
|
||||||
|
width: calc(100% + 1px) !important;
|
||||||
|
height: calc(100% + 1px) !important;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .AnimatedSticker {
|
||||||
|
width: var(--custom-emoji-size) !important;
|
||||||
|
height: var(--custom-emoji-size) !important;
|
||||||
|
display: flex !important;
|
||||||
|
|
||||||
|
& > canvas {
|
||||||
|
width: var(--custom-emoji-size) !important;
|
||||||
|
height: var(--custom-emoji-size) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.text-entity-code {
|
.text-entity-code {
|
||||||
color: var(--color-code);
|
color: var(--color-code);
|
||||||
background: var(--color-code-bg);
|
background: var(--color-code-bg);
|
||||||
|
|||||||
@ -317,14 +317,9 @@ const PaymentModal: FC<OwnProps & StateProps & GlobalStateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [step, lang]);
|
}, [step, lang]);
|
||||||
|
|
||||||
const buttonText = useMemo(() => {
|
const buttonText = step === PaymentStep.Checkout
|
||||||
switch (step) {
|
? lang('Checkout.PayPrice', formatCurrency(totalPrice, currency!, lang.code))
|
||||||
case PaymentStep.Checkout:
|
: lang('Next');
|
||||||
return lang('Checkout.PayPrice', formatCurrency(totalPrice, currency!, lang.code));
|
|
||||||
default:
|
|
||||||
return lang('Next');
|
|
||||||
}
|
|
||||||
}, [step, lang, currency, totalPrice]);
|
|
||||||
|
|
||||||
const isSubmitDisabled = isLoading
|
const isSubmitDisabled = isLoading
|
||||||
|| Boolean(step === PaymentStep.Checkout && invoiceContent?.isRecurring && !isTosAccepted);
|
|| Boolean(step === PaymentStep.Checkout && invoiceContent?.isRecurring && !isTosAccepted);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useEffect, useRef, useState,
|
memo, useEffect, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -24,6 +24,7 @@ type StateProps = {
|
|||||||
query?: string;
|
query?: string;
|
||||||
featuredIds?: string[];
|
featuredIds?: string[];
|
||||||
resultIds?: string[];
|
resultIds?: string[];
|
||||||
|
isModalOpen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
@ -31,11 +32,12 @@ const INTERSECTION_THROTTLE = 200;
|
|||||||
const runThrottled = throttle((cb) => cb(), 60000, true);
|
const runThrottled = throttle((cb) => cb(), 60000, true);
|
||||||
|
|
||||||
const StickerSearch: FC<OwnProps & StateProps> = ({
|
const StickerSearch: FC<OwnProps & StateProps> = ({
|
||||||
onClose,
|
|
||||||
isActive,
|
isActive,
|
||||||
query,
|
query,
|
||||||
featuredIds,
|
featuredIds,
|
||||||
resultIds,
|
resultIds,
|
||||||
|
isModalOpen,
|
||||||
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadFeaturedStickers } = getActions();
|
const { loadFeaturedStickers } = getActions();
|
||||||
|
|
||||||
@ -44,8 +46,6 @@ const StickerSearch: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
observe: observeIntersection,
|
observe: observeIntersection,
|
||||||
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE });
|
} = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE });
|
||||||
@ -74,8 +74,7 @@ const StickerSearch: FC<OwnProps & StateProps> = ({
|
|||||||
key={id}
|
key={id}
|
||||||
stickerSetId={id}
|
stickerSetId={id}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
isSomeModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
onModalToggle={setIsModalOpen}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -90,8 +89,7 @@ const StickerSearch: FC<OwnProps & StateProps> = ({
|
|||||||
key={id}
|
key={id}
|
||||||
stickerSetId={id}
|
stickerSetId={id}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
isSomeModalOpen={isModalOpen}
|
isModalOpen={isModalOpen}
|
||||||
onModalToggle={setIsModalOpen}
|
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -116,6 +114,7 @@ export default memo(withGlobal(
|
|||||||
query,
|
query,
|
||||||
featuredIds: featured.setIds,
|
featuredIds: featured.setIds,
|
||||||
resultIds,
|
resultIds,
|
||||||
|
isModalOpen: Boolean(global.openedStickerSetShortName),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(StickerSearch));
|
)(StickerSearch));
|
||||||
|
|||||||
@ -4,25 +4,21 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiStickerSet } from '../../api/types';
|
import type { ApiSticker, ApiStickerSet } from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import { STICKER_SIZE_SEARCH } from '../../config';
|
import { STICKER_SIZE_SEARCH } from '../../config';
|
||||||
import { selectIsCurrentUserPremium, selectShouldLoopStickers, selectStickerSet } from '../../global/selectors';
|
import { selectIsCurrentUserPremium, selectShouldLoopStickers, selectStickerSet } from '../../global/selectors';
|
||||||
import useFlag from '../../hooks/useFlag';
|
|
||||||
import useOnChange from '../../hooks/useOnChange';
|
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import StickerButton from '../common/StickerButton';
|
import StickerButton from '../common/StickerButton';
|
||||||
import StickerSetModal from '../common/StickerSetModal.async';
|
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
stickerSetId: string;
|
stickerSetId: string;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
isSomeModalOpen: boolean;
|
isModalOpen?: boolean;
|
||||||
onModalToggle: (isOpen: boolean) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -36,20 +32,14 @@ const STICKERS_TO_DISPLAY = 5;
|
|||||||
|
|
||||||
const StickerSetResult: FC<OwnProps & StateProps> = ({
|
const StickerSetResult: FC<OwnProps & StateProps> = ({
|
||||||
stickerSetId, observeIntersection, set, shouldPlay,
|
stickerSetId, observeIntersection, set, shouldPlay,
|
||||||
isSomeModalOpen, onModalToggle, isCurrentUserPremium,
|
isModalOpen, isCurrentUserPremium,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadStickers, toggleStickerSet } = getActions();
|
const { loadStickers, toggleStickerSet, openStickerSet } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const isAdded = set && Boolean(set.installedDate);
|
const isAdded = set && Boolean(set.installedDate);
|
||||||
const areStickersLoaded = Boolean(set?.stickers);
|
const areStickersLoaded = Boolean(set?.stickers);
|
||||||
|
|
||||||
const [isModalOpen, openModal, closeModal] = useFlag();
|
|
||||||
|
|
||||||
useOnChange(() => {
|
|
||||||
onModalToggle(isModalOpen);
|
|
||||||
}, [isModalOpen, onModalToggle]);
|
|
||||||
|
|
||||||
const displayedStickers = useMemo(() => {
|
const displayedStickers = useMemo(() => {
|
||||||
if (!set) {
|
if (!set) {
|
||||||
return [];
|
return [];
|
||||||
@ -65,15 +55,23 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Featured stickers are initialized with one sticker in collection (cover of SickerSet)
|
// Featured stickers are initialized with one sticker in collection (cover of SickerSet)
|
||||||
if (!areStickersLoaded && displayedStickers.length < STICKERS_TO_DISPLAY) {
|
if (!areStickersLoaded && displayedStickers.length < STICKERS_TO_DISPLAY && set) {
|
||||||
loadStickers({ stickerSetId });
|
loadStickers({
|
||||||
|
stickerSetInfo: {
|
||||||
|
shortName: set.shortName,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [areStickersLoaded, displayedStickers.length, loadStickers, stickerSetId]);
|
}, [areStickersLoaded, displayedStickers.length, loadStickers, set, stickerSetId]);
|
||||||
|
|
||||||
const handleAddClick = useCallback(() => {
|
const handleAddClick = useCallback(() => {
|
||||||
toggleStickerSet({ stickerSetId });
|
toggleStickerSet({ stickerSetId });
|
||||||
}, [toggleStickerSet, stickerSetId]);
|
}, [toggleStickerSet, stickerSetId]);
|
||||||
|
|
||||||
|
const handleStickerClick = useCallback((sticker: ApiSticker) => {
|
||||||
|
openStickerSet({ stickerSetInfo: sticker.stickerSetInfo });
|
||||||
|
}, [openStickerSet]);
|
||||||
|
|
||||||
if (!set) {
|
if (!set) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -105,21 +103,14 @@ const StickerSetResult: FC<OwnProps & StateProps> = ({
|
|||||||
sticker={sticker}
|
sticker={sticker}
|
||||||
size={STICKER_SIZE_SEARCH}
|
size={STICKER_SIZE_SEARCH}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
noAnimate={!shouldPlay || isModalOpen || isSomeModalOpen}
|
noAnimate={!shouldPlay || isModalOpen}
|
||||||
clickArg={undefined}
|
clickArg={sticker}
|
||||||
onClick={openModal}
|
onClick={handleStickerClick}
|
||||||
noContextMenu
|
noContextMenu
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{canRenderStickers && (
|
|
||||||
<StickerSetModal
|
|
||||||
isOpen={isModalOpen}
|
|
||||||
fromSticker={displayedStickers[0]}
|
|
||||||
onClose={closeModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -45,6 +45,8 @@
|
|||||||
transition: background-color 0.15s, color 0.15s;
|
transition: background-color 0.15s, color 0.15s;
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
|
|
||||||
|
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
||||||
|
|
||||||
// @optimization
|
// @optimization
|
||||||
&:active,
|
&:active,
|
||||||
&.clicked,
|
&.clicked,
|
||||||
@ -339,8 +341,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.shiny::before {
|
&.shiny::before {
|
||||||
position: absolute;
|
|
||||||
content: "";
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -359,4 +363,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.premium {
|
||||||
|
background: var(--premium-gradient);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ export type OwnProps = {
|
|||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
isRtl?: boolean;
|
isRtl?: boolean;
|
||||||
isShiny?: boolean;
|
isShiny?: boolean;
|
||||||
|
withPremiumGradient?: boolean;
|
||||||
noPreventDefault?: boolean;
|
noPreventDefault?: boolean;
|
||||||
shouldStopPropagation?: boolean;
|
shouldStopPropagation?: boolean;
|
||||||
style?: string;
|
style?: string;
|
||||||
@ -74,6 +75,7 @@ const Button: FC<OwnProps> = ({
|
|||||||
isText,
|
isText,
|
||||||
isLoading,
|
isLoading,
|
||||||
isShiny,
|
isShiny,
|
||||||
|
withPremiumGradient,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
ariaControls,
|
ariaControls,
|
||||||
hasPopup,
|
hasPopup,
|
||||||
@ -114,6 +116,7 @@ const Button: FC<OwnProps> = ({
|
|||||||
isClicked && 'clicked',
|
isClicked && 'clicked',
|
||||||
backgroundImage && 'with-image',
|
backgroundImage && 'with-image',
|
||||||
isShiny && 'shiny',
|
isShiny && 'shiny',
|
||||||
|
withPremiumGradient && 'premium',
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = useCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
const handleClick = useCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
|||||||
@ -94,4 +94,9 @@
|
|||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-loading-row {
|
||||||
|
margin: 0.125rem 1rem;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,4 +133,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wrap {
|
||||||
|
display: block;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.menu-custom-emoji-sets {
|
||||||
|
margin: 0 0.25rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: small;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ type OwnProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
destructive?: boolean;
|
destructive?: boolean;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
withWrap?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MenuItem: FC<OwnProps> = (props) => {
|
const MenuItem: FC<OwnProps> = (props) => {
|
||||||
@ -36,6 +37,7 @@ const MenuItem: FC<OwnProps> = (props) => {
|
|||||||
disabled,
|
disabled,
|
||||||
destructive,
|
destructive,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
withWrap,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@ -72,6 +74,7 @@ const MenuItem: FC<OwnProps> = (props) => {
|
|||||||
disabled && 'disabled',
|
disabled && 'disabled',
|
||||||
destructive && 'destructive',
|
destructive && 'destructive',
|
||||||
IS_COMPACT_MENU && 'compact',
|
IS_COMPACT_MENU && 'compact',
|
||||||
|
withWrap && 'wrap',
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
|
|||||||
@ -5,6 +5,12 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
&.round {
|
&.round {
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
@ -37,6 +43,7 @@
|
|||||||
&.wave::before {
|
&.wave::before {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(to right, transparent 0%, var(--color-skeleton-foreground) 50%, transparent 100%);
|
background: linear-gradient(to right, transparent 0%, var(--color-skeleton-foreground) 50%, transparent 100%);
|
||||||
|
|||||||
@ -12,6 +12,7 @@ type OwnProps = {
|
|||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
forceAspectRatio?: boolean;
|
forceAspectRatio?: boolean;
|
||||||
|
inline?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -21,14 +22,15 @@ const Skeleton: FC<OwnProps> = ({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
forceAspectRatio,
|
forceAspectRatio,
|
||||||
|
inline,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const classNames = buildClassName('Skeleton', variant, animation, className);
|
const classNames = buildClassName('Skeleton', variant, animation, className, inline && 'inline');
|
||||||
const aspectRatio = (width && height) ? `aspect-ratio: ${width}/${height}` : undefined;
|
const aspectRatio = (width && height) ? `aspect-ratio: ${width}/${height}` : undefined;
|
||||||
const style = forceAspectRatio ? aspectRatio
|
const style = forceAspectRatio ? aspectRatio
|
||||||
: buildStyle(Boolean(width) && `width: ${width}px`, Boolean(height) && `height: ${height}px`);
|
: buildStyle(Boolean(width) && `width: ${width}px`, Boolean(height) && `height: ${height}px`);
|
||||||
return (
|
return (
|
||||||
<div className={classNames} style={style} />
|
<div className={classNames} style={style}>{inline && '\u00A0'}</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';
|
|||||||
export const GLOBAL_STATE_CACHE_USER_LIST_LIMIT = 500;
|
export const GLOBAL_STATE_CACHE_USER_LIST_LIMIT = 500;
|
||||||
export const GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT = 200;
|
export const GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT = 200;
|
||||||
export const GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT = 30;
|
export const GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT = 30;
|
||||||
|
export const GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT = 150;
|
||||||
|
|
||||||
export const MEDIA_CACHE_DISABLED = false;
|
export const MEDIA_CACHE_DISABLED = false;
|
||||||
export const MEDIA_CACHE_NAME = 'tt-media';
|
export const MEDIA_CACHE_NAME = 'tt-media';
|
||||||
@ -134,10 +135,12 @@ export const STICKER_SIZE_INLINE_MOBILE_FACTOR = 11;
|
|||||||
export const STICKER_SIZE_AUTH = 160;
|
export const STICKER_SIZE_AUTH = 160;
|
||||||
export const STICKER_SIZE_AUTH_MOBILE = 120;
|
export const STICKER_SIZE_AUTH_MOBILE = 120;
|
||||||
export const STICKER_SIZE_PICKER = 64;
|
export const STICKER_SIZE_PICKER = 64;
|
||||||
|
export const EMOJI_SIZE_PICKER = 40;
|
||||||
export const STICKER_SIZE_GENERAL_SETTINGS = 48;
|
export const STICKER_SIZE_GENERAL_SETTINGS = 48;
|
||||||
export const STICKER_SIZE_PICKER_HEADER = 32;
|
export const STICKER_SIZE_PICKER_HEADER = 32;
|
||||||
export const STICKER_SIZE_SEARCH = 64;
|
export const STICKER_SIZE_SEARCH = 64;
|
||||||
export const STICKER_SIZE_MODAL = 64;
|
export const STICKER_SIZE_MODAL = 64;
|
||||||
|
export const EMOJI_SIZE_MODAL = 40;
|
||||||
export const STICKER_SIZE_TWO_FA = 160;
|
export const STICKER_SIZE_TWO_FA = 160;
|
||||||
export const STICKER_SIZE_PASSCODE = 160;
|
export const STICKER_SIZE_PASSCODE = 160;
|
||||||
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
|
||||||
@ -146,7 +149,6 @@ export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
|||||||
export const STICKER_SIZE_JOIN_REQUESTS = 140;
|
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 NO_STICKER_SET_ID = 'NO_STICKER_SET';
|
|
||||||
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';
|
||||||
|
|||||||
@ -615,8 +615,10 @@ addActionHandler('openTelegramLink', (global, actions, payload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (part1 === 'addstickers' || part1 === 'addemoji') {
|
if (part1 === 'addstickers' || part1 === 'addemoji') {
|
||||||
actions.openStickerSetShortName({
|
actions.openStickerSet({
|
||||||
stickerSetShortName: part2,
|
stickerSetInfo: {
|
||||||
|
shortName: part2,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1237,9 +1239,10 @@ export async function loadFullChat(chat: ApiChat) {
|
|||||||
const stickerSet = fullInfo.stickerSet;
|
const stickerSet = fullInfo.stickerSet;
|
||||||
if (stickerSet) {
|
if (stickerSet) {
|
||||||
getActions().loadStickers({
|
getActions().loadStickers({
|
||||||
stickerSetId: stickerSet.id,
|
stickerSetInfo: {
|
||||||
stickerSetAccessHash: stickerSet.accessHash,
|
id: stickerSet.id,
|
||||||
stickerSetShortName: stickerSet.shortName,
|
accessHash: stickerSet.accessHash,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,6 +69,7 @@ import {
|
|||||||
selectUser,
|
selectUser,
|
||||||
selectSendAs,
|
selectSendAs,
|
||||||
selectSponsoredMessage,
|
selectSponsoredMessage,
|
||||||
|
selectIsCurrentUserPremium,
|
||||||
selectForwardsContainVoiceMessages,
|
selectForwardsContainVoiceMessages,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import {
|
import {
|
||||||
@ -607,6 +608,7 @@ addActionHandler('forwardMessages', (global, action, payload) => {
|
|||||||
const {
|
const {
|
||||||
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions,
|
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions,
|
||||||
} = global.forwardMessages;
|
} = global.forwardMessages;
|
||||||
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||||
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
||||||
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
||||||
const messages = fromChatId && messageIds
|
const messages = fromChatId && messageIds
|
||||||
@ -635,6 +637,7 @@ addActionHandler('forwardMessages', (global, action, payload) => {
|
|||||||
withMyScore,
|
withMyScore,
|
||||||
noAuthors,
|
noAuthors,
|
||||||
noCaptions,
|
noCaptions,
|
||||||
|
isCurrentUserPremium,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -740,6 +743,28 @@ addActionHandler('transcribeAudio', async (global, actions, payload) => {
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadCustomEmojis', async (global, actions, payload) => {
|
||||||
|
const { ids, ignoreCache } = payload;
|
||||||
|
const newCustomEmojiIds = ignoreCache ? ids
|
||||||
|
: unique(ids.filter((documentId) => !global.customEmojis.byId[documentId]));
|
||||||
|
const customEmoji = await callApi('fetchCustomEmoji', {
|
||||||
|
documentId: newCustomEmojiIds,
|
||||||
|
});
|
||||||
|
if (!customEmoji) return;
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
byId: {
|
||||||
|
...global.customEmojis.byId,
|
||||||
|
...buildCollectionByKey(customEmoji, 'id'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function loadWebPagePreview(message: string) {
|
async function loadWebPagePreview(message: string) {
|
||||||
const webPagePreview = await callApi('fetchWebPagePreview', { message });
|
const webPagePreview = await callApi('fetchWebPagePreview', { message });
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import {
|
|||||||
addActionHandler, getActions, getGlobal, setGlobal,
|
addActionHandler, getActions, getGlobal, setGlobal,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
|
|
||||||
import type { ApiSticker } from '../../../api/types';
|
import type { ApiStickerSetInfo, ApiSticker } from '../../../api/types';
|
||||||
import type { LangCode } from '../../../types';
|
import type { LangCode } from '../../../types';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { onTickEnd, pause, throttle } from '../../../util/schedulers';
|
import { onTickEnd, pause, throttle } from '../../../util/schedulers';
|
||||||
@ -25,24 +25,39 @@ const ADDED_SETS_THROTTLE_CHUNK = 10;
|
|||||||
|
|
||||||
const searchThrottled = throttle((cb) => cb(), 500, false);
|
const searchThrottled = throttle((cb) => cb(), 500, false);
|
||||||
|
|
||||||
addActionHandler('loadStickerSets', (global) => {
|
addActionHandler('loadStickerSets', (global, actions) => {
|
||||||
const { hash } = global.stickers.added || {};
|
void loadStickerSets(global.stickers.added.hash);
|
||||||
void loadStickerSets(hash);
|
void loadCustomEmojiSets(global.customEmojis.added.hash);
|
||||||
|
actions.loadCustomEmojis({
|
||||||
|
ids: global.recentCustomEmojis,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadAddedStickers', async (global, actions) => {
|
addActionHandler('loadAddedStickers', async (global, actions) => {
|
||||||
const { setIds: addedSetIds } = global.stickers.added;
|
const {
|
||||||
const cached = global.stickers.setsById;
|
added: {
|
||||||
if (!addedSetIds || !addedSetIds.length) {
|
setIds: addedSetIds = [],
|
||||||
|
},
|
||||||
|
setsById: cached,
|
||||||
|
} = global.stickers;
|
||||||
|
const {
|
||||||
|
added: {
|
||||||
|
setIds: customEmojiSetIds = [],
|
||||||
|
},
|
||||||
|
} = global.customEmojis;
|
||||||
|
const setIdsToLoad = [...addedSetIds, ...customEmojiSetIds];
|
||||||
|
if (!setIdsToLoad.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < addedSetIds.length; i++) {
|
for (let i = 0; i < setIdsToLoad.length; i++) {
|
||||||
const id = addedSetIds[i];
|
const id = setIdsToLoad[i];
|
||||||
if (cached[id]?.stickers) {
|
if (cached[id]?.stickers) {
|
||||||
continue; // Already loaded
|
continue; // Already loaded
|
||||||
}
|
}
|
||||||
actions.loadStickers({ stickerSetId: id });
|
actions.loadStickers({
|
||||||
|
stickerSetInfo: { id, accessHash: cached[id].accessHash },
|
||||||
|
});
|
||||||
|
|
||||||
if (i % ADDED_SETS_THROTTLE_CHUNK === 0 && i > 0) {
|
if (i % ADDED_SETS_THROTTLE_CHUNK === 0 && i > 0) {
|
||||||
await pause(ADDED_SETS_THROTTLE);
|
await pause(ADDED_SETS_THROTTLE);
|
||||||
@ -82,6 +97,28 @@ addActionHandler('loadPremiumStickers', async (global) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadPremiumSetStickers', async (global) => {
|
||||||
|
const { hash } = global.stickers.premium || {};
|
||||||
|
|
||||||
|
const result = await callApi('fetchStickersForEmoji', { emoji: '📂⭐️', hash });
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
setGlobal({
|
||||||
|
...global,
|
||||||
|
stickers: {
|
||||||
|
...global.stickers,
|
||||||
|
premiumSet: {
|
||||||
|
hash: result.hash,
|
||||||
|
stickers: result.stickers,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('loadGreetingStickers', async (global) => {
|
addActionHandler('loadGreetingStickers', async (global) => {
|
||||||
const { hash } = global.stickers.greeting || {};
|
const { hash } = global.stickers.greeting || {};
|
||||||
|
|
||||||
@ -124,25 +161,10 @@ addActionHandler('loadPremiumGifts', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadStickers', (global, actions, payload) => {
|
addActionHandler('loadStickers', (global, actions, payload) => {
|
||||||
const { stickerSetId, stickerSetShortName } = payload!;
|
const { stickerSetInfo } = payload;
|
||||||
let { stickerSetAccessHash } = payload!;
|
const cachedSet = selectStickerSet(global, stickerSetInfo);
|
||||||
|
if (cachedSet && cachedSet.count === cachedSet?.stickers?.length) return; // Already fully loaded
|
||||||
if (!stickerSetAccessHash && !stickerSetShortName) {
|
void loadStickers(stickerSetInfo);
|
||||||
const stickerSet = selectStickerSet(global, stickerSetId);
|
|
||||||
if (!stickerSet) {
|
|
||||||
if (global.openedStickerSetShortName === stickerSetShortName) {
|
|
||||||
setGlobal({
|
|
||||||
...global,
|
|
||||||
openedStickerSetShortName: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stickerSetAccessHash = stickerSet.accessHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadStickers(stickerSetId, stickerSetAccessHash!, stickerSetShortName);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadAnimatedEmojis', () => {
|
addActionHandler('loadAnimatedEmojis', () => {
|
||||||
@ -323,6 +345,20 @@ addActionHandler('loadEmojiKeywords', async (global, actions, payload: { languag
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadCustomEmojiSets(hash?: string) {
|
||||||
|
const addedCustomEmojis = await callApi('fetchCustomEmojiSets', { hash });
|
||||||
|
if (!addedCustomEmojis) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGlobal(updateStickerSets(
|
||||||
|
getGlobal(),
|
||||||
|
'added',
|
||||||
|
addedCustomEmojis.hash,
|
||||||
|
addedCustomEmojis.sets,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStickerSets(hash?: string) {
|
async function loadStickerSets(hash?: string) {
|
||||||
const addedStickers = await callApi('fetchStickerSets', { hash });
|
const addedStickers = await callApi('fetchStickerSets', { hash });
|
||||||
if (!addedStickers) {
|
if (!addedStickers) {
|
||||||
@ -385,10 +421,10 @@ async function loadFeaturedStickers(hash?: string) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStickers(stickerSetId: string, accessHash: string, stickerSetShortName?: string) {
|
async function loadStickers(stickerSetInfo: ApiStickerSetInfo) {
|
||||||
const stickerSet = await callApi(
|
const stickerSet = await callApi(
|
||||||
'fetchStickers',
|
'fetchStickers',
|
||||||
{ stickerSetShortName, stickerSetId, accessHash },
|
{ stickerSetInfo },
|
||||||
);
|
);
|
||||||
let global = getGlobal();
|
let global = getGlobal();
|
||||||
|
|
||||||
@ -398,7 +434,7 @@ async function loadStickers(stickerSetId: string, accessHash: string, stickerSet
|
|||||||
message: getTranslation('StickerPack.ErrorNotFound'),
|
message: getTranslation('StickerPack.ErrorNotFound'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (global.openedStickerSetShortName === stickerSetShortName) {
|
if ('shortName' in stickerSetInfo && global.openedStickerSetShortName === stickerSetInfo.shortName) {
|
||||||
setGlobal({
|
setGlobal({
|
||||||
...global,
|
...global,
|
||||||
openedStickerSetShortName: undefined,
|
openedStickerSetShortName: undefined,
|
||||||
@ -494,7 +530,7 @@ addActionHandler('searchMoreGifs', (global) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadStickersForEmoji', (global, actions, payload) => {
|
addActionHandler('loadStickersForEmoji', (global, actions, payload) => {
|
||||||
const { emoji } = payload!;
|
const { emoji } = payload;
|
||||||
const { hash } = global.stickers.forEmoji;
|
const { hash } = global.stickers.forEmoji;
|
||||||
|
|
||||||
void searchThrottled(() => {
|
void searchThrottled(() => {
|
||||||
@ -512,31 +548,18 @@ addActionHandler('clearStickersForEmoji', (global) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('openStickerSetShortName', (global, actions, payload) => {
|
|
||||||
const { stickerSetShortName } = payload;
|
|
||||||
return {
|
|
||||||
...global,
|
|
||||||
openedStickerSetShortName: stickerSetShortName,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('openStickerSet', async (global, actions, payload) => {
|
addActionHandler('openStickerSet', async (global, actions, payload) => {
|
||||||
const { sticker } = payload;
|
const { stickerSetInfo } = payload;
|
||||||
|
if (!selectStickerSet(global, stickerSetInfo)) {
|
||||||
if (!selectStickerSet(global, sticker.stickerSetId)) {
|
await loadStickers(stickerSetInfo);
|
||||||
if (!sticker.stickerSetAccessHash) {
|
|
||||||
actions.showNotification({
|
|
||||||
message: getTranslation('StickerPack.ErrorNotFound'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await loadStickers(sticker.stickerSetId, sticker.stickerSetAccessHash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
const set = selectStickerSet(global, sticker.stickerSetId);
|
const set = selectStickerSet(global, stickerSetInfo);
|
||||||
if (!set?.shortName) {
|
if (!set?.shortName) {
|
||||||
|
actions.showNotification({
|
||||||
|
message: getTranslation('StickerPack.ErrorNotFound'),
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { init as initFolderManager } from '../../../util/folderManager';
|
import { init as initFolderManager } from '../../../util/folderManager';
|
||||||
|
|
||||||
const RELEASE_STATUS_TIMEOUT = 15000; // 10 sec;
|
const RELEASE_STATUS_TIMEOUT = 15000; // 15 sec;
|
||||||
|
|
||||||
let releaseStatusTimeout: number | undefined;
|
let releaseStatusTimeout: number | undefined;
|
||||||
|
|
||||||
|
|||||||
@ -37,7 +37,7 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'updateStickerSetsOrder':
|
case 'updateStickerSetsOrder':
|
||||||
actions.reorderStickerSets({ order: update.order });
|
actions.reorderStickerSets({ order: update.order, isCustomEmoji: update.isCustomEmoji });
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'updateSavedGifs':
|
case 'updateSavedGifs':
|
||||||
|
|||||||
@ -2,14 +2,16 @@ import { addActionHandler, setGlobal } from '../../index';
|
|||||||
|
|
||||||
import type { ApiError } from '../../../api/types';
|
import type { ApiError } from '../../../api/types';
|
||||||
|
|
||||||
|
import { GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT } from '../../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import getReadableErrorText from '../../../util/getReadableErrorText';
|
import getReadableErrorText from '../../../util/getReadableErrorText';
|
||||||
import {
|
import {
|
||||||
selectChatMessage, selectCurrentMessageList, selectIsTrustedBot,
|
selectChatMessage, selectCurrentMessageList, selectIsTrustedBot,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import generateIdFor from '../../../util/generateIdFor';
|
import generateIdFor from '../../../util/generateIdFor';
|
||||||
|
import { unique } from '../../../util/iteratees';
|
||||||
|
|
||||||
const MAX_STORED_EMOJIS = 18; // Represents two rows of recent emojis
|
const MAX_STORED_EMOJIS = 8 * 4; // Represents four rows of recent emojis
|
||||||
|
|
||||||
addActionHandler('toggleChatInfo', (global, action, payload) => {
|
addActionHandler('toggleChatInfo', (global, action, payload) => {
|
||||||
return {
|
return {
|
||||||
@ -139,7 +141,7 @@ addActionHandler('toggleLeftColumn', (global) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('addRecentEmoji', (global, action, payload) => {
|
addActionHandler('addRecentEmoji', (global, action, payload) => {
|
||||||
const { emoji } = payload!;
|
const { emoji } = payload;
|
||||||
const { recentEmojis } = global;
|
const { recentEmojis } = global;
|
||||||
if (!recentEmojis) {
|
if (!recentEmojis) {
|
||||||
return {
|
return {
|
||||||
@ -192,12 +194,12 @@ addActionHandler('addRecentSticker', (global, action, payload) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('reorderStickerSets', (global, action, payload) => {
|
addActionHandler('reorderStickerSets', (global, action, payload) => {
|
||||||
const { order } = payload;
|
const { order, isCustomEmoji } = payload;
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
stickers: {
|
stickers: {
|
||||||
...global.stickers,
|
...global.stickers,
|
||||||
added: {
|
[isCustomEmoji ? 'customEmoji' : 'added']: {
|
||||||
setIds: order,
|
setIds: order,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -368,3 +370,38 @@ addActionHandler('closeLimitReachedModal', (global) => {
|
|||||||
limitReachedModal: undefined,
|
limitReachedModal: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeStickerSetModal', (global) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
openedStickerSetShortName: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('openCustomEmojiSets', (global, actions, payload) => {
|
||||||
|
const { setIds } = payload;
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
openedCustomEmojiSetIds: setIds,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeCustomEmojiSets', (global) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
openedCustomEmojiSetIds: undefined,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('updateLastRenderedCustomEmojis', (global, actions, payload) => {
|
||||||
|
const { ids } = payload;
|
||||||
|
const { lastRendered } = global.customEmojis;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
lastRendered: unique([...lastRendered, ...ids]).slice(0, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
ALL_FOLDER_ID,
|
ALL_FOLDER_ID,
|
||||||
ARCHIVED_FOLDER_ID,
|
ARCHIVED_FOLDER_ID,
|
||||||
DEFAULT_LIMITS,
|
DEFAULT_LIMITS,
|
||||||
|
GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT,
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
|
||||||
import { isHeavyAnimating } from '../hooks/useHeavyAnimationCheck';
|
import { isHeavyAnimating } from '../hooks/useHeavyAnimationCheck';
|
||||||
@ -272,6 +273,20 @@ export function migrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
if (cached.appConfig && !cached.appConfig.limits) {
|
if (cached.appConfig && !cached.appConfig.limits) {
|
||||||
cached.appConfig.limits = DEFAULT_LIMITS;
|
cached.appConfig.limits = DEFAULT_LIMITS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.customEmojis) {
|
||||||
|
cached.customEmojis = {
|
||||||
|
added: {},
|
||||||
|
byId: {},
|
||||||
|
lastRendered: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cached.stickers.premiumSet) {
|
||||||
|
cached.stickers.premiumSet = {
|
||||||
|
stickers: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache() {
|
function updateCache() {
|
||||||
@ -316,6 +331,7 @@ export function serializeGlobal(global: GlobalState) {
|
|||||||
'topPeers',
|
'topPeers',
|
||||||
'topInlineBots',
|
'topInlineBots',
|
||||||
'recentEmojis',
|
'recentEmojis',
|
||||||
|
'recentCustomEmojis',
|
||||||
'push',
|
'push',
|
||||||
'shouldShowContextMenuHint',
|
'shouldShowContextMenuHint',
|
||||||
'leftColumnWidth',
|
'leftColumnWidth',
|
||||||
@ -326,6 +342,7 @@ export function serializeGlobal(global: GlobalState) {
|
|||||||
playbackRate: global.audioPlayer.playbackRate,
|
playbackRate: global.audioPlayer.playbackRate,
|
||||||
isMuted: global.audioPlayer.isMuted,
|
isMuted: global.audioPlayer.isMuted,
|
||||||
},
|
},
|
||||||
|
customEmojis: reduceCustomEmojis(global),
|
||||||
mediaViewer: {
|
mediaViewer: {
|
||||||
volume: global.mediaViewer.volume,
|
volume: global.mediaViewer.volume,
|
||||||
playbackRate: global.mediaViewer.playbackRate,
|
playbackRate: global.mediaViewer.playbackRate,
|
||||||
@ -354,6 +371,18 @@ export function serializeGlobal(global: GlobalState) {
|
|||||||
return JSON.stringify(reducedGlobal);
|
return JSON.stringify(reducedGlobal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reduceCustomEmojis(global: GlobalState): GlobalState['customEmojis'] {
|
||||||
|
const { lastRendered, byId } = global.customEmojis;
|
||||||
|
const idsToSave = lastRendered.slice(0, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT);
|
||||||
|
const byIdToSave = pick(byId, idsToSave);
|
||||||
|
|
||||||
|
return {
|
||||||
|
byId: byIdToSave,
|
||||||
|
lastRendered: idsToSave,
|
||||||
|
added: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function reduceShowChatInfo(global: GlobalState): boolean {
|
function reduceShowChatInfo(global: GlobalState): boolean {
|
||||||
return window.innerWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
return window.innerWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
||||||
? global.isChatInfoShown
|
? global.isChatInfoShown
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiChat, ApiMessage, ApiReactions, ApiUser,
|
ApiChat, ApiMessage, ApiMessageEntityTextUrl, ApiReactions, ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import { ApiMessageEntityTypes } from '../../api/types';
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
import type { LangFn } from '../../hooks/useLang';
|
import type { LangFn } from '../../hooks/useLang';
|
||||||
@ -78,7 +78,7 @@ export function getMessageCustomShape(message: ApiMessage): boolean | number {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!text || photo || video || audio || voice || document || poll || webPage || contact) {
|
if (!text || text.entities?.length || photo || video || audio || voice || document || poll || webPage || contact) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ export function getMessageCustomShape(message: ApiMessage): boolean | number {
|
|||||||
|
|
||||||
export function getMessageSingleEmoji(message: ApiMessage) {
|
export function getMessageSingleEmoji(message: ApiMessage) {
|
||||||
const { text } = message.content;
|
const { text } = message.content;
|
||||||
if (!(text && text.text.length <= 6)) {
|
if (!(text && text.text.length <= 6) || text.entities?.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,15 +104,17 @@ export function getFirstLinkInMessage(message: ApiMessage) {
|
|||||||
|
|
||||||
let match: RegExpMatchArray | null | undefined;
|
let match: RegExpMatchArray | null | undefined;
|
||||||
if (text?.entities) {
|
if (text?.entities) {
|
||||||
let link = text.entities.find((entity) => entity.type === ApiMessageEntityTypes.TextUrl);
|
const firstTextUrl = text.entities.find((entity): entity is ApiMessageEntityTextUrl => (
|
||||||
if (link) {
|
entity.type === ApiMessageEntityTypes.TextUrl
|
||||||
match = link.url!.match(RE_LINK);
|
));
|
||||||
|
if (firstTextUrl) {
|
||||||
|
match = firstTextUrl.url.match(RE_LINK);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
link = text.entities.find((entity) => entity.type === ApiMessageEntityTypes.Url);
|
const firstUrl = text.entities.find((entity) => entity.type === ApiMessageEntityTypes.Url);
|
||||||
if (link) {
|
if (firstUrl) {
|
||||||
const { offset, length } = link;
|
const { offset, length } = firstUrl;
|
||||||
match = text.text.substring(offset, offset + length).match(RE_LINK);
|
match = text.text.substring(offset, offset + length).match(RE_LINK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,7 +63,8 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
byMessageLocalId: {},
|
byMessageLocalId: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
recentEmojis: ['grinning', 'kissing_heart', 'christmas_tree', 'brain', 'trophy'],
|
recentEmojis: ['grinning', 'kissing_heart', 'christmas_tree', 'brain', 'trophy', 'duck', 'cherries'],
|
||||||
|
recentCustomEmojis: ['5377305978079288312'],
|
||||||
|
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: {},
|
setsById: {},
|
||||||
@ -80,6 +81,9 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
premium: {
|
premium: {
|
||||||
stickers: [],
|
stickers: [],
|
||||||
},
|
},
|
||||||
|
premiumSet: {
|
||||||
|
stickers: [],
|
||||||
|
},
|
||||||
featured: {
|
featured: {
|
||||||
setIds: [],
|
setIds: [],
|
||||||
},
|
},
|
||||||
@ -87,6 +91,12 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
forEmoji: {},
|
forEmoji: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
customEmojis: {
|
||||||
|
lastRendered: [],
|
||||||
|
byId: {},
|
||||||
|
added: {},
|
||||||
|
},
|
||||||
|
|
||||||
emojiKeywords: {},
|
emojiKeywords: {},
|
||||||
|
|
||||||
gifs: {
|
gifs: {
|
||||||
|
|||||||
@ -22,6 +22,13 @@ export function updateStickerSets(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const regularSetIds = sets.filter((set) => !set.isEmoji).map((set) => set.id);
|
||||||
|
const addedEmojiSetIds = category === 'added' ? sets.filter((set) => set.isEmoji).map((set) => set.id) : [];
|
||||||
|
const customEmojis = sets.filter((set) => set.isEmoji)
|
||||||
|
.map((set) => set.stickers)
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
stickers: {
|
stickers: {
|
||||||
@ -36,10 +43,30 @@ export function updateStickerSets(
|
|||||||
...(
|
...(
|
||||||
category === 'search'
|
category === 'search'
|
||||||
? { resultIds }
|
? { resultIds }
|
||||||
: { setIds: sets.map(({ id }) => id) }
|
: {
|
||||||
|
setIds: [
|
||||||
|
...(global.stickers[category].setIds || []),
|
||||||
|
...regularSetIds,
|
||||||
|
],
|
||||||
|
}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
added: {
|
||||||
|
...global.customEmojis.added,
|
||||||
|
hash,
|
||||||
|
setIds: [
|
||||||
|
...(global.customEmojis.added.setIds || []),
|
||||||
|
...addedEmojiSetIds,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
byId: {
|
||||||
|
...global.customEmojis.byId,
|
||||||
|
...buildCollectionByKey(customEmojis, 'id'),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,7 +74,8 @@ export function updateStickerSet(
|
|||||||
global: GlobalState, stickerSetId: string, update: Partial<ApiStickerSet>,
|
global: GlobalState, stickerSetId: string, update: Partial<ApiStickerSet>,
|
||||||
): GlobalState {
|
): GlobalState {
|
||||||
const currentStickerSet = global.stickers.setsById[stickerSetId] || {};
|
const currentStickerSet = global.stickers.setsById[stickerSetId] || {};
|
||||||
const addedSets = global.stickers.added.setIds || [];
|
const isCustomEmoji = update.isEmoji || currentStickerSet.isEmoji;
|
||||||
|
const addedSets = (isCustomEmoji ? global.customEmojis.added.setIds : global.stickers.added.setIds) || [];
|
||||||
let setIds: string[] = addedSets;
|
let setIds: string[] = addedSets;
|
||||||
if (update.installedDate && addedSets && !addedSets.includes(stickerSetId)) {
|
if (update.installedDate && addedSets && !addedSets.includes(stickerSetId)) {
|
||||||
setIds = [stickerSetId, ...setIds];
|
setIds = [stickerSetId, ...setIds];
|
||||||
@ -57,13 +85,16 @@ export function updateStickerSet(
|
|||||||
setIds = setIds.filter((id) => id !== stickerSetId);
|
setIds = setIds.filter((id) => id !== stickerSetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const customEmojiById = isCustomEmoji && currentStickerSet.stickers
|
||||||
|
&& buildCollectionByKey(currentStickerSet.stickers, 'id');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...global,
|
...global,
|
||||||
stickers: {
|
stickers: {
|
||||||
...global.stickers,
|
...global.stickers,
|
||||||
added: {
|
added: {
|
||||||
...global.stickers.added,
|
...global.stickers.added,
|
||||||
setIds,
|
...(!isCustomEmoji && { setIds }),
|
||||||
},
|
},
|
||||||
setsById: {
|
setsById: {
|
||||||
...global.stickers.setsById,
|
...global.stickers.setsById,
|
||||||
@ -73,6 +104,17 @@ export function updateStickerSet(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
byId: {
|
||||||
|
...global.customEmojis.byId,
|
||||||
|
...customEmojiById,
|
||||||
|
},
|
||||||
|
added: {
|
||||||
|
...global.customEmojis.added,
|
||||||
|
...(isCustomEmoji && { setIds }),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +177,14 @@ export function updateStickersForEmoji(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function rebuildStickersForEmoji(global: GlobalState): GlobalState {
|
export function rebuildStickersForEmoji(global: GlobalState): GlobalState {
|
||||||
const { emoji, stickers, hash } = global.stickers.forEmoji || {};
|
if (global.stickers.forEmoji) {
|
||||||
if (!emoji) {
|
const { emoji, stickers, hash } = global.stickers.forEmoji;
|
||||||
return global;
|
if (!emoji) {
|
||||||
|
return global;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateStickersForEmoji(global, emoji, stickers, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateStickersForEmoji(global, emoji, stickers, hash);
|
return global;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import type { GlobalState, MessageListType, Thread } from '../types';
|
import type { GlobalState, MessageListType, Thread } from '../types';
|
||||||
import type {
|
import type {
|
||||||
ApiChat,
|
ApiChat,
|
||||||
|
ApiStickerSetInfo,
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
|
ApiMessageEntityCustomEmoji,
|
||||||
ApiMessageOutgoingStatus,
|
ApiMessageOutgoingStatus,
|
||||||
ApiUser,
|
ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import {
|
import {
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
|
ApiMessageEntityTypes,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
|
|
||||||
import { LOCAL_MESSAGE_MIN_ID, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
import { LOCAL_MESSAGE_MIN_ID, REPLIES_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
||||||
@ -976,6 +979,40 @@ export function selectCanScheduleUntilOnline(global: GlobalState, id: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectCustomEmojis(message: ApiMessage) {
|
||||||
|
const entities = message.content.text?.entities;
|
||||||
|
return entities?.filter((entity): entity is ApiMessageEntityCustomEmoji => (
|
||||||
|
entity.type === ApiMessageEntityTypes.CustomEmoji
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectMessageCustomEmojiSets(
|
||||||
|
global: GlobalState, message: ApiMessage,
|
||||||
|
): ApiStickerSetInfo[] | undefined {
|
||||||
|
const customEmojis = selectCustomEmojis(message);
|
||||||
|
if (!customEmojis) return MEMO_EMPTY_ARRAY;
|
||||||
|
const documents = customEmojis.map((entity) => global.customEmojis.byId[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);
|
||||||
|
const setsWithoutDuplicates = sets.reduce((acc, set) => {
|
||||||
|
if ('shortName' in set) {
|
||||||
|
if (acc.some((s) => 'shortName' in s && s.shortName === set.shortName)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('id' in set) {
|
||||||
|
if (acc.some((s) => 'id' in s && s.id === set.id)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
acc.push(set); // Optimization
|
||||||
|
return acc;
|
||||||
|
}, [] as ApiStickerSetInfo[]);
|
||||||
|
return setsWithoutDuplicates;
|
||||||
|
}
|
||||||
|
|
||||||
export function selectForwardsContainVoiceMessages(global: GlobalState) {
|
export function selectForwardsContainVoiceMessages(global: GlobalState) {
|
||||||
const { messageIds, fromChatId } = global.forwardMessages;
|
const { messageIds, fromChatId } = global.forwardMessages;
|
||||||
if (!messageIds) return false;
|
if (!messageIds) return false;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import type { GlobalState } from '../types';
|
import type { GlobalState } from '../types';
|
||||||
import type { ApiSticker } from '../../api/types';
|
import type { ApiStickerSetInfo, ApiSticker, ApiStickerSet } from '../../api/types';
|
||||||
|
|
||||||
|
import { selectIsCurrentUserPremium } from './users';
|
||||||
|
|
||||||
export function selectIsStickerFavorite(global: GlobalState, sticker: ApiSticker) {
|
export function selectIsStickerFavorite(global: GlobalState, sticker: ApiSticker) {
|
||||||
const { stickers } = global.stickers.favorite;
|
const { stickers } = global.stickers.favorite;
|
||||||
@ -14,16 +16,24 @@ export function selectCurrentGifSearch(global: GlobalState) {
|
|||||||
return global.gifs.search;
|
return global.gifs.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectStickerSet(global: GlobalState, id: string) {
|
export function selectStickerSet(global: GlobalState, id: string | ApiStickerSetInfo) {
|
||||||
return global.stickers.setsById[id];
|
if (typeof id === 'string') {
|
||||||
}
|
return global.stickers.setsById[id];
|
||||||
|
}
|
||||||
|
|
||||||
export function selectStickerSetByShortName(global: GlobalState, shortName: string) {
|
if ('id' in id) {
|
||||||
return Object.values(global.stickers.setsById).find((l) => l.shortName.toLowerCase() === shortName.toLowerCase());
|
return global.stickers.setsById[id.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('isMissing' in id) return undefined;
|
||||||
|
|
||||||
|
return Object.values(global.stickers.setsById).find(({ shortName }) => (
|
||||||
|
shortName.toLowerCase() === id.shortName.toLowerCase()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectStickersForEmoji(global: GlobalState, emoji: string) {
|
export function selectStickersForEmoji(global: GlobalState, emoji: string) {
|
||||||
const stickerSets = Object.values(global.stickers.setsById);
|
const addedSets = global.stickers.added.setIds;
|
||||||
let stickersForEmoji: ApiSticker[] = [];
|
let stickersForEmoji: ApiSticker[] = [];
|
||||||
// Favorites
|
// Favorites
|
||||||
global.stickers.favorite.stickers.forEach((sticker) => {
|
global.stickers.favorite.stickers.forEach((sticker) => {
|
||||||
@ -31,7 +41,8 @@ export function selectStickersForEmoji(global: GlobalState, emoji: string) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Added sets
|
// Added sets
|
||||||
stickerSets.forEach(({ packs }) => {
|
addedSets?.forEach((id) => {
|
||||||
|
const packs = global.stickers.setsById[id].packs;
|
||||||
if (!packs) {
|
if (!packs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,6 +52,27 @@ export function selectStickersForEmoji(global: GlobalState, emoji: string) {
|
|||||||
return stickersForEmoji;
|
return stickersForEmoji;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectCustomEmojiForEmoji(global: GlobalState, emoji: string) {
|
||||||
|
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||||
|
const addedCustomSets = global.customEmojis.added.setIds;
|
||||||
|
let customEmojiForEmoji: ApiSticker[] = [];
|
||||||
|
|
||||||
|
// Added sets
|
||||||
|
addedCustomSets?.forEach((id) => {
|
||||||
|
const packs = global.stickers.setsById[id].packs;
|
||||||
|
if (!packs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customEmojiForEmoji = customEmojiForEmoji.concat(packs[emoji] || [], packs[cleanEmoji(emoji)] || []);
|
||||||
|
});
|
||||||
|
return isCurrentUserPremium ? customEmojiForEmoji : customEmojiForEmoji.filter(({ isFree }) => isFree);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectIsSetPremium(stickerSet: ApiStickerSet) {
|
||||||
|
return stickerSet.isEmoji && stickerSet.stickers?.some((sticker) => !sticker.isFree);
|
||||||
|
}
|
||||||
|
|
||||||
function cleanEmoji(emoji: string) {
|
function cleanEmoji(emoji: string) {
|
||||||
// Some emojis (❤️ for example) with a service symbol 'VARIATION SELECTOR-16' are not recognized as animated
|
// Some emojis (❤️ for example) with a service symbol 'VARIATION SELECTOR-16' are not recognized as animated
|
||||||
return emoji.replace('\ufe0f', '');
|
return emoji.replace('\ufe0f', '');
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import type {
|
|||||||
ApiTranscription,
|
ApiTranscription,
|
||||||
ApiInputInvoice,
|
ApiInputInvoice,
|
||||||
ApiInvoice,
|
ApiInvoice,
|
||||||
|
ApiStickerSetInfo,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import type {
|
import type {
|
||||||
FocusDirection,
|
FocusDirection,
|
||||||
@ -274,6 +275,7 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
recentEmojis: string[];
|
recentEmojis: string[];
|
||||||
|
recentCustomEmojis: string[];
|
||||||
|
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: Record<string, ApiStickerSet>;
|
setsById: Record<string, ApiStickerSet>;
|
||||||
@ -297,6 +299,10 @@ export type GlobalState = {
|
|||||||
hash?: string;
|
hash?: string;
|
||||||
stickers: ApiSticker[];
|
stickers: ApiSticker[];
|
||||||
};
|
};
|
||||||
|
premiumSet: {
|
||||||
|
hash?: string;
|
||||||
|
stickers: ApiSticker[];
|
||||||
|
};
|
||||||
featured: {
|
featured: {
|
||||||
hash?: string;
|
hash?: string;
|
||||||
setIds?: string[];
|
setIds?: string[];
|
||||||
@ -312,6 +318,15 @@ export type GlobalState = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
customEmojis: {
|
||||||
|
added: {
|
||||||
|
hash?: string;
|
||||||
|
setIds?: string[];
|
||||||
|
};
|
||||||
|
lastRendered: string[];
|
||||||
|
byId: Record<string, ApiSticker>;
|
||||||
|
};
|
||||||
|
|
||||||
animatedEmojis?: ApiStickerSet;
|
animatedEmojis?: ApiStickerSet;
|
||||||
animatedEmojiEffects?: ApiStickerSet;
|
animatedEmojiEffects?: ApiStickerSet;
|
||||||
premiumGifts?: ApiStickerSet;
|
premiumGifts?: ApiStickerSet;
|
||||||
@ -542,6 +557,7 @@ export type GlobalState = {
|
|||||||
safeLinkModalUrl?: string;
|
safeLinkModalUrl?: string;
|
||||||
historyCalendarSelectedAt?: number;
|
historyCalendarSelectedAt?: number;
|
||||||
openedStickerSetShortName?: string;
|
openedStickerSetShortName?: string;
|
||||||
|
openedCustomEmojiSetIds?: string[];
|
||||||
|
|
||||||
activeDownloads: {
|
activeDownloads: {
|
||||||
byChatId: Record<string, number[]>;
|
byChatId: Record<string, number[]>;
|
||||||
@ -857,7 +873,16 @@ export interface ActionPayloads {
|
|||||||
exitForwardMode: never;
|
exitForwardMode: never;
|
||||||
changeForwardRecipient: never;
|
changeForwardRecipient: never;
|
||||||
|
|
||||||
|
// GIFs
|
||||||
|
loadSavedGifs: never;
|
||||||
|
|
||||||
// Stickers
|
// Stickers
|
||||||
|
loadStickers: {
|
||||||
|
stickerSetInfo: ApiStickerSetInfo;
|
||||||
|
};
|
||||||
|
loadAnimatedEmojis: never;
|
||||||
|
loadGreetingStickers: never;
|
||||||
|
|
||||||
addRecentSticker: {
|
addRecentSticker: {
|
||||||
sticker: ApiSticker;
|
sticker: ApiSticker;
|
||||||
};
|
};
|
||||||
@ -866,15 +891,16 @@ export interface ActionPayloads {
|
|||||||
sticker: ApiSticker;
|
sticker: ApiSticker;
|
||||||
};
|
};
|
||||||
|
|
||||||
clearRecentStickers: {};
|
clearRecentStickers: never;
|
||||||
|
|
||||||
loadStickerSets: {};
|
loadStickerSets: never;
|
||||||
loadAddedStickers: {};
|
loadAddedStickers: never;
|
||||||
loadRecentStickers: {};
|
loadRecentStickers: never;
|
||||||
loadFavoriteStickers: {};
|
loadFavoriteStickers: never;
|
||||||
loadFeaturedStickers: {};
|
loadFeaturedStickers: never;
|
||||||
|
|
||||||
reorderStickerSets: {
|
reorderStickerSets: {
|
||||||
|
isCustomEmoji?: boolean;
|
||||||
order: string[];
|
order: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -882,13 +908,31 @@ export interface ActionPayloads {
|
|||||||
stickerSet: ApiStickerSet;
|
stickerSet: ApiStickerSet;
|
||||||
};
|
};
|
||||||
|
|
||||||
openStickerSetShortName: {
|
openStickerSet: {
|
||||||
stickerSetShortName?: string;
|
stickerSetInfo: ApiStickerSetInfo;
|
||||||
|
};
|
||||||
|
closeStickerSetModal: never;
|
||||||
|
|
||||||
|
loadStickersForEmoji: {
|
||||||
|
emoji: string;
|
||||||
|
};
|
||||||
|
clearStickersForEmoji: never;
|
||||||
|
|
||||||
|
addRecentEmoji: {
|
||||||
|
emoji: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
openStickerSet: {
|
loadCustomEmojis: {
|
||||||
sticker: ApiSticker;
|
ids: string[];
|
||||||
|
ignoreCache?: boolean;
|
||||||
};
|
};
|
||||||
|
updateLastRenderedCustomEmojis: {
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
openCustomEmojiSets: {
|
||||||
|
setIds: string[];
|
||||||
|
};
|
||||||
|
closeCustomEmojiSets: never;
|
||||||
|
|
||||||
// Bots
|
// Bots
|
||||||
startBot: {
|
startBot: {
|
||||||
@ -1091,6 +1135,9 @@ export interface ActionPayloads {
|
|||||||
loadPremiumStickers: {
|
loadPremiumStickers: {
|
||||||
hash?: string;
|
hash?: string;
|
||||||
};
|
};
|
||||||
|
loadPremiumSetStickers: {
|
||||||
|
hash?: string;
|
||||||
|
};
|
||||||
|
|
||||||
openGiftPremiumModal: {
|
openGiftPremiumModal: {
|
||||||
forUserId?: string;
|
forUserId?: string;
|
||||||
@ -1107,7 +1154,7 @@ export type NonTypedActionNames = (
|
|||||||
'init' | 'reset' | 'disconnect' | 'initApi' | 'sync' | 'saveSession' |
|
'init' | 'reset' | 'disconnect' | 'initApi' | 'sync' | 'saveSession' |
|
||||||
'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' |
|
'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' |
|
||||||
// ui
|
// ui
|
||||||
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'toggleLeftColumn' |
|
'toggleChatInfo' | 'setIsUiReady' | 'toggleLeftColumn' |
|
||||||
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||||
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
'setNewChatMembersDialogState' | 'disableHistoryAnimations' | 'setLeftColumnWidth' | 'resetLeftColumnWidth' |
|
||||||
'openSeenByModal' | 'closeSeenByModal' | 'closeReactorListModal' | 'openReactorListModal' |
|
'openSeenByModal' | 'closeSeenByModal' | 'closeReactorListModal' | 'openReactorListModal' |
|
||||||
@ -1172,9 +1219,9 @@ export type NonTypedActionNames = (
|
|||||||
'loadContentSettings' | 'updateContentSettings' |
|
'loadContentSettings' | 'updateContentSettings' |
|
||||||
'loadCountryList' | 'ensureTimeFormat' | 'loadAppConfig' |
|
'loadCountryList' | 'ensureTimeFormat' | 'loadAppConfig' |
|
||||||
// stickers & GIFs
|
// stickers & GIFs
|
||||||
'setStickerSearchQuery' | 'loadSavedGifs' | 'saveGif' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
'setStickerSearchQuery' | 'saveGif' | 'setGifSearchQuery' | 'searchMoreGifs' |
|
||||||
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' | 'loadStickers' |
|
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' |
|
||||||
'loadStickersForEmoji' | 'clearStickersForEmoji' | 'loadEmojiKeywords' | 'loadGreetingStickers' |
|
'loadEmojiKeywords' |
|
||||||
// bots
|
// bots
|
||||||
'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
|
||||||
'resetInlineBot' |
|
'resetInlineBot' |
|
||||||
|
|||||||
33
src/hooks/useEnsureCustomEmoji.ts
Normal file
33
src/hooks/useEnsureCustomEmoji.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { getActions, getGlobal } from '../global';
|
||||||
|
import { throttle } from '../util/schedulers';
|
||||||
|
|
||||||
|
const LOAD_QUEUE = new Set<string>();
|
||||||
|
const RENDER_HISTORY = new Set<string>();
|
||||||
|
const THROTTLE = 200;
|
||||||
|
|
||||||
|
const loadFromQueue = throttle(() => {
|
||||||
|
getActions().loadCustomEmojis({
|
||||||
|
ids: [...LOAD_QUEUE],
|
||||||
|
});
|
||||||
|
|
||||||
|
LOAD_QUEUE.clear();
|
||||||
|
}, THROTTLE, false);
|
||||||
|
|
||||||
|
const updateLastRendered = throttle(() => {
|
||||||
|
getActions().updateLastRenderedCustomEmojis({
|
||||||
|
ids: [...RENDER_HISTORY].reverse(),
|
||||||
|
});
|
||||||
|
|
||||||
|
RENDER_HISTORY.clear();
|
||||||
|
}, THROTTLE, false);
|
||||||
|
|
||||||
|
export default function useEnsureCustomEmoji(id: string) {
|
||||||
|
RENDER_HISTORY.add(id);
|
||||||
|
updateLastRendered();
|
||||||
|
|
||||||
|
if (getGlobal().customEmojis.byId[id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOAD_QUEUE.add(id);
|
||||||
|
loadFromQueue();
|
||||||
|
}
|
||||||
46
src/hooks/useThumbnail.ts
Normal file
46
src/hooks/useThumbnail.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { useLayoutEffect, useMemo, useState } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiMessage, ApiSticker } from '../api/types';
|
||||||
|
|
||||||
|
import { DEBUG } from '../config';
|
||||||
|
import { isWebpSupported } from '../util/environment';
|
||||||
|
import { EMPTY_IMAGE_DATA_URI, webpToPngBase64 } from '../util/webpToPng';
|
||||||
|
import { getMessageMediaThumbDataUri } from '../global/helpers';
|
||||||
|
import { selectTheme } from '../global/selectors';
|
||||||
|
import { getGlobal } from '../global';
|
||||||
|
|
||||||
|
export default function useThumbnail(media?: ApiMessage | ApiSticker) {
|
||||||
|
const isMessage = media && 'content' in media;
|
||||||
|
const thumbDataUri = isMessage ? getMessageMediaThumbDataUri(media) : media?.thumbnail?.dataUri;
|
||||||
|
const sticker = isMessage ? media.content?.sticker : media;
|
||||||
|
const shouldDecodeThumbnail = thumbDataUri && sticker && !isWebpSupported() && thumbDataUri.includes('image/webp');
|
||||||
|
const [thumbnailDecoded, setThumbnailDecoded] = useState(EMPTY_IMAGE_DATA_URI);
|
||||||
|
const id = media?.id;
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!shouldDecodeThumbnail) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
webpToPngBase64(`b64-${id}`, thumbDataUri!)
|
||||||
|
.then(setThumbnailDecoded)
|
||||||
|
.catch((err) => {
|
||||||
|
if (DEBUG) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [id, shouldDecodeThumbnail, thumbDataUri]);
|
||||||
|
|
||||||
|
// TODO Find a way to update thumbnail on theme change
|
||||||
|
const theme = selectTheme(getGlobal());
|
||||||
|
|
||||||
|
const dataUri = useMemo(() => {
|
||||||
|
const uri = shouldDecodeThumbnail ? thumbnailDecoded : thumbDataUri;
|
||||||
|
if (!uri || theme !== 'dark') return uri;
|
||||||
|
|
||||||
|
return uri.replace('<svg', '<svg fill="white"');
|
||||||
|
}, [shouldDecodeThumbnail, thumbDataUri, thumbnailDecoded, theme]);
|
||||||
|
|
||||||
|
return dataUri;
|
||||||
|
}
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import { useLayoutEffect, useState } from '../lib/teact/teact';
|
|
||||||
|
|
||||||
import type { ApiMessage } from '../api/types';
|
|
||||||
|
|
||||||
import { DEBUG } from '../config';
|
|
||||||
import { isWebpSupported } from '../util/environment';
|
|
||||||
import { EMPTY_IMAGE_DATA_URI, webpToPngBase64 } from '../util/webpToPng';
|
|
||||||
import { getMessageMediaThumbDataUri } from '../global/helpers';
|
|
||||||
|
|
||||||
export default function useWebpThumbnail(message?: ApiMessage) {
|
|
||||||
const thumbDataUri = message && getMessageMediaThumbDataUri(message);
|
|
||||||
const sticker = message?.content?.sticker;
|
|
||||||
const shouldDecodeThumbnail = thumbDataUri && sticker && !isWebpSupported() && thumbDataUri.includes('image/webp');
|
|
||||||
const [thumbnailDecoded, setThumbnailDecoded] = useState(EMPTY_IMAGE_DATA_URI);
|
|
||||||
const messageId = message?.id;
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!shouldDecodeThumbnail) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
webpToPngBase64(`b64-${messageId}`, thumbDataUri!)
|
|
||||||
.then(setThumbnailDecoded)
|
|
||||||
.catch((err) => {
|
|
||||||
if (DEBUG) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [messageId, shouldDecodeThumbnail, thumbDataUri]);
|
|
||||||
|
|
||||||
return shouldDecodeThumbnail ? thumbnailDecoded : thumbDataUri;
|
|
||||||
}
|
|
||||||
@ -1189,6 +1189,9 @@ messages.requestSimpleWebView#6abb2f73 flags:# bot:InputUser url:string theme_pa
|
|||||||
messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent;
|
messages.sendWebViewResultMessage#a4314f5 bot_query_id:string result:InputBotInlineResult = WebViewMessageSent;
|
||||||
messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates;
|
messages.sendWebViewData#dc0242c8 bot:InputUser random_id:long button_text:string data:string = Updates;
|
||||||
messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio;
|
messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.TranscribedAudio;
|
||||||
|
messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;
|
||||||
|
messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;
|
||||||
|
messages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||||
|
|||||||
@ -247,10 +247,13 @@
|
|||||||
"messages.requestSimpleWebView",
|
"messages.requestSimpleWebView",
|
||||||
"messages.sendWebViewResultMessage",
|
"messages.sendWebViewResultMessage",
|
||||||
"messages.sendWebViewData",
|
"messages.sendWebViewData",
|
||||||
|
"messages.transcribeAudio",
|
||||||
|
"messages.getCustomEmojiDocuments",
|
||||||
|
"messages.getEmojiStickers",
|
||||||
|
"messages.getFeaturedEmojiStickers",
|
||||||
"messages.readReactions",
|
"messages.readReactions",
|
||||||
"messages.getUnreadReactions",
|
"messages.getUnreadReactions",
|
||||||
"messages.readMentions",
|
"messages.readMentions",
|
||||||
"messages.getUnreadMentions",
|
"messages.getUnreadMentions",
|
||||||
"help.getPremiumPromo",
|
"help.getPremiumPromo"
|
||||||
"messages.transcribeAudio"
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -108,7 +108,8 @@ export type TeactNode =
|
|||||||
ReactElement
|
ReactElement
|
||||||
| string
|
| string
|
||||||
| number
|
| number
|
||||||
| boolean;
|
| boolean
|
||||||
|
| TeactNode[];
|
||||||
|
|
||||||
const Fragment = Symbol('Fragment');
|
const Fragment = Symbol('Fragment');
|
||||||
|
|
||||||
|
|||||||
@ -229,10 +229,12 @@ export enum SettingsScreens {
|
|||||||
PasscodeTurnOff,
|
PasscodeTurnOff,
|
||||||
PasscodeCongratulations,
|
PasscodeCongratulations,
|
||||||
Experimental,
|
Experimental,
|
||||||
|
Stickers,
|
||||||
|
CustomEmoji,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
||||||
'id' | 'title' | 'count' | 'stickers' | 'hasThumbnail' | 'isLottie' | 'isVideos'
|
'id' | 'title' | 'count' | 'stickers' | 'hasThumbnail' | 'isLottie' | 'isVideos' | 'isEmoji' | 'installedDate'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
export enum LeftColumnContent {
|
export enum LeftColumnContent {
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export const processDeepLink = (url: string) => {
|
|||||||
openChatByInvite,
|
openChatByInvite,
|
||||||
openChatByUsername,
|
openChatByUsername,
|
||||||
openChatByPhoneNumber,
|
openChatByPhoneNumber,
|
||||||
openStickerSetShortName,
|
openStickerSet,
|
||||||
focusMessage,
|
focusMessage,
|
||||||
joinVoiceChatByLink,
|
joinVoiceChatByLink,
|
||||||
openInvoice,
|
openInvoice,
|
||||||
@ -85,8 +85,10 @@ export const processDeepLink = (url: string) => {
|
|||||||
case 'addstickers': {
|
case 'addstickers': {
|
||||||
const { set } = params;
|
const { set } = params;
|
||||||
|
|
||||||
openStickerSetShortName({
|
openStickerSet({
|
||||||
stickerSetShortName: set,
|
stickerSetInfo: {
|
||||||
|
shortName: set,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,6 +84,10 @@ export function unique<T extends any>(array: T[]): T[] {
|
|||||||
return Array.from(new Set(array));
|
return Array.from(new Set(array));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function uniqueByField<T extends any>(array: T[], field: keyof T): T[] {
|
||||||
|
return [...new Map(array.map((item) => [item[field], item])).values()];
|
||||||
|
}
|
||||||
|
|
||||||
export function compact<T extends any>(array: T[]) {
|
export function compact<T extends any>(array: T[]) {
|
||||||
return array.filter(Boolean);
|
return array.filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ApiMessageEntityTypes } from '../api/types';
|
|||||||
import { IS_EMOJI_SUPPORTED } from './environment';
|
import { IS_EMOJI_SUPPORTED } from './environment';
|
||||||
import { RE_LINK_TEMPLATE } from '../config';
|
import { RE_LINK_TEMPLATE } from '../config';
|
||||||
|
|
||||||
const ENTITY_CLASS_BY_NODE_NAME: Record<string, string> = {
|
const ENTITY_CLASS_BY_NODE_NAME: Record<string, ApiMessageEntityTypes> = {
|
||||||
B: ApiMessageEntityTypes.Bold,
|
B: ApiMessageEntityTypes.Bold,
|
||||||
STRONG: ApiMessageEntityTypes.Bold,
|
STRONG: ApiMessageEntityTypes.Bold,
|
||||||
I: ApiMessageEntityTypes.Italic,
|
I: ApiMessageEntityTypes.Italic,
|
||||||
@ -15,6 +15,7 @@ const ENTITY_CLASS_BY_NODE_NAME: Record<string, string> = {
|
|||||||
CODE: ApiMessageEntityTypes.Code,
|
CODE: ApiMessageEntityTypes.Code,
|
||||||
PRE: ApiMessageEntityTypes.Pre,
|
PRE: ApiMessageEntityTypes.Pre,
|
||||||
BLOCKQUOTE: ApiMessageEntityTypes.Blockquote,
|
BLOCKQUOTE: ApiMessageEntityTypes.Blockquote,
|
||||||
|
'CUSTOM-EMOJI': ApiMessageEntityTypes.CustomEmoji,
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_TAG_DEEPNESS = 3;
|
const MAX_TAG_DEEPNESS = 3;
|
||||||
@ -90,6 +91,12 @@ function parseMarkdown(html: string) {
|
|||||||
'<code>$2</code>',
|
'<code>$2</code>',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Custom Emoji markdown tag
|
||||||
|
parsedHtml = parsedHtml.replace(
|
||||||
|
/(^|\s)(?!<(?:code|pre)[^<]*|<\/)\[([^\]\n]+)\]\(customEmoji:(\d+)\)(?![^<]*<\/(?:code|pre)>)(\s|$)/g,
|
||||||
|
'$1<custom-emoji document-id="$3" alt="$2">$2</custom-emoji>$4',
|
||||||
|
);
|
||||||
|
|
||||||
// Other simple markdown
|
// Other simple markdown
|
||||||
parsedHtml = parsedHtml.replace(
|
parsedHtml = parsedHtml.replace(
|
||||||
/(^|\s)(?!<(code|pre)[^<]*|<\/)[*]{2}([^*\n]+)[*]{2}(?![^<]*<\/(code|pre)>)(\s|$)/g,
|
/(^|\s)(?!<(code|pre)[^<]*|<\/)[*]{2}([^*\n]+)[*]{2}(?![^<]*<\/(code|pre)>)(\s|$)/g,
|
||||||
@ -138,18 +145,51 @@ function getEntityDataFromNode(
|
|||||||
const offset = rawText.substring(0, index).length;
|
const offset = rawText.substring(0, index).length;
|
||||||
const { length } = rawText.substring(index, index + node.textContent.length);
|
const { length } = rawText.substring(index, index + node.textContent.length);
|
||||||
|
|
||||||
let url: string | undefined;
|
|
||||||
let userId: string | undefined;
|
|
||||||
let language: string | undefined;
|
|
||||||
if (type === ApiMessageEntityTypes.TextUrl) {
|
if (type === ApiMessageEntityTypes.TextUrl) {
|
||||||
url = (node as HTMLAnchorElement).href;
|
return {
|
||||||
|
index,
|
||||||
|
entity: {
|
||||||
|
type,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
url: (node as HTMLAnchorElement).href,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (type === ApiMessageEntityTypes.MentionName) {
|
if (type === ApiMessageEntityTypes.MentionName) {
|
||||||
userId = (node as HTMLAnchorElement).dataset.userId;
|
return {
|
||||||
|
index,
|
||||||
|
entity: {
|
||||||
|
type,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
userId: (node as HTMLAnchorElement).dataset.userId!,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === ApiMessageEntityTypes.Pre) {
|
if (type === ApiMessageEntityTypes.Pre) {
|
||||||
language = (node as HTMLPreElement).dataset.language;
|
return {
|
||||||
|
index,
|
||||||
|
entity: {
|
||||||
|
type,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
language: (node as HTMLPreElement).dataset.language,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === ApiMessageEntityTypes.CustomEmoji) {
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
entity: {
|
||||||
|
type,
|
||||||
|
offset,
|
||||||
|
length,
|
||||||
|
documentId: (node as HTMLElement).getAttribute('document-id')!,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -158,14 +198,11 @@ function getEntityDataFromNode(
|
|||||||
type,
|
type,
|
||||||
offset,
|
offset,
|
||||||
length,
|
length,
|
||||||
...(url && { url }),
|
|
||||||
...(userId && { userId }),
|
|
||||||
...(language && { language }),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntityTypeFromNode(node: ChildNode) {
|
function getEntityTypeFromNode(node: ChildNode): ApiMessageEntityTypes | undefined {
|
||||||
if (ENTITY_CLASS_BY_NODE_NAME[node.nodeName]) {
|
if (ENTITY_CLASS_BY_NODE_NAME[node.nodeName]) {
|
||||||
return ENTITY_CLASS_BY_NODE_NAME[node.nodeName];
|
return ENTITY_CLASS_BY_NODE_NAME[node.nodeName];
|
||||||
}
|
}
|
||||||
@ -192,7 +229,7 @@ function getEntityTypeFromNode(node: ChildNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.nodeName === 'SPAN') {
|
if (node.nodeName === 'SPAN') {
|
||||||
return (node as HTMLElement).dataset.entityType;
|
return (node as HTMLElement).dataset.entityType as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
export default function insertHtmlInSelection(html: string) {
|
export function insertHtmlInSelection(html: string) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
|
|
||||||
if (selection?.getRangeAt && selection.rangeCount) {
|
if (selection?.getRangeAt && selection.rangeCount) {
|
||||||
Loading…
x
Reference in New Issue
Block a user