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