Introduce Emoji statuses (#2329)
This commit is contained in:
parent
88f9920fd5
commit
d6e4bac389
35
package-lock.json
generated
35
package-lock.json
generated
@ -104,6 +104,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dev/eslint-multitab": {
|
"dev/eslint-multitab": {
|
||||||
|
"name": "eslint-plugin-eslint-multitab-tt",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
@ -7185,15 +7186,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-doc-generator": {
|
"node_modules/eslint-doc-generator": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-doc-generator/-/eslint-doc-generator-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-doc-generator/-/eslint-doc-generator-1.4.2.tgz",
|
||||||
"integrity": "sha512-cn9KXE7xuKlxKi/9VbirR3cbz7W1geRObwWzZjJAnpTeNBoqA8Rj+lD8/HHHJ7PnOdaTrRyhhoYdCtxqq3U7Bw==",
|
"integrity": "sha512-axO5W9Nt/n/cppE75nJ8JVoqmzUyinzhNQ42vJFqjSnK8KeQoWE6IWqqT1wZly5uSneMuuWYYlCGicc0UeJp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/utils": "^5.38.1",
|
"@typescript-eslint/utils": "^5.38.1",
|
||||||
"ajv": "^8.11.2",
|
"ajv": "^8.11.2",
|
||||||
"boolean": "^3.2.0",
|
"boolean": "^3.2.0",
|
||||||
"commander": "^10.0.0",
|
"commander": "^9.4.0",
|
||||||
"cosmiconfig": "^8.0.0",
|
"cosmiconfig": "^8.0.0",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"dot-prop": "^7.2.0",
|
"dot-prop": "^7.2.0",
|
||||||
@ -7230,12 +7231,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-doc-generator/node_modules/commander": {
|
"node_modules/eslint-doc-generator/node_modules/commander": {
|
||||||
"version": "10.0.0",
|
"version": "9.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
"integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==",
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": "^12.20.0 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-doc-generator/node_modules/cosmiconfig": {
|
"node_modules/eslint-doc-generator/node_modules/cosmiconfig": {
|
||||||
@ -7260,9 +7261,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint-doc-generator/node_modules/type-fest": {
|
"node_modules/eslint-doc-generator/node_modules/type-fest": {
|
||||||
"version": "3.5.6",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.2.tgz",
|
||||||
"integrity": "sha512-6bd2bflx8ed7c99tc6zSTIzHr1/QG29bQoK4Qh8MYGnlPbODUzGxklLShjwc/xWQQFHgIci+y5Arv7Rbb0LjXw==",
|
"integrity": "sha512-Ph7S4EhXzWy0sbljEuZo0tTNoLl+K2tPauGrQpcwUWrOVneLePTuhVzcuzVJJ6RU5DsNwQZka+8YtkXXU4z9cA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.16"
|
"node": ">=14.16"
|
||||||
@ -7408,9 +7409,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-eslint-plugin": {
|
"node_modules/eslint-plugin-eslint-plugin": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-5.0.7.tgz",
|
||||||
"integrity": "sha512-bxPMZ3L/+5YypErWQMKUI9XdkLpgqOOO0CgbtHjk5Zxzcg4EVsWYPy8duvGSLxSyR60LBIoXNzVMueEZ3/j0AQ==",
|
"integrity": "sha512-hcz4Bze1ECwv3Q/Bi/ZMZZNiuvI2YclNuxjnczkblQ0skrlPhdO83rSM7felf5n+7ZJOZi4GS8y8gNiRtvI0hA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-utils": "^3.0.0",
|
"eslint-utils": "^3.0.0",
|
||||||
@ -15196,9 +15197,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.8.0",
|
"version": "1.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
|
||||||
"integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==",
|
"integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
|||||||
@ -54,8 +54,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
? String(mtpUser.photo.photoId)
|
? String(mtpUser.photo.photoId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const userType = buildApiUserType(mtpUser);
|
const userType = buildApiUserType(mtpUser);
|
||||||
|
|
||||||
const usernames = buildApiUsernames(mtpUser);
|
const usernames = buildApiUsernames(mtpUser);
|
||||||
|
const emojiStatus = mtpUser.emojiStatus ? buildApiUserEmojiStatus(mtpUser.emojiStatus) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: buildApiPeerId(id, 'user'),
|
id: buildApiPeerId(id, 'user'),
|
||||||
@ -74,7 +74,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
noStatus: !mtpUser.status,
|
noStatus: !mtpUser.status,
|
||||||
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
|
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
|
||||||
...(avatarHash && { avatarHash }),
|
...(avatarHash && { avatarHash }),
|
||||||
...(mtpUser.emojiStatus && { emojiStatus: buildApiUserEmojiStatus(mtpUser.emojiStatus) }),
|
emojiStatus,
|
||||||
hasVideoAvatar,
|
hasVideoAvatar,
|
||||||
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
|
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
|
||||||
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
|
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import {
|
|||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import { deserializeBytes } from '../helpers';
|
import { deserializeBytes } from '../helpers';
|
||||||
|
import { DEFAULT_STATUS_ICON_ID } from '../../../config';
|
||||||
|
|
||||||
const CHANNEL_ID_MIN_LENGTH = 11; // Example: -1000000000
|
const CHANNEL_ID_MIN_LENGTH = 11; // Example: -1000000000
|
||||||
|
|
||||||
@ -580,3 +581,20 @@ export function buildInputChatReactions(chatReactions?: ApiChatReactions) {
|
|||||||
|
|
||||||
return new GramJs.ChatReactionsNone();
|
return new GramJs.ChatReactionsNone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildInputEmojiStatus(emojiStatus: ApiSticker, expires?: number) {
|
||||||
|
if (emojiStatus.id === DEFAULT_STATUS_ICON_ID) {
|
||||||
|
return new GramJs.EmojiStatusEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expires) {
|
||||||
|
return new GramJs.EmojiStatusUntil({
|
||||||
|
documentId: BigInt(emojiStatus.id),
|
||||||
|
until: expires,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GramJs.EmojiStatus({
|
||||||
|
documentId: BigInt(emojiStatus.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -35,15 +35,15 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
|
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
|
||||||
updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam,
|
updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam, updateEmojiStatus,
|
||||||
} from './users';
|
} from './users';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers, fetchRecentEmojiStatuses,
|
||||||
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||||
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
||||||
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, fetchDefaultTopicIcons,
|
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, fetchDefaultTopicIcons, fetchDefaultStatusEmojis,
|
||||||
} from './symbols';
|
} from './symbols';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -8,9 +8,10 @@ import { invokeRequest } from './client';
|
|||||||
import {
|
import {
|
||||||
buildStickerSet, buildStickerSetCovered, processStickerPackResult, processStickerResult,
|
buildStickerSet, buildStickerSetCovered, processStickerPackResult, processStickerResult,
|
||||||
} from '../apiBuilders/symbols';
|
} from '../apiBuilders/symbols';
|
||||||
|
import { buildApiUserEmojiStatus } from '../apiBuilders/users';
|
||||||
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
||||||
import { buildVideoFromDocument } from '../apiBuilders/messages';
|
import { buildVideoFromDocument } from '../apiBuilders/messages';
|
||||||
import { DEFAULT_GIF_SEARCH_BOT_USERNAME, RECENT_STICKERS_LIMIT } from '../../../config';
|
import { DEFAULT_GIF_SEARCH_BOT_USERNAME, RECENT_STATUS_LIMIT, RECENT_STICKERS_LIMIT } from '../../../config';
|
||||||
|
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
|
|
||||||
@ -263,6 +264,21 @@ export async function fetchDefaultTopicIcons() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchDefaultStatusEmojis() {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||||
|
stickerset: new GramJs.InputStickerSetEmojiDefaultStatuses(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!(result instanceof GramJs.messages.StickerSet)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set: buildStickerSet(result.set),
|
||||||
|
stickers: processStickerResult(result.documents),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
|
export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
|
||||||
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
|
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
|
||||||
q: query,
|
q: query,
|
||||||
@ -418,6 +434,26 @@ export async function fetchEmojiKeywords({ language, fromVersion }: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchRecentEmojiStatuses(hash = '0') {
|
||||||
|
const result = await invokeRequest(new GramJs.account.GetRecentEmojiStatuses({ hash: BigInt(hash) }));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.account.EmojiStatusesNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const documentIds = result.statuses
|
||||||
|
.slice(0, RECENT_STATUS_LIMIT)
|
||||||
|
.map(buildApiUserEmojiStatus)
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(({ documentId }) => documentId);
|
||||||
|
const emojiStatuses = await fetchCustomEmoji({ documentId: documentIds });
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: String(result.hash),
|
||||||
|
emojiStatuses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function processGifResult(gifs: GramJs.TypeDocument[]) {
|
function processGifResult(gifs: GramJs.TypeDocument[]) {
|
||||||
return gifs
|
return gifs
|
||||||
.map((document) => {
|
.map((document) => {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import BigInt from 'big-integer';
|
import BigInt from 'big-integer';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import type {
|
import type {
|
||||||
OnApiUpdate, ApiUser, ApiChat,
|
OnApiUpdate, ApiUser, ApiChat, ApiSticker,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import { COMMON_CHATS_LIMIT, PROFILE_PHOTOS_LIMIT } from '../../../config';
|
import { COMMON_CHATS_LIMIT, PROFILE_PHOTOS_LIMIT } from '../../../config';
|
||||||
@ -13,6 +13,7 @@ import {
|
|||||||
buildInputContact,
|
buildInputContact,
|
||||||
buildMtpPeerId,
|
buildMtpPeerId,
|
||||||
getEntityTypeById,
|
getEntityTypeById,
|
||||||
|
buildInputEmojiStatus,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||||
@ -296,6 +297,12 @@ export function reportSpam(userOrChat: ApiUser | ApiChat) {
|
|||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateEmojiStatus(emojiStatus: ApiSticker, expires?: number) {
|
||||||
|
return invokeRequest(new GramJs.account.UpdateEmojiStatus({
|
||||||
|
emojiStatus: buildInputEmojiStatus(emojiStatus, expires),
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) {
|
function updateLocalDb(result: (GramJs.photos.Photos | GramJs.photos.PhotosSlice | GramJs.messages.Chats)) {
|
||||||
if ('chats' in result) {
|
if ('chats' in result) {
|
||||||
addEntitiesWithPhotosToLocalDb(result.chats);
|
addEntitiesWithPhotosToLocalDb(result.chats);
|
||||||
|
|||||||
@ -1104,6 +1104,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||||
order: update.order || [],
|
order: update.order || [],
|
||||||
});
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdateRecentEmojiStatuses) {
|
||||||
|
onUpdate({ '@type': 'updateRecentEmojiStatuses' });
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
||||||
log('UNEXPECTED UPDATE', params);
|
log('UNEXPECTED UPDATE', params);
|
||||||
|
|||||||
@ -359,6 +359,10 @@ export type ApiUpdateUserEmojiStatus = {
|
|||||||
emojiStatus?: ApiEmojiStatus;
|
emojiStatus?: ApiEmojiStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateRecentEmojiStatuses = {
|
||||||
|
'@type': 'updateRecentEmojiStatuses';
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdateUserFullInfo = {
|
export type ApiUpdateUserFullInfo = {
|
||||||
'@type': 'updateUserFullInfo';
|
'@type': 'updateUserFullInfo';
|
||||||
id: string;
|
id: string;
|
||||||
@ -624,7 +628,7 @@ export type ApiUpdate = (
|
|||||||
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
||||||
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
||||||
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
||||||
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics
|
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||||
|
|||||||
4
src/assets/premium/PremiumStatus.svg
Normal file
4
src/assets/premium/PremiumStatus.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.13145 12.6908C7.13145 12.0004 7.69109 11.4408 8.38145 11.4408H14.947C15.4121 11.4408 15.8388 11.6991 16.0545 12.1112C16.2701 12.5233 16.2392 13.021 15.9741 13.4032L10.5563 21.214H14.18C14.8703 21.214 15.43 21.7736 15.43 22.464C15.43 23.1543 14.8703 23.714 14.18 23.714H8.16797C7.70284 23.714 7.27617 23.4557 7.06049 23.0436C6.8448 22.6315 6.87577 22.1337 7.14087 21.7516L12.5587 13.9408H8.38145C7.69109 13.9408 7.13145 13.3812 7.13145 12.6908Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.2134 9.53604C17.2134 8.84569 17.7731 8.28604 18.4634 8.28604H23.8321C24.2972 8.28604 24.7239 8.5443 24.9396 8.9564C25.1552 9.3685 25.1243 9.86628 24.8592 10.2485L20.4735 16.5712H23.3477C24.0381 16.5712 24.5977 17.1309 24.5977 17.8212C24.5977 18.5116 24.0381 19.0712 23.3477 19.0712H18.0852C17.6201 19.0712 17.1934 18.813 16.9777 18.4009C16.762 17.9888 16.793 17.491 17.0581 17.1088L21.4438 10.786H18.4634C17.7731 10.786 17.2134 10.2264 17.2134 9.53604Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -17,6 +17,7 @@ export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDi
|
|||||||
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
|
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
|
||||||
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
||||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||||
|
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||||
|
|
||||||
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||||
export { default as CalendarModal } from '../components/common/CalendarModal';
|
export { default as CalendarModal } from '../components/common/CalendarModal';
|
||||||
|
|||||||
@ -108,7 +108,6 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
|||||||
const isConnecting = connectionState !== 'connected';
|
const isConnecting = connectionState !== 'connected';
|
||||||
const canSelfUnmute = meParticipant?.canSelfUnmute;
|
const canSelfUnmute = meParticipant?.canSelfUnmute;
|
||||||
const shouldRaiseHand = !canSelfUnmute && meParticipant?.isMuted;
|
const shouldRaiseHand = !canSelfUnmute && meParticipant?.isMuted;
|
||||||
|
|
||||||
const handleOpenParticipantMenu = useCallback((anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => {
|
const handleOpenParticipantMenu = useCallback((anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => {
|
||||||
const rect = anchor.getBoundingClientRect();
|
const rect = anchor.getBoundingClientRect();
|
||||||
const container = containerRef.current!;
|
const container = containerRef.current!;
|
||||||
|
|||||||
@ -13,7 +13,6 @@ import generateIdFor from '../../util/generateIdFor';
|
|||||||
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck';
|
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck';
|
||||||
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
||||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
ref?: RefObject<HTMLDivElement>;
|
ref?: RefObject<HTMLDivElement>;
|
||||||
@ -89,8 +88,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
|
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
|
||||||
|
|
||||||
const { isMobile } = useAppLayout();
|
|
||||||
const [animation, setAnimation] = useState<RLottieInstance>();
|
const [animation, setAnimation] = useState<RLottieInstance>();
|
||||||
|
const animationRef = useRef<RLottieInstance>();
|
||||||
const wasPlaying = useRef(false);
|
const wasPlaying = useRef(false);
|
||||||
const isFrozen = useRef(false);
|
const isFrozen = useRef(false);
|
||||||
const isFirstRender = useRef(true);
|
const isFirstRender = useRef(true);
|
||||||
@ -140,7 +139,6 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
quality,
|
quality,
|
||||||
isLowPriority,
|
isLowPriority,
|
||||||
coords: sharedCanvasCoords,
|
coords: sharedCanvasCoords,
|
||||||
isMobile,
|
|
||||||
},
|
},
|
||||||
color,
|
color,
|
||||||
onEnded,
|
onEnded,
|
||||||
@ -152,6 +150,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setAnimation(newAnimation);
|
setAnimation(newAnimation);
|
||||||
|
animationRef.current = newAnimation;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (RLottie) {
|
if (RLottie) {
|
||||||
@ -167,7 +166,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
|
animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
|
||||||
containerId, sharedCanvas, sharedCanvasCoords, isMobile,
|
containerId, sharedCanvas, sharedCanvasCoords,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -178,11 +177,12 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (animation) {
|
if (animationRef.current) {
|
||||||
animation.removeContainer(containerId);
|
animationRef.current.removeContainer(containerId);
|
||||||
|
animationRef.current = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [animation, containerId]);
|
}, [containerId]);
|
||||||
|
|
||||||
const playAnimation = useCallback((shouldRestart = false) => {
|
const playAnimation = useCallback((shouldRestart = false) => {
|
||||||
if (animation && (playRef.current || playSegmentRef.current)) {
|
if (animation && (playRef.current || playSegmentRef.current)) {
|
||||||
@ -235,14 +235,11 @@ const AnimatedSticker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [noLoop, animation]);
|
}, [noLoop, animation]);
|
||||||
|
|
||||||
useSyncEffect(([prevSharedCanvasCoords, prevIsMobile]) => {
|
useSyncEffect(([prevSharedCanvasCoords]) => {
|
||||||
if (
|
if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) {
|
||||||
(prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords)
|
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords);
|
||||||
|| (prevIsMobile !== undefined && isMobile !== prevIsMobile)
|
|
||||||
) {
|
|
||||||
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords, isMobile);
|
|
||||||
}
|
}
|
||||||
}, [sharedCanvasCoords, isMobile, containerId, animation]);
|
}, [sharedCanvasCoords, containerId, animation]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!animation) {
|
if (!animation) {
|
||||||
|
|||||||
@ -16,6 +16,13 @@
|
|||||||
width: var(--custom-emoji-size);
|
width: var(--custom-emoji-size);
|
||||||
height: var(--custom-emoji-size);
|
height: var(--custom-emoji-size);
|
||||||
margin: 0.3125rem;
|
margin: 0.3125rem;
|
||||||
|
|
||||||
|
&.status-default {
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
padding: 0.25rem;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.set-expand {
|
&.set-expand {
|
||||||
@ -117,13 +124,14 @@
|
|||||||
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sticker-context-menu {
|
.sticker-context-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
z-index: var(--z-header-menu);
|
||||||
|
|
||||||
.bubble {
|
.bubble {
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMe
|
|||||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||||
import { hexToRgb } from '../../util/switchTheme';
|
import { hexToRgb } from '../../util/switchTheme';
|
||||||
|
import { getServerTimeOffset } from '../../util/serverTime';
|
||||||
|
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
@ -33,6 +34,7 @@ type OwnProps<T> = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
noContextMenu?: boolean;
|
noContextMenu?: boolean;
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
|
isStatusPicker?: boolean;
|
||||||
canViewSet?: boolean;
|
canViewSet?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
@ -43,8 +45,19 @@ type OwnProps<T> = {
|
|||||||
onFaveClick?: (sticker: ApiSticker) => void;
|
onFaveClick?: (sticker: ApiSticker) => void;
|
||||||
onUnfaveClick?: (sticker: ApiSticker) => void;
|
onUnfaveClick?: (sticker: ApiSticker) => void;
|
||||||
onRemoveRecentClick?: (sticker: ApiSticker) => void;
|
onRemoveRecentClick?: (sticker: ApiSticker) => void;
|
||||||
|
onContextMenuOpen?: NoneToVoidFunction;
|
||||||
|
onContextMenuClose?: NoneToVoidFunction;
|
||||||
|
onContextMenuClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const contentForStatusMenuContext = [
|
||||||
|
{ title: 'SetTimeoutFor.Hours', value: 1, arg: 3600 },
|
||||||
|
{ title: 'SetTimeoutFor.Hours', value: 2, arg: 7200 },
|
||||||
|
{ title: 'SetTimeoutFor.Hours', value: 8, arg: 28800 },
|
||||||
|
{ title: 'SetTimeoutFor.Days', value: 1, arg: 86400 },
|
||||||
|
{ title: 'SetTimeoutFor.Days', value: 2, arg: 172800 },
|
||||||
|
];
|
||||||
|
|
||||||
const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult | undefined = undefined>({
|
const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult | undefined = undefined>({
|
||||||
sticker,
|
sticker,
|
||||||
size,
|
size,
|
||||||
@ -53,6 +66,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
className,
|
className,
|
||||||
noContextMenu,
|
noContextMenu,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
|
isStatusPicker,
|
||||||
canViewSet,
|
canViewSet,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
@ -63,10 +77,15 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
onFaveClick,
|
onFaveClick,
|
||||||
onUnfaveClick,
|
onUnfaveClick,
|
||||||
onRemoveRecentClick,
|
onRemoveRecentClick,
|
||||||
|
onContextMenuOpen,
|
||||||
|
onContextMenuClose,
|
||||||
|
onContextMenuClick,
|
||||||
}: OwnProps<T>) => {
|
}: OwnProps<T>) => {
|
||||||
const { openStickerSet, openPremiumModal } = getActions();
|
const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
||||||
const hasCustomColor = sticker.shouldUseTextColor;
|
const hasCustomColor = sticker.shouldUseTextColor;
|
||||||
@ -99,6 +118,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
handleBeforeContextMenu, handleContextMenu,
|
handleBeforeContextMenu, handleContextMenu,
|
||||||
handleContextMenuClose, handleContextMenuHide,
|
handleContextMenuClose, handleContextMenuHide,
|
||||||
} = useContextMenuHandlers(ref);
|
} = useContextMenuHandlers(ref);
|
||||||
|
const shouldRenderContextMenu = Boolean(!noContextMenu && contextMenuPosition);
|
||||||
|
|
||||||
const getTriggerElement = useCallback(() => ref.current, []);
|
const getTriggerElement = useCallback(() => ref.current, []);
|
||||||
|
|
||||||
@ -108,10 +128,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getMenuElement = useCallback(
|
const getMenuElement = useCallback(
|
||||||
() => ref.current!.querySelector('.sticker-context-menu .bubble'),
|
() => {
|
||||||
[],
|
return isStatusPicker ? menuRef.current : ref.current!.querySelector('.sticker-context-menu .bubble');
|
||||||
|
},
|
||||||
|
[isStatusPicker],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getLayout = () => ({ withPortal: isStatusPicker });
|
||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
} = useContextMenuPosition(
|
} = useContextMenuPosition(
|
||||||
@ -119,8 +143,17 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
getTriggerElement,
|
getTriggerElement,
|
||||||
getRootElement,
|
getRootElement,
|
||||||
getMenuElement,
|
getMenuElement,
|
||||||
|
getLayout,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isContextMenuOpen) {
|
||||||
|
onContextMenuOpen?.();
|
||||||
|
} else {
|
||||||
|
onContextMenuClose?.();
|
||||||
|
}
|
||||||
|
}, [isContextMenuOpen, onContextMenuClose, onContextMenuOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isIntersecting) handleContextMenuClose();
|
if (!isIntersecting) handleContextMenuClose();
|
||||||
}, [handleContextMenuClose, isIntersecting]);
|
}, [handleContextMenuClose, isIntersecting]);
|
||||||
@ -170,6 +203,18 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
openStickerSet({ stickerSetInfo });
|
openStickerSet({ stickerSetInfo });
|
||||||
}, [openStickerSet, stickerSetInfo]);
|
}, [openStickerSet, stickerSetInfo]);
|
||||||
|
|
||||||
|
const handleEmojiStatusExpiresClick = useCallback((e: React.SyntheticEvent, duration = 0) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
handleContextMenuClose();
|
||||||
|
onContextMenuClick?.();
|
||||||
|
setEmojiStatus({
|
||||||
|
emojiStatus: sticker,
|
||||||
|
expires: Date.now() / 1000 + duration + getServerTimeOffset(),
|
||||||
|
});
|
||||||
|
}, [setEmojiStatus, sticker, handleContextMenuClose, onContextMenuClick]);
|
||||||
|
|
||||||
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
const shouldShowCloseButton = !IS_TOUCH_ENV && onRemoveRecentClick;
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
@ -181,10 +226,22 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
);
|
);
|
||||||
|
|
||||||
const contextMenuItems = useMemo(() => {
|
const contextMenuItems = useMemo(() => {
|
||||||
if (noContextMenu || isCustomEmoji) return [];
|
if (!shouldRenderContextMenu || noContextMenu || (isCustomEmoji && !isStatusPicker)) return [];
|
||||||
|
|
||||||
const items: ReactNode[] = [];
|
const items: ReactNode[] = [];
|
||||||
|
|
||||||
|
if (isCustomEmoji) {
|
||||||
|
contentForStatusMenuContext.forEach((item) => {
|
||||||
|
items.push(
|
||||||
|
<MenuItem onClick={handleEmojiStatusExpiresClick} clickArg={item.arg}>
|
||||||
|
{lang(item.title, item.value, 'i')}
|
||||||
|
</MenuItem>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
if (onUnfaveClick) {
|
if (onUnfaveClick) {
|
||||||
items.push(
|
items.push(
|
||||||
<MenuItem icon="favorite" onClick={handleContextUnfave}>
|
<MenuItem icon="favorite" onClick={handleContextUnfave}>
|
||||||
@ -228,9 +285,9 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
}, [
|
}, [
|
||||||
canViewSet, handleContextFave, handleContextRemoveRecent, handleContextUnfave, handleOpenSet, handleSendQuiet,
|
shouldRenderContextMenu, noContextMenu, isCustomEmoji, isStatusPicker, onUnfaveClick, onFaveClick, isLocked,
|
||||||
handleSendScheduled, isLocked, isSavedMessages, lang, onFaveClick, onRemoveRecentClick, onUnfaveClick, onClick,
|
onClick, canViewSet, onRemoveRecentClick, handleEmojiStatusExpiresClick, lang, handleContextUnfave,
|
||||||
noContextMenu, isCustomEmoji,
|
handleContextFave, isSavedMessages, handleSendScheduled, handleSendQuiet, handleOpenSet, handleContextRemoveRecent,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -278,8 +335,9 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
<i className="icon-close" />
|
<i className="icon-close" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{Boolean(contextMenuItems.length) && contextMenuPosition !== undefined && (
|
{Boolean(contextMenuItems.length) && (
|
||||||
<Menu
|
<Menu
|
||||||
|
ref={menuRef}
|
||||||
isOpen={isContextMenuOpen}
|
isOpen={isContextMenuOpen}
|
||||||
transformOriginX={transformOriginX}
|
transformOriginX={transformOriginX}
|
||||||
transformOriginY={transformOriginY}
|
transformOriginY={transformOriginY}
|
||||||
@ -288,6 +346,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
style={menuStyle}
|
style={menuStyle}
|
||||||
className="sticker-context-menu"
|
className="sticker-context-menu"
|
||||||
autoClose
|
autoClose
|
||||||
|
withPortal={isStatusPicker}
|
||||||
onClose={handleContextMenuClose}
|
onClose={handleContextMenuClose}
|
||||||
onCloseAnimationEnd={handleContextMenuHide}
|
onCloseAnimationEnd={handleContextMenuHide}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -115,7 +115,7 @@ const StickerView: FC<OwnProps> = ({
|
|||||||
const noTransition = isLottie && preloadedPreviewData;
|
const noTransition = isLottie && preloadedPreviewData;
|
||||||
|
|
||||||
const bounds = useBoundsInSharedCanvas(containerRef, sharedCanvasRef);
|
const bounds = useBoundsInSharedCanvas(containerRef, sharedCanvasRef);
|
||||||
const realSize = bounds.size || size;
|
const realSize = size || bounds.size;
|
||||||
|
|
||||||
// Preload preview for Message Input and local message
|
// Preload preview for Message Input and local message
|
||||||
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
||||||
|
|||||||
@ -90,10 +90,26 @@
|
|||||||
@include overflow-y-overlay();
|
@include overflow-y-overlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
.passcode-lock {
|
.extra-spacing {
|
||||||
|
position: relative;
|
||||||
margin-left: 0.8125rem;
|
margin-left: 0.8125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.emoji-status-effect {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-status {
|
||||||
|
--custom-emoji-size: 1.5rem;
|
||||||
|
--color-text: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.PremiumIcon {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
// @optimization
|
// @optimization
|
||||||
@include while-transition() {
|
@include while-transition() {
|
||||||
.Menu .bubble {
|
.Menu .bubble {
|
||||||
|
|||||||
@ -25,7 +25,9 @@ import { formatDateToString } from '../../../util/dateFormat';
|
|||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
||||||
import { clearWebsync } from '../../../util/websync';
|
import { clearWebsync } from '../../../util/websync';
|
||||||
import { selectCurrentMessageList, selectTabState, selectTheme } from '../../../global/selectors';
|
import {
|
||||||
|
selectCurrentMessageList, selectIsCurrentUserPremium, selectTabState, selectTheme,
|
||||||
|
} from '../../../global/selectors';
|
||||||
import { isChatArchived } from '../../../global/helpers';
|
import { isChatArchived } from '../../../global/helpers';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||||
@ -43,6 +45,7 @@ import PickerSelectedItem from '../../common/PickerSelectedItem';
|
|||||||
import Switcher from '../../ui/Switcher';
|
import Switcher from '../../ui/Switcher';
|
||||||
import ShowTransition from '../../ui/ShowTransition';
|
import ShowTransition from '../../ui/ShowTransition';
|
||||||
import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
||||||
|
import StatusButton from './StatusButton';
|
||||||
|
|
||||||
import './LeftMainHeader.scss';
|
import './LeftMainHeader.scss';
|
||||||
|
|
||||||
@ -70,6 +73,7 @@ type StateProps =
|
|||||||
animationLevel: AnimationLevel;
|
animationLevel: AnimationLevel;
|
||||||
chatsById?: Record<string, ApiChat>;
|
chatsById?: Record<string, ApiChat>;
|
||||||
isMessageListOpen: boolean;
|
isMessageListOpen: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
||||||
areChatsLoaded?: boolean;
|
areChatsLoaded?: boolean;
|
||||||
hasPasscode?: boolean;
|
hasPasscode?: boolean;
|
||||||
@ -92,6 +96,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
onReset,
|
onReset,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isCurrentUserPremium,
|
||||||
shouldSkipTransition,
|
shouldSkipTransition,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
globalSearchChatId,
|
globalSearchChatId,
|
||||||
@ -430,6 +435,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SearchInput>
|
</SearchInput>
|
||||||
|
{isCurrentUserPremium && <StatusButton />}
|
||||||
{hasPasscode && (
|
{hasPasscode && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
@ -438,7 +444,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
color="translucent"
|
color="translucent"
|
||||||
ariaLabel={`${lang('ShortcutsController.Others.LockByPasscode')} (Ctrl+Shift+L)`}
|
ariaLabel={`${lang('ShortcutsController.Others.LockByPasscode')} (Ctrl+Shift+L)`}
|
||||||
onClick={handleLockScreen}
|
onClick={handleLockScreen}
|
||||||
className="passcode-lock"
|
className={buildClassName(!isCurrentUserPremium && 'extra-spacing')}
|
||||||
>
|
>
|
||||||
<i className="icon-lock" />
|
<i className="icon-lock" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -482,6 +488,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isSyncing,
|
isSyncing,
|
||||||
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
||||||
isConnectionStatusMinimized,
|
isConnectionStatusMinimized,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
areChatsLoaded: Boolean(global.chats.listIds.active),
|
areChatsLoaded: Boolean(global.chats.listIds.active),
|
||||||
hasPasscode: Boolean(global.passcode.hasPasscode),
|
hasPasscode: Boolean(global.passcode.hasPasscode),
|
||||||
canInstall: Boolean(tabState.canInstall),
|
canInstall: Boolean(tabState.canInstall),
|
||||||
|
|||||||
103
src/components/left/main/StatusButton.tsx
Normal file
103
src/components/left/main/StatusButton.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, { memo, useCallback, useRef } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiEmojiStatus, ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
|
import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config';
|
||||||
|
import { selectUser } from '../../../global/selectors';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
import useTimeout from '../../../hooks/useTimeout';
|
||||||
|
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||||
|
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||||
|
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import CustomEmoji from '../../common/CustomEmoji';
|
||||||
|
import PremiumIcon from '../../common/PremiumIcon';
|
||||||
|
import CustomEmojiEffect from '../../middle/message/CustomEmojiEffect';
|
||||||
|
import StatusPickerMenu from './StatusPickerMenu.async';
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
emojiStatus?: ApiEmojiStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EFFECT_DURATION_MS = 1500;
|
||||||
|
const EMOJI_STATUS_SIZE = 24;
|
||||||
|
|
||||||
|
const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
|
||||||
|
const { setEmojiStatus, loadCurrentUser } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const [shouldShowEffect, markShouldShowEffect, unmarkShouldShowEffect] = useFlag(false);
|
||||||
|
const [isEffectShown, showEffect, hideEffect] = useFlag(false);
|
||||||
|
const [isStatusPickerOpen, openStatusPicker, closeStatusPicker] = useFlag(false);
|
||||||
|
const { isMobile } = useAppLayout();
|
||||||
|
|
||||||
|
const delay = emojiStatus?.until ? emojiStatus.until * 1000 - Date.now() + getServerTimeOffset() * 1000 : undefined;
|
||||||
|
useTimeout(loadCurrentUser, delay);
|
||||||
|
|
||||||
|
useEffectWithPrevDeps(([prevEmojiStatus]) => {
|
||||||
|
if (shouldShowEffect && emojiStatus && prevEmojiStatus && emojiStatus.documentId !== prevEmojiStatus.documentId) {
|
||||||
|
showEffect();
|
||||||
|
unmarkShouldShowEffect();
|
||||||
|
}
|
||||||
|
}, [emojiStatus, shouldShowEffect, showEffect, unmarkShouldShowEffect] as const);
|
||||||
|
|
||||||
|
const handleEmojiStatusSet = useCallback((sticker: ApiSticker) => {
|
||||||
|
markShouldShowEffect();
|
||||||
|
setEmojiStatus({ emojiStatus: sticker });
|
||||||
|
}, [markShouldShowEffect, setEmojiStatus]);
|
||||||
|
|
||||||
|
useTimeout(hideEffect, isEffectShown ? EFFECT_DURATION_MS : undefined);
|
||||||
|
|
||||||
|
const handleEmojiStatusClick = useCallback(() => {
|
||||||
|
openStatusPicker();
|
||||||
|
}, [openStatusPicker]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="extra-spacing">
|
||||||
|
{Boolean(isEffectShown && emojiStatus) && (
|
||||||
|
<CustomEmojiEffect
|
||||||
|
reaction={emojiStatus!}
|
||||||
|
isLottie
|
||||||
|
className="emoji-status-effect"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
ref={buttonRef}
|
||||||
|
ripple={!isMobile}
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
className="emoji-status"
|
||||||
|
onClick={handleEmojiStatusClick}
|
||||||
|
>
|
||||||
|
{emojiStatus ? (
|
||||||
|
<CustomEmoji
|
||||||
|
key={emojiStatus.documentId}
|
||||||
|
documentId={emojiStatus.documentId}
|
||||||
|
size={EMOJI_STATUS_SIZE}
|
||||||
|
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
||||||
|
/>
|
||||||
|
) : <PremiumIcon />}
|
||||||
|
</Button>
|
||||||
|
<StatusPickerMenu
|
||||||
|
statusButtonRef={buttonRef}
|
||||||
|
isOpen={isStatusPickerOpen}
|
||||||
|
onEmojiStatusSelect={handleEmojiStatusSet}
|
||||||
|
onClose={closeStatusPicker}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal((global) => {
|
||||||
|
const { currentUserId } = global;
|
||||||
|
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
emojiStatus: currentUser?.emojiStatus,
|
||||||
|
};
|
||||||
|
})(StatusButton));
|
||||||
18
src/components/left/main/StatusPickerMenu.async.tsx
Normal file
18
src/components/left/main/StatusPickerMenu.async.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { OwnProps } from './StatusPickerMenu';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const StatusPickerMenuAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const StatusPickerMenu = useModuleLoader(Bundles.Extra, 'StatusPickerMenu', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return StatusPickerMenu ? <StatusPickerMenu {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(StatusPickerMenuAsync);
|
||||||
17
src/components/left/main/StatusPickerMenu.module.scss
Normal file
17
src/components/left/main/StatusPickerMenu.module.scss
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.menuContent {
|
||||||
|
--offset-y: 3.25rem !important;
|
||||||
|
--offset-x: auto !important;
|
||||||
|
--color-text: var(--color-primary);
|
||||||
|
|
||||||
|
left: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 26rem;
|
||||||
|
height: 26rem;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
@media (max-width: 26rem) {
|
||||||
|
left: 0.5rem !important;
|
||||||
|
right: 0.5rem !important;
|
||||||
|
width: calc(100vw - 1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
src/components/left/main/StatusPickerMenu.tsx
Normal file
84
src/components/left/main/StatusPickerMenu.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import type { RefObject } from 'react';
|
||||||
|
import React, {
|
||||||
|
useCallback, memo, useEffect, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
|
||||||
|
import Menu from '../../ui/Menu';
|
||||||
|
import Portal from '../../ui/Portal';
|
||||||
|
import CustomEmojiPicker from '../../middle/composer/CustomEmojiPicker';
|
||||||
|
|
||||||
|
import styles from './StatusPickerMenu.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
statusButtonRef: RefObject<HTMLButtonElement>;
|
||||||
|
onEmojiStatusSelect: (emojiStatus: ApiSticker) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
areFeaturedStickersLoaded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||||
|
isOpen,
|
||||||
|
statusButtonRef,
|
||||||
|
areFeaturedStickersLoaded,
|
||||||
|
onEmojiStatusSelect,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { loadFeaturedEmojiStickers } = getActions();
|
||||||
|
|
||||||
|
const transformOriginX = useRef<number>();
|
||||||
|
const [isContextMenuShown, markContextMenuShown, unmarkContextMenuShown] = useFlag();
|
||||||
|
useEffect(() => {
|
||||||
|
transformOriginX.current = statusButtonRef.current!.getBoundingClientRect().right;
|
||||||
|
}, [isOpen, statusButtonRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && !areFeaturedStickersLoaded) {
|
||||||
|
loadFeaturedEmojiStickers();
|
||||||
|
}
|
||||||
|
}, [areFeaturedStickersLoaded, isOpen, loadFeaturedEmojiStickers]);
|
||||||
|
|
||||||
|
const handleEmojiSelect = useCallback((sticker: ApiSticker) => {
|
||||||
|
onEmojiStatusSelect(sticker);
|
||||||
|
onClose();
|
||||||
|
}, [onClose, onEmojiStatusSelect]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Menu
|
||||||
|
isOpen={isOpen}
|
||||||
|
noCompact
|
||||||
|
positionX="right"
|
||||||
|
bubbleClassName={styles.menuContent}
|
||||||
|
onClose={onClose}
|
||||||
|
transformOriginX={transformOriginX.current}
|
||||||
|
noCloseOnBackdrop={isContextMenuShown}
|
||||||
|
>
|
||||||
|
<CustomEmojiPicker
|
||||||
|
idPrefix="status-emoji-set-"
|
||||||
|
loadAndPlay={isOpen}
|
||||||
|
isStatusPicker
|
||||||
|
onContextMenuOpen={markContextMenuShown}
|
||||||
|
onContextMenuClose={unmarkContextMenuShown}
|
||||||
|
onCustomEmojiSelect={handleEmojiSelect}
|
||||||
|
onContextMenuClick={onClose}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||||
|
return {
|
||||||
|
areFeaturedStickersLoaded: Boolean(global.customEmojis.featuredIds?.length),
|
||||||
|
};
|
||||||
|
})(StatusPickerMenu));
|
||||||
@ -20,6 +20,7 @@ import {
|
|||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectTabState,
|
selectTabState,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
|
selectIsCurrentUserPremium,
|
||||||
selectIsForwardModalOpen,
|
selectIsForwardModalOpen,
|
||||||
selectIsMediaViewerOpen,
|
selectIsMediaViewerOpen,
|
||||||
selectIsRightColumnShown,
|
selectIsRightColumnShown,
|
||||||
@ -128,6 +129,7 @@ type StateProps = {
|
|||||||
deleteFolderDialogId?: number;
|
deleteFolderDialogId?: number;
|
||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
isReceiptModalOpen?: boolean;
|
isReceiptModalOpen?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
||||||
@ -176,6 +178,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
isPremiumModalOpen,
|
isPremiumModalOpen,
|
||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
|
isCurrentUserPremium,
|
||||||
deleteFolderDialogId,
|
deleteFolderDialogId,
|
||||||
isMasterTab,
|
isMasterTab,
|
||||||
}) => {
|
}) => {
|
||||||
@ -194,6 +197,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
loadDefaultTopicIcons,
|
loadDefaultTopicIcons,
|
||||||
loadAddedStickers,
|
loadAddedStickers,
|
||||||
loadFavoriteStickers,
|
loadFavoriteStickers,
|
||||||
|
loadDefaultStatusIcons,
|
||||||
ensureTimeFormat,
|
ensureTimeFormat,
|
||||||
closeStickerSetModal,
|
closeStickerSetModal,
|
||||||
closeCustomEmojiSets,
|
closeCustomEmojiSets,
|
||||||
@ -209,6 +213,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
checkAppVersion,
|
checkAppVersion,
|
||||||
openChat,
|
openChat,
|
||||||
toggleLeftColumn,
|
toggleLeftColumn,
|
||||||
|
loadRecentEmojiStatuses,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
@ -245,12 +250,17 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
loadContactList();
|
loadContactList();
|
||||||
loadPremiumGifts();
|
loadPremiumGifts();
|
||||||
loadDefaultTopicIcons();
|
loadDefaultTopicIcons();
|
||||||
|
loadDefaultStatusIcons();
|
||||||
checkAppVersion();
|
checkAppVersion();
|
||||||
|
if (isCurrentUserPremium) {
|
||||||
|
loadRecentEmojiStatuses();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||||
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
|
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
|
||||||
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, isMasterTab,
|
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons,
|
||||||
|
loadDefaultStatusIcons, loadRecentEmojiStatuses, isCurrentUserPremium, isMasterTab,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Language-based API calls
|
// Language-based API calls
|
||||||
@ -553,6 +563,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
webApp,
|
webApp,
|
||||||
currentUser,
|
currentUser,
|
||||||
urlAuth,
|
urlAuth,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
isPremiumModalOpen: premiumModal?.isOpen,
|
isPremiumModalOpen: premiumModal?.isOpen,
|
||||||
limitReached: limitReachedModal?.limit,
|
limitReached: limitReachedModal?.limit,
|
||||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
|||||||
more_upload: 'PremiumPreviewUploads',
|
more_upload: 'PremiumPreviewUploads',
|
||||||
advanced_chat_management: 'PremiumPreviewAdvancedChatManagement',
|
advanced_chat_management: 'PremiumPreviewAdvancedChatManagement',
|
||||||
animated_userpics: 'PremiumPreviewAnimatedProfiles',
|
animated_userpics: 'PremiumPreviewAnimatedProfiles',
|
||||||
|
emoji_status: 'PremiumPreviewEmojiStatus',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||||
@ -48,6 +49,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
|||||||
more_upload: 'PremiumPreviewUploadsDescription',
|
more_upload: 'PremiumPreviewUploadsDescription',
|
||||||
advanced_chat_management: 'PremiumPreviewAdvancedChatManagementDescription',
|
advanced_chat_management: 'PremiumPreviewAdvancedChatManagementDescription',
|
||||||
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
|
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
|
||||||
|
emoji_status: 'PremiumPreviewEmojiStatusDescription',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PREMIUM_FEATURE_SECTIONS = [
|
export const PREMIUM_FEATURE_SECTIONS = [
|
||||||
@ -62,6 +64,7 @@ export const PREMIUM_FEATURE_SECTIONS = [
|
|||||||
'advanced_chat_management',
|
'advanced_chat_management',
|
||||||
'profile_badge',
|
'profile_badge',
|
||||||
'animated_userpics',
|
'animated_userpics',
|
||||||
|
'emoji_status',
|
||||||
];
|
];
|
||||||
|
|
||||||
const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||||
@ -70,6 +73,7 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
|||||||
'advanced_chat_management',
|
'advanced_chat_management',
|
||||||
'profile_badge',
|
'profile_badge',
|
||||||
'animated_userpics',
|
'animated_userpics',
|
||||||
|
'emoji_status',
|
||||||
];
|
];
|
||||||
|
|
||||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts'>;
|
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts'>;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef, useState,
|
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ import PremiumChats from '../../../assets/premium/PremiumChats.svg';
|
|||||||
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
||||||
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
|
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
|
||||||
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
|
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
|
||||||
|
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
|
||||||
|
|
||||||
import styles from './PremiumMainModal.module.scss';
|
import styles from './PremiumMainModal.module.scss';
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
|||||||
more_upload: PremiumFile,
|
more_upload: PremiumFile,
|
||||||
advanced_chat_management: PremiumChats,
|
advanced_chat_management: PremiumChats,
|
||||||
animated_userpics: PremiumVideo,
|
animated_userpics: PremiumVideo,
|
||||||
|
emoji_status: PremiumStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
@ -176,6 +178,11 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
|||||||
showConfetti();
|
showConfetti();
|
||||||
}, [isPremium, showConfetti]);
|
}, [isPremium, showConfetti]);
|
||||||
|
|
||||||
|
const filteredSections = useMemo(() => {
|
||||||
|
if (!premiumPromoOrder) return PREMIUM_FEATURE_SECTIONS;
|
||||||
|
return premiumPromoOrder.filter((section) => PREMIUM_FEATURE_SECTIONS.includes(section));
|
||||||
|
}, [premiumPromoOrder]);
|
||||||
|
|
||||||
if (!promo) return undefined;
|
if (!promo) return undefined;
|
||||||
|
|
||||||
// TODO Support all subscription options
|
// TODO Support all subscription options
|
||||||
@ -254,8 +261,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
||||||
{(premiumPromoOrder || PREMIUM_FEATURE_SECTIONS).map((section, index) => {
|
{filteredSections.map((section, index) => {
|
||||||
if (!PREMIUM_FEATURE_SECTIONS.includes(section)) return undefined;
|
|
||||||
return (
|
return (
|
||||||
<PremiumFeatureItem
|
<PremiumFeatureItem
|
||||||
key={section}
|
key={section}
|
||||||
|
|||||||
@ -12,7 +12,8 @@ import {
|
|||||||
FAVORITE_SYMBOL_SET_ID,
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
PREMIUM_STICKER_SET_ID,
|
PREMIUM_STICKER_SET_ID,
|
||||||
RECENT_SYMBOL_SET_ID,
|
RECENT_SYMBOL_SET_ID,
|
||||||
SLIDE_TRANSITION_DURATION, STICKER_PICKER_MAX_SHARED_COVERS,
|
SLIDE_TRANSITION_DURATION,
|
||||||
|
STICKER_PICKER_MAX_SHARED_COVERS,
|
||||||
STICKER_SIZE_PICKER_HEADER,
|
STICKER_SIZE_PICKER_HEADER,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||||
@ -44,16 +45,23 @@ type OwnProps = {
|
|||||||
chatId?: string;
|
chatId?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
|
isStatusPicker?: boolean;
|
||||||
|
idPrefix?: string;
|
||||||
withDefaultTopicIcons?: boolean;
|
withDefaultTopicIcons?: boolean;
|
||||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||||
|
onContextMenuOpen?: NoneToVoidFunction;
|
||||||
|
onContextMenuClose?: NoneToVoidFunction;
|
||||||
|
onContextMenuClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
|
customEmojisById?: Record<string, ApiSticker>;
|
||||||
|
recentCustomEmojiIds?: string[];
|
||||||
|
recentStatusEmojis?: ApiSticker[];
|
||||||
stickerSetsById: Record<string, ApiStickerSet>;
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
addedCustomEmojiIds?: string[];
|
addedCustomEmojiIds?: string[];
|
||||||
customEmojisById: Record<string, ApiSticker>;
|
|
||||||
recentCustomEmojiIds: string[];
|
|
||||||
defaultTopicIconsId?: string;
|
defaultTopicIconsId?: string;
|
||||||
|
defaultStatusIconsId?: string;
|
||||||
customEmojiFeaturedIds?: string[];
|
customEmojiFeaturedIds?: string[];
|
||||||
canAnimate?: boolean;
|
canAnimate?: boolean;
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
@ -63,6 +71,7 @@ type StateProps = {
|
|||||||
const SMOOTH_SCROLL_DISTANCE = 500;
|
const SMOOTH_SCROLL_DISTANCE = 500;
|
||||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||||
|
const DEFAULT_ID_PREFIX = 'custom-emoji-set';
|
||||||
|
|
||||||
const stickerSetIntersections: boolean[] = [];
|
const stickerSetIntersections: boolean[] = [];
|
||||||
|
|
||||||
@ -72,14 +81,21 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
addedCustomEmojiIds,
|
addedCustomEmojiIds,
|
||||||
customEmojisById,
|
customEmojisById,
|
||||||
recentCustomEmojiIds,
|
recentCustomEmojiIds,
|
||||||
|
recentStatusEmojis,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
|
idPrefix = DEFAULT_ID_PREFIX,
|
||||||
customEmojiFeaturedIds,
|
customEmojiFeaturedIds,
|
||||||
canAnimate,
|
canAnimate,
|
||||||
|
isStatusPicker,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
withDefaultTopicIcons,
|
withDefaultTopicIcons,
|
||||||
defaultTopicIconsId,
|
defaultTopicIconsId,
|
||||||
|
defaultStatusIconsId,
|
||||||
onCustomEmojiSelect,
|
onCustomEmojiSelect,
|
||||||
|
onContextMenuOpen,
|
||||||
|
onContextMenuClose,
|
||||||
|
onContextMenuClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -92,17 +108,23 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||||
|
|
||||||
|
const recentCustomEmojis = useMemo(() => {
|
||||||
|
return isStatusPicker
|
||||||
|
? recentStatusEmojis
|
||||||
|
: Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!));
|
||||||
|
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]);
|
||||||
|
|
||||||
const { observe: observeIntersection } = useIntersectionObserver({
|
const { observe: observeIntersection } = useIntersectionObserver({
|
||||||
rootRef: containerRef,
|
rootRef: containerRef,
|
||||||
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||||
}, (entries) => {
|
}, (entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
const { id } = entry.target as HTMLDivElement;
|
const { id } = entry.target as HTMLDivElement;
|
||||||
if (!id || !id.startsWith('custom-emoji-set-')) {
|
if (!id || !id.startsWith(idPrefix)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = Number(id.replace('custom-emoji-set-', ''));
|
const index = Number(id.replace(`${idPrefix}-`, ''));
|
||||||
stickerSetIntersections[index] = entry.isIntersecting;
|
stickerSetIntersections[index] = entry.isIntersecting;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -122,10 +144,6 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const areAddedLoaded = Boolean(addedCustomEmojiIds);
|
const areAddedLoaded = Boolean(addedCustomEmojiIds);
|
||||||
|
|
||||||
const recentCustomEmojis = useMemo(() => {
|
|
||||||
return Object.values(pickTruthy(customEmojisById, recentCustomEmojiIds));
|
|
||||||
}, [customEmojisById, recentCustomEmojiIds]);
|
|
||||||
|
|
||||||
const allSets = useMemo(() => {
|
const allSets = useMemo(() => {
|
||||||
if (!addedCustomEmojiIds) {
|
if (!addedCustomEmojiIds) {
|
||||||
return MEMO_EMPTY_ARRAY;
|
return MEMO_EMPTY_ARRAY;
|
||||||
@ -133,7 +151,19 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const defaultSets: StickerSetOrRecent[] = [];
|
const defaultSets: StickerSetOrRecent[] = [];
|
||||||
|
|
||||||
if (withDefaultTopicIcons) {
|
if (isStatusPicker) {
|
||||||
|
const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!];
|
||||||
|
if (defaultStatusIconsPack.stickers?.length) {
|
||||||
|
const stickers = (defaultStatusIconsPack.stickers || []).concat(recentCustomEmojis || []);
|
||||||
|
defaultSets.push({
|
||||||
|
...defaultStatusIconsPack,
|
||||||
|
stickers,
|
||||||
|
count: stickers.length,
|
||||||
|
id: RECENT_SYMBOL_SET_ID,
|
||||||
|
title: lang('RecentStickers'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (withDefaultTopicIcons) {
|
||||||
const defaultTopicIconsPack = stickerSetsById[defaultTopicIconsId!];
|
const defaultTopicIconsPack = stickerSetsById[defaultTopicIconsId!];
|
||||||
if (defaultTopicIconsPack.stickers?.length) {
|
if (defaultTopicIconsPack.stickers?.length) {
|
||||||
defaultSets.push({
|
defaultSets.push({
|
||||||
@ -142,7 +172,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
title: lang('RecentStickers'),
|
title: lang('RecentStickers'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (recentCustomEmojis.length) {
|
} else if (recentCustomEmojis?.length) {
|
||||||
defaultSets.push({
|
defaultSets.push({
|
||||||
id: RECENT_SYMBOL_SET_ID,
|
id: RECENT_SYMBOL_SET_ID,
|
||||||
accessHash: '0',
|
accessHash: '0',
|
||||||
@ -162,8 +192,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
...setsToDisplay,
|
...setsToDisplay,
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
addedCustomEmojiIds, defaultTopicIconsId, customEmojiFeaturedIds, lang, recentCustomEmojis, stickerSetsById,
|
addedCustomEmojiIds, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||||
withDefaultTopicIcons,
|
customEmojiFeaturedIds, stickerSetsById, defaultStatusIconsId, lang, defaultTopicIconsId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const noPopulatedSets = useMemo(() => (
|
const noPopulatedSets = useMemo(() => (
|
||||||
@ -194,9 +224,9 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const selectStickerSet = useCallback((index: number) => {
|
const selectStickerSet = useCallback((index: number) => {
|
||||||
setActiveSetIndex(index);
|
setActiveSetIndex(index);
|
||||||
const stickerSetEl = document.getElementById(`custom-emoji-set-${index}`)!;
|
const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!;
|
||||||
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
fastSmoothScroll(containerRef.current!, stickerSetEl, 'start', undefined, SMOOTH_SCROLL_DISTANCE);
|
||||||
}, []);
|
}, [idPrefix]);
|
||||||
|
|
||||||
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
|
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||||
onCustomEmojiSelect(emoji);
|
onCustomEmojiSelect(emoji);
|
||||||
@ -298,14 +328,19 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
stickerSet={stickerSet}
|
stickerSet={stickerSet}
|
||||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||||
index={i}
|
index={i}
|
||||||
|
idPrefix={idPrefix}
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||||
isSavedMessages={isSavedMessages}
|
isSavedMessages={isSavedMessages}
|
||||||
shouldHideRecentHeader={withDefaultTopicIcons}
|
isStatusPicker={isStatusPicker}
|
||||||
|
shouldHideRecentHeader={withDefaultTopicIcons || isStatusPicker}
|
||||||
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||||
isCustomEmojiPicker
|
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
onStickerSelect={handleEmojiSelect}
|
onStickerSelect={handleEmojiSelect}
|
||||||
|
onContextMenuOpen={onContextMenuOpen}
|
||||||
|
onContextMenuClose={onContextMenuClose}
|
||||||
|
onContextMenuClick={onContextMenuClick}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -314,7 +349,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId }): StateProps => {
|
(global, { chatId, isStatusPicker }): StateProps => {
|
||||||
const {
|
const {
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: stickerSetsById,
|
setsById: stickerSetsById,
|
||||||
@ -322,6 +357,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
customEmojis: {
|
customEmojis: {
|
||||||
byId: customEmojisById,
|
byId: customEmojisById,
|
||||||
featuredIds: customEmojiFeaturedIds,
|
featuredIds: customEmojiFeaturedIds,
|
||||||
|
statusRecent: {
|
||||||
|
emojis: recentStatusEmojis,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
recentCustomEmojis: recentCustomEmojiIds,
|
recentCustomEmojis: recentCustomEmojiIds,
|
||||||
} = global;
|
} = global;
|
||||||
@ -329,15 +367,17 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
customEmojisById: !isStatusPicker ? customEmojisById : undefined,
|
||||||
|
recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined,
|
||||||
|
recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
addedCustomEmojiIds: global.customEmojis.added.setIds,
|
addedCustomEmojiIds: global.customEmojis.added.setIds,
|
||||||
canAnimate: global.settings.byKey.shouldLoopStickers,
|
canAnimate: global.settings.byKey.shouldLoopStickers,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
customEmojisById,
|
|
||||||
recentCustomEmojiIds,
|
|
||||||
customEmojiFeaturedIds,
|
customEmojiFeaturedIds,
|
||||||
defaultTopicIconsId: global.defaultTopicIconsId,
|
defaultTopicIconsId: global.defaultTopicIconsId,
|
||||||
|
defaultStatusIconsId: global.defaultStatusIconsId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(CustomEmojiPicker));
|
)(CustomEmojiPicker));
|
||||||
|
|||||||
@ -98,4 +98,12 @@
|
|||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
border-radius: var(--border-radius-messages-small);
|
border-radius: var(--border-radius-messages-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.picker-disabled {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import type { StickerSetOrRecent } from '../../../types';
|
|||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_STATUS_ICON_ID,
|
||||||
DEFAULT_TOPIC_ICON_STICKER_ID,
|
DEFAULT_TOPIC_ICON_STICKER_ID,
|
||||||
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
|
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
@ -32,18 +33,23 @@ type OwnProps = {
|
|||||||
stickerSet: StickerSetOrRecent;
|
stickerSet: StickerSetOrRecent;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
|
idPrefix?: string;
|
||||||
shouldRender: boolean;
|
shouldRender: boolean;
|
||||||
favoriteStickers?: ApiSticker[];
|
favoriteStickers?: ApiSticker[];
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
|
isStatusPicker?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
isCustomEmojiPicker?: boolean;
|
|
||||||
shouldHideRecentHeader?: boolean;
|
shouldHideRecentHeader?: boolean;
|
||||||
withDefaultTopicIcon?: boolean;
|
withDefaultTopicIcon?: boolean;
|
||||||
|
withDefaultStatusIcon?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
onStickerUnfave?: (sticker: ApiSticker) => void;
|
onStickerUnfave?: (sticker: ApiSticker) => void;
|
||||||
onStickerFave?: (sticker: ApiSticker) => void;
|
onStickerFave?: (sticker: ApiSticker) => void;
|
||||||
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
||||||
|
onContextMenuOpen?: NoneToVoidFunction;
|
||||||
|
onContextMenuClose?: NoneToVoidFunction;
|
||||||
|
onContextMenuClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ITEMS_PER_ROW_FALLBACK = 8;
|
const ITEMS_PER_ROW_FALLBACK = 8;
|
||||||
@ -52,18 +58,23 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
stickerSet,
|
stickerSet,
|
||||||
loadAndPlay,
|
loadAndPlay,
|
||||||
index,
|
index,
|
||||||
|
idPrefix,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
favoriteStickers,
|
favoriteStickers,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
|
isStatusPicker,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
isCustomEmojiPicker,
|
|
||||||
shouldHideRecentHeader,
|
shouldHideRecentHeader,
|
||||||
withDefaultTopicIcon,
|
withDefaultTopicIcon,
|
||||||
|
withDefaultStatusIcon,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
onStickerUnfave,
|
onStickerUnfave,
|
||||||
onStickerFave,
|
onStickerFave,
|
||||||
onStickerRemoveRecent,
|
onStickerRemoveRecent,
|
||||||
|
onContextMenuOpen,
|
||||||
|
onContextMenuClose,
|
||||||
|
onContextMenuClick,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
clearRecentStickers,
|
clearRecentStickers,
|
||||||
@ -93,7 +104,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const stickerMarginPx = isMobile ? 8 : 16;
|
const stickerMarginPx = isMobile ? 8 : 16;
|
||||||
const emojiMarginPx = isMobile ? 8 : 10;
|
const emojiMarginPx = isMobile ? 8 : 10;
|
||||||
const containerPaddingPx = isMobile ? 8 : 0;
|
const containerPaddingPx = isMobile && !isStatusPicker ? 8 : 0;
|
||||||
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
||||||
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
|
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
|
||||||
const isEmoji = stickerSet.isEmoji;
|
const isEmoji = stickerSet.isEmoji;
|
||||||
@ -131,6 +142,17 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
} satisfies ApiSticker);
|
} satisfies ApiSticker);
|
||||||
}, [onStickerSelect]);
|
}, [onStickerSelect]);
|
||||||
|
|
||||||
|
const handleDefaultStatusIconClick = useCallback(() => {
|
||||||
|
onStickerSelect?.({
|
||||||
|
id: DEFAULT_STATUS_ICON_ID,
|
||||||
|
isLottie: false,
|
||||||
|
isVideo: false,
|
||||||
|
stickerSetInfo: {
|
||||||
|
shortName: 'dummy',
|
||||||
|
},
|
||||||
|
} satisfies ApiSticker);
|
||||||
|
}, [onStickerSelect]);
|
||||||
|
|
||||||
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
|
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
|
||||||
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
||||||
|
|
||||||
@ -183,7 +205,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
id={`${isCustomEmojiPicker ? 'custom-emoji-set' : 'sticker-set'}-${index}`}
|
id={`${idPrefix || 'sticker-set'}-${index}`}
|
||||||
className={
|
className={
|
||||||
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
||||||
}
|
}
|
||||||
@ -231,6 +253,16 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
<img src={grey} alt="Reset" />
|
<img src={grey} alt="Reset" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{withDefaultStatusIcon && (
|
||||||
|
<Button
|
||||||
|
className="StickerButton custom-emoji status-default"
|
||||||
|
color="translucent"
|
||||||
|
onClick={handleDefaultStatusIconClick}
|
||||||
|
key="default-status-icon"
|
||||||
|
>
|
||||||
|
<i className="icon-premium" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{shouldRender && stickerSet.stickers && stickerSet.stickers
|
{shouldRender && stickerSet.stickers && stickerSet.stickers
|
||||||
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
||||||
.map((sticker, i) => {
|
.map((sticker, i) => {
|
||||||
@ -248,6 +280,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
noAnimate={!loadAndPlay}
|
noAnimate={!loadAndPlay}
|
||||||
isSavedMessages={isSavedMessages}
|
isSavedMessages={isSavedMessages}
|
||||||
|
isStatusPicker={isStatusPicker}
|
||||||
canViewSet
|
canViewSet
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
sharedCanvasRef={canvasRef}
|
sharedCanvasRef={canvasRef}
|
||||||
@ -256,6 +289,9 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
|
onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
|
||||||
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
||||||
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
|
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
|
||||||
|
onContextMenuOpen={onContextMenuOpen}
|
||||||
|
onContextMenuClose={onContextMenuClose}
|
||||||
|
onContextMenuClick={onContextMenuClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -20,15 +20,16 @@ import { REM } from '../../../common/helpers/mediaDimensions';
|
|||||||
|
|
||||||
import useResizeObserver from '../../../../hooks/useResizeObserver';
|
import useResizeObserver from '../../../../hooks/useResizeObserver';
|
||||||
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
|
||||||
|
|
||||||
const SIZE = 1.25 * REM;
|
const SIZE = 1.25 * REM;
|
||||||
|
const THROTTLE_MS = 300;
|
||||||
|
|
||||||
type CustomEmojiPlayer = {
|
type CustomEmojiPlayer = {
|
||||||
play: () => void;
|
play: () => void;
|
||||||
pause: () => void;
|
pause: () => void;
|
||||||
destroy: () => void;
|
destroy: () => void;
|
||||||
updatePosition: (x: number, y: number, isMobile?: boolean) => void;
|
updatePosition: (x: number, y: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useInputCustomEmojis(
|
export default function useInputCustomEmojis(
|
||||||
@ -42,8 +43,6 @@ export default function useInputCustomEmojis(
|
|||||||
) {
|
) {
|
||||||
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map());
|
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map());
|
||||||
|
|
||||||
const { isMobile } = useAppLayout();
|
|
||||||
|
|
||||||
const removeContainers = useCallback((ids: string[]) => {
|
const removeContainers = useCallback((ids: string[]) => {
|
||||||
ids.forEach((id) => {
|
ids.forEach((id) => {
|
||||||
const player = mapRef.current.get(id);
|
const player = mapRef.current.get(id);
|
||||||
@ -80,7 +79,7 @@ export default function useInputCustomEmojis(
|
|||||||
|
|
||||||
if (mapRef.current.has(id)) {
|
if (mapRef.current.has(id)) {
|
||||||
const player = mapRef.current.get(id)!;
|
const player = mapRef.current.get(id)!;
|
||||||
player.updatePosition(x, y, isMobile);
|
player.updatePosition(x, y);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +98,6 @@ export default function useInputCustomEmojis(
|
|||||||
mediaUrl,
|
mediaUrl,
|
||||||
isHq,
|
isHq,
|
||||||
position: { x, y },
|
position: { x, y },
|
||||||
isMobile,
|
|
||||||
});
|
});
|
||||||
animation.play();
|
animation.play();
|
||||||
|
|
||||||
@ -107,10 +105,7 @@ export default function useInputCustomEmojis(
|
|||||||
});
|
});
|
||||||
|
|
||||||
removeContainers(Array.from(removedContainers));
|
removeContainers(Array.from(removedContainers));
|
||||||
}, [
|
}, [absoluteContainerRef, inputRef, prefixId, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
|
||||||
absoluteContainerRef, inputRef, prefixId, isMobile, removeContainers, sharedCanvasHqRef,
|
|
||||||
sharedCanvasRef,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addCustomEmojiInputRenderCallback(synchronizeElements);
|
addCustomEmojiInputRenderCallback(synchronizeElements);
|
||||||
@ -129,7 +124,13 @@ export default function useInputCustomEmojis(
|
|||||||
synchronizeElements();
|
synchronizeElements();
|
||||||
}, [getHtml, synchronizeElements, inputRef, removeContainers, sharedCanvasRef, isActive]);
|
}, [getHtml, synchronizeElements, inputRef, removeContainers, sharedCanvasRef, isActive]);
|
||||||
|
|
||||||
useResizeObserver(sharedCanvasRef, synchronizeElements, true);
|
const throttledSynchronizeElements = useThrottledCallback(
|
||||||
|
synchronizeElements,
|
||||||
|
[synchronizeElements],
|
||||||
|
THROTTLE_MS,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
useResizeObserver(sharedCanvasRef, throttledSynchronizeElements);
|
||||||
|
|
||||||
const freezeAnimation = useCallback(() => {
|
const freezeAnimation = useCallback(() => {
|
||||||
mapRef.current.forEach((player) => {
|
mapRef.current.forEach((player) => {
|
||||||
@ -162,7 +163,6 @@ function createPlayer({
|
|||||||
mediaUrl,
|
mediaUrl,
|
||||||
position,
|
position,
|
||||||
isHq,
|
isHq,
|
||||||
isMobile,
|
|
||||||
}: {
|
}: {
|
||||||
customEmoji: ApiSticker;
|
customEmoji: ApiSticker;
|
||||||
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
|
||||||
@ -172,7 +172,6 @@ function createPlayer({
|
|||||||
mediaUrl: string;
|
mediaUrl: string;
|
||||||
position: { x: number; y: number };
|
position: { x: number; y: number };
|
||||||
isHq?: boolean;
|
isHq?: boolean;
|
||||||
isMobile?: boolean;
|
|
||||||
}): CustomEmojiPlayer {
|
}): CustomEmojiPlayer {
|
||||||
if (customEmoji.isLottie) {
|
if (customEmoji.isLottie) {
|
||||||
const lottie = RLottie.init(
|
const lottie = RLottie.init(
|
||||||
@ -185,15 +184,14 @@ function createPlayer({
|
|||||||
size: SIZE,
|
size: SIZE,
|
||||||
coords: position,
|
coords: position,
|
||||||
isLowPriority: !isHq,
|
isLowPriority: !isHq,
|
||||||
isMobile,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
play: () => lottie.play(),
|
play: () => lottie.play(),
|
||||||
pause: () => lottie.pause(),
|
pause: () => lottie.pause(),
|
||||||
destroy: () => lottie.removeContainer(uniqueId),
|
destroy: () => lottie.removeContainer(uniqueId),
|
||||||
updatePosition: (x: number, y: number, isMobileNew?: boolean) => {
|
updatePosition: (x: number, y: number) => {
|
||||||
return lottie.setSharedCanvasCoords(uniqueId, { x, y }, isMobileNew);
|
return lottie.setSharedCanvasCoords(uniqueId, { x, y });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.particle {
|
.particle {
|
||||||
|
--color-text: var(--color-primary);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
@ -11,6 +13,7 @@
|
|||||||
offset-path: var(--offset-path);
|
offset-path: var(--offset-path);
|
||||||
offset-rotate: 0deg;
|
offset-rotate: 0deg;
|
||||||
animation: 1.5s particle ease-out;
|
animation: 1.5s particle ease-out;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes particle {
|
@keyframes particle {
|
||||||
@ -1,38 +1,58 @@
|
|||||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiReactionCustomEmoji } from '../../../api/types';
|
import type { ApiEmojiStatus, ApiReactionCustomEmoji } from '../../../api/types';
|
||||||
|
|
||||||
import { getStickerPreviewHash } from '../../../global/helpers';
|
|
||||||
import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/environment';
|
import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/environment';
|
||||||
|
import { getStickerPreviewHash } from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
|
||||||
import styles from './CustomReactionAnimation.module.scss';
|
import CustomEmoji from '../../common/CustomEmoji';
|
||||||
|
|
||||||
|
import styles from './CustomEmojiEffect.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
reaction: ApiReactionCustomEmoji;
|
reaction: ApiReactionCustomEmoji | ApiEmojiStatus;
|
||||||
|
className?: string;
|
||||||
|
isLottie?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EFFECT_AMOUNT = 7;
|
const EFFECT_AMOUNT = 7;
|
||||||
|
|
||||||
const CustomReactionAnimation: FC<OwnProps> = ({
|
const CustomEmojiEffect: FC<OwnProps> = ({
|
||||||
reaction,
|
reaction,
|
||||||
|
isLottie,
|
||||||
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const stickerHash = getStickerPreviewHash(reaction.documentId);
|
const stickerHash = getStickerPreviewHash(reaction.documentId);
|
||||||
|
|
||||||
const previewMediaData = useMedia(stickerHash);
|
const previewMediaData = useMedia(!isLottie ? stickerHash : undefined);
|
||||||
|
|
||||||
const paths: string[] = useMemo(() => {
|
const paths: string[] = useMemo(() => {
|
||||||
if (!IS_OFFSET_PATH_SUPPORTED) return [];
|
if (!IS_OFFSET_PATH_SUPPORTED) return [];
|
||||||
return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath());
|
return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!previewMediaData) return undefined;
|
if (!previewMediaData && !isLottie) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={buildClassName(styles.root, className)}>
|
||||||
{paths.map((path) => {
|
{paths.map((path) => {
|
||||||
const style = `--offset-path: path('${path}');`;
|
const style = `--offset-path: path('${path}');`;
|
||||||
|
if (isLottie) {
|
||||||
|
return (
|
||||||
|
<CustomEmoji
|
||||||
|
documentId={reaction.documentId}
|
||||||
|
className={styles.particle}
|
||||||
|
style={style}
|
||||||
|
withSharedAnimation
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
src={previewMediaData}
|
src={previewMediaData}
|
||||||
@ -46,7 +66,7 @@ const CustomReactionAnimation: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(CustomReactionAnimation);
|
export default memo(CustomEmojiEffect);
|
||||||
|
|
||||||
function generateRandomDropPath() {
|
function generateRandomDropPath() {
|
||||||
const x = (10 + Math.random() * 60) * (Math.random() > 0.5 ? 1 : -1);
|
const x = (10 + Math.random() * 60) * (Math.random() > 0.5 ? 1 : -1);
|
||||||
@ -113,6 +113,7 @@ import useOuterHandlers from './hooks/useOuterHandlers';
|
|||||||
import useInnerHandlers from './hooks/useInnerHandlers';
|
import useInnerHandlers from './hooks/useInnerHandlers';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||||
|
import useThrottledCallback from '../../../hooks/useThrottledCallback';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
@ -252,6 +253,7 @@ const APPEARANCE_DELAY = 10;
|
|||||||
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
||||||
const QUICK_REACTION_SIZE = 1.75 * REM;
|
const QUICK_REACTION_SIZE = 1.75 * REM;
|
||||||
const BOTTOM_FOCUS_SCROLL_THRESHOLD = 5;
|
const BOTTOM_FOCUS_SCROLL_THRESHOLD = 5;
|
||||||
|
const THROTTLE_MS = 300;
|
||||||
|
|
||||||
const Message: FC<OwnProps & StateProps> = ({
|
const Message: FC<OwnProps & StateProps> = ({
|
||||||
message,
|
message,
|
||||||
@ -613,7 +615,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [focusLastMessage]);
|
}, [focusLastMessage]);
|
||||||
|
|
||||||
useResizeObserver(shouldFocusOnResize ? ref : undefined, handleResize, true);
|
const throttledResize = useThrottledCallback(handleResize, [handleResize], THROTTLE_MS, false);
|
||||||
|
|
||||||
|
useResizeObserver(shouldFocusOnResize ? ref : undefined, throttledResize);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const bottomMarker = bottomMarkerRef.current;
|
const bottomMarker = bottomMarkerRef.current;
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import useCustomEmoji from '../../common/hooks/useCustomEmoji';
|
|||||||
import CustomEmoji from '../../common/CustomEmoji';
|
import CustomEmoji from '../../common/CustomEmoji';
|
||||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||||
import CustomReactionAnimation from './CustomReactionAnimation';
|
import CustomEmojiEffect from './CustomEmojiEffect';
|
||||||
|
|
||||||
import styles from './ReactionAnimatedEmoji.module.scss';
|
import styles from './ReactionAnimatedEmoji.module.scss';
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
|||||||
onEnded={handleEnded}
|
onEnded={handleEnded}
|
||||||
/>
|
/>
|
||||||
{isCustom ? (
|
{isCustom ? (
|
||||||
!assignedEffectId && isIntersecting && <CustomReactionAnimation reaction={reaction} />
|
!assignedEffectId && isIntersecting && <CustomEmojiEffect reaction={reaction} />
|
||||||
) : (
|
) : (
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
key={centerIconId}
|
key={centerIconId}
|
||||||
|
|||||||
@ -134,6 +134,7 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className={buildClassName(styles.section, styles.bottom)}>
|
<div className={buildClassName(styles.section, styles.bottom)}>
|
||||||
<CustomEmojiPicker
|
<CustomEmojiPicker
|
||||||
|
idPrefix="create-topic-icons-set-"
|
||||||
loadAndPlay={isActive}
|
loadAndPlay={isActive}
|
||||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||||
className={styles.iconPicker}
|
className={styles.iconPicker}
|
||||||
|
|||||||
@ -145,6 +145,7 @@ const EditTopic: FC<OwnProps & StateProps> = ({
|
|||||||
{!isGeneral && (
|
{!isGeneral && (
|
||||||
<div className={buildClassName(styles.section, styles.bottom)}>
|
<div className={buildClassName(styles.section, styles.bottom)}>
|
||||||
<CustomEmojiPicker
|
<CustomEmojiPicker
|
||||||
|
idPrefix="edit-topic-icons-set-"
|
||||||
loadAndPlay={isActive}
|
loadAndPlay={isActive}
|
||||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||||
className={styles.iconPicker}
|
className={styles.iconPicker}
|
||||||
|
|||||||
@ -24,6 +24,7 @@ type OwnProps = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
bubbleClassName?: string;
|
||||||
style?: string;
|
style?: string;
|
||||||
bubbleStyle?: string;
|
bubbleStyle?: string;
|
||||||
ariaLabelledBy?: string;
|
ariaLabelledBy?: string;
|
||||||
@ -54,6 +55,7 @@ const Menu: FC<OwnProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
id,
|
id,
|
||||||
className,
|
className,
|
||||||
|
bubbleClassName,
|
||||||
style,
|
style,
|
||||||
bubbleStyle,
|
bubbleStyle,
|
||||||
ariaLabelledBy,
|
ariaLabelledBy,
|
||||||
@ -116,12 +118,13 @@ const Menu: FC<OwnProps> = ({
|
|||||||
noCloseOnBackdrop ? undefined : onClose,
|
noCloseOnBackdrop ? undefined : onClose,
|
||||||
);
|
);
|
||||||
|
|
||||||
const bubbleClassName = buildClassName(
|
const bubbleFullClassName = buildClassName(
|
||||||
'bubble menu-container custom-scroll',
|
'bubble menu-container custom-scroll',
|
||||||
positionY,
|
positionY,
|
||||||
positionX,
|
positionX,
|
||||||
footer && 'with-footer',
|
footer && 'with-footer',
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
|
bubbleClassName,
|
||||||
);
|
);
|
||||||
|
|
||||||
const transformOriginYStyle = transformOriginY !== undefined ? `${transformOriginY}px` : undefined;
|
const transformOriginYStyle = transformOriginY !== undefined ? `${transformOriginY}px` : undefined;
|
||||||
@ -154,7 +157,7 @@ const Menu: FC<OwnProps> = ({
|
|||||||
<div
|
<div
|
||||||
role="presentation"
|
role="presentation"
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={bubbleClassName}
|
className={bubbleFullClassName}
|
||||||
style={buildStyle(
|
style={buildStyle(
|
||||||
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
|
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
|
||||||
bubbleStyle,
|
bubbleStyle,
|
||||||
|
|||||||
@ -8,14 +8,13 @@ import { IS_COMPACT_MENU } from '../../util/environment';
|
|||||||
|
|
||||||
import './MenuItem.scss';
|
import './MenuItem.scss';
|
||||||
|
|
||||||
type OnClickHandler = (e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>) => void;
|
|
||||||
|
|
||||||
export type MenuItemProps = {
|
export type MenuItemProps = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
customIcon?: React.ReactNode;
|
customIcon?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onClick?: OnClickHandler;
|
onClick?: (e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>, arg?: number) => void;
|
||||||
|
clickArg?: number;
|
||||||
onContextMenu?: (e: React.UIEvent) => void;
|
onContextMenu?: (e: React.UIEvent) => void;
|
||||||
href?: string;
|
href?: string;
|
||||||
download?: string;
|
download?: string;
|
||||||
@ -39,6 +38,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
|||||||
ariaLabel,
|
ariaLabel,
|
||||||
withWrap,
|
withWrap,
|
||||||
onContextMenu,
|
onContextMenu,
|
||||||
|
clickArg,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -50,8 +50,8 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(e);
|
onClick(e, clickArg);
|
||||||
}, [disabled, onClick]);
|
}, [clickArg, disabled, onClick]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||||
@ -65,8 +65,8 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(e);
|
onClick(e, clickArg);
|
||||||
}, [disabled, onClick]);
|
}, [clickArg, disabled, onClick]);
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'MenuItem',
|
'MenuItem',
|
||||||
|
|||||||
@ -168,6 +168,7 @@ export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
|||||||
export const STICKER_SIZE_JOIN_REQUESTS = 140;
|
export const STICKER_SIZE_JOIN_REQUESTS = 140;
|
||||||
export const STICKER_SIZE_INVITES = 140;
|
export const STICKER_SIZE_INVITES = 140;
|
||||||
export const RECENT_STICKERS_LIMIT = 20;
|
export const RECENT_STICKERS_LIMIT = 20;
|
||||||
|
export const RECENT_STATUS_LIMIT = 20;
|
||||||
export const EMOJI_STATUS_LOOP_LIMIT = 2;
|
export const EMOJI_STATUS_LOOP_LIMIT = 2;
|
||||||
export const EMOJI_SIZES = 7;
|
export const EMOJI_SIZES = 7;
|
||||||
export const RECENT_SYMBOL_SET_ID = 'recent';
|
export const RECENT_SYMBOL_SET_ID = 'recent';
|
||||||
@ -175,6 +176,7 @@ export const FAVORITE_SYMBOL_SET_ID = 'favorite';
|
|||||||
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
||||||
export const PREMIUM_STICKER_SET_ID = 'premium';
|
export const PREMIUM_STICKER_SET_ID = 'premium';
|
||||||
export const DEFAULT_TOPIC_ICON_STICKER_ID = 'topic-default-icon';
|
export const DEFAULT_TOPIC_ICON_STICKER_ID = 'topic-default-icon';
|
||||||
|
export const DEFAULT_STATUS_ICON_ID = 'status-default-icon';
|
||||||
export const EMOJI_IMG_REGEX = /<img[^>]+alt="([^"]+)"(?![^>]*data-document-id)[^>]*>/gm;
|
export const EMOJI_IMG_REGEX = /<img[^>]+alt="([^"]+)"(?![^>]*data-document-id)[^>]*>/gm;
|
||||||
|
|
||||||
export const BASE_EMOJI_KEYWORD_LANG = 'en';
|
export const BASE_EMOJI_KEYWORD_LANG = 'en';
|
||||||
|
|||||||
@ -18,7 +18,9 @@ import {
|
|||||||
updateStickersForEmoji,
|
updateStickersForEmoji,
|
||||||
rebuildStickersForEmoji,
|
rebuildStickersForEmoji,
|
||||||
updateCustomEmojiForEmoji,
|
updateCustomEmojiForEmoji,
|
||||||
updateCustomEmojiSets, updateStickerSearch,
|
updateCustomEmojiSets,
|
||||||
|
updateRecentStatusCustomEmojis,
|
||||||
|
updateStickerSearch,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import searchWords from '../../../util/searchWords';
|
import searchWords from '../../../util/searchWords';
|
||||||
import { selectTabState, selectIsCurrentUserPremium, selectStickerSet } from '../../selectors';
|
import { selectTabState, selectIsCurrentUserPremium, selectStickerSet } from '../../selectors';
|
||||||
@ -246,6 +248,21 @@ addActionHandler('loadDefaultTopicIcons', async (global): Promise<void> => {
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadDefaultStatusIcons', async (global): Promise<void> => {
|
||||||
|
const stickerSet = await callApi('fetchDefaultStatusEmojis');
|
||||||
|
if (!stickerSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
const { set, stickers } = stickerSet;
|
||||||
|
const fullSet = { ...set, stickers };
|
||||||
|
|
||||||
|
global = updateStickerSet(global, fullSet.id, fullSet);
|
||||||
|
global = { ...global, defaultStatusIconsId: fullSet.id };
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('loadStickers', (global, actions, payload): ActionReturnType => {
|
addActionHandler('loadStickers', (global, actions, payload): ActionReturnType => {
|
||||||
const { stickerSetInfo, tabId = getCurrentTabId() } = payload;
|
const { stickerSetInfo, tabId = getCurrentTabId() } = payload;
|
||||||
const cachedSet = selectStickerSet(global, stickerSetInfo);
|
const cachedSet = selectStickerSet(global, stickerSetInfo);
|
||||||
@ -746,6 +763,17 @@ addActionHandler('openStickerSet', async (global, actions, payload): Promise<voi
|
|||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadRecentEmojiStatuses', async (global): Promise<void> => {
|
||||||
|
const result = await callApi('fetchRecentEmojiStatuses');
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = updateRecentStatusCustomEmojis(global, result.hash, result.emojiStatuses!);
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
async function searchGifs<T extends GlobalState>(global: T, query: string, botUsername?: string, offset?: string,
|
async function searchGifs<T extends GlobalState>(global: T, query: string, botUsername?: string, offset?: string,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>) {
|
...[tabId = getCurrentTabId()]: TabArgs<T>) {
|
||||||
const result = await callApi('searchGifs', { query, offset, username: botUsername });
|
const result = await callApi('searchGifs', { query, offset, username: botUsername });
|
||||||
|
|||||||
@ -347,3 +347,9 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
|
|||||||
|
|
||||||
void callApi('reportSpam', userOrChat);
|
void callApi('reportSpam', userOrChat);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('setEmojiStatus', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { emojiStatus, expires } = payload!;
|
||||||
|
|
||||||
|
void callApi('updateEmojiStatus', emojiStatus, expires);
|
||||||
|
});
|
||||||
|
|||||||
@ -38,6 +38,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
actions.loadRecentStickers();
|
actions.loadRecentStickers();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'updateRecentEmojiStatuses':
|
||||||
|
actions.loadRecentEmojiStatuses();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'updateMoveStickerSetToTop': {
|
case 'updateMoveStickerSetToTop': {
|
||||||
const oldOrder = update.isCustomEmoji ? global.customEmojis.added.setIds : global.stickers.added.setIds;
|
const oldOrder = update.isCustomEmoji ? global.customEmojis.added.setIds : global.stickers.added.setIds;
|
||||||
if (!oldOrder) return global;
|
if (!oldOrder) return global;
|
||||||
|
|||||||
@ -249,9 +249,14 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
byId: {},
|
byId: {},
|
||||||
lastRendered: [],
|
lastRendered: [],
|
||||||
forEmoji: {},
|
forEmoji: {},
|
||||||
|
statusRecent: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.customEmojis.statusRecent) {
|
||||||
|
cached.customEmojis.statusRecent = {};
|
||||||
|
}
|
||||||
|
|
||||||
if (!cached.recentCustomEmojis) {
|
if (!cached.recentCustomEmojis) {
|
||||||
cached.recentCustomEmojis = [];
|
cached.recentCustomEmojis = [];
|
||||||
}
|
}
|
||||||
@ -426,6 +431,7 @@ function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['cust
|
|||||||
lastRendered: idsToSave,
|
lastRendered: idsToSave,
|
||||||
forEmoji: {},
|
forEmoji: {},
|
||||||
added: {},
|
added: {},
|
||||||
|
statusRecent: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
byId: {},
|
byId: {},
|
||||||
added: {},
|
added: {},
|
||||||
forEmoji: {},
|
forEmoji: {},
|
||||||
|
statusRecent: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
emojiKeywords: {},
|
emojiKeywords: {},
|
||||||
|
|||||||
@ -242,6 +242,22 @@ export function updateCustomEmojiForEmoji<T extends GlobalState>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateRecentStatusCustomEmojis<T extends GlobalState>(
|
||||||
|
global: T, hash: string, emojis: ApiSticker[],
|
||||||
|
): T {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
customEmojis: {
|
||||||
|
...global.customEmojis,
|
||||||
|
statusRecent: {
|
||||||
|
...global.customEmojis.statusRecent,
|
||||||
|
hash,
|
||||||
|
emojis,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function rebuildStickersForEmoji<T extends GlobalState>(global: T): T {
|
export function rebuildStickersForEmoji<T extends GlobalState>(global: T): T {
|
||||||
if (global.stickers.forEmoji) {
|
if (global.stickers.forEmoji) {
|
||||||
const { emoji, stickers, hash } = global.stickers.forEmoji;
|
const { emoji, stickers, hash } = global.stickers.forEmoji;
|
||||||
|
|||||||
@ -718,12 +718,17 @@ export type GlobalState = {
|
|||||||
stickers?: ApiSticker[];
|
stickers?: ApiSticker[];
|
||||||
};
|
};
|
||||||
featuredIds?: string[];
|
featuredIds?: string[];
|
||||||
|
statusRecent: {
|
||||||
|
hash?: string;
|
||||||
|
emojis?: ApiSticker[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
animatedEmojis?: ApiStickerSet;
|
animatedEmojis?: ApiStickerSet;
|
||||||
animatedEmojiEffects?: ApiStickerSet;
|
animatedEmojiEffects?: ApiStickerSet;
|
||||||
genericEmojiEffects?: ApiStickerSet;
|
genericEmojiEffects?: ApiStickerSet;
|
||||||
defaultTopicIconsId?: string;
|
defaultTopicIconsId?: string;
|
||||||
|
defaultStatusIconsId?: string;
|
||||||
premiumGifts?: ApiStickerSet;
|
premiumGifts?: ApiStickerSet;
|
||||||
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
||||||
|
|
||||||
@ -1886,6 +1891,8 @@ export interface ActionPayloads {
|
|||||||
};
|
};
|
||||||
clearRecentCustomEmoji: undefined;
|
clearRecentCustomEmoji: undefined;
|
||||||
loadFeaturedEmojiStickers: undefined;
|
loadFeaturedEmojiStickers: undefined;
|
||||||
|
loadDefaultStatusIcons: undefined;
|
||||||
|
loadRecentEmojiStatuses: undefined;
|
||||||
|
|
||||||
// Bots
|
// Bots
|
||||||
sendBotCommand: {
|
sendBotCommand: {
|
||||||
@ -2212,6 +2219,10 @@ export interface ActionPayloads {
|
|||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
|
|
||||||
closeGiftPremiumModal: WithTabId | undefined;
|
closeGiftPremiumModal: WithTabId | undefined;
|
||||||
|
setEmojiStatus: {
|
||||||
|
emojiStatus: ApiSticker;
|
||||||
|
expires?: number;
|
||||||
|
};
|
||||||
|
|
||||||
// Invoice
|
// Invoice
|
||||||
openInvoice: ApiInputInvoice & WithTabId;
|
openInvoice: ApiInputInvoice & WithTabId;
|
||||||
|
|||||||
@ -5,8 +5,10 @@ import {
|
|||||||
import { round } from '../util/math';
|
import { round } from '../util/math';
|
||||||
|
|
||||||
import useResizeObserver from './useResizeObserver';
|
import useResizeObserver from './useResizeObserver';
|
||||||
|
import useThrottledCallback from './useThrottledCallback';
|
||||||
|
|
||||||
const ANIMATION_END_TIMEOUT = 500;
|
const ANIMATION_END_TIMEOUT = 500;
|
||||||
|
const THROTTLE_MS = 300;
|
||||||
|
|
||||||
export default function useBoundsInSharedCanvas(
|
export default function useBoundsInSharedCanvas(
|
||||||
containerRef: React.RefObject<HTMLDivElement>,
|
containerRef: React.RefObject<HTMLDivElement>,
|
||||||
@ -39,9 +41,11 @@ export default function useBoundsInSharedCanvas(
|
|||||||
setSize(Math.round(targetBounds.width));
|
setSize(Math.round(targetBounds.width));
|
||||||
}, [containerRef, sharedCanvasRef]);
|
}, [containerRef, sharedCanvasRef]);
|
||||||
|
|
||||||
|
const throttledRecalculate = useThrottledCallback(recalculate, [recalculate], THROTTLE_MS, false);
|
||||||
|
|
||||||
useLayoutEffect(recalculate, [recalculate]);
|
useLayoutEffect(recalculate, [recalculate]);
|
||||||
|
|
||||||
useResizeObserver(sharedCanvasRef, recalculate, true);
|
useResizeObserver(sharedCanvasRef, throttledRecalculate);
|
||||||
|
|
||||||
const coords = useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
|
const coords = useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { useEffect } from '../lib/teact/teact';
|
import { useEffect } from '../lib/teact/teact';
|
||||||
import { throttle } from '../util/schedulers';
|
|
||||||
|
|
||||||
const THROTTLE = 300;
|
import type { CallbackManager } from '../util/callbacks';
|
||||||
|
|
||||||
|
import { createCallbackManager } from '../util/callbacks';
|
||||||
|
|
||||||
|
const elementObserverMap = new Map<HTMLElement, [ResizeObserver, CallbackManager]>();
|
||||||
|
|
||||||
export default function useResizeObserver(
|
export default function useResizeObserver(
|
||||||
ref: React.RefObject<HTMLElement> | undefined,
|
ref: React.RefObject<HTMLElement> | undefined,
|
||||||
onResize: (entry: ResizeObserverEntry) => void,
|
onResize: (entry: ResizeObserverEntry) => void,
|
||||||
withThrottle = false,
|
|
||||||
) {
|
) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!('ResizeObserver' in window) || !ref?.current) {
|
if (!('ResizeObserver' in window) || !ref?.current) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const el = ref.current;
|
||||||
const callback: ResizeObserverCallback = ([entry]) => {
|
const callback: ResizeObserverCallback = ([entry]) => {
|
||||||
// During animation
|
// During animation
|
||||||
if (!(entry.target as HTMLElement).offsetParent) {
|
if (!(entry.target as HTMLElement).offsetParent) {
|
||||||
@ -21,12 +23,23 @@ export default function useResizeObserver(
|
|||||||
|
|
||||||
onResize(entry);
|
onResize(entry);
|
||||||
};
|
};
|
||||||
const observer = new ResizeObserver(withThrottle ? throttle(callback, THROTTLE, false) : callback);
|
|
||||||
|
|
||||||
observer.observe(ref.current);
|
let [observer, callbackManager] = elementObserverMap.get(el) || [undefined, undefined];
|
||||||
|
if (!observer) {
|
||||||
|
callbackManager = createCallbackManager();
|
||||||
|
observer = new ResizeObserver(callbackManager.runCallbacks);
|
||||||
|
elementObserverMap.set(el, [observer, callbackManager]);
|
||||||
|
observer.observe(el);
|
||||||
|
}
|
||||||
|
callbackManager!.addCallback(callback);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
callbackManager!.removeCallback(callback);
|
||||||
|
if (!callbackManager!.hasCallbacks()) {
|
||||||
|
observer!.unobserve(el);
|
||||||
|
observer!.disconnect();
|
||||||
|
elementObserverMap.delete(el);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [onResize, ref, withThrottle]);
|
}, [onResize, ref]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1127,6 +1127,8 @@ account.setGlobalPrivacySettings#1edaaac2 settings:GlobalPrivacySettings = Globa
|
|||||||
account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool;
|
account.reportProfilePhoto#fa8cc6f5 peer:InputPeer photo_id:InputPhoto reason:ReportReason message:string = Bool;
|
||||||
account.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool;
|
account.setAuthorizationTTL#bf899aa0 authorization_ttl_days:int = Bool;
|
||||||
account.changeAuthorizationSettings#40f48462 flags:# hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool;
|
account.changeAuthorizationSettings#40f48462 flags:# hash:long encrypted_requests_disabled:flags.0?Bool call_requests_disabled:flags.1?Bool = Bool;
|
||||||
|
account.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;
|
||||||
|
account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;
|
||||||
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
|
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
|
||||||
account.toggleUsername#58d6b376 username:string active:Bool = Bool;
|
account.toggleUsername#58d6b376 username:string active:Bool = Bool;
|
||||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||||
|
|||||||
@ -53,6 +53,8 @@
|
|||||||
"account.reportProfilePhoto",
|
"account.reportProfilePhoto",
|
||||||
"account.changeAuthorizationSettings",
|
"account.changeAuthorizationSettings",
|
||||||
"account.setAuthorizationTTL",
|
"account.setAuthorizationTTL",
|
||||||
|
"account.updateEmojiStatus",
|
||||||
|
"account.getRecentEmojiStatuses",
|
||||||
"account.reorderUsernames",
|
"account.reorderUsernames",
|
||||||
"account.toggleUsername",
|
"account.toggleUsername",
|
||||||
"users.getUsers",
|
"users.getUsers",
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { DPR, IS_SAFARI, IS_ANDROID } from '../../util/environment';
|
import {
|
||||||
|
DPR, IS_SAFARI, IS_ANDROID, IS_IOS,
|
||||||
|
} from '../../util/environment';
|
||||||
import WorkerConnector from '../../util/WorkerConnector';
|
import WorkerConnector from '../../util/WorkerConnector';
|
||||||
import { animate } from '../../util/animation';
|
import { animate } from '../../util/animation';
|
||||||
import cycleRestrict from '../../util/cycleRestrict';
|
import cycleRestrict from '../../util/cycleRestrict';
|
||||||
@ -9,7 +11,6 @@ interface Params {
|
|||||||
size?: number;
|
size?: number;
|
||||||
quality?: number;
|
quality?: number;
|
||||||
isLowPriority?: boolean;
|
isLowPriority?: boolean;
|
||||||
isMobile?: boolean;
|
|
||||||
coords?: { x: number; y: number };
|
coords?: { x: number; y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,8 +21,7 @@ type Frame =
|
|||||||
| ImageBitmap;
|
| ImageBitmap;
|
||||||
|
|
||||||
const MAX_WORKERS = 4;
|
const MAX_WORKERS = 4;
|
||||||
const HIGH_PRIORITY_QUALITY_MOBILE = 0.75;
|
const HIGH_PRIORITY_QUALITY = (IS_ANDROID || IS_IOS) ? 0.75 : 1;
|
||||||
const HIGH_PRIORITY_QUALITY_DESKTOP = 1;
|
|
||||||
const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
|
const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
|
||||||
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
|
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
|
||||||
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
||||||
@ -96,7 +96,7 @@ class RLottie {
|
|||||||
instance = new RLottie(...args);
|
instance = new RLottie(...args);
|
||||||
instancesById.set(id, instance);
|
instancesById.set(id, instance);
|
||||||
} else {
|
} else {
|
||||||
instance.addContainer(container, canvas, onLoad, params?.coords, params?.isMobile);
|
instance.addContainer(container, canvas, onLoad, params?.coords);
|
||||||
}
|
}
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
@ -113,7 +113,7 @@ class RLottie {
|
|||||||
private onEnded?: (isDestroyed?: boolean) => void,
|
private onEnded?: (isDestroyed?: boolean) => void,
|
||||||
private onLoop?: () => void,
|
private onLoop?: () => void,
|
||||||
) {
|
) {
|
||||||
this.addContainer(containerId, container, onLoad, params.coords, params.isMobile);
|
this.addContainer(containerId, container, onLoad, params.coords);
|
||||||
this.initConfig();
|
this.initConfig();
|
||||||
this.initRenderer();
|
this.initRenderer();
|
||||||
}
|
}
|
||||||
@ -200,18 +200,13 @@ class RLottie {
|
|||||||
this.params.noLoop = noLoop;
|
this.params.noLoop = noLoop;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsMobile(isMobile?: Params['isMobile']) {
|
setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) {
|
||||||
this.params.isMobile = isMobile;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSharedCanvasCoords(containerId: string, newCoords: Params['coords'], isMobile?: Params['isMobile']) {
|
|
||||||
const containerInfo = this.containers.get(containerId)!;
|
const containerInfo = this.containers.get(containerId)!;
|
||||||
const {
|
const {
|
||||||
canvas, ctx,
|
canvas, ctx,
|
||||||
} = containerInfo;
|
} = containerInfo;
|
||||||
|
|
||||||
if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') {
|
if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') {
|
||||||
this.setIsMobile(isMobile);
|
|
||||||
const sizeFactor = this.calcSizeFactor();
|
const sizeFactor = this.calcSizeFactor();
|
||||||
ensureCanvasSize(canvas, sizeFactor);
|
ensureCanvasSize(canvas, sizeFactor);
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
@ -238,9 +233,7 @@ class RLottie {
|
|||||||
container: HTMLDivElement | HTMLCanvasElement,
|
container: HTMLDivElement | HTMLCanvasElement,
|
||||||
onLoad?: NoneToVoidFunction,
|
onLoad?: NoneToVoidFunction,
|
||||||
coords?: Params['coords'],
|
coords?: Params['coords'],
|
||||||
isMobile?: Params['isMobile'],
|
|
||||||
) {
|
) {
|
||||||
this.setIsMobile(isMobile);
|
|
||||||
const sizeFactor = this.calcSizeFactor();
|
const sizeFactor = this.calcSizeFactor();
|
||||||
|
|
||||||
let imgSize: number;
|
let imgSize: number;
|
||||||
@ -318,11 +311,9 @@ class RLottie {
|
|||||||
const {
|
const {
|
||||||
isLowPriority,
|
isLowPriority,
|
||||||
size,
|
size,
|
||||||
isMobile,
|
|
||||||
// Reduced quality only looks acceptable on big enough images
|
// Reduced quality only looks acceptable on big enough images
|
||||||
quality = isLowPriority && (!size || size > LOW_PRIORITY_QUALITY_SIZE_THRESHOLD)
|
quality = isLowPriority && (!size || size > LOW_PRIORITY_QUALITY_SIZE_THRESHOLD)
|
||||||
? LOW_PRIORITY_QUALITY
|
? LOW_PRIORITY_QUALITY : HIGH_PRIORITY_QUALITY,
|
||||||
: (isMobile ? HIGH_PRIORITY_QUALITY_MOBILE : HIGH_PRIORITY_QUALITY_DESKTOP),
|
|
||||||
} = this.params;
|
} = this.params;
|
||||||
|
|
||||||
// Reduced quality only looks acceptable on high DPR screens
|
// Reduced quality only looks acceptable on high DPR screens
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user