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": {
|
||||
"name": "eslint-plugin-eslint-multitab-tt",
|
||||
"version": "0.0.0",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
@ -7185,15 +7186,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-doc-generator": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-doc-generator/-/eslint-doc-generator-1.4.3.tgz",
|
||||
"integrity": "sha512-cn9KXE7xuKlxKi/9VbirR3cbz7W1geRObwWzZjJAnpTeNBoqA8Rj+lD8/HHHJ7PnOdaTrRyhhoYdCtxqq3U7Bw==",
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-doc-generator/-/eslint-doc-generator-1.4.2.tgz",
|
||||
"integrity": "sha512-axO5W9Nt/n/cppE75nJ8JVoqmzUyinzhNQ42vJFqjSnK8KeQoWE6IWqqT1wZly5uSneMuuWYYlCGicc0UeJp2g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^5.38.1",
|
||||
"ajv": "^8.11.2",
|
||||
"boolean": "^3.2.0",
|
||||
"commander": "^10.0.0",
|
||||
"commander": "^9.4.0",
|
||||
"cosmiconfig": "^8.0.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"dot-prop": "^7.2.0",
|
||||
@ -7230,12 +7231,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-doc-generator/node_modules/commander": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz",
|
||||
"integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==",
|
||||
"version": "9.5.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-doc-generator/node_modules/cosmiconfig": {
|
||||
@ -7260,9 +7261,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/eslint-doc-generator/node_modules/type-fest": {
|
||||
"version": "3.5.6",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.6.tgz",
|
||||
"integrity": "sha512-6bd2bflx8ed7c99tc6zSTIzHr1/QG29bQoK4Qh8MYGnlPbODUzGxklLShjwc/xWQQFHgIci+y5Arv7Rbb0LjXw==",
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.5.2.tgz",
|
||||
"integrity": "sha512-Ph7S4EhXzWy0sbljEuZo0tTNoLl+K2tPauGrQpcwUWrOVneLePTuhVzcuzVJJ6RU5DsNwQZka+8YtkXXU4z9cA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
@ -7408,9 +7409,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/eslint-plugin-eslint-plugin": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-5.0.8.tgz",
|
||||
"integrity": "sha512-bxPMZ3L/+5YypErWQMKUI9XdkLpgqOOO0CgbtHjk5Zxzcg4EVsWYPy8duvGSLxSyR60LBIoXNzVMueEZ3/j0AQ==",
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-5.0.7.tgz",
|
||||
"integrity": "sha512-hcz4Bze1ECwv3Q/Bi/ZMZZNiuvI2YclNuxjnczkblQ0skrlPhdO83rSM7felf5n+7ZJOZi4GS8y8gNiRtvI0hA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eslint-utils": "^3.0.0",
|
||||
@ -15196,9 +15197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.0.tgz",
|
||||
"integrity": "sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ==",
|
||||
"version": "1.7.4",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.4.tgz",
|
||||
"integrity": "sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
|
||||
@ -54,8 +54,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
? String(mtpUser.photo.photoId)
|
||||
: undefined;
|
||||
const userType = buildApiUserType(mtpUser);
|
||||
|
||||
const usernames = buildApiUsernames(mtpUser);
|
||||
const emojiStatus = mtpUser.emojiStatus ? buildApiUserEmojiStatus(mtpUser.emojiStatus) : undefined;
|
||||
|
||||
return {
|
||||
id: buildApiPeerId(id, 'user'),
|
||||
@ -74,7 +74,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
noStatus: !mtpUser.status,
|
||||
...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }),
|
||||
...(avatarHash && { avatarHash }),
|
||||
...(mtpUser.emojiStatus && { emojiStatus: buildApiUserEmojiStatus(mtpUser.emojiStatus) }),
|
||||
emojiStatus,
|
||||
hasVideoAvatar,
|
||||
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
|
||||
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
import localDb from '../localDb';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { deserializeBytes } from '../helpers';
|
||||
import { DEFAULT_STATUS_ICON_ID } from '../../../config';
|
||||
|
||||
const CHANNEL_ID_MIN_LENGTH = 11; // Example: -1000000000
|
||||
|
||||
@ -580,3 +581,20 @@ export function buildInputChatReactions(chatReactions?: ApiChatReactions) {
|
||||
|
||||
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 {
|
||||
fetchFullUser, fetchNearestCountry, fetchTopUsers, fetchContactList, fetchUsers,
|
||||
updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam,
|
||||
updateContact, importContact, deleteContact, fetchProfilePhotos, fetchCommonChats, reportSpam, updateEmojiStatus,
|
||||
} from './users';
|
||||
|
||||
export {
|
||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers, fetchRecentEmojiStatuses,
|
||||
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
|
||||
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, fetchDefaultTopicIcons,
|
||||
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, fetchDefaultTopicIcons, fetchDefaultStatusEmojis,
|
||||
} from './symbols';
|
||||
|
||||
export {
|
||||
|
||||
@ -8,9 +8,10 @@ import { invokeRequest } from './client';
|
||||
import {
|
||||
buildStickerSet, buildStickerSetCovered, processStickerPackResult, processStickerResult,
|
||||
} from '../apiBuilders/symbols';
|
||||
import { buildApiUserEmojiStatus } from '../apiBuilders/users';
|
||||
import { buildInputStickerSet, buildInputDocument, buildInputStickerSetShortName } from '../gramjsBuilders';
|
||||
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';
|
||||
|
||||
@ -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 }) {
|
||||
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
|
||||
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[]) {
|
||||
return gifs
|
||||
.map((document) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import type {
|
||||
OnApiUpdate, ApiUser, ApiChat,
|
||||
OnApiUpdate, ApiUser, ApiChat, ApiSticker,
|
||||
} from '../../types';
|
||||
|
||||
import { COMMON_CHATS_LIMIT, PROFILE_PHOTOS_LIMIT } from '../../../config';
|
||||
@ -13,6 +13,7 @@ import {
|
||||
buildInputContact,
|
||||
buildMtpPeerId,
|
||||
getEntityTypeById,
|
||||
buildInputEmojiStatus,
|
||||
} from '../gramjsBuilders';
|
||||
import { buildApiUser, buildApiUserFromFull, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
@ -296,6 +297,12 @@ export function reportSpam(userOrChat: ApiUser | ApiChat) {
|
||||
}), 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)) {
|
||||
if ('chats' in result) {
|
||||
addEntitiesWithPhotosToLocalDb(result.chats);
|
||||
|
||||
@ -1104,6 +1104,8 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
order: update.order || [],
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateRecentEmojiStatuses) {
|
||||
onUpdate({ '@type': 'updateRecentEmojiStatuses' });
|
||||
} else if (DEBUG) {
|
||||
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
||||
log('UNEXPECTED UPDATE', params);
|
||||
|
||||
@ -359,6 +359,10 @@ export type ApiUpdateUserEmojiStatus = {
|
||||
emojiStatus?: ApiEmojiStatus;
|
||||
};
|
||||
|
||||
export type ApiUpdateRecentEmojiStatuses = {
|
||||
'@type': 'updateRecentEmojiStatuses';
|
||||
};
|
||||
|
||||
export type ApiUpdateUserFullInfo = {
|
||||
'@type': 'updateUserFullInfo';
|
||||
id: string;
|
||||
@ -624,7 +628,7 @@ export type ApiUpdate = (
|
||||
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
||||
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
||||
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
||||
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics
|
||||
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses
|
||||
);
|
||||
|
||||
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 GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
||||
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 CalendarModal } from '../components/common/CalendarModal';
|
||||
|
||||
@ -108,7 +108,6 @@ const GroupCall: FC<OwnProps & StateProps> = ({
|
||||
const isConnecting = connectionState !== 'connected';
|
||||
const canSelfUnmute = meParticipant?.canSelfUnmute;
|
||||
const shouldRaiseHand = !canSelfUnmute && meParticipant?.isMuted;
|
||||
|
||||
const handleOpenParticipantMenu = useCallback((anchor: HTMLDivElement, participant: TypeGroupCallParticipant) => {
|
||||
const rect = anchor.getBoundingClientRect();
|
||||
const container = containerRef.current!;
|
||||
|
||||
@ -13,7 +13,6 @@ import generateIdFor from '../../util/generateIdFor';
|
||||
import useHeavyAnimationCheck from '../../hooks/useHeavyAnimationCheck';
|
||||
import useBackgroundMode from '../../hooks/useBackgroundMode';
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
|
||||
export type OwnProps = {
|
||||
ref?: RefObject<HTMLDivElement>;
|
||||
@ -89,8 +88,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
|
||||
const containerId = useMemo(() => generateIdFor(ID_STORE, true), []);
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const [animation, setAnimation] = useState<RLottieInstance>();
|
||||
const animationRef = useRef<RLottieInstance>();
|
||||
const wasPlaying = useRef(false);
|
||||
const isFrozen = useRef(false);
|
||||
const isFirstRender = useRef(true);
|
||||
@ -140,7 +139,6 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
quality,
|
||||
isLowPriority,
|
||||
coords: sharedCanvasCoords,
|
||||
isMobile,
|
||||
},
|
||||
color,
|
||||
onEnded,
|
||||
@ -152,6 +150,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
setAnimation(newAnimation);
|
||||
animationRef.current = newAnimation;
|
||||
};
|
||||
|
||||
if (RLottie) {
|
||||
@ -167,7 +166,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
}
|
||||
}, [
|
||||
animation, animationId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
|
||||
containerId, sharedCanvas, sharedCanvasCoords, isMobile,
|
||||
containerId, sharedCanvas, sharedCanvasCoords,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -178,11 +177,12 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (animation) {
|
||||
animation.removeContainer(containerId);
|
||||
if (animationRef.current) {
|
||||
animationRef.current.removeContainer(containerId);
|
||||
animationRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, [animation, containerId]);
|
||||
}, [containerId]);
|
||||
|
||||
const playAnimation = useCallback((shouldRestart = false) => {
|
||||
if (animation && (playRef.current || playSegmentRef.current)) {
|
||||
@ -235,14 +235,11 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
}
|
||||
}, [noLoop, animation]);
|
||||
|
||||
useSyncEffect(([prevSharedCanvasCoords, prevIsMobile]) => {
|
||||
if (
|
||||
(prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords)
|
||||
|| (prevIsMobile !== undefined && isMobile !== prevIsMobile)
|
||||
) {
|
||||
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords, isMobile);
|
||||
useSyncEffect(([prevSharedCanvasCoords]) => {
|
||||
if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) {
|
||||
animation?.setSharedCanvasCoords(containerId, sharedCanvasCoords);
|
||||
}
|
||||
}, [sharedCanvasCoords, isMobile, containerId, animation]);
|
||||
}, [sharedCanvasCoords, containerId, animation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!animation) {
|
||||
|
||||
@ -16,6 +16,13 @@
|
||||
width: var(--custom-emoji-size);
|
||||
height: var(--custom-emoji-size);
|
||||
margin: 0.3125rem;
|
||||
|
||||
&.status-default {
|
||||
font-size: 2rem;
|
||||
|
||||
padding: 0.25rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.set-expand {
|
||||
@ -117,13 +124,14 @@
|
||||
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sticker-context-menu {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
.sticker-context-menu {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: var(--z-header-menu);
|
||||
|
||||
.bubble {
|
||||
width: auto;
|
||||
}
|
||||
.bubble {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMe
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||
import { hexToRgb } from '../../util/switchTheme';
|
||||
import { getServerTimeOffset } from '../../util/serverTime';
|
||||
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
@ -33,6 +34,7 @@ type OwnProps<T> = {
|
||||
className?: string;
|
||||
noContextMenu?: boolean;
|
||||
isSavedMessages?: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
canViewSet?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||
@ -43,8 +45,19 @@ type OwnProps<T> = {
|
||||
onFaveClick?: (sticker: ApiSticker) => void;
|
||||
onUnfaveClick?: (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>({
|
||||
sticker,
|
||||
size,
|
||||
@ -53,6 +66,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
className,
|
||||
noContextMenu,
|
||||
isSavedMessages,
|
||||
isStatusPicker,
|
||||
canViewSet,
|
||||
observeIntersection,
|
||||
isCurrentUserPremium,
|
||||
@ -63,10 +77,15 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
onFaveClick,
|
||||
onUnfaveClick,
|
||||
onRemoveRecentClick,
|
||||
onContextMenuOpen,
|
||||
onContextMenuClose,
|
||||
onContextMenuClick,
|
||||
}: OwnProps<T>) => {
|
||||
const { openStickerSet, openPremiumModal } = getActions();
|
||||
const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const [customColor, setCustomColor] = useState<[number, number, number] | undefined>();
|
||||
const hasCustomColor = sticker.shouldUseTextColor;
|
||||
@ -99,6 +118,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
const shouldRenderContextMenu = Boolean(!noContextMenu && contextMenuPosition);
|
||||
|
||||
const getTriggerElement = useCallback(() => ref.current, []);
|
||||
|
||||
@ -108,10 +128,14 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
);
|
||||
|
||||
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 {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
@ -119,8 +143,17 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isContextMenuOpen) {
|
||||
onContextMenuOpen?.();
|
||||
} else {
|
||||
onContextMenuClose?.();
|
||||
}
|
||||
}, [isContextMenuOpen, onContextMenuClose, onContextMenuOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIntersecting) handleContextMenuClose();
|
||||
}, [handleContextMenuClose, isIntersecting]);
|
||||
@ -170,6 +203,18 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
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 fullClassName = buildClassName(
|
||||
@ -181,10 +226,22 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
);
|
||||
|
||||
const contextMenuItems = useMemo(() => {
|
||||
if (noContextMenu || isCustomEmoji) return [];
|
||||
if (!shouldRenderContextMenu || noContextMenu || (isCustomEmoji && !isStatusPicker)) return [];
|
||||
|
||||
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) {
|
||||
items.push(
|
||||
<MenuItem icon="favorite" onClick={handleContextUnfave}>
|
||||
@ -228,9 +285,9 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
}
|
||||
return items;
|
||||
}, [
|
||||
canViewSet, handleContextFave, handleContextRemoveRecent, handleContextUnfave, handleOpenSet, handleSendQuiet,
|
||||
handleSendScheduled, isLocked, isSavedMessages, lang, onFaveClick, onRemoveRecentClick, onUnfaveClick, onClick,
|
||||
noContextMenu, isCustomEmoji,
|
||||
shouldRenderContextMenu, noContextMenu, isCustomEmoji, isStatusPicker, onUnfaveClick, onFaveClick, isLocked,
|
||||
onClick, canViewSet, onRemoveRecentClick, handleEmojiStatusExpiresClick, lang, handleContextUnfave,
|
||||
handleContextFave, isSavedMessages, handleSendScheduled, handleSendQuiet, handleOpenSet, handleContextRemoveRecent,
|
||||
]);
|
||||
|
||||
return (
|
||||
@ -278,8 +335,9 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
<i className="icon-close" />
|
||||
</Button>
|
||||
)}
|
||||
{Boolean(contextMenuItems.length) && contextMenuPosition !== undefined && (
|
||||
{Boolean(contextMenuItems.length) && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
@ -288,6 +346,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
style={menuStyle}
|
||||
className="sticker-context-menu"
|
||||
autoClose
|
||||
withPortal={isStatusPicker}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
>
|
||||
|
||||
@ -115,7 +115,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
const noTransition = isLottie && preloadedPreviewData;
|
||||
|
||||
const bounds = useBoundsInSharedCanvas(containerRef, sharedCanvasRef);
|
||||
const realSize = bounds.size || size;
|
||||
const realSize = size || bounds.size;
|
||||
|
||||
// Preload preview for Message Input and local message
|
||||
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
||||
|
||||
@ -90,10 +90,26 @@
|
||||
@include overflow-y-overlay();
|
||||
}
|
||||
|
||||
.passcode-lock {
|
||||
.extra-spacing {
|
||||
position: relative;
|
||||
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
|
||||
@include while-transition() {
|
||||
.Menu .bubble {
|
||||
|
||||
@ -25,7 +25,9 @@ import { formatDateToString } from '../../../util/dateFormat';
|
||||
import switchTheme from '../../../util/switchTheme';
|
||||
import { setPermanentWebVersion } from '../../../util/permanentWebVersion';
|
||||
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 useLang from '../../../hooks/useLang';
|
||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||
@ -43,6 +45,7 @@ import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import ShowTransition from '../../ui/ShowTransition';
|
||||
import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
||||
import StatusButton from './StatusButton';
|
||||
|
||||
import './LeftMainHeader.scss';
|
||||
|
||||
@ -70,6 +73,7 @@ type StateProps =
|
||||
animationLevel: AnimationLevel;
|
||||
chatsById?: Record<string, ApiChat>;
|
||||
isMessageListOpen: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
isConnectionStatusMinimized: ISettings['isConnectionStatusMinimized'];
|
||||
areChatsLoaded?: boolean;
|
||||
hasPasscode?: boolean;
|
||||
@ -92,6 +96,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
onReset,
|
||||
searchQuery,
|
||||
isLoading,
|
||||
isCurrentUserPremium,
|
||||
shouldSkipTransition,
|
||||
currentUserId,
|
||||
globalSearchChatId,
|
||||
@ -430,6 +435,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
</SearchInput>
|
||||
{isCurrentUserPremium && <StatusButton />}
|
||||
{hasPasscode && (
|
||||
<Button
|
||||
round
|
||||
@ -438,7 +444,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
color="translucent"
|
||||
ariaLabel={`${lang('ShortcutsController.Others.LockByPasscode')} (Ctrl+Shift+L)`}
|
||||
onClick={handleLockScreen}
|
||||
className="passcode-lock"
|
||||
className={buildClassName(!isCurrentUserPremium && 'extra-spacing')}
|
||||
>
|
||||
<i className="icon-lock" />
|
||||
</Button>
|
||||
@ -482,6 +488,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isSyncing,
|
||||
isMessageListOpen: Boolean(selectCurrentMessageList(global)),
|
||||
isConnectionStatusMinimized,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
areChatsLoaded: Boolean(global.chats.listIds.active),
|
||||
hasPasscode: Boolean(global.passcode.hasPasscode),
|
||||
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,
|
||||
selectTabState,
|
||||
selectCurrentMessageList,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsForwardModalOpen,
|
||||
selectIsMediaViewerOpen,
|
||||
selectIsRightColumnShown,
|
||||
@ -128,6 +129,7 @@ type StateProps = {
|
||||
deleteFolderDialogId?: number;
|
||||
isPaymentModalOpen?: boolean;
|
||||
isReceiptModalOpen?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
||||
@ -176,6 +178,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
isPremiumModalOpen,
|
||||
isPaymentModalOpen,
|
||||
isReceiptModalOpen,
|
||||
isCurrentUserPremium,
|
||||
deleteFolderDialogId,
|
||||
isMasterTab,
|
||||
}) => {
|
||||
@ -194,6 +197,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
loadDefaultTopicIcons,
|
||||
loadAddedStickers,
|
||||
loadFavoriteStickers,
|
||||
loadDefaultStatusIcons,
|
||||
ensureTimeFormat,
|
||||
closeStickerSetModal,
|
||||
closeCustomEmojiSets,
|
||||
@ -209,6 +213,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
checkAppVersion,
|
||||
openChat,
|
||||
toggleLeftColumn,
|
||||
loadRecentEmojiStatuses,
|
||||
} = getActions();
|
||||
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
@ -245,12 +250,17 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
loadContactList();
|
||||
loadPremiumGifts();
|
||||
loadDefaultTopicIcons();
|
||||
loadDefaultStatusIcons();
|
||||
checkAppVersion();
|
||||
if (isCurrentUserPremium) {
|
||||
loadRecentEmojiStatuses();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||
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
|
||||
@ -553,6 +563,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
webApp,
|
||||
currentUser,
|
||||
urlAuth,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
isPremiumModalOpen: premiumModal?.isOpen,
|
||||
limitReached: limitReachedModal?.limit,
|
||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||
|
||||
@ -34,6 +34,7 @@ export const PREMIUM_FEATURE_TITLES: Record<string, string> = {
|
||||
more_upload: 'PremiumPreviewUploads',
|
||||
advanced_chat_management: 'PremiumPreviewAdvancedChatManagement',
|
||||
animated_userpics: 'PremiumPreviewAnimatedProfiles',
|
||||
emoji_status: 'PremiumPreviewEmojiStatus',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
@ -48,6 +49,7 @@ export const PREMIUM_FEATURE_DESCRIPTIONS: Record<string, string> = {
|
||||
more_upload: 'PremiumPreviewUploadsDescription',
|
||||
advanced_chat_management: 'PremiumPreviewAdvancedChatManagementDescription',
|
||||
animated_userpics: 'PremiumPreviewAnimatedProfilesDescription',
|
||||
emoji_status: 'PremiumPreviewEmojiStatusDescription',
|
||||
};
|
||||
|
||||
export const PREMIUM_FEATURE_SECTIONS = [
|
||||
@ -62,6 +64,7 @@ export const PREMIUM_FEATURE_SECTIONS = [
|
||||
'advanced_chat_management',
|
||||
'profile_badge',
|
||||
'animated_userpics',
|
||||
'emoji_status',
|
||||
];
|
||||
|
||||
const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||
@ -70,6 +73,7 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||
'advanced_chat_management',
|
||||
'profile_badge',
|
||||
'animated_userpics',
|
||||
'emoji_status',
|
||||
];
|
||||
|
||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts'>;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef, useState,
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -41,6 +41,7 @@ import PremiumChats from '../../../assets/premium/PremiumChats.svg';
|
||||
import PremiumBadge from '../../../assets/premium/PremiumBadge.svg';
|
||||
import PremiumVideo from '../../../assets/premium/PremiumVideo.svg';
|
||||
import PremiumEmoji from '../../../assets/premium/PremiumEmoji.svg';
|
||||
import PremiumStatus from '../../../assets/premium/PremiumStatus.svg';
|
||||
|
||||
import styles from './PremiumMainModal.module.scss';
|
||||
|
||||
@ -58,6 +59,7 @@ const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
|
||||
more_upload: PremiumFile,
|
||||
advanced_chat_management: PremiumChats,
|
||||
animated_userpics: PremiumVideo,
|
||||
emoji_status: PremiumStatus,
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
@ -176,6 +178,11 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
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;
|
||||
|
||||
// TODO Support all subscription options
|
||||
@ -254,8 +261,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
|
||||
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
||||
{(premiumPromoOrder || PREMIUM_FEATURE_SECTIONS).map((section, index) => {
|
||||
if (!PREMIUM_FEATURE_SECTIONS.includes(section)) return undefined;
|
||||
{filteredSections.map((section, index) => {
|
||||
return (
|
||||
<PremiumFeatureItem
|
||||
key={section}
|
||||
|
||||
@ -12,7 +12,8 @@ import {
|
||||
FAVORITE_SYMBOL_SET_ID,
|
||||
PREMIUM_STICKER_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,
|
||||
} from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||
@ -44,16 +45,23 @@ type OwnProps = {
|
||||
chatId?: string;
|
||||
className?: string;
|
||||
loadAndPlay: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
idPrefix?: string;
|
||||
withDefaultTopicIcons?: boolean;
|
||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||
onContextMenuOpen?: NoneToVoidFunction;
|
||||
onContextMenuClose?: NoneToVoidFunction;
|
||||
onContextMenuClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
customEmojisById?: Record<string, ApiSticker>;
|
||||
recentCustomEmojiIds?: string[];
|
||||
recentStatusEmojis?: ApiSticker[];
|
||||
stickerSetsById: Record<string, ApiStickerSet>;
|
||||
addedCustomEmojiIds?: string[];
|
||||
customEmojisById: Record<string, ApiSticker>;
|
||||
recentCustomEmojiIds: string[];
|
||||
defaultTopicIconsId?: string;
|
||||
defaultStatusIconsId?: string;
|
||||
customEmojiFeaturedIds?: string[];
|
||||
canAnimate?: boolean;
|
||||
isSavedMessages?: boolean;
|
||||
@ -63,6 +71,7 @@ type StateProps = {
|
||||
const SMOOTH_SCROLL_DISTANCE = 500;
|
||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||
const DEFAULT_ID_PREFIX = 'custom-emoji-set';
|
||||
|
||||
const stickerSetIntersections: boolean[] = [];
|
||||
|
||||
@ -72,14 +81,21 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
addedCustomEmojiIds,
|
||||
customEmojisById,
|
||||
recentCustomEmojiIds,
|
||||
recentStatusEmojis,
|
||||
stickerSetsById,
|
||||
idPrefix = DEFAULT_ID_PREFIX,
|
||||
customEmojiFeaturedIds,
|
||||
canAnimate,
|
||||
isStatusPicker,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium,
|
||||
withDefaultTopicIcons,
|
||||
defaultTopicIconsId,
|
||||
defaultStatusIconsId,
|
||||
onCustomEmojiSelect,
|
||||
onContextMenuOpen,
|
||||
onContextMenuClose,
|
||||
onContextMenuClick,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -92,17 +108,23 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
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({
|
||||
rootRef: containerRef,
|
||||
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||
}, (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const { id } = entry.target as HTMLDivElement;
|
||||
if (!id || !id.startsWith('custom-emoji-set-')) {
|
||||
if (!id || !id.startsWith(idPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(id.replace('custom-emoji-set-', ''));
|
||||
const index = Number(id.replace(`${idPrefix}-`, ''));
|
||||
stickerSetIntersections[index] = entry.isIntersecting;
|
||||
});
|
||||
|
||||
@ -122,10 +144,6 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const areAddedLoaded = Boolean(addedCustomEmojiIds);
|
||||
|
||||
const recentCustomEmojis = useMemo(() => {
|
||||
return Object.values(pickTruthy(customEmojisById, recentCustomEmojiIds));
|
||||
}, [customEmojisById, recentCustomEmojiIds]);
|
||||
|
||||
const allSets = useMemo(() => {
|
||||
if (!addedCustomEmojiIds) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
@ -133,7 +151,19 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
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!];
|
||||
if (defaultTopicIconsPack.stickers?.length) {
|
||||
defaultSets.push({
|
||||
@ -142,7 +172,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
title: lang('RecentStickers'),
|
||||
});
|
||||
}
|
||||
} else if (recentCustomEmojis.length) {
|
||||
} else if (recentCustomEmojis?.length) {
|
||||
defaultSets.push({
|
||||
id: RECENT_SYMBOL_SET_ID,
|
||||
accessHash: '0',
|
||||
@ -162,8 +192,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
...setsToDisplay,
|
||||
];
|
||||
}, [
|
||||
addedCustomEmojiIds, defaultTopicIconsId, customEmojiFeaturedIds, lang, recentCustomEmojis, stickerSetsById,
|
||||
withDefaultTopicIcons,
|
||||
addedCustomEmojiIds, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||
customEmojiFeaturedIds, stickerSetsById, defaultStatusIconsId, lang, defaultTopicIconsId,
|
||||
]);
|
||||
|
||||
const noPopulatedSets = useMemo(() => (
|
||||
@ -194,9 +224,9 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const selectStickerSet = useCallback((index: number) => {
|
||||
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);
|
||||
}, []);
|
||||
}, [idPrefix]);
|
||||
|
||||
const handleEmojiSelect = useCallback((emoji: ApiSticker) => {
|
||||
onCustomEmojiSelect(emoji);
|
||||
@ -298,14 +328,19 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
stickerSet={stickerSet}
|
||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||
index={i}
|
||||
idPrefix={idPrefix}
|
||||
observeIntersection={observeIntersection}
|
||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
isSavedMessages={isSavedMessages}
|
||||
shouldHideRecentHeader={withDefaultTopicIcons}
|
||||
isStatusPicker={isStatusPicker}
|
||||
shouldHideRecentHeader={withDefaultTopicIcons || isStatusPicker}
|
||||
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
isCustomEmojiPicker
|
||||
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
onStickerSelect={handleEmojiSelect}
|
||||
onContextMenuOpen={onContextMenuOpen}
|
||||
onContextMenuClose={onContextMenuClose}
|
||||
onContextMenuClick={onContextMenuClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -314,7 +349,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
(global, { chatId, isStatusPicker }): StateProps => {
|
||||
const {
|
||||
stickers: {
|
||||
setsById: stickerSetsById,
|
||||
@ -322,6 +357,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
customEmojis: {
|
||||
byId: customEmojisById,
|
||||
featuredIds: customEmojiFeaturedIds,
|
||||
statusRecent: {
|
||||
emojis: recentStatusEmojis,
|
||||
},
|
||||
},
|
||||
recentCustomEmojis: recentCustomEmojiIds,
|
||||
} = global;
|
||||
@ -329,15 +367,17 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
||||
|
||||
return {
|
||||
customEmojisById: !isStatusPicker ? customEmojisById : undefined,
|
||||
recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined,
|
||||
recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined,
|
||||
stickerSetsById,
|
||||
addedCustomEmojiIds: global.customEmojis.added.setIds,
|
||||
canAnimate: global.settings.byKey.shouldLoopStickers,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
customEmojisById,
|
||||
recentCustomEmojiIds,
|
||||
customEmojiFeaturedIds,
|
||||
defaultTopicIconsId: global.defaultTopicIconsId,
|
||||
defaultStatusIconsId: global.defaultStatusIconsId,
|
||||
};
|
||||
},
|
||||
)(CustomEmojiPicker));
|
||||
|
||||
@ -98,4 +98,12 @@
|
||||
margin: 0 0.25rem;
|
||||
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 {
|
||||
DEFAULT_STATUS_ICON_ID,
|
||||
DEFAULT_TOPIC_ICON_STICKER_ID,
|
||||
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
|
||||
} from '../../../config';
|
||||
@ -32,18 +33,23 @@ type OwnProps = {
|
||||
stickerSet: StickerSetOrRecent;
|
||||
loadAndPlay: boolean;
|
||||
index: number;
|
||||
idPrefix?: string;
|
||||
shouldRender: boolean;
|
||||
favoriteStickers?: ApiSticker[];
|
||||
isSavedMessages?: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
isCustomEmojiPicker?: boolean;
|
||||
shouldHideRecentHeader?: boolean;
|
||||
withDefaultTopicIcon?: boolean;
|
||||
withDefaultStatusIcon?: boolean;
|
||||
observeIntersection: ObserveFn;
|
||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onStickerUnfave?: (sticker: ApiSticker) => void;
|
||||
onStickerFave?: (sticker: ApiSticker) => void;
|
||||
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
||||
onContextMenuOpen?: NoneToVoidFunction;
|
||||
onContextMenuClose?: NoneToVoidFunction;
|
||||
onContextMenuClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const ITEMS_PER_ROW_FALLBACK = 8;
|
||||
@ -52,18 +58,23 @@ const StickerSet: FC<OwnProps> = ({
|
||||
stickerSet,
|
||||
loadAndPlay,
|
||||
index,
|
||||
idPrefix,
|
||||
shouldRender,
|
||||
favoriteStickers,
|
||||
isSavedMessages,
|
||||
isStatusPicker,
|
||||
isCurrentUserPremium,
|
||||
isCustomEmojiPicker,
|
||||
shouldHideRecentHeader,
|
||||
withDefaultTopicIcon,
|
||||
withDefaultStatusIcon,
|
||||
observeIntersection,
|
||||
onStickerSelect,
|
||||
onStickerUnfave,
|
||||
onStickerFave,
|
||||
onStickerRemoveRecent,
|
||||
onContextMenuOpen,
|
||||
onContextMenuClose,
|
||||
onContextMenuClick,
|
||||
}) => {
|
||||
const {
|
||||
clearRecentStickers,
|
||||
@ -93,7 +104,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
|
||||
const stickerMarginPx = isMobile ? 8 : 16;
|
||||
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 isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
|
||||
const isEmoji = stickerSet.isEmoji;
|
||||
@ -131,6 +142,17 @@ const StickerSet: FC<OwnProps> = ({
|
||||
} satisfies ApiSticker);
|
||||
}, [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 margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
||||
|
||||
@ -183,7 +205,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
<div
|
||||
ref={ref}
|
||||
key={stickerSet.id}
|
||||
id={`${isCustomEmojiPicker ? 'custom-emoji-set' : 'sticker-set'}-${index}`}
|
||||
id={`${idPrefix || 'sticker-set'}-${index}`}
|
||||
className={
|
||||
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
|
||||
}
|
||||
@ -231,6 +253,16 @@ const StickerSet: FC<OwnProps> = ({
|
||||
<img src={grey} alt="Reset" />
|
||||
</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
|
||||
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
||||
.map((sticker, i) => {
|
||||
@ -248,6 +280,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
observeIntersection={observeIntersection}
|
||||
noAnimate={!loadAndPlay}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isStatusPicker={isStatusPicker}
|
||||
canViewSet
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
sharedCanvasRef={canvasRef}
|
||||
@ -256,6 +289,9 @@ const StickerSet: FC<OwnProps> = ({
|
||||
onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
|
||||
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : 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 useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
|
||||
|
||||
const SIZE = 1.25 * REM;
|
||||
const THROTTLE_MS = 300;
|
||||
|
||||
type CustomEmojiPlayer = {
|
||||
play: () => void;
|
||||
pause: () => void;
|
||||
destroy: () => void;
|
||||
updatePosition: (x: number, y: number, isMobile?: boolean) => void;
|
||||
updatePosition: (x: number, y: number) => void;
|
||||
};
|
||||
|
||||
export default function useInputCustomEmojis(
|
||||
@ -42,8 +43,6 @@ export default function useInputCustomEmojis(
|
||||
) {
|
||||
const mapRef = useRef<Map<string, CustomEmojiPlayer>>(new Map());
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const removeContainers = useCallback((ids: string[]) => {
|
||||
ids.forEach((id) => {
|
||||
const player = mapRef.current.get(id);
|
||||
@ -80,7 +79,7 @@ export default function useInputCustomEmojis(
|
||||
|
||||
if (mapRef.current.has(id)) {
|
||||
const player = mapRef.current.get(id)!;
|
||||
player.updatePosition(x, y, isMobile);
|
||||
player.updatePosition(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -99,7 +98,6 @@ export default function useInputCustomEmojis(
|
||||
mediaUrl,
|
||||
isHq,
|
||||
position: { x, y },
|
||||
isMobile,
|
||||
});
|
||||
animation.play();
|
||||
|
||||
@ -107,10 +105,7 @@ export default function useInputCustomEmojis(
|
||||
});
|
||||
|
||||
removeContainers(Array.from(removedContainers));
|
||||
}, [
|
||||
absoluteContainerRef, inputRef, prefixId, isMobile, removeContainers, sharedCanvasHqRef,
|
||||
sharedCanvasRef,
|
||||
]);
|
||||
}, [absoluteContainerRef, inputRef, prefixId, removeContainers, sharedCanvasHqRef, sharedCanvasRef]);
|
||||
|
||||
useEffect(() => {
|
||||
addCustomEmojiInputRenderCallback(synchronizeElements);
|
||||
@ -129,7 +124,13 @@ export default function useInputCustomEmojis(
|
||||
synchronizeElements();
|
||||
}, [getHtml, synchronizeElements, inputRef, removeContainers, sharedCanvasRef, isActive]);
|
||||
|
||||
useResizeObserver(sharedCanvasRef, synchronizeElements, true);
|
||||
const throttledSynchronizeElements = useThrottledCallback(
|
||||
synchronizeElements,
|
||||
[synchronizeElements],
|
||||
THROTTLE_MS,
|
||||
false,
|
||||
);
|
||||
useResizeObserver(sharedCanvasRef, throttledSynchronizeElements);
|
||||
|
||||
const freezeAnimation = useCallback(() => {
|
||||
mapRef.current.forEach((player) => {
|
||||
@ -162,7 +163,6 @@ function createPlayer({
|
||||
mediaUrl,
|
||||
position,
|
||||
isHq,
|
||||
isMobile,
|
||||
}: {
|
||||
customEmoji: ApiSticker;
|
||||
sharedCanvasRef: React.RefObject<HTMLCanvasElement>;
|
||||
@ -172,7 +172,6 @@ function createPlayer({
|
||||
mediaUrl: string;
|
||||
position: { x: number; y: number };
|
||||
isHq?: boolean;
|
||||
isMobile?: boolean;
|
||||
}): CustomEmojiPlayer {
|
||||
if (customEmoji.isLottie) {
|
||||
const lottie = RLottie.init(
|
||||
@ -185,15 +184,14 @@ function createPlayer({
|
||||
size: SIZE,
|
||||
coords: position,
|
||||
isLowPriority: !isHq,
|
||||
isMobile,
|
||||
},
|
||||
);
|
||||
return {
|
||||
play: () => lottie.play(),
|
||||
pause: () => lottie.pause(),
|
||||
destroy: () => lottie.removeContainer(uniqueId),
|
||||
updatePosition: (x: number, y: number, isMobileNew?: boolean) => {
|
||||
return lottie.setSharedCanvasCoords(uniqueId, { x, y }, isMobileNew);
|
||||
updatePosition: (x: number, y: number) => {
|
||||
return lottie.setSharedCanvasCoords(uniqueId, { x, y });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
}
|
||||
|
||||
.particle {
|
||||
--color-text: var(--color-primary);
|
||||
|
||||
position: absolute;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@ -11,6 +13,7 @@
|
||||
offset-path: var(--offset-path);
|
||||
offset-rotate: 0deg;
|
||||
animation: 1.5s particle ease-out;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes particle {
|
||||
@ -1,38 +1,58 @@
|
||||
import React, { memo, useMemo } 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 { getStickerPreviewHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
import styles from './CustomReactionAnimation.module.scss';
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
|
||||
import styles from './CustomEmojiEffect.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
reaction: ApiReactionCustomEmoji;
|
||||
reaction: ApiReactionCustomEmoji | ApiEmojiStatus;
|
||||
className?: string;
|
||||
isLottie?: boolean;
|
||||
};
|
||||
|
||||
const EFFECT_AMOUNT = 7;
|
||||
|
||||
const CustomReactionAnimation: FC<OwnProps> = ({
|
||||
const CustomEmojiEffect: FC<OwnProps> = ({
|
||||
reaction,
|
||||
isLottie,
|
||||
className,
|
||||
}) => {
|
||||
const stickerHash = getStickerPreviewHash(reaction.documentId);
|
||||
|
||||
const previewMediaData = useMedia(stickerHash);
|
||||
const previewMediaData = useMedia(!isLottie ? stickerHash : undefined);
|
||||
|
||||
const paths: string[] = useMemo(() => {
|
||||
if (!IS_OFFSET_PATH_SUPPORTED) return [];
|
||||
return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath());
|
||||
}, []);
|
||||
|
||||
if (!previewMediaData) return undefined;
|
||||
if (!previewMediaData && !isLottie) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={buildClassName(styles.root, className)}>
|
||||
{paths.map((path) => {
|
||||
const style = `--offset-path: path('${path}');`;
|
||||
if (isLottie) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
className={styles.particle}
|
||||
style={style}
|
||||
withSharedAnimation
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={previewMediaData}
|
||||
@ -46,7 +66,7 @@ const CustomReactionAnimation: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CustomReactionAnimation);
|
||||
export default memo(CustomEmojiEffect);
|
||||
|
||||
function generateRandomDropPath() {
|
||||
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 useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||
import useThrottledCallback from '../../../hooks/useThrottledCallback';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -252,6 +253,7 @@ const APPEARANCE_DELAY = 10;
|
||||
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
||||
const QUICK_REACTION_SIZE = 1.75 * REM;
|
||||
const BOTTOM_FOCUS_SCROLL_THRESHOLD = 5;
|
||||
const THROTTLE_MS = 300;
|
||||
|
||||
const Message: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
@ -613,7 +615,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [focusLastMessage]);
|
||||
|
||||
useResizeObserver(shouldFocusOnResize ? ref : undefined, handleResize, true);
|
||||
const throttledResize = useThrottledCallback(handleResize, [handleResize], THROTTLE_MS, false);
|
||||
|
||||
useResizeObserver(shouldFocusOnResize ? ref : undefined, throttledResize);
|
||||
|
||||
useEffect(() => {
|
||||
const bottomMarker = bottomMarkerRef.current;
|
||||
|
||||
@ -21,7 +21,7 @@ import useCustomEmoji from '../../common/hooks/useCustomEmoji';
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import AnimatedSticker from '../../common/AnimatedSticker';
|
||||
import CustomReactionAnimation from './CustomReactionAnimation';
|
||||
import CustomEmojiEffect from './CustomEmojiEffect';
|
||||
|
||||
import styles from './ReactionAnimatedEmoji.module.scss';
|
||||
|
||||
@ -136,7 +136,7 @@ const ReactionAnimatedEmoji: FC<OwnProps> = ({
|
||||
onEnded={handleEnded}
|
||||
/>
|
||||
{isCustom ? (
|
||||
!assignedEffectId && isIntersecting && <CustomReactionAnimation reaction={reaction} />
|
||||
!assignedEffectId && isIntersecting && <CustomEmojiEffect reaction={reaction} />
|
||||
) : (
|
||||
<AnimatedSticker
|
||||
key={centerIconId}
|
||||
|
||||
@ -134,6 +134,7 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
<div className={buildClassName(styles.section, styles.bottom)}>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="create-topic-icons-set-"
|
||||
loadAndPlay={isActive}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
className={styles.iconPicker}
|
||||
|
||||
@ -145,6 +145,7 @@ const EditTopic: FC<OwnProps & StateProps> = ({
|
||||
{!isGeneral && (
|
||||
<div className={buildClassName(styles.section, styles.bottom)}>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="edit-topic-icons-set-"
|
||||
loadAndPlay={isActive}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
className={styles.iconPicker}
|
||||
|
||||
@ -24,6 +24,7 @@ type OwnProps = {
|
||||
isOpen: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
bubbleClassName?: string;
|
||||
style?: string;
|
||||
bubbleStyle?: string;
|
||||
ariaLabelledBy?: string;
|
||||
@ -54,6 +55,7 @@ const Menu: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
id,
|
||||
className,
|
||||
bubbleClassName,
|
||||
style,
|
||||
bubbleStyle,
|
||||
ariaLabelledBy,
|
||||
@ -116,12 +118,13 @@ const Menu: FC<OwnProps> = ({
|
||||
noCloseOnBackdrop ? undefined : onClose,
|
||||
);
|
||||
|
||||
const bubbleClassName = buildClassName(
|
||||
const bubbleFullClassName = buildClassName(
|
||||
'bubble menu-container custom-scroll',
|
||||
positionY,
|
||||
positionX,
|
||||
footer && 'with-footer',
|
||||
transitionClassNames,
|
||||
bubbleClassName,
|
||||
);
|
||||
|
||||
const transformOriginYStyle = transformOriginY !== undefined ? `${transformOriginY}px` : undefined;
|
||||
@ -154,7 +157,7 @@ const Menu: FC<OwnProps> = ({
|
||||
<div
|
||||
role="presentation"
|
||||
ref={menuRef}
|
||||
className={bubbleClassName}
|
||||
className={bubbleFullClassName}
|
||||
style={buildStyle(
|
||||
`transform-origin: ${transformOriginXStyle || positionX} ${transformOriginYStyle || positionY}`,
|
||||
bubbleStyle,
|
||||
|
||||
@ -8,14 +8,13 @@ import { IS_COMPACT_MENU } from '../../util/environment';
|
||||
|
||||
import './MenuItem.scss';
|
||||
|
||||
type OnClickHandler = (e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>) => void;
|
||||
|
||||
export type MenuItemProps = {
|
||||
icon?: string;
|
||||
customIcon?: React.ReactNode;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: OnClickHandler;
|
||||
onClick?: (e: React.SyntheticEvent<HTMLDivElement | HTMLAnchorElement>, arg?: number) => void;
|
||||
clickArg?: number;
|
||||
onContextMenu?: (e: React.UIEvent) => void;
|
||||
href?: string;
|
||||
download?: string;
|
||||
@ -39,6 +38,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
||||
ariaLabel,
|
||||
withWrap,
|
||||
onContextMenu,
|
||||
clickArg,
|
||||
} = props;
|
||||
|
||||
const lang = useLang();
|
||||
@ -50,8 +50,8 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick(e);
|
||||
}, [disabled, onClick]);
|
||||
onClick(e, clickArg);
|
||||
}, [clickArg, disabled, onClick]);
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||
@ -65,8 +65,8 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick(e);
|
||||
}, [disabled, onClick]);
|
||||
onClick(e, clickArg);
|
||||
}, [clickArg, disabled, onClick]);
|
||||
|
||||
const fullClassName = buildClassName(
|
||||
'MenuItem',
|
||||
|
||||
@ -168,6 +168,7 @@ export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
|
||||
export const STICKER_SIZE_JOIN_REQUESTS = 140;
|
||||
export const STICKER_SIZE_INVITES = 140;
|
||||
export const RECENT_STICKERS_LIMIT = 20;
|
||||
export const RECENT_STATUS_LIMIT = 20;
|
||||
export const EMOJI_STATUS_LOOP_LIMIT = 2;
|
||||
export const EMOJI_SIZES = 7;
|
||||
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 PREMIUM_STICKER_SET_ID = 'premium';
|
||||
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 BASE_EMOJI_KEYWORD_LANG = 'en';
|
||||
|
||||
@ -18,7 +18,9 @@ import {
|
||||
updateStickersForEmoji,
|
||||
rebuildStickersForEmoji,
|
||||
updateCustomEmojiForEmoji,
|
||||
updateCustomEmojiSets, updateStickerSearch,
|
||||
updateCustomEmojiSets,
|
||||
updateRecentStatusCustomEmojis,
|
||||
updateStickerSearch,
|
||||
} from '../../reducers';
|
||||
import searchWords from '../../../util/searchWords';
|
||||
import { selectTabState, selectIsCurrentUserPremium, selectStickerSet } from '../../selectors';
|
||||
@ -246,6 +248,21 @@ addActionHandler('loadDefaultTopicIcons', async (global): Promise<void> => {
|
||||
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 => {
|
||||
const { stickerSetInfo, tabId = getCurrentTabId() } = payload;
|
||||
const cachedSet = selectStickerSet(global, stickerSetInfo);
|
||||
@ -746,6 +763,17 @@ addActionHandler('openStickerSet', async (global, actions, payload): Promise<voi
|
||||
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,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>) {
|
||||
const result = await callApi('searchGifs', { query, offset, username: botUsername });
|
||||
|
||||
@ -347,3 +347,9 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
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();
|
||||
break;
|
||||
|
||||
case 'updateRecentEmojiStatuses':
|
||||
actions.loadRecentEmojiStatuses();
|
||||
break;
|
||||
|
||||
case 'updateMoveStickerSetToTop': {
|
||||
const oldOrder = update.isCustomEmoji ? global.customEmojis.added.setIds : global.stickers.added.setIds;
|
||||
if (!oldOrder) return global;
|
||||
|
||||
@ -249,9 +249,14 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
byId: {},
|
||||
lastRendered: [],
|
||||
forEmoji: {},
|
||||
statusRecent: {},
|
||||
};
|
||||
}
|
||||
|
||||
if (!cached.customEmojis.statusRecent) {
|
||||
cached.customEmojis.statusRecent = {};
|
||||
}
|
||||
|
||||
if (!cached.recentCustomEmojis) {
|
||||
cached.recentCustomEmojis = [];
|
||||
}
|
||||
@ -426,6 +431,7 @@ function reduceCustomEmojis<T extends GlobalState>(global: T): GlobalState['cust
|
||||
lastRendered: idsToSave,
|
||||
forEmoji: {},
|
||||
added: {},
|
||||
statusRecent: {},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -107,6 +107,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
byId: {},
|
||||
added: {},
|
||||
forEmoji: {},
|
||||
statusRecent: {},
|
||||
},
|
||||
|
||||
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 {
|
||||
if (global.stickers.forEmoji) {
|
||||
const { emoji, stickers, hash } = global.stickers.forEmoji;
|
||||
|
||||
@ -718,12 +718,17 @@ export type GlobalState = {
|
||||
stickers?: ApiSticker[];
|
||||
};
|
||||
featuredIds?: string[];
|
||||
statusRecent: {
|
||||
hash?: string;
|
||||
emojis?: ApiSticker[];
|
||||
};
|
||||
};
|
||||
|
||||
animatedEmojis?: ApiStickerSet;
|
||||
animatedEmojiEffects?: ApiStickerSet;
|
||||
genericEmojiEffects?: ApiStickerSet;
|
||||
defaultTopicIconsId?: string;
|
||||
defaultStatusIconsId?: string;
|
||||
premiumGifts?: ApiStickerSet;
|
||||
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
||||
|
||||
@ -1886,6 +1891,8 @@ export interface ActionPayloads {
|
||||
};
|
||||
clearRecentCustomEmoji: undefined;
|
||||
loadFeaturedEmojiStickers: undefined;
|
||||
loadDefaultStatusIcons: undefined;
|
||||
loadRecentEmojiStatuses: undefined;
|
||||
|
||||
// Bots
|
||||
sendBotCommand: {
|
||||
@ -2212,6 +2219,10 @@ export interface ActionPayloads {
|
||||
} & WithTabId) | undefined;
|
||||
|
||||
closeGiftPremiumModal: WithTabId | undefined;
|
||||
setEmojiStatus: {
|
||||
emojiStatus: ApiSticker;
|
||||
expires?: number;
|
||||
};
|
||||
|
||||
// Invoice
|
||||
openInvoice: ApiInputInvoice & WithTabId;
|
||||
|
||||
@ -5,8 +5,10 @@ import {
|
||||
import { round } from '../util/math';
|
||||
|
||||
import useResizeObserver from './useResizeObserver';
|
||||
import useThrottledCallback from './useThrottledCallback';
|
||||
|
||||
const ANIMATION_END_TIMEOUT = 500;
|
||||
const THROTTLE_MS = 300;
|
||||
|
||||
export default function useBoundsInSharedCanvas(
|
||||
containerRef: React.RefObject<HTMLDivElement>,
|
||||
@ -39,9 +41,11 @@ export default function useBoundsInSharedCanvas(
|
||||
setSize(Math.round(targetBounds.width));
|
||||
}, [containerRef, sharedCanvasRef]);
|
||||
|
||||
const throttledRecalculate = useThrottledCallback(recalculate, [recalculate], THROTTLE_MS, false);
|
||||
|
||||
useLayoutEffect(recalculate, [recalculate]);
|
||||
|
||||
useResizeObserver(sharedCanvasRef, recalculate, true);
|
||||
useResizeObserver(sharedCanvasRef, throttledRecalculate);
|
||||
|
||||
const coords = useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
|
||||
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
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(
|
||||
ref: React.RefObject<HTMLElement> | undefined,
|
||||
onResize: (entry: ResizeObserverEntry) => void,
|
||||
withThrottle = false,
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (!('ResizeObserver' in window) || !ref?.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const el = ref.current;
|
||||
const callback: ResizeObserverCallback = ([entry]) => {
|
||||
// During animation
|
||||
if (!(entry.target as HTMLElement).offsetParent) {
|
||||
@ -21,12 +23,23 @@ export default function useResizeObserver(
|
||||
|
||||
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 () => {
|
||||
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.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.updateEmojiStatus#fbd3de6b emoji_status:EmojiStatus = Bool;
|
||||
account.getRecentEmojiStatuses#f578105 hash:long = account.EmojiStatuses;
|
||||
account.reorderUsernames#ef500eab order:Vector<string> = Bool;
|
||||
account.toggleUsername#58d6b376 username:string active:Bool = Bool;
|
||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||
|
||||
@ -53,6 +53,8 @@
|
||||
"account.reportProfilePhoto",
|
||||
"account.changeAuthorizationSettings",
|
||||
"account.setAuthorizationTTL",
|
||||
"account.updateEmojiStatus",
|
||||
"account.getRecentEmojiStatuses",
|
||||
"account.reorderUsernames",
|
||||
"account.toggleUsername",
|
||||
"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 { animate } from '../../util/animation';
|
||||
import cycleRestrict from '../../util/cycleRestrict';
|
||||
@ -9,7 +11,6 @@ interface Params {
|
||||
size?: number;
|
||||
quality?: number;
|
||||
isLowPriority?: boolean;
|
||||
isMobile?: boolean;
|
||||
coords?: { x: number; y: number };
|
||||
}
|
||||
|
||||
@ -20,8 +21,7 @@ type Frame =
|
||||
| ImageBitmap;
|
||||
|
||||
const MAX_WORKERS = 4;
|
||||
const HIGH_PRIORITY_QUALITY_MOBILE = 0.75;
|
||||
const HIGH_PRIORITY_QUALITY_DESKTOP = 1;
|
||||
const HIGH_PRIORITY_QUALITY = (IS_ANDROID || IS_IOS) ? 0.75 : 1;
|
||||
const LOW_PRIORITY_QUALITY = IS_ANDROID ? 0.5 : 0.75;
|
||||
const LOW_PRIORITY_QUALITY_SIZE_THRESHOLD = 24;
|
||||
const HIGH_PRIORITY_CACHE_MODULO = IS_SAFARI ? 2 : 4;
|
||||
@ -96,7 +96,7 @@ class RLottie {
|
||||
instance = new RLottie(...args);
|
||||
instancesById.set(id, instance);
|
||||
} else {
|
||||
instance.addContainer(container, canvas, onLoad, params?.coords, params?.isMobile);
|
||||
instance.addContainer(container, canvas, onLoad, params?.coords);
|
||||
}
|
||||
|
||||
return instance;
|
||||
@ -113,7 +113,7 @@ class RLottie {
|
||||
private onEnded?: (isDestroyed?: boolean) => void,
|
||||
private onLoop?: () => void,
|
||||
) {
|
||||
this.addContainer(containerId, container, onLoad, params.coords, params.isMobile);
|
||||
this.addContainer(containerId, container, onLoad, params.coords);
|
||||
this.initConfig();
|
||||
this.initRenderer();
|
||||
}
|
||||
@ -200,18 +200,13 @@ class RLottie {
|
||||
this.params.noLoop = noLoop;
|
||||
}
|
||||
|
||||
setIsMobile(isMobile?: Params['isMobile']) {
|
||||
this.params.isMobile = isMobile;
|
||||
}
|
||||
|
||||
setSharedCanvasCoords(containerId: string, newCoords: Params['coords'], isMobile?: Params['isMobile']) {
|
||||
setSharedCanvasCoords(containerId: string, newCoords: Params['coords']) {
|
||||
const containerInfo = this.containers.get(containerId)!;
|
||||
const {
|
||||
canvas, ctx,
|
||||
} = containerInfo;
|
||||
|
||||
if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') {
|
||||
this.setIsMobile(isMobile);
|
||||
const sizeFactor = this.calcSizeFactor();
|
||||
ensureCanvasSize(canvas, sizeFactor);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
@ -238,9 +233,7 @@ class RLottie {
|
||||
container: HTMLDivElement | HTMLCanvasElement,
|
||||
onLoad?: NoneToVoidFunction,
|
||||
coords?: Params['coords'],
|
||||
isMobile?: Params['isMobile'],
|
||||
) {
|
||||
this.setIsMobile(isMobile);
|
||||
const sizeFactor = this.calcSizeFactor();
|
||||
|
||||
let imgSize: number;
|
||||
@ -318,11 +311,9 @@ class RLottie {
|
||||
const {
|
||||
isLowPriority,
|
||||
size,
|
||||
isMobile,
|
||||
// Reduced quality only looks acceptable on big enough images
|
||||
quality = isLowPriority && (!size || size > LOW_PRIORITY_QUALITY_SIZE_THRESHOLD)
|
||||
? LOW_PRIORITY_QUALITY
|
||||
: (isMobile ? HIGH_PRIORITY_QUALITY_MOBILE : HIGH_PRIORITY_QUALITY_DESKTOP),
|
||||
? LOW_PRIORITY_QUALITY : HIGH_PRIORITY_QUALITY,
|
||||
} = this.params;
|
||||
|
||||
// Reduced quality only looks acceptable on high DPR screens
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user