Introduce Reaction Picker
This commit is contained in:
parent
c2d7234748
commit
22054c4416
@ -54,7 +54,9 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { pick } from '../../../util/iteratees';
|
import { pick } from '../../../util/iteratees';
|
||||||
import { buildStickerFromDocument } from './symbols';
|
import { buildStickerFromDocument } from './symbols';
|
||||||
import { buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromStripped } from './common';
|
import {
|
||||||
|
buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromPath, buildApiThumbnailFromStripped,
|
||||||
|
} from './common';
|
||||||
import { interpolateArray } from '../../../util/waveform';
|
import { interpolateArray } from '../../../util/waveform';
|
||||||
import { buildPeer } from '../gramjsBuilders';
|
import { buildPeer } from '../gramjsBuilders';
|
||||||
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
import { addPhotoToLocalDb, resolveMessageApiChatId, serializeBytes } from '../helpers';
|
||||||
@ -313,13 +315,14 @@ export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | u
|
|||||||
|
|
||||||
export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction {
|
export function buildApiAvailableReaction(availableReaction: GramJs.AvailableReaction): ApiAvailableReaction {
|
||||||
const {
|
const {
|
||||||
selectAnimation, staticIcon, reaction, title,
|
selectAnimation, staticIcon, reaction, title, appearAnimation,
|
||||||
inactive, aroundAnimation, centerIcon, effectAnimation, activateAnimation,
|
inactive, aroundAnimation, centerIcon, effectAnimation, activateAnimation,
|
||||||
premium,
|
premium,
|
||||||
} = availableReaction;
|
} = availableReaction;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectAnimation: buildApiDocument(selectAnimation),
|
selectAnimation: buildApiDocument(selectAnimation),
|
||||||
|
appearAnimation: buildApiDocument(appearAnimation),
|
||||||
activateAnimation: buildApiDocument(activateAnimation),
|
activateAnimation: buildApiDocument(activateAnimation),
|
||||||
effectAnimation: buildApiDocument(effectAnimation),
|
effectAnimation: buildApiDocument(effectAnimation),
|
||||||
staticIcon: buildApiDocument(staticIcon),
|
staticIcon: buildApiDocument(staticIcon),
|
||||||
@ -609,11 +612,17 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
|
|||||||
id, size, mimeType, date, thumbs, attributes,
|
id, size, mimeType, date, thumbs, attributes,
|
||||||
} = document;
|
} = document;
|
||||||
|
|
||||||
const thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs);
|
const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize);
|
||||||
|
let thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs);
|
||||||
|
if (!thumbnail && thumbs && photoSize) {
|
||||||
|
const photoPath = thumbs.find((s: any): s is GramJs.PhotoPathSize => s instanceof GramJs.PhotoPathSize);
|
||||||
|
if (photoPath) {
|
||||||
|
thumbnail = buildApiThumbnailFromPath(photoPath, photoSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mediaType: ApiDocument['mediaType'] | undefined;
|
let mediaType: ApiDocument['mediaType'] | undefined;
|
||||||
let mediaSize: ApiDocument['mediaSize'] | undefined;
|
let mediaSize: ApiDocument['mediaSize'] | undefined;
|
||||||
const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize);
|
|
||||||
if (photoSize) {
|
if (photoSize) {
|
||||||
mediaSize = {
|
mediaSize = {
|
||||||
width: photoSize.w,
|
width: photoSize.w,
|
||||||
|
|||||||
@ -87,8 +87,8 @@ export {
|
|||||||
} from './calls';
|
} from './calls';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList,
|
getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList, clearRecentReactions,
|
||||||
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
|
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction, fetchRecentReactions, fetchTopReactions,
|
||||||
} from './reactions';
|
} from './reactions';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -1,12 +1,15 @@
|
|||||||
import type { ApiChat, ApiReaction } from '../../types';
|
import BigInt from 'big-integer';
|
||||||
import { invokeRequest } from './client';
|
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiReaction } from '../../types';
|
||||||
|
|
||||||
|
import { REACTION_LIST_LIMIT, RECENT_REACTIONS_LIMIT, TOP_REACTIONS_LIMIT } from '../../../config';
|
||||||
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
||||||
import localDb from '../localDb';
|
|
||||||
import { buildApiAvailableReaction, buildMessagePeerReaction } from '../apiBuilders/messages';
|
|
||||||
import { REACTION_LIST_LIMIT } from '../../../config';
|
|
||||||
import { addEntitiesWithPhotosToLocalDb } from '../helpers';
|
|
||||||
import { buildApiUser } from '../apiBuilders/users';
|
import { buildApiUser } from '../apiBuilders/users';
|
||||||
|
import { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/messages';
|
||||||
|
import { invokeRequest } from './client';
|
||||||
|
import localDb from '../localDb';
|
||||||
|
import { addEntitiesWithPhotosToLocalDb } from '../helpers';
|
||||||
|
|
||||||
export function sendWatchingEmojiInteraction({
|
export function sendWatchingEmojiInteraction({
|
||||||
chat,
|
chat,
|
||||||
@ -65,6 +68,9 @@ export async function getAvailableReactions() {
|
|||||||
if (reaction.aroundAnimation instanceof GramJs.Document) {
|
if (reaction.aroundAnimation instanceof GramJs.Document) {
|
||||||
localDb.documents[String(reaction.aroundAnimation.id)] = reaction.aroundAnimation;
|
localDb.documents[String(reaction.aroundAnimation.id)] = reaction.aroundAnimation;
|
||||||
}
|
}
|
||||||
|
if (reaction.appearAnimation instanceof GramJs.Document) {
|
||||||
|
localDb.documents[String(reaction.appearAnimation.id)] = reaction.appearAnimation;
|
||||||
|
}
|
||||||
if (reaction.centerIcon instanceof GramJs.Document) {
|
if (reaction.centerIcon instanceof GramJs.Document) {
|
||||||
localDb.documents[String(reaction.centerIcon.id)] = reaction.centerIcon;
|
localDb.documents[String(reaction.centerIcon.id)] = reaction.centerIcon;
|
||||||
}
|
}
|
||||||
@ -74,15 +80,19 @@ export async function getAvailableReactions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function sendReaction({
|
export function sendReaction({
|
||||||
chat, messageId, reactions,
|
chat, messageId, reactions, shouldAddToRecent,
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat; messageId: number; reactions?: ApiReaction[];
|
chat: ApiChat;
|
||||||
|
messageId: number;
|
||||||
|
reactions?: ApiReaction[];
|
||||||
|
shouldAddToRecent?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return invokeRequest(new GramJs.messages.SendReaction({
|
return invokeRequest(new GramJs.messages.SendReaction({
|
||||||
reaction: reactions?.map((r) => buildInputReaction(r)),
|
reaction: reactions?.map((r) => buildInputReaction(r)),
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
msgId: messageId,
|
msgId: messageId,
|
||||||
}), true);
|
...(shouldAddToRecent && { addToRecent: true }),
|
||||||
|
}), true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchMessageReactions({
|
export function fetchMessageReactions({
|
||||||
@ -134,3 +144,39 @@ export function setDefaultReaction({
|
|||||||
reaction: buildInputReaction(reaction),
|
reaction: buildInputReaction(reaction),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchTopReactions({ hash = '0' }: { hash?: string }) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetTopReactions({
|
||||||
|
limit: TOP_REACTIONS_LIMIT,
|
||||||
|
hash: BigInt(hash),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.messages.ReactionsNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: String(result.hash),
|
||||||
|
reactions: result.reactions.map(buildApiReaction).filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRecentReactions({ hash = '0' }: { hash?: string }) {
|
||||||
|
const result = await invokeRequest(new GramJs.messages.GetRecentReactions({
|
||||||
|
limit: RECENT_REACTIONS_LIMIT,
|
||||||
|
hash: BigInt(hash),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.messages.ReactionsNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash: String(result.hash),
|
||||||
|
reactions: result.reactions.map(buildApiReaction).filter(Boolean),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearRecentReactions() {
|
||||||
|
return invokeRequest(new GramJs.messages.ClearRecentReactions());
|
||||||
|
}
|
||||||
|
|||||||
@ -888,6 +888,8 @@ export function updater(update: Update) {
|
|||||||
onUpdate({ '@type': 'updateFavoriteStickers' });
|
onUpdate({ '@type': 'updateFavoriteStickers' });
|
||||||
} else if (update instanceof GramJs.UpdateRecentStickers) {
|
} else if (update instanceof GramJs.UpdateRecentStickers) {
|
||||||
onUpdate({ '@type': 'updateRecentStickers' });
|
onUpdate({ '@type': 'updateRecentStickers' });
|
||||||
|
} else if (update instanceof GramJs.UpdateRecentReactions) {
|
||||||
|
onUpdate({ '@type': 'updateRecentReactions' });
|
||||||
} else if (update instanceof GramJs.UpdateMoveStickerSetToTop) {
|
} else if (update instanceof GramJs.UpdateMoveStickerSetToTop) {
|
||||||
if (!update.masks) {
|
if (!update.masks) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
|
|||||||
@ -459,6 +459,7 @@ export interface ApiReactionCount {
|
|||||||
|
|
||||||
export interface ApiAvailableReaction {
|
export interface ApiAvailableReaction {
|
||||||
selectAnimation?: ApiDocument;
|
selectAnimation?: ApiDocument;
|
||||||
|
appearAnimation?: ApiDocument;
|
||||||
activateAnimation?: ApiDocument;
|
activateAnimation?: ApiDocument;
|
||||||
effectAnimation?: ApiDocument;
|
effectAnimation?: ApiDocument;
|
||||||
staticIcon?: ApiDocument;
|
staticIcon?: ApiDocument;
|
||||||
|
|||||||
@ -400,6 +400,10 @@ export type ApiUpdateRecentStickers = {
|
|||||||
'@type': 'updateRecentStickers';
|
'@type': 'updateRecentStickers';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateRecentReactions = {
|
||||||
|
'@type': 'updateRecentReactions';
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdateMoveStickerSetToTop = {
|
export type ApiUpdateMoveStickerSetToTop = {
|
||||||
'@type': 'updateMoveStickerSetToTop';
|
'@type': 'updateMoveStickerSetToTop';
|
||||||
isCustomEmoji?: boolean;
|
isCustomEmoji?: boolean;
|
||||||
@ -638,7 +642,8 @@ 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 | ApiUpdateRecentEmojiStatuses | ApiRequestInitApi
|
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses |
|
||||||
|
ApiUpdateRecentReactions | ApiRequestInitApi
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
@ -45,6 +45,7 @@ export { default as StickerSetModal } from '../components/common/StickerSetModal
|
|||||||
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
|
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
|
||||||
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
||||||
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
||||||
|
export { default as ReactionPicker } from '../components/middle/message/ReactionPicker';
|
||||||
|
|
||||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||||
export { default as PollModal } from '../components/middle/composer/PollModal';
|
export { default as PollModal } from '../components/middle/composer/PollModal';
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type OwnProps = {
|
|||||||
loopLimit?: number;
|
loopLimit?: number;
|
||||||
style?: string;
|
style?: string;
|
||||||
isBig?: boolean;
|
isBig?: boolean;
|
||||||
|
noPlay?: boolean;
|
||||||
withGridFix?: boolean;
|
withGridFix?: boolean;
|
||||||
withSharedAnimation?: boolean;
|
withSharedAnimation?: boolean;
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
@ -48,6 +49,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
documentId,
|
documentId,
|
||||||
size = STICKER_SIZE,
|
size = STICKER_SIZE,
|
||||||
isBig,
|
isBig,
|
||||||
|
noPlay,
|
||||||
className,
|
className,
|
||||||
loopLimit,
|
loopLimit,
|
||||||
style,
|
style,
|
||||||
@ -129,6 +131,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
|||||||
sticker={customEmoji}
|
sticker={customEmoji}
|
||||||
isSmall={!isBig}
|
isSmall={!isBig}
|
||||||
size={size}
|
size={size}
|
||||||
|
noPlay={noPlay}
|
||||||
customColor={customColor}
|
customColor={customColor}
|
||||||
thumbClassName={styles.thumb}
|
thumbClassName={styles.thumb}
|
||||||
fullMediaClassName={styles.media}
|
fullMediaClassName={styles.media}
|
||||||
|
|||||||
5
src/components/common/CustomEmojiPicker.module.scss
Normal file
5
src/components/common/CustomEmojiPicker.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.root {
|
||||||
|
--emoji-size: 2.5rem;
|
||||||
|
|
||||||
|
max-height: calc(100 * var(--vh));
|
||||||
|
}
|
||||||
@ -1,54 +1,67 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { RefObject } from 'react';
|
||||||
import React, {
|
import React, {
|
||||||
useState, useEffect, memo, useRef, useMemo, useCallback,
|
useState, useEffect, memo, useRef, useMemo, useCallback,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getGlobal, withGlobal } from '../../../global';
|
import { getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiStickerSet, ApiSticker } from '../../../api/types';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import type { StickerSetOrRecent } from '../../../types';
|
import type {
|
||||||
|
ApiStickerSet, ApiSticker, ApiReaction, ApiAvailableReaction,
|
||||||
|
} from '../../api/types';
|
||||||
|
import type { StickerSetOrReactionsSetOrRecent } from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CHAT_STICKER_SET_ID,
|
CHAT_STICKER_SET_ID,
|
||||||
FAVORITE_SYMBOL_SET_ID,
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
|
POPULAR_SYMBOL_SET_ID,
|
||||||
PREMIUM_STICKER_SET_ID,
|
PREMIUM_STICKER_SET_ID,
|
||||||
RECENT_SYMBOL_SET_ID,
|
RECENT_SYMBOL_SET_ID,
|
||||||
SLIDE_TRANSITION_DURATION,
|
SLIDE_TRANSITION_DURATION,
|
||||||
STICKER_PICKER_MAX_SHARED_COVERS,
|
STICKER_PICKER_MAX_SHARED_COVERS,
|
||||||
STICKER_SIZE_PICKER_HEADER,
|
STICKER_SIZE_PICKER_HEADER,
|
||||||
} from '../../../config';
|
TOP_SYMBOL_SET_ID,
|
||||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
} from '../../config';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
||||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { pickTruthy, unique } from '../../../util/iteratees';
|
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
||||||
|
import { pickTruthy, unique } from '../../util/iteratees';
|
||||||
|
import { isSameReaction } from '../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectIsAlwaysHighPriorityEmoji,
|
selectIsAlwaysHighPriorityEmoji,
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
selectIsCurrentUserPremium,
|
selectIsCurrentUserPremium,
|
||||||
} from '../../../global/selectors';
|
} from '../../global/selectors';
|
||||||
|
|
||||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
import useAsyncRendering from '../right/hooks/useAsyncRendering';
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
|
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import StickerButton from '../../common/StickerButton';
|
import StickerButton from './StickerButton';
|
||||||
import StickerSet from './StickerSet';
|
import StickerSet from './StickerSet';
|
||||||
import StickerSetCover from './StickerSetCover';
|
import StickerSetCover from '../middle/composer/StickerSetCover';
|
||||||
|
|
||||||
import './StickerPicker.scss';
|
import '../middle/composer/StickerPicker.scss';
|
||||||
|
import styles from './CustomEmojiPicker.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
|
scrollContainerRef?: RefObject<HTMLDivElement>;
|
||||||
|
scrollHeaderRef?: RefObject<HTMLDivElement>;
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
isStatusPicker?: boolean;
|
|
||||||
idPrefix?: string;
|
idPrefix?: string;
|
||||||
withDefaultTopicIcons?: boolean;
|
withDefaultTopicIcons?: boolean;
|
||||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||||
|
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||||
|
selectedReactionIds?: string[];
|
||||||
|
isStatusPicker?: boolean;
|
||||||
|
isReactionPicker?: boolean;
|
||||||
onContextMenuOpen?: NoneToVoidFunction;
|
onContextMenuOpen?: NoneToVoidFunction;
|
||||||
onContextMenuClose?: NoneToVoidFunction;
|
onContextMenuClose?: NoneToVoidFunction;
|
||||||
onContextMenuClick?: NoneToVoidFunction;
|
onContextMenuClick?: NoneToVoidFunction;
|
||||||
@ -58,7 +71,10 @@ type StateProps = {
|
|||||||
customEmojisById?: Record<string, ApiSticker>;
|
customEmojisById?: Record<string, ApiSticker>;
|
||||||
recentCustomEmojiIds?: string[];
|
recentCustomEmojiIds?: string[];
|
||||||
recentStatusEmojis?: ApiSticker[];
|
recentStatusEmojis?: ApiSticker[];
|
||||||
|
topReactions?: ApiReaction[];
|
||||||
|
recentReactions?: ApiReaction[];
|
||||||
stickerSetsById: Record<string, ApiStickerSet>;
|
stickerSetsById: Record<string, ApiStickerSet>;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
addedCustomEmojiIds?: string[];
|
addedCustomEmojiIds?: string[];
|
||||||
defaultTopicIconsId?: string;
|
defaultTopicIconsId?: string;
|
||||||
defaultStatusIconsId?: string;
|
defaultStatusIconsId?: string;
|
||||||
@ -72,20 +88,37 @@ 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 DEFAULT_ID_PREFIX = 'custom-emoji-set';
|
||||||
|
const TOP_REACTIONS_COUNT = 16;
|
||||||
|
const RECENT_REACTIONS_COUNT = 32;
|
||||||
|
const FADED_BUTTON_SET_IDS = new Set([RECENT_SYMBOL_SET_ID, FAVORITE_SYMBOL_SET_ID, POPULAR_SYMBOL_SET_ID]);
|
||||||
|
const STICKER_SET_IDS_WITH_COVER = new Set([
|
||||||
|
RECENT_SYMBOL_SET_ID,
|
||||||
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
|
POPULAR_SYMBOL_SET_ID,
|
||||||
|
CHAT_STICKER_SET_ID,
|
||||||
|
PREMIUM_STICKER_SET_ID,
|
||||||
|
]);
|
||||||
|
|
||||||
const stickerSetIntersections: boolean[] = [];
|
const stickerSetIntersections: boolean[] = [];
|
||||||
|
|
||||||
const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||||
|
scrollContainerRef,
|
||||||
|
scrollHeaderRef,
|
||||||
className,
|
className,
|
||||||
loadAndPlay,
|
loadAndPlay,
|
||||||
addedCustomEmojiIds,
|
addedCustomEmojiIds,
|
||||||
customEmojisById,
|
customEmojisById,
|
||||||
recentCustomEmojiIds,
|
recentCustomEmojiIds,
|
||||||
|
selectedReactionIds,
|
||||||
recentStatusEmojis,
|
recentStatusEmojis,
|
||||||
stickerSetsById,
|
stickerSetsById,
|
||||||
|
topReactions,
|
||||||
|
recentReactions,
|
||||||
|
availableReactions,
|
||||||
idPrefix = DEFAULT_ID_PREFIX,
|
idPrefix = DEFAULT_ID_PREFIX,
|
||||||
customEmojiFeaturedIds,
|
customEmojiFeaturedIds,
|
||||||
canAnimate,
|
canAnimate,
|
||||||
|
isReactionPicker,
|
||||||
isStatusPicker,
|
isStatusPicker,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
@ -93,20 +126,28 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
defaultTopicIconsId,
|
defaultTopicIconsId,
|
||||||
defaultStatusIconsId,
|
defaultStatusIconsId,
|
||||||
onCustomEmojiSelect,
|
onCustomEmojiSelect,
|
||||||
|
onReactionSelect,
|
||||||
onContextMenuOpen,
|
onContextMenuOpen,
|
||||||
onContextMenuClose,
|
onContextMenuClose,
|
||||||
onContextMenuClick,
|
onContextMenuClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
let containerRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const headerRef = useRef<HTMLDivElement>(null);
|
let headerRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
if (scrollContainerRef) {
|
||||||
|
containerRef = scrollContainerRef;
|
||||||
|
}
|
||||||
|
if (scrollHeaderRef) {
|
||||||
|
headerRef = scrollHeaderRef;
|
||||||
|
}
|
||||||
|
|
||||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||||
|
const { isMobile } = useAppLayout();
|
||||||
|
|
||||||
const recentCustomEmojis = useMemo(() => {
|
const recentCustomEmojis = useMemo(() => {
|
||||||
return isStatusPicker
|
return isStatusPicker
|
||||||
@ -149,9 +190,43 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
return MEMO_EMPTY_ARRAY;
|
return MEMO_EMPTY_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSets: StickerSetOrRecent[] = [];
|
const defaultSets: StickerSetOrReactionsSetOrRecent[] = [];
|
||||||
|
|
||||||
if (isStatusPicker) {
|
if (isReactionPicker) {
|
||||||
|
const topReactionsSlice = topReactions?.slice(0, TOP_REACTIONS_COUNT) || [];
|
||||||
|
if (topReactionsSlice?.length) {
|
||||||
|
defaultSets.push({
|
||||||
|
id: TOP_SYMBOL_SET_ID,
|
||||||
|
accessHash: '',
|
||||||
|
title: lang('Reactions'),
|
||||||
|
reactions: topReactionsSlice,
|
||||||
|
count: topReactionsSlice.length,
|
||||||
|
isEmoji: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanRecentReactions = (recentReactions || [])
|
||||||
|
.filter((reaction) => !topReactionsSlice.some((topReaction) => isSameReaction(topReaction, reaction)))
|
||||||
|
.slice(0, RECENT_REACTIONS_COUNT);
|
||||||
|
const cleanAvailableReactions = (availableReactions || [])
|
||||||
|
.map(({ reaction }) => reaction)
|
||||||
|
.filter((reaction) => {
|
||||||
|
return !topReactionsSlice.some((topReaction) => isSameReaction(topReaction, reaction))
|
||||||
|
&& !cleanRecentReactions.some((topReaction) => isSameReaction(topReaction, reaction));
|
||||||
|
});
|
||||||
|
if (cleanAvailableReactions?.length || cleanRecentReactions?.length) {
|
||||||
|
const isPopular = !cleanRecentReactions?.length;
|
||||||
|
const allRecentReactions = cleanRecentReactions.concat(cleanAvailableReactions);
|
||||||
|
defaultSets.push({
|
||||||
|
id: isPopular ? POPULAR_SYMBOL_SET_ID : RECENT_SYMBOL_SET_ID,
|
||||||
|
accessHash: '',
|
||||||
|
title: lang(isPopular ? 'PopularReactions' : 'RecentStickers'),
|
||||||
|
reactions: allRecentReactions,
|
||||||
|
count: allRecentReactions.length,
|
||||||
|
isEmoji: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (isStatusPicker) {
|
||||||
const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!];
|
const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!];
|
||||||
if (defaultStatusIconsPack.stickers?.length) {
|
if (defaultStatusIconsPack.stickers?.length) {
|
||||||
const stickers = (defaultStatusIconsPack.stickers || []).concat(recentCustomEmojis || []);
|
const stickers = (defaultStatusIconsPack.stickers || []).concat(recentCustomEmojis || []);
|
||||||
@ -179,7 +254,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
title: lang('RecentStickers'),
|
title: lang('RecentStickers'),
|
||||||
stickers: recentCustomEmojis,
|
stickers: recentCustomEmojis,
|
||||||
count: recentCustomEmojis.length,
|
count: recentCustomEmojis.length,
|
||||||
isEmoji: true as true,
|
isEmoji: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,8 +267,9 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
...setsToDisplay,
|
...setsToDisplay,
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
addedCustomEmojiIds, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||||
customEmojiFeaturedIds, stickerSetsById, defaultStatusIconsId, lang, defaultTopicIconsId,
|
customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions,
|
||||||
|
defaultStatusIconsId, defaultTopicIconsId,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const noPopulatedSets = useMemo(() => (
|
const noPopulatedSets = useMemo(() => (
|
||||||
@ -201,10 +277,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
&& allSets.filter((set) => set.stickers?.length).length === 0
|
&& allSets.filter((set) => set.stickers?.length).length === 0
|
||||||
), [allSets, areAddedLoaded]);
|
), [allSets, areAddedLoaded]);
|
||||||
|
|
||||||
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
const canRenderContent = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||||
const shouldRenderContents = areAddedLoaded && canRenderContents && !noPopulatedSets;
|
const shouldRenderContent = areAddedLoaded && canRenderContent && !noPopulatedSets;
|
||||||
|
|
||||||
useHorizontalScroll(headerRef, !shouldRenderContents);
|
useHorizontalScroll(headerRef, !(isMobile && shouldRenderContent));
|
||||||
|
|
||||||
// Scroll container and header when active set changes
|
// Scroll container and header when active set changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -232,7 +308,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
onCustomEmojiSelect(emoji);
|
onCustomEmojiSelect(emoji);
|
||||||
}, [onCustomEmojiSelect]);
|
}, [onCustomEmojiSelect]);
|
||||||
|
|
||||||
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
|
const handleReactionSelect = useCallback((reaction: ApiReaction) => {
|
||||||
|
onReactionSelect?.(reaction);
|
||||||
|
}, [onReactionSelect]);
|
||||||
|
|
||||||
|
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
||||||
const firstSticker = stickerSet.stickers?.[0];
|
const firstSticker = stickerSet.stickers?.[0];
|
||||||
const buttonClassName = buildClassName(
|
const buttonClassName = buildClassName(
|
||||||
'symbol-set-button sticker-set-button',
|
'symbol-set-button sticker-set-button',
|
||||||
@ -242,25 +322,24 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
||||||
const isHq = selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet as ApiStickerSet);
|
const isHq = selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet as ApiStickerSet);
|
||||||
|
|
||||||
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
if (stickerSet.id === TOP_SYMBOL_SET_ID) {
|
||||||
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
return undefined;
|
||||||
|| stickerSet.id === CHAT_STICKER_SET_ID
|
}
|
||||||
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
|
||||||
|| stickerSet.hasThumbnail
|
if (STICKER_SET_IDS_WITH_COVER.has(stickerSet.id) || stickerSet.hasThumbnail || !firstSticker) {
|
||||||
|| !firstSticker
|
const isFaded = FADED_BUTTON_SET_IDS.has(stickerSet.id);
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={stickerSet.id}
|
key={stickerSet.id}
|
||||||
className={buttonClassName}
|
className={buttonClassName}
|
||||||
ariaLabel={stickerSet.title}
|
ariaLabel={stickerSet.title}
|
||||||
round
|
round
|
||||||
faded={stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === FAVORITE_SYMBOL_SET_ID}
|
faded={isFaded}
|
||||||
color="translucent"
|
color="translucent"
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => selectStickerSet(index)}
|
onClick={() => selectStickerSet(index)}
|
||||||
>
|
>
|
||||||
{stickerSet.id === RECENT_SYMBOL_SET_ID ? (
|
{(stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === POPULAR_SYMBOL_SET_ID) ? (
|
||||||
<i className="icon-recent" />
|
<i className="icon-recent" />
|
||||||
) : (
|
) : (
|
||||||
<StickerSetCover
|
<StickerSetCover
|
||||||
@ -272,29 +351,29 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<StickerButton
|
|
||||||
key={stickerSet.id}
|
|
||||||
sticker={firstSticker}
|
|
||||||
size={STICKER_SIZE_PICKER_HEADER}
|
|
||||||
title={stickerSet.title}
|
|
||||||
className={buttonClassName}
|
|
||||||
noAnimate={!canAnimate || !loadAndPlay}
|
|
||||||
observeIntersection={observeIntersectionForCovers}
|
|
||||||
noContextMenu
|
|
||||||
isCurrentUserPremium
|
|
||||||
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
|
||||||
onClick={selectStickerSet}
|
|
||||||
clickArg={index}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StickerButton
|
||||||
|
key={stickerSet.id}
|
||||||
|
sticker={firstSticker}
|
||||||
|
size={STICKER_SIZE_PICKER_HEADER}
|
||||||
|
title={stickerSet.title}
|
||||||
|
className={buttonClassName}
|
||||||
|
noAnimate={!canAnimate || !loadAndPlay}
|
||||||
|
observeIntersection={observeIntersectionForCovers}
|
||||||
|
noContextMenu
|
||||||
|
isCurrentUserPremium
|
||||||
|
sharedCanvasRef={withSharedCanvas ? (isHq ? sharedCanvasHqRef : sharedCanvasRef) : undefined}
|
||||||
|
onClick={selectStickerSet}
|
||||||
|
clickArg={index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullClassName = buildClassName('StickerPicker', 'CustomEmojiPicker', className);
|
const fullClassName = buildClassName('StickerPicker', styles.root, className);
|
||||||
|
|
||||||
if (!shouldRenderContents) {
|
if (!shouldRenderContent) {
|
||||||
return (
|
return (
|
||||||
<div className={fullClassName}>
|
<div className={fullClassName}>
|
||||||
{noPopulatedSets ? (
|
{noPopulatedSets ? (
|
||||||
@ -322,34 +401,43 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
|||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||||
>
|
>
|
||||||
{allSets.map((stickerSet, i) => (
|
{allSets.map((stickerSet, i) => {
|
||||||
<StickerSet
|
const shouldHideHeader = stickerSet.id === TOP_SYMBOL_SET_ID
|
||||||
key={stickerSet.id}
|
|| (stickerSet.id === RECENT_SYMBOL_SET_ID && (withDefaultTopicIcons || isStatusPicker));
|
||||||
stickerSet={stickerSet}
|
|
||||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
return (
|
||||||
index={i}
|
<StickerSet
|
||||||
idPrefix={idPrefix}
|
key={stickerSet.id}
|
||||||
observeIntersection={observeIntersection}
|
stickerSet={stickerSet}
|
||||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||||
isSavedMessages={isSavedMessages}
|
index={i}
|
||||||
isStatusPicker={isStatusPicker}
|
idPrefix={idPrefix}
|
||||||
shouldHideRecentHeader={withDefaultTopicIcons || isStatusPicker}
|
observeIntersection={observeIntersection}
|
||||||
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||||
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
isSavedMessages={isSavedMessages}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isStatusPicker={isStatusPicker}
|
||||||
onStickerSelect={handleEmojiSelect}
|
isReactionPicker={isReactionPicker}
|
||||||
onContextMenuOpen={onContextMenuOpen}
|
shouldHideHeader={shouldHideHeader}
|
||||||
onContextMenuClose={onContextMenuClose}
|
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||||
onContextMenuClick={onContextMenuClick}
|
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||||
/>
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
))}
|
selectedReactionIds={selectedReactionIds}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
onReactionSelect={handleReactionSelect}
|
||||||
|
onStickerSelect={handleEmojiSelect}
|
||||||
|
onContextMenuOpen={onContextMenuOpen}
|
||||||
|
onContextMenuClose={onContextMenuClose}
|
||||||
|
onContextMenuClick={onContextMenuClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId, isStatusPicker }): StateProps => {
|
(global, { chatId, isStatusPicker, isReactionPicker }): StateProps => {
|
||||||
const {
|
const {
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: stickerSetsById,
|
setsById: stickerSetsById,
|
||||||
@ -362,6 +450,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
recentCustomEmojis: recentCustomEmojiIds,
|
recentCustomEmojis: recentCustomEmojiIds,
|
||||||
|
recentReactions,
|
||||||
|
topReactions,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
||||||
@ -378,6 +468,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
customEmojiFeaturedIds,
|
customEmojiFeaturedIds,
|
||||||
defaultTopicIconsId: global.defaultTopicIconsId,
|
defaultTopicIconsId: global.defaultTopicIconsId,
|
||||||
defaultStatusIconsId: global.defaultStatusIconsId,
|
defaultStatusIconsId: global.defaultStatusIconsId,
|
||||||
|
topReactions: isReactionPicker ? topReactions : undefined,
|
||||||
|
recentReactions: isReactionPicker ? recentReactions : undefined,
|
||||||
|
availableReactions: isReactionPicker ? global.availableReactions : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(CustomEmojiPicker));
|
)(CustomEmojiPicker));
|
||||||
@ -16,7 +16,7 @@ import useMedia from '../../hooks/useMedia';
|
|||||||
import useBuffering from '../../hooks/useBuffering';
|
import useBuffering from '../../hooks/useBuffering';
|
||||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
|
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
@ -83,7 +83,7 @@ const GifButton: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
} = useContextMenuPosition(
|
} = useMenuPosition(
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
getTriggerElement,
|
getTriggerElement,
|
||||||
getRootElement,
|
getRootElement,
|
||||||
|
|||||||
18
src/components/common/ReactionEmoji.module.scss
Normal file
18
src/components/common/ReactionEmoji.module.scss
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
.root {
|
||||||
|
--custom-emoji-size: 2.5rem;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-block;
|
||||||
|
width: var(--custom-emoji-size);
|
||||||
|
height: var(--custom-emoji-size);
|
||||||
|
border-radius: var(--border-radius-messages-small);
|
||||||
|
background: transparent no-repeat center;
|
||||||
|
background-size: contain;
|
||||||
|
transition: background-color 0.15s ease, opacity 0.3s ease !important;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.selected,
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-interactive-element-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/components/common/ReactionEmoji.tsx
Normal file
101
src/components/common/ReactionEmoji.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useMemo, useRef,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ApiAvailableReaction, ApiReaction } from '../../api/types';
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
|
import { EMOJI_SIZE_PICKER } from '../../config';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { getDocumentMediaHash, isSameReaction } from '../../global/helpers';
|
||||||
|
|
||||||
|
import useBoundsInSharedCanvas from '../../hooks/useBoundsInSharedCanvas';
|
||||||
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
|
import useMedia from '../../hooks/useMedia';
|
||||||
|
|
||||||
|
import CustomEmoji from './CustomEmoji';
|
||||||
|
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
|
||||||
|
|
||||||
|
import styles from './ReactionEmoji.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
reaction: ApiReaction;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
className?: string;
|
||||||
|
isSelected?: boolean;
|
||||||
|
loadAndPlay?: boolean;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
|
onClick: (reaction: ApiReaction) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReactionEmoji: FC<OwnProps> = ({
|
||||||
|
reaction,
|
||||||
|
availableReactions,
|
||||||
|
isSelected,
|
||||||
|
loadAndPlay,
|
||||||
|
observeIntersection,
|
||||||
|
sharedCanvasRef,
|
||||||
|
sharedCanvasHqRef,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const isCustom = 'documentId' in reaction;
|
||||||
|
const availableReaction = useMemo(() => (
|
||||||
|
availableReactions?.find((available) => isSameReaction(available.reaction, reaction))
|
||||||
|
), [availableReactions, reaction]);
|
||||||
|
const thumbDataUri = availableReaction?.staticIcon?.thumbnail?.dataUri;
|
||||||
|
const animationId = availableReaction?.selectAnimation?.id;
|
||||||
|
const bounds = useBoundsInSharedCanvas(ref, sharedCanvasHqRef);
|
||||||
|
const mediaData = useMedia(
|
||||||
|
availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation) : undefined,
|
||||||
|
!animationId,
|
||||||
|
);
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(reaction);
|
||||||
|
}, [onClick, reaction]);
|
||||||
|
|
||||||
|
const transitionClassNames = useMediaTransition(mediaData);
|
||||||
|
const fullClassName = buildClassName(
|
||||||
|
styles.root,
|
||||||
|
isSelected && styles.selected,
|
||||||
|
!isCustom && 'sticker-reaction',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={fullClassName}
|
||||||
|
onClick={handleClick}
|
||||||
|
title={availableReaction?.title}
|
||||||
|
data-sticker-id={isCustom ? reaction.documentId : undefined}
|
||||||
|
>
|
||||||
|
{isCustom ? (
|
||||||
|
<CustomEmoji
|
||||||
|
ref={ref}
|
||||||
|
documentId={reaction.documentId}
|
||||||
|
size={EMOJI_SIZE_PICKER}
|
||||||
|
noPlay={!loadAndPlay}
|
||||||
|
observeIntersectionForPlaying={observeIntersection}
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AnimatedIconWithPreview
|
||||||
|
tgsUrl={mediaData}
|
||||||
|
thumbDataUri={thumbDataUri}
|
||||||
|
play={loadAndPlay}
|
||||||
|
size={EMOJI_SIZE_PICKER}
|
||||||
|
className={transitionClassNames}
|
||||||
|
sharedCanvas={sharedCanvasHqRef!.current || undefined}
|
||||||
|
sharedCanvasCoords={bounds.coords}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactionEmoji);
|
||||||
@ -13,6 +13,8 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.custom-emoji {
|
&.custom-emoji {
|
||||||
|
color: var(--color-primary);
|
||||||
|
|
||||||
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;
|
||||||
@ -63,6 +65,10 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--color-interactive-element-hover);
|
||||||
|
}
|
||||||
|
|
||||||
&.interactive {
|
&.interactive {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
|||||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||||
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
||||||
|
|
||||||
import StickerView from './StickerView';
|
import StickerView from './StickerView';
|
||||||
@ -35,6 +35,7 @@ type OwnProps<T> = {
|
|||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
isStatusPicker?: boolean;
|
isStatusPicker?: boolean;
|
||||||
canViewSet?: boolean;
|
canViewSet?: boolean;
|
||||||
|
isSelected?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
@ -68,6 +69,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
isStatusPicker,
|
isStatusPicker,
|
||||||
canViewSet,
|
canViewSet,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
|
isSelected,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
noShowPremium,
|
noShowPremium,
|
||||||
sharedCanvasRef,
|
sharedCanvasRef,
|
||||||
@ -123,7 +125,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
} = useContextMenuPosition(
|
} = useMenuPosition(
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
getTriggerElement,
|
getTriggerElement,
|
||||||
getRootElement,
|
getRootElement,
|
||||||
@ -205,8 +207,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
|||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'StickerButton',
|
'StickerButton',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
|
isSelected && 'selected',
|
||||||
isCustomEmoji && 'custom-emoji',
|
isCustomEmoji && 'custom-emoji',
|
||||||
`sticker-button-${id}`,
|
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,43 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, getGlobal } from '../../../global';
|
import { getActions, getGlobal } from '../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import type { ApiSticker } from '../../../api/types';
|
import type { ApiAvailableReaction, ApiReaction, ApiSticker } from '../../api/types';
|
||||||
import type { StickerSetOrRecent } from '../../../types';
|
import type { StickerSetOrReactionsSetOrRecent } from '../../types';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_STATUS_ICON_ID,
|
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,
|
||||||
} from '../../../config';
|
FAVORITE_SYMBOL_SET_ID,
|
||||||
import buildClassName from '../../../util/buildClassName';
|
POPULAR_SYMBOL_SET_ID,
|
||||||
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
|
RECENT_SYMBOL_SET_ID,
|
||||||
|
STICKER_SIZE_PICKER,
|
||||||
|
} from '../../config';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../global/selectors';
|
||||||
|
import { getReactionUniqueKey } from '../../global/helpers';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
|
import useWindowSize from '../../hooks/useWindowSize';
|
||||||
|
|
||||||
import StickerButton from '../../common/StickerButton';
|
import StickerButton from './StickerButton';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../ui/Button';
|
||||||
|
import ReactionEmoji from './ReactionEmoji';
|
||||||
|
|
||||||
import grey from '../../../assets/icons/forumTopic/grey.svg';
|
import grey from '../../assets/icons/forumTopic/grey.svg';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
stickerSet: StickerSetOrRecent;
|
stickerSet: StickerSetOrReactionsSetOrRecent;
|
||||||
loadAndPlay: boolean;
|
loadAndPlay: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
idPrefix?: string;
|
idPrefix?: string;
|
||||||
@ -38,12 +45,16 @@ type OwnProps = {
|
|||||||
favoriteStickers?: ApiSticker[];
|
favoriteStickers?: ApiSticker[];
|
||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
isStatusPicker?: boolean;
|
isStatusPicker?: boolean;
|
||||||
|
isReactionPicker?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
shouldHideRecentHeader?: boolean;
|
shouldHideHeader?: boolean;
|
||||||
|
selectedReactionIds?: string[];
|
||||||
withDefaultTopicIcon?: boolean;
|
withDefaultTopicIcon?: boolean;
|
||||||
withDefaultStatusIcon?: boolean;
|
withDefaultStatusIcon?: boolean;
|
||||||
observeIntersection: ObserveFn;
|
observeIntersection: ObserveFn;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||||
|
onReactionSelect?: (reaction: ApiReaction) => 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;
|
||||||
@ -53,6 +64,10 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ITEMS_PER_ROW_FALLBACK = 8;
|
const ITEMS_PER_ROW_FALLBACK = 8;
|
||||||
|
const ITEMS_MOBILE_PER_ROW_FALLBACK = 7;
|
||||||
|
const ITEMS_MINI_MOBILE_PER_ROW_FALLBACK = 6;
|
||||||
|
const MOBILE_WIDTH_THRESHOLD_PX = 440;
|
||||||
|
const MINI_MOBILE_WIDTH_THRESHOLD_PX = 362;
|
||||||
|
|
||||||
const StickerSet: FC<OwnProps> = ({
|
const StickerSet: FC<OwnProps> = ({
|
||||||
stickerSet,
|
stickerSet,
|
||||||
@ -61,13 +76,17 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
idPrefix,
|
idPrefix,
|
||||||
shouldRender,
|
shouldRender,
|
||||||
favoriteStickers,
|
favoriteStickers,
|
||||||
|
availableReactions,
|
||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
isStatusPicker,
|
isStatusPicker,
|
||||||
|
isReactionPicker,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
shouldHideRecentHeader,
|
shouldHideHeader,
|
||||||
withDefaultTopicIcon,
|
withDefaultTopicIcon,
|
||||||
|
selectedReactionIds,
|
||||||
withDefaultStatusIcon,
|
withDefaultStatusIcon,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
|
onReactionSelect,
|
||||||
onStickerSelect,
|
onStickerSelect,
|
||||||
onStickerUnfave,
|
onStickerUnfave,
|
||||||
onStickerFave,
|
onStickerFave,
|
||||||
@ -79,6 +98,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const {
|
const {
|
||||||
clearRecentStickers,
|
clearRecentStickers,
|
||||||
clearRecentCustomEmoji,
|
clearRecentCustomEmoji,
|
||||||
|
clearRecentReactions,
|
||||||
openPremiumModal,
|
openPremiumModal,
|
||||||
toggleStickerSet,
|
toggleStickerSet,
|
||||||
loadStickers,
|
loadStickers,
|
||||||
@ -93,10 +113,11 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
const { width: windowWidth } = useWindowSize();
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||||
const { isMobile } = useAppLayout();
|
const { isMobile } = useAppLayout();
|
||||||
|
|
||||||
const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK);
|
const [itemsPerRow, setItemsPerRow] = useState(getItemsPerRowFallback(windowWidth));
|
||||||
|
|
||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
@ -106,17 +127,22 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const emojiMarginPx = isMobile ? 8 : 10;
|
const emojiMarginPx = isMobile ? 8 : 10;
|
||||||
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 isPopular = stickerSet.id === POPULAR_SYMBOL_SET_ID;
|
||||||
const isEmoji = stickerSet.isEmoji;
|
const isEmoji = stickerSet.isEmoji;
|
||||||
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
|
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
|
||||||
|
|
||||||
const handleClearRecent = useCallback(() => {
|
const handleClearRecent = useCallback(() => {
|
||||||
if (isEmoji) {
|
if (isReactionPicker) {
|
||||||
|
clearRecentReactions();
|
||||||
|
} else if (isEmoji) {
|
||||||
clearRecentCustomEmoji();
|
clearRecentCustomEmoji();
|
||||||
} else {
|
} else {
|
||||||
clearRecentStickers();
|
clearRecentStickers();
|
||||||
}
|
}
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
}, [clearRecentCustomEmoji, clearRecentStickers, closeConfirmModal, isEmoji]);
|
}, [
|
||||||
|
clearRecentCustomEmoji, clearRecentReactions, clearRecentStickers, closeConfirmModal, isEmoji, isReactionPicker,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleAddClick = useCallback(() => {
|
const handleAddClick = useCallback(() => {
|
||||||
if (isPremiumSet && !isCurrentUserPremium) {
|
if (isPremiumSet && !isCurrentUserPremium) {
|
||||||
@ -156,10 +182,12 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
||||||
|
|
||||||
const calculateItemsPerRow = useCallback((width: number) => {
|
const calculateItemsPerRow = useCallback((width: number) => {
|
||||||
if (!width) return ITEMS_PER_ROW_FALLBACK;
|
if (!width) {
|
||||||
|
return getItemsPerRowFallback(windowWidth);
|
||||||
|
}
|
||||||
|
|
||||||
return Math.floor(width / (itemSize + margin));
|
return Math.floor(width / (itemSize + margin));
|
||||||
}, [itemSize, margin]);
|
}, [itemSize, margin, windowWidth]);
|
||||||
|
|
||||||
const handleResize = useCallback((entry: ResizeObserverEntry) => {
|
const handleResize = useCallback((entry: ResizeObserverEntry) => {
|
||||||
setItemsPerRow(calculateItemsPerRow(entry.contentRect.width));
|
setItemsPerRow(calculateItemsPerRow(entry.contentRect.width));
|
||||||
@ -172,7 +200,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
}, [calculateItemsPerRow]);
|
}, [calculateItemsPerRow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isIntersecting && !stickerSet.stickers?.length && stickerSet.accessHash) {
|
if (isIntersecting && !stickerSet.stickers?.length && !stickerSet.reactions?.length && stickerSet.accessHash) {
|
||||||
loadStickers({
|
loadStickers({
|
||||||
stickerSetInfo: {
|
stickerSetInfo: {
|
||||||
id: stickerSet.id,
|
id: stickerSet.id,
|
||||||
@ -186,7 +214,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
|
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
|
||||||
|
|
||||||
const isInstalled = stickerSet.installedDate && !stickerSet.isArchived;
|
const isInstalled = stickerSet.installedDate && !stickerSet.isArchived;
|
||||||
const canCut = !isInstalled && stickerSet.id !== RECENT_SYMBOL_SET_ID;
|
const canCut = !isInstalled && stickerSet.id !== RECENT_SYMBOL_SET_ID && stickerSet.id !== POPULAR_SYMBOL_SET_ID;
|
||||||
const [isCut, , expand] = useFlag(canCut);
|
const [isCut, , expand] = useFlag(canCut);
|
||||||
const itemsBeforeCutout = itemsPerRow * 3 - 1;
|
const itemsBeforeCutout = itemsPerRow * 3 - 1;
|
||||||
const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count;
|
const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count;
|
||||||
@ -194,8 +222,6 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, totalItemsCount) / itemsPerRow) * (itemSize + margin);
|
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, totalItemsCount) / itemsPerRow) * (itemSize + margin);
|
||||||
const height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * (itemSize + margin);
|
const height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * (itemSize + margin);
|
||||||
|
|
||||||
const shouldHideHeader = isRecent && shouldHideRecentHeader;
|
|
||||||
|
|
||||||
const favoriteStickerIdsSet = useMemo(() => (
|
const favoriteStickerIdsSet = useMemo(() => (
|
||||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||||
), [favoriteStickers]);
|
), [favoriteStickers]);
|
||||||
@ -218,7 +244,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
{isRecent && (
|
{isRecent && (
|
||||||
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
||||||
)}
|
)}
|
||||||
{!isRecent && isEmoji && !isInstalled && (
|
{!isRecent && isEmoji && !isInstalled && !isPopular && (
|
||||||
<Button
|
<Button
|
||||||
className="symbol-set-add-button"
|
className="symbol-set-add-button"
|
||||||
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
|
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
|
||||||
@ -262,14 +288,33 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
<i className="icon-premium" />
|
<i className="icon-premium" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{shouldRender && stickerSet.stickers && stickerSet.stickers
|
{shouldRender && stickerSet.reactions?.map((reaction) => {
|
||||||
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
const reactionId = getReactionUniqueKey(reaction);
|
||||||
|
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactionEmoji
|
||||||
|
key={`${stickerSet.id}_${reactionId}`}
|
||||||
|
reaction={reaction}
|
||||||
|
isSelected={isSelected}
|
||||||
|
loadAndPlay={loadAndPlay}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
onClick={onReactionSelect!}
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{shouldRender && stickerSet.stickers?.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
||||||
.map((sticker, i) => {
|
.map((sticker, i) => {
|
||||||
const isHqEmoji = (isRecent || isFavorite)
|
const isHqEmoji = (isRecent || isFavorite)
|
||||||
&& selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo);
|
&& selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo);
|
||||||
const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji
|
const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji
|
||||||
? sharedCanvasHqRef
|
? sharedCanvasHqRef
|
||||||
: sharedCanvasRef;
|
: sharedCanvasRef;
|
||||||
|
const reactionId = sticker.isCustomEmoji ? sticker.id : sticker.emoji;
|
||||||
|
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StickerButton
|
<StickerButton
|
||||||
@ -285,6 +330,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
sharedCanvasRef={canvasRef}
|
sharedCanvasRef={canvasRef}
|
||||||
onClick={onStickerSelect}
|
onClick={onStickerSelect}
|
||||||
clickArg={sticker}
|
clickArg={sticker}
|
||||||
|
isSelected={isSelected}
|
||||||
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}
|
||||||
@ -309,7 +355,7 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
|
|
||||||
{isRecent && (
|
{isRecent && (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
text={lang('ClearRecentStickersAlertMessage')}
|
text={lang(isReactionPicker ? 'ClearRecentReactionsAlertMessage' : 'ClearRecentStickersAlertMessage')}
|
||||||
isOpen={isConfirmModalOpen}
|
isOpen={isConfirmModalOpen}
|
||||||
onClose={closeConfirmModal}
|
onClose={closeConfirmModal}
|
||||||
confirmHandler={handleClearRecent}
|
confirmHandler={handleClearRecent}
|
||||||
@ -321,3 +367,11 @@ const StickerSet: FC<OwnProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(StickerSet);
|
export default memo(StickerSet);
|
||||||
|
|
||||||
|
function getItemsPerRowFallback(windowWidth: number): number {
|
||||||
|
return windowWidth > MOBILE_WIDTH_THRESHOLD_PX
|
||||||
|
? ITEMS_PER_ROW_FALLBACK
|
||||||
|
: (windowWidth < MINI_MOBILE_WIDTH_THRESHOLD_PX
|
||||||
|
? ITEMS_MINI_MOBILE_PER_ROW_FALLBACK
|
||||||
|
: ITEMS_MOBILE_PER_ROW_FALLBACK);
|
||||||
|
}
|
||||||
@ -26,7 +26,6 @@ import '../ui/Modal.scss';
|
|||||||
import './Avatar.scss';
|
import './Avatar.scss';
|
||||||
|
|
||||||
import telegramLogoPath from '../../assets/telegram-logo.svg';
|
import telegramLogoPath from '../../assets/telegram-logo.svg';
|
||||||
import reactionThumbsPath from '../../assets/reaction-thumbs.png';
|
|
||||||
import lockPreviewPath from '../../assets/lock.png';
|
import lockPreviewPath from '../../assets/lock.png';
|
||||||
import monkeyPath from '../../assets/monkey.svg';
|
import monkeyPath from '../../assets/monkey.svg';
|
||||||
import spoilerMaskPath from '../../assets/spoilers/mask.svg';
|
import spoilerMaskPath from '../../assets/spoilers/mask.svg';
|
||||||
@ -81,7 +80,6 @@ const preloadTasks = {
|
|||||||
loadModule(Bundles.Main)
|
loadModule(Bundles.Main)
|
||||||
.then(preloadFonts),
|
.then(preloadFonts),
|
||||||
preloadAvatars(),
|
preloadAvatars(),
|
||||||
preloadImage(reactionThumbsPath),
|
|
||||||
preloadImage(spoilerMaskPath),
|
preloadImage(spoilerMaskPath),
|
||||||
]),
|
]),
|
||||||
authPhoneNumber: () => Promise.all([
|
authPhoneNumber: () => Promise.all([
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import useFlag from '../../../hooks/useFlag';
|
|||||||
|
|
||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import Portal from '../../ui/Portal';
|
import Portal from '../../ui/Portal';
|
||||||
import CustomEmojiPicker from '../../middle/composer/CustomEmojiPicker';
|
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||||
|
|
||||||
import styles from './StatusPickerMenu.module.scss';
|
import styles from './StatusPickerMenu.module.scss';
|
||||||
|
|
||||||
@ -35,6 +35,11 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { loadFeaturedEmojiStickers } = getActions();
|
const { loadFeaturedEmojiStickers } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const scrollHeaderRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const transformOriginX = useRef<number>();
|
const transformOriginX = useRef<number>();
|
||||||
const [isContextMenuShown, markContextMenuShown, unmarkContextMenuShown] = useFlag();
|
const [isContextMenuShown, markContextMenuShown, unmarkContextMenuShown] = useFlag();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -47,6 +52,15 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [areFeaturedStickersLoaded, isOpen, loadFeaturedEmojiStickers]);
|
}, [areFeaturedStickersLoaded, isOpen, loadFeaturedEmojiStickers]);
|
||||||
|
|
||||||
|
const handleResetScrollPosition = useCallback(() => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
if (scrollHeaderRef.current) {
|
||||||
|
scrollHeaderRef.current.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleEmojiSelect = useCallback((sticker: ApiSticker) => {
|
const handleEmojiSelect = useCallback((sticker: ApiSticker) => {
|
||||||
onEmojiStatusSelect(sticker);
|
onEmojiStatusSelect(sticker);
|
||||||
onClose();
|
onClose();
|
||||||
@ -62,11 +76,14 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
transformOriginX={transformOriginX.current}
|
transformOriginX={transformOriginX.current}
|
||||||
noCloseOnBackdrop={isContextMenuShown}
|
noCloseOnBackdrop={isContextMenuShown}
|
||||||
|
onCloseAnimationEnd={handleResetScrollPosition}
|
||||||
>
|
>
|
||||||
<CustomEmojiPicker
|
<CustomEmojiPicker
|
||||||
idPrefix="status-emoji-set-"
|
idPrefix="status-emoji-set-"
|
||||||
loadAndPlay={isOpen}
|
loadAndPlay={isOpen}
|
||||||
isStatusPicker
|
isStatusPicker
|
||||||
|
scrollHeaderRef={scrollHeaderRef}
|
||||||
|
scrollContainerRef={scrollContainerRef}
|
||||||
onContextMenuOpen={markContextMenuShown}
|
onContextMenuOpen={markContextMenuShown}
|
||||||
onContextMenuClose={unmarkContextMenuShown}
|
onContextMenuClose={unmarkContextMenuShown}
|
||||||
onCustomEmojiSelect={handleEmojiSelect}
|
onCustomEmojiSelect={handleEmojiSelect}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import {
|
|||||||
selectIsMediaViewerOpen,
|
selectIsMediaViewerOpen,
|
||||||
selectIsRightColumnShown,
|
selectIsRightColumnShown,
|
||||||
selectIsServiceChatReady,
|
selectIsServiceChatReady,
|
||||||
selectUser,
|
selectUser, selectIsReactionPickerOpen,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||||
@ -47,6 +47,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
|
|||||||
import useInterval from '../../hooks/useInterval';
|
import useInterval from '../../hooks/useInterval';
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useTimeout from '../../hooks/useTimeout';
|
import useTimeout from '../../hooks/useTimeout';
|
||||||
|
import useFlag from '../../hooks/useFlag';
|
||||||
|
|
||||||
import StickerSetModal from '../common/StickerSetModal.async';
|
import StickerSetModal from '../common/StickerSetModal.async';
|
||||||
import UnreadCount from '../common/UnreadCounter';
|
import UnreadCount from '../common/UnreadCounter';
|
||||||
@ -81,6 +82,7 @@ import DeleteFolderDialog from './DeleteFolderDialog.async';
|
|||||||
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
||||||
import DraftRecipientPicker from './DraftRecipientPicker.async';
|
import DraftRecipientPicker from './DraftRecipientPicker.async';
|
||||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||||
|
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -131,11 +133,13 @@ type StateProps = {
|
|||||||
deleteFolderDialogId?: number;
|
deleteFolderDialogId?: number;
|
||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
isReceiptModalOpen?: boolean;
|
isReceiptModalOpen?: boolean;
|
||||||
|
isReactionPickerOpen: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
||||||
const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec
|
const CALL_BUNDLE_LOADING_DELAY_MS = 5000; // 5 sec
|
||||||
|
const REACTION_PICKER_LOADING_DELAY_MS = 7000; // 7 sec
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
let DEBUG_isLogged = false;
|
let DEBUG_isLogged = false;
|
||||||
@ -181,6 +185,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
isPremiumModalOpen,
|
isPremiumModalOpen,
|
||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
|
isReactionPickerOpen,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
deleteFolderDialogId,
|
deleteFolderDialogId,
|
||||||
isMasterTab,
|
isMasterTab,
|
||||||
@ -219,6 +224,9 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
toggleLeftColumn,
|
toggleLeftColumn,
|
||||||
loadRecentEmojiStatuses,
|
loadRecentEmojiStatuses,
|
||||||
updatePageTitle,
|
updatePageTitle,
|
||||||
|
loadTopReactions,
|
||||||
|
loadRecentReactions,
|
||||||
|
loadFeaturedEmojiStickers,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
if (DEBUG && !DEBUG_isLogged) {
|
if (DEBUG && !DEBUG_isLogged) {
|
||||||
@ -232,6 +240,9 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
void loadBundle(Bundles.Calls);
|
void loadBundle(Bundles.Calls);
|
||||||
}, CALL_BUNDLE_LOADING_DELAY_MS);
|
}, CALL_BUNDLE_LOADING_DELAY_MS);
|
||||||
|
|
||||||
|
const [shouldLoadReactionPicker, markShouldLoadReactionPicker] = useFlag(false);
|
||||||
|
useTimeout(markShouldLoadReactionPicker, REACTION_PICKER_LOADING_DELAY_MS);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
@ -266,19 +277,26 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
loadContactList();
|
loadContactList();
|
||||||
loadPremiumGifts();
|
loadPremiumGifts();
|
||||||
loadDefaultTopicIcons();
|
loadDefaultTopicIcons();
|
||||||
loadDefaultStatusIcons();
|
|
||||||
checkAppVersion();
|
checkAppVersion();
|
||||||
if (isCurrentUserPremium) {
|
loadTopReactions();
|
||||||
loadRecentEmojiStatuses();
|
loadRecentReactions();
|
||||||
}
|
loadFeaturedEmojiStickers();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
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,
|
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, loadTopReactions,
|
||||||
loadDefaultStatusIcons, loadRecentEmojiStatuses, isCurrentUserPremium, isMasterTab, initMain,
|
loadDefaultStatusIcons, loadRecentReactions, loadRecentEmojiStatuses, isCurrentUserPremium, isMasterTab, initMain,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Initial Premium API calls
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastSyncTime && isMasterTab && isCurrentUserPremium) {
|
||||||
|
loadDefaultStatusIcons();
|
||||||
|
loadRecentEmojiStatuses();
|
||||||
|
}
|
||||||
|
}, [isCurrentUserPremium, isMasterTab, lastSyncTime, loadDefaultStatusIcons, loadRecentEmojiStatuses]);
|
||||||
|
|
||||||
// Language-based API calls
|
// Language-based API calls
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (lastSyncTime && isMasterTab) {
|
if (lastSyncTime && isMasterTab) {
|
||||||
@ -502,6 +520,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||||
<DeleteFolderDialog deleteFolderDialogId={deleteFolderDialogId} />
|
<DeleteFolderDialog deleteFolderDialogId={deleteFolderDialogId} />
|
||||||
|
<ReactionPicker isOpen={isReactionPickerOpen} shouldLoad={shouldLoadReactionPicker} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -559,6 +578,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
||||||
isMediaViewerOpen: selectIsMediaViewerOpen(global),
|
isMediaViewerOpen: selectIsMediaViewerOpen(global),
|
||||||
isForwardModalOpen: selectIsForwardModalOpen(global),
|
isForwardModalOpen: selectIsForwardModalOpen(global),
|
||||||
|
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||||
hasNotifications: Boolean(notifications.length),
|
hasNotifications: Boolean(notifications.length),
|
||||||
hasDialogs: Boolean(dialogs.length),
|
hasDialogs: Boolean(dialogs.length),
|
||||||
audioMessage,
|
audioMessage,
|
||||||
|
|||||||
@ -5,10 +5,10 @@ import type { ISettings } from '../../../types';
|
|||||||
import type { ApiDocument } from '../../../api/types';
|
import type { ApiDocument } from '../../../api/types';
|
||||||
import { ApiMediaFormat } from '../../../api/types';
|
import { ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
import { IS_COMPACT_MENU } from '../../../util/windowEnvironment';
|
|
||||||
import useMedia from '../../../hooks/useMedia';
|
|
||||||
import { getDocumentMediaHash } from '../../../global/helpers';
|
import { getDocumentMediaHash } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
import useMedia from '../../../hooks/useMedia';
|
||||||
|
|
||||||
import styles from './AttachBotIcon.module.scss';
|
import styles from './AttachBotIcon.module.scss';
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ const COLOR_REPLACE_PATTERN = /#fff/gi;
|
|||||||
const AttachBotIcon: FC<OwnProps> = ({
|
const AttachBotIcon: FC<OwnProps> = ({
|
||||||
icon, theme,
|
icon, theme,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { isTouchScreen } = useAppLayout();
|
||||||
const mediaData = useMedia(getDocumentMediaHash(icon), false, ApiMediaFormat.Text);
|
const mediaData = useMedia(getDocumentMediaHash(icon), false, ApiMediaFormat.Text);
|
||||||
|
|
||||||
const iconSvg = useMemo(() => {
|
const iconSvg = useMemo(() => {
|
||||||
@ -42,8 +43,8 @@ const AttachBotIcon: FC<OwnProps> = ({
|
|||||||
}, [mediaData, theme]);
|
}, [mediaData, theme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<i className={buildClassName(styles.root, IS_COMPACT_MENU && styles.compact)}>
|
<i className={buildClassName(styles.root, !isTouchScreen && styles.compact)}>
|
||||||
<img src={iconSvg} alt="" className={buildClassName(styles.image, IS_COMPACT_MENU && styles.compact)} />
|
<img src={iconSvg} alt="" className={buildClassName(styles.image, !isTouchScreen && styles.compact)} />
|
||||||
</i>
|
</i>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
|||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import EmbeddedMessage from '../../common/EmbeddedMessage';
|
import EmbeddedMessage from '../../common/EmbeddedMessage';
|
||||||
@ -141,7 +141,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
} = useContextMenuPosition(
|
} = useMenuPosition(
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
getTriggerElement,
|
getTriggerElement,
|
||||||
getRootElement,
|
getRootElement,
|
||||||
|
|||||||
@ -107,7 +107,3 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.CustomEmojiPicker {
|
|
||||||
--emoji-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, {
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
|
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
|
||||||
import type { StickerSetOrRecent } from '../../../types';
|
import type { StickerSetOrReactionsSetOrRecent } from '../../../types';
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -33,7 +33,7 @@ import Avatar from '../../common/Avatar';
|
|||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import StickerButton from '../../common/StickerButton';
|
import StickerButton from '../../common/StickerButton';
|
||||||
import StickerSet from './StickerSet';
|
import StickerSet from '../../common/StickerSet';
|
||||||
import StickerSetCover from './StickerSetCover';
|
import StickerSetCover from './StickerSetCover';
|
||||||
import PremiumIcon from '../../common/PremiumIcon';
|
import PremiumIcon from '../../common/PremiumIcon';
|
||||||
|
|
||||||
@ -263,7 +263,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
|||||||
removeRecentSticker({ sticker });
|
removeRecentSticker({ sticker });
|
||||||
}, [removeRecentSticker]);
|
}, [removeRecentSticker]);
|
||||||
|
|
||||||
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
|
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
||||||
const firstSticker = stickerSet.stickers?.[0];
|
const firstSticker = stickerSet.stickers?.[0];
|
||||||
const buttonClassName = buildClassName(
|
const buttonClassName = buildClassName(
|
||||||
'symbol-set-button sticker-set-button',
|
'symbol-set-button sticker-set-button',
|
||||||
|
|||||||
@ -236,10 +236,19 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
border-radius: 50%;
|
||||||
|
padding: 0.25rem;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
|
||||||
&-container {
|
&:active,
|
||||||
text-align: initial;
|
&:focus {
|
||||||
|
background-color: var(--color-interactive-element-hover);
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-interactive-element-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-button {
|
&-button {
|
||||||
@ -253,29 +262,30 @@
|
|||||||
@include while-transition() {
|
@include while-transition() {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.symbol-set-container {
|
.symbol-set-container {
|
||||||
display: grid;
|
display: grid !important;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4rem));
|
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4rem));
|
||||||
grid-gap: 0.625rem;
|
grid-gap: 0.625rem;
|
||||||
padding: 0.3125rem;
|
padding: 0.3125rem;
|
||||||
|
text-align: initial;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
grid-gap: 0.5rem;
|
grid-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.shown) {
|
&:not(.shown) {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.closing {
|
&.closing {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .EmojiButton,
|
> .EmojiButton,
|
||||||
> .StickerButton {
|
> .StickerButton {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import Button from '../../ui/Button';
|
|||||||
import Menu from '../../ui/Menu';
|
import Menu from '../../ui/Menu';
|
||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
import EmojiPicker from './EmojiPicker';
|
import EmojiPicker from './EmojiPicker';
|
||||||
import CustomEmojiPicker from './CustomEmojiPicker';
|
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||||
import StickerPicker from './StickerPicker';
|
import StickerPicker from './StickerPicker';
|
||||||
import GifPicker from './GifPicker';
|
import GifPicker from './GifPicker';
|
||||||
import SymbolMenuFooter, { SYMBOL_MENU_TAB_TITLES, SymbolMenuTabs } from './SymbolMenuFooter';
|
import SymbolMenuFooter, { SYMBOL_MENU_TAB_TITLES, SymbolMenuTabs } from './SymbolMenuFooter';
|
||||||
@ -100,7 +100,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
transformOriginY,
|
transformOriginY,
|
||||||
style,
|
style,
|
||||||
}) => {
|
}) => {
|
||||||
const { loadPremiumSetStickers, loadFeaturedEmojiStickers } = getActions();
|
const { loadPremiumSetStickers } = getActions();
|
||||||
const [activeTab, setActiveTab] = useState<number>(0);
|
const [activeTab, setActiveTab] = useState<number>(0);
|
||||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
|
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
|
||||||
@ -124,12 +124,10 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
|||||||
}, [canSendPlainText]);
|
}, [canSendPlainText]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!lastSyncTime) return;
|
if (lastSyncTime && isCurrentUserPremium) {
|
||||||
if (isCurrentUserPremium) {
|
|
||||||
loadPremiumSetStickers();
|
loadPremiumSetStickers();
|
||||||
}
|
}
|
||||||
loadFeaturedEmojiStickers();
|
}, [isCurrentUserPremium, lastSyncTime, loadPremiumSetStickers]);
|
||||||
}, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]);
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!isMobile || isAttachmentModal) {
|
if (!isMobile || isAttachmentModal) {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import type { ApiVideo, ApiSticker } from '../../../api/types';
|
|||||||
import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_MODAL_CSS_SELECTOR } from '../../../config';
|
import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_MODAL_CSS_SELECTOR } from '../../../config';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||||
|
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
@ -146,7 +146,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
} = useContextMenuPosition(
|
} = useMenuPosition(
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
getTriggerElement,
|
getTriggerElement,
|
||||||
getRootElement,
|
getRootElement,
|
||||||
|
|||||||
@ -61,13 +61,14 @@ export type OwnProps = {
|
|||||||
messageListType: MessageListType;
|
messageListType: MessageListType;
|
||||||
noReplies?: boolean;
|
noReplies?: boolean;
|
||||||
detectedLanguage?: string;
|
detectedLanguage?: string;
|
||||||
onClose: () => void;
|
|
||||||
onCloseAnimationEnd: () => void;
|
|
||||||
repliesThreadInfo?: ApiThreadInfo;
|
repliesThreadInfo?: ApiThreadInfo;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
onCloseAnimationEnd: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
availableReactions?: ApiAvailableReaction[];
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
topReactions?: ApiReaction[];
|
||||||
customEmojiSetsInfo?: ApiStickerSetInfo[];
|
customEmojiSetsInfo?: ApiStickerSetInfo[];
|
||||||
customEmojiSets?: ApiStickerSet[];
|
customEmojiSets?: ApiStickerSet[];
|
||||||
noOptions?: boolean;
|
noOptions?: boolean;
|
||||||
@ -106,8 +107,11 @@ type StateProps = {
|
|||||||
threadId?: number;
|
threadId?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const REACTION_PICKER_APPEARANCE_DURATION_MS = 250;
|
||||||
|
|
||||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||||
availableReactions,
|
availableReactions,
|
||||||
|
topReactions,
|
||||||
isOpen,
|
isOpen,
|
||||||
messageListType,
|
messageListType,
|
||||||
chatUsername,
|
chatUsername,
|
||||||
@ -182,11 +186,13 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
requestMessageTranslation,
|
requestMessageTranslation,
|
||||||
showOriginalMessage,
|
showOriginalMessage,
|
||||||
openMessageLanguageModal,
|
openMessageLanguageModal,
|
||||||
|
openReactionPicker,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
|
const [noAnimationOnClose, setNoAnimationOnClose] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||||
@ -255,7 +261,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
setIsReportModalOpen(true);
|
setIsReportModalOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const closeMenu = useCallback(() => {
|
const closeMenu = useCallback((noCloseAnimation = false) => {
|
||||||
|
setNoAnimationOnClose(noCloseAnimation);
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
@ -409,11 +416,18 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const handleToggleReaction = useCallback((reaction: ApiReaction) => {
|
const handleToggleReaction = useCallback((reaction: ApiReaction) => {
|
||||||
toggleReaction({
|
toggleReaction({
|
||||||
chatId: message.chatId, messageId: message.id, reaction,
|
chatId: message.chatId, messageId: message.id, reaction, shouldAddToRecent: true,
|
||||||
});
|
});
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}, [closeMenu, message, toggleReaction]);
|
}, [closeMenu, message, toggleReaction]);
|
||||||
|
|
||||||
|
const handleReactionPickerOpen = useCallback((position: IAnchorPosition) => {
|
||||||
|
openReactionPicker({ chatId: message.chatId, messageId: message.id, position });
|
||||||
|
setTimeout(() => {
|
||||||
|
closeMenu(true);
|
||||||
|
}, REACTION_PICKER_APPEARANCE_DURATION_MS);
|
||||||
|
}, [closeMenu, message.chatId, message.id]);
|
||||||
|
|
||||||
const handleTranslate = useCallback(() => {
|
const handleTranslate = useCallback(() => {
|
||||||
requestMessageTranslation({
|
requestMessageTranslation({
|
||||||
chatId: message.chatId,
|
chatId: message.chatId,
|
||||||
@ -453,6 +467,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
<div className={buildClassName('ContextMenuContainer', transitionClassNames)}>
|
<div className={buildClassName('ContextMenuContainer', transitionClassNames)}>
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
availableReactions={availableReactions}
|
availableReactions={availableReactions}
|
||||||
|
topReactions={topReactions}
|
||||||
message={message}
|
message={message}
|
||||||
isPrivate={isPrivate}
|
isPrivate={isPrivate}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
@ -488,6 +503,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canSelectLanguage={canSelectLanguage}
|
canSelectLanguage={canSelectLanguage}
|
||||||
hasCustomEmoji={hasCustomEmoji}
|
hasCustomEmoji={hasCustomEmoji}
|
||||||
customEmojiSets={customEmojiSets}
|
customEmojiSets={customEmojiSets}
|
||||||
|
noTransition={noAnimationOnClose}
|
||||||
isDownloading={isDownloading}
|
isDownloading={isDownloading}
|
||||||
seenByRecentUsers={seenByRecentUsers}
|
seenByRecentUsers={seenByRecentUsers}
|
||||||
noReplies={noReplies}
|
noReplies={noReplies}
|
||||||
@ -515,6 +531,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onShowSeenBy={handleOpenSeenByModal}
|
onShowSeenBy={handleOpenSeenByModal}
|
||||||
onToggleReaction={handleToggleReaction}
|
onToggleReaction={handleToggleReaction}
|
||||||
onShowReactors={handleOpenReactorListModal}
|
onShowReactors={handleOpenReactorListModal}
|
||||||
|
onReactionPickerOpen={handleReactionPickerOpen}
|
||||||
onTranslate={handleTranslate}
|
onTranslate={handleTranslate}
|
||||||
onShowOriginal={handleShowOriginal}
|
onShowOriginal={handleShowOriginal}
|
||||||
onSelectLanguage={handleSelectLanguage}
|
onSelectLanguage={handleSelectLanguage}
|
||||||
@ -617,6 +634,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
availableReactions: global.availableReactions,
|
availableReactions: global.availableReactions,
|
||||||
|
topReactions: global.topReactions,
|
||||||
noOptions,
|
noOptions,
|
||||||
canSendNow: isScheduled,
|
canSendNow: isScheduled,
|
||||||
canReschedule: isScheduled,
|
canReschedule: isScheduled,
|
||||||
|
|||||||
@ -21,9 +21,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.with-reactions .bubble {
|
&.with-reactions .bubble {
|
||||||
margin-top: 3.5rem;
|
background: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 3.5rem 0 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-reactions .scrollable-content {
|
||||||
|
background: var(--color-background-compact-menu);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
|
||||||
|
border-radius: var(--border-radius-default);
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.backdrop {
|
.backdrop {
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import type {
|
|||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { IAnchorPosition } from '../../../types';
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
import { getMessageCopyOptions } from './helpers/copyOptions';
|
import { getMessageCopyOptions } from './helpers/copyOptions';
|
||||||
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
|
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
|
||||||
import { getUserFullName } from '../../../global/helpers';
|
import { getUserFullName } from '../../../global/helpers';
|
||||||
@ -23,7 +24,7 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ import './MessageContextMenu.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
availableReactions?: ApiAvailableReaction[];
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
topReactions?: ApiReaction[];
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
anchor: IAnchorPosition;
|
anchor: IAnchorPosition;
|
||||||
message: ApiMessage | ApiSponsoredMessage;
|
message: ApiMessage | ApiSponsoredMessage;
|
||||||
@ -76,44 +78,48 @@ type OwnProps = {
|
|||||||
noReplies?: boolean;
|
noReplies?: boolean;
|
||||||
hasCustomEmoji?: boolean;
|
hasCustomEmoji?: boolean;
|
||||||
customEmojiSets?: ApiStickerSet[];
|
customEmojiSets?: ApiStickerSet[];
|
||||||
onReply?: () => void;
|
noTransition?: boolean;
|
||||||
|
onReply?: NoneToVoidFunction;
|
||||||
onOpenThread?: VoidFunction;
|
onOpenThread?: VoidFunction;
|
||||||
onEdit?: () => void;
|
onEdit?: NoneToVoidFunction;
|
||||||
onPin?: () => void;
|
onPin?: NoneToVoidFunction;
|
||||||
onUnpin?: () => void;
|
onUnpin?: NoneToVoidFunction;
|
||||||
onForward?: () => void;
|
onForward?: NoneToVoidFunction;
|
||||||
onDelete?: () => void;
|
onDelete?: NoneToVoidFunction;
|
||||||
onReport?: () => void;
|
onReport?: NoneToVoidFunction;
|
||||||
onFaveSticker?: () => void;
|
onFaveSticker?: NoneToVoidFunction;
|
||||||
onUnfaveSticker?: () => void;
|
onUnfaveSticker?: NoneToVoidFunction;
|
||||||
onSelect?: () => void;
|
onSelect?: NoneToVoidFunction;
|
||||||
onSend?: () => void;
|
onSend?: NoneToVoidFunction;
|
||||||
onReschedule?: () => void;
|
onReschedule?: NoneToVoidFunction;
|
||||||
onClose: () => void;
|
onClose: NoneToVoidFunction;
|
||||||
onCloseAnimationEnd?: () => void;
|
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||||
onCopyLink?: () => void;
|
onCopyLink?: NoneToVoidFunction;
|
||||||
onCopyMessages?: (messageIds: number[]) => void;
|
onCopyMessages?: (messageIds: number[]) => void;
|
||||||
onCopyNumber?: () => void;
|
onCopyNumber?: NoneToVoidFunction;
|
||||||
onDownload?: () => void;
|
onDownload?: NoneToVoidFunction;
|
||||||
onSaveGif?: () => void;
|
onSaveGif?: NoneToVoidFunction;
|
||||||
onCancelVote?: () => void;
|
onCancelVote?: NoneToVoidFunction;
|
||||||
onClosePoll?: () => void;
|
onClosePoll?: NoneToVoidFunction;
|
||||||
onShowSeenBy?: () => void;
|
onShowSeenBy?: NoneToVoidFunction;
|
||||||
onShowReactors?: () => void;
|
onShowReactors?: NoneToVoidFunction;
|
||||||
onAboutAds?: () => void;
|
onAboutAds?: NoneToVoidFunction;
|
||||||
onSponsoredHide?: () => void;
|
onSponsoredHide?: NoneToVoidFunction;
|
||||||
onTranslate?: () => void;
|
onTranslate?: NoneToVoidFunction;
|
||||||
onShowOriginal?: () => void;
|
onShowOriginal?: NoneToVoidFunction;
|
||||||
onSelectLanguage?: () => void;
|
onSelectLanguage?: NoneToVoidFunction;
|
||||||
onToggleReaction?: (reaction: ApiReaction) => void;
|
onToggleReaction?: (reaction: ApiReaction) => void;
|
||||||
|
onReactionPickerOpen?: (position: IAnchorPosition) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SCROLLBAR_WIDTH = 10;
|
const SCROLLBAR_WIDTH = 10;
|
||||||
const REACTION_BUBBLE_EXTRA_WIDTH = 32;
|
const REACTION_BUBBLE_EXTRA_WIDTH = 32;
|
||||||
|
const REACTION_SELECTOR_WIDTH_REM = 19.25;
|
||||||
const ANIMATION_DURATION = 200;
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
const MessageContextMenu: FC<OwnProps> = ({
|
const MessageContextMenu: FC<OwnProps> = ({
|
||||||
availableReactions,
|
availableReactions,
|
||||||
|
topReactions,
|
||||||
isOpen,
|
isOpen,
|
||||||
message,
|
message,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
@ -152,6 +158,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
seenByRecentUsers,
|
seenByRecentUsers,
|
||||||
hasCustomEmoji,
|
hasCustomEmoji,
|
||||||
customEmojiSets,
|
customEmojiSets,
|
||||||
|
noTransition,
|
||||||
onReply,
|
onReply,
|
||||||
onOpenThread,
|
onOpenThread,
|
||||||
onEdit,
|
onEdit,
|
||||||
@ -179,6 +186,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
onCopyMessages,
|
onCopyMessages,
|
||||||
onAboutAds,
|
onAboutAds,
|
||||||
onSponsoredHide,
|
onSponsoredHide,
|
||||||
|
onReactionPickerOpen,
|
||||||
onTranslate,
|
onTranslate,
|
||||||
onShowOriginal,
|
onShowOriginal,
|
||||||
onSelectLanguage,
|
onSelectLanguage,
|
||||||
@ -255,6 +263,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
|
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
|
||||||
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
|
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
|
||||||
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
|
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
|
||||||
|
shouldAvoidNegativePosition: true,
|
||||||
|
menuElMinWidth: withReactions && isMobile ? REACTION_SELECTOR_WIDTH_REM * REM : undefined,
|
||||||
};
|
};
|
||||||
}, [isMobile, withReactions]);
|
}, [isMobile, withReactions]);
|
||||||
|
|
||||||
@ -271,7 +281,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style, menuStyle, withScroll,
|
positionX, positionY, transformOriginX, transformOriginY, style, menuStyle, withScroll,
|
||||||
} = useContextMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
} = useMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
|
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
|
||||||
@ -292,20 +302,23 @@ const MessageContextMenu: FC<OwnProps> = ({
|
|||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
|
'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
|
||||||
)}
|
)}
|
||||||
|
shouldSkipTransition={noTransition}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
{withReactions && (
|
{withReactions && (
|
||||||
<ReactionSelector
|
<ReactionSelector
|
||||||
enabledReactions={enabledReactions}
|
enabledReactions={enabledReactions}
|
||||||
|
topReactions={topReactions}
|
||||||
|
allAvailableReactions={availableReactions}
|
||||||
currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined}
|
currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined}
|
||||||
maxUniqueReactions={maxUniqueReactions}
|
maxUniqueReactions={maxUniqueReactions}
|
||||||
onToggleReaction={onToggleReaction!}
|
onToggleReaction={onToggleReaction!}
|
||||||
isPrivate={isPrivate}
|
isPrivate={isPrivate}
|
||||||
availableReactions={availableReactions}
|
|
||||||
isReady={isReady}
|
isReady={isReady}
|
||||||
canBuyPremium={canBuyPremium}
|
canBuyPremium={canBuyPremium}
|
||||||
isCurrentUserPremium={isCurrentUserPremium}
|
isCurrentUserPremium={isCurrentUserPremium}
|
||||||
|
onShowMore={onReactionPickerOpen!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
21
src/components/middle/message/ReactionPicker.async.tsx
Normal file
21
src/components/middle/message/ReactionPicker.async.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { OwnProps } from './ReactionPicker';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
interface LocalOwnProps {
|
||||||
|
shouldLoad?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReactionPickerAsync: FC<OwnProps & LocalOwnProps> = (props) => {
|
||||||
|
const { isOpen, shouldLoad } = props;
|
||||||
|
const ReactionPicker = useModuleLoader(Bundles.Extra, 'ReactionPicker', !isOpen && !shouldLoad);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return ReactionPicker ? <ReactionPicker {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ReactionPickerAsync);
|
||||||
49
src/components/middle/message/ReactionPicker.module.scss
Normal file
49
src/components/middle/message/ReactionPicker.module.scss
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
.menu {
|
||||||
|
position: absolute;
|
||||||
|
z-index: var(--z-reaction-picker);
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
max-width: 100%;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menuContent {
|
||||||
|
width: 26.25rem;
|
||||||
|
height: 26.25rem;
|
||||||
|
padding: 0 !important;
|
||||||
|
transform-origin: 50% 3.5rem !important;
|
||||||
|
|
||||||
|
&:global(.bubble) {
|
||||||
|
transform: scale(0.7) !important;
|
||||||
|
transition: opacity 140ms cubic-bezier(0.2, 0, 0.2, 1), transform 140ms cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.bubble.open) {
|
||||||
|
transform: scale(1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
max-width: min(calc(100% - 1rem), 26.25rem);
|
||||||
|
left: 50% !important;
|
||||||
|
right: auto !important;
|
||||||
|
|
||||||
|
&:global(.bubble) {
|
||||||
|
transform: scale(0.5) translateX(-50%) !important;
|
||||||
|
transform-origin: 0 3.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.bubble.open) {
|
||||||
|
transform: scale(1) translateX(-50%) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.onlyReactions {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
191
src/components/middle/message/ReactionPicker.tsx
Normal file
191
src/components/middle/message/ReactionPicker.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useMemo, useRef,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type {
|
||||||
|
ApiMessage, ApiReaction, ApiSticker, ApiReactionCustomEmoji,
|
||||||
|
} from '../../../api/types';
|
||||||
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { isUserId } from '../../../global/helpers';
|
||||||
|
import { selectChat, selectChatMessage, selectTabState } from '../../../global/selectors';
|
||||||
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
|
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||||
|
|
||||||
|
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||||
|
import ReactionPickerLimited from './ReactionPickerLimited';
|
||||||
|
import Menu from '../../ui/Menu';
|
||||||
|
|
||||||
|
import styles from './ReactionPicker.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
withCustomReactions?: boolean;
|
||||||
|
message?: ApiMessage;
|
||||||
|
position?: IAnchorPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FULL_PICKER_SHIFT_DELTA = { x: -30, y: -66 };
|
||||||
|
const LIMITED_PICKER_SHIFT_DELTA = { x: -25, y: -10 };
|
||||||
|
|
||||||
|
const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||||
|
isOpen,
|
||||||
|
message,
|
||||||
|
position,
|
||||||
|
withCustomReactions,
|
||||||
|
}) => {
|
||||||
|
const { toggleReaction, closeReactionPicker } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const scrollHeaderRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const limitedScrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const renderedMessageId = useCurrentOrPrev(message?.id, true);
|
||||||
|
const renderedChatId = useCurrentOrPrev(message?.chatId, true);
|
||||||
|
const storedPosition = useCurrentOrPrev(position, true);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const renderingPosition = useMemo((): IAnchorPosition | undefined => {
|
||||||
|
if (!storedPosition) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: storedPosition.x + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.x : LIMITED_PICKER_SHIFT_DELTA.x),
|
||||||
|
y: storedPosition.y + (withCustomReactions ? FULL_PICKER_SHIFT_DELTA.y : LIMITED_PICKER_SHIFT_DELTA.y),
|
||||||
|
};
|
||||||
|
}, [storedPosition, withCustomReactions]);
|
||||||
|
|
||||||
|
const getMenuElement = useCallback(() => menuRef.current, []);
|
||||||
|
const getLayout = useCallback(() => ({ withPortal: true, isDense: true }), []);
|
||||||
|
const {
|
||||||
|
positionX, positionY, transformOriginX, transformOriginY, style,
|
||||||
|
} = useMenuPosition(renderingPosition, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||||
|
|
||||||
|
const handleResetScrollPosition = useCallback(() => {
|
||||||
|
if (scrollContainerRef.current) {
|
||||||
|
scrollContainerRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
if (scrollHeaderRef.current) {
|
||||||
|
scrollHeaderRef.current.scrollLeft = 0;
|
||||||
|
}
|
||||||
|
if (limitedScrollContainerRef.current) {
|
||||||
|
limitedScrollContainerRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleToggleCustomReaction = useCallback((sticker: ApiSticker) => {
|
||||||
|
if (!renderedChatId || !renderedMessageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reaction = sticker.isCustomEmoji
|
||||||
|
? { documentId: sticker.id } as ApiReactionCustomEmoji
|
||||||
|
: { emoticon: sticker.emoji } as ApiReaction;
|
||||||
|
|
||||||
|
toggleReaction({
|
||||||
|
chatId: renderedChatId, messageId: renderedMessageId, reaction, shouldAddToRecent: true,
|
||||||
|
});
|
||||||
|
closeReactionPicker();
|
||||||
|
}, [renderedChatId, renderedMessageId]);
|
||||||
|
|
||||||
|
const handleToggleReaction = useCallback((reaction: ApiReaction) => {
|
||||||
|
if (!renderedChatId || !renderedMessageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleReaction({
|
||||||
|
chatId: renderedChatId, messageId: renderedMessageId, reaction, shouldAddToRecent: true,
|
||||||
|
});
|
||||||
|
closeReactionPicker();
|
||||||
|
}, [renderedChatId, renderedMessageId]);
|
||||||
|
|
||||||
|
const selectedReactionIds = useMemo(() => {
|
||||||
|
return (message?.reactions?.results || []).reduce<string[]>((acc, { chosenOrder, reaction }) => {
|
||||||
|
if (chosenOrder !== undefined) {
|
||||||
|
acc.push('emoticon' in reaction ? reaction.emoticon : reaction.documentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}, [message?.reactions?.results]);
|
||||||
|
|
||||||
|
const bubbleFullClassName = buildClassName(
|
||||||
|
styles.menuContent,
|
||||||
|
!withCustomReactions && styles.onlyReactions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu
|
||||||
|
isOpen={isOpen}
|
||||||
|
ref={menuRef}
|
||||||
|
className={styles.menu}
|
||||||
|
bubbleClassName={bubbleFullClassName}
|
||||||
|
withPortal
|
||||||
|
noCompact
|
||||||
|
positionX={positionX}
|
||||||
|
positionY={positionY}
|
||||||
|
transformOriginX={transformOriginX}
|
||||||
|
transformOriginY={transformOriginY}
|
||||||
|
style={style}
|
||||||
|
onClose={closeReactionPicker}
|
||||||
|
onCloseAnimationEnd={handleResetScrollPosition}
|
||||||
|
>
|
||||||
|
<CustomEmojiPicker
|
||||||
|
idPrefix="message-emoji-set-"
|
||||||
|
loadAndPlay={isOpen}
|
||||||
|
isReactionPicker
|
||||||
|
className={!withCustomReactions ? styles.hidden : undefined}
|
||||||
|
scrollHeaderRef={scrollHeaderRef}
|
||||||
|
scrollContainerRef={scrollContainerRef}
|
||||||
|
onCustomEmojiSelect={handleToggleCustomReaction}
|
||||||
|
onReactionSelect={handleToggleReaction}
|
||||||
|
selectedReactionIds={selectedReactionIds}
|
||||||
|
/>
|
||||||
|
{!withCustomReactions && Boolean(renderedChatId) && (
|
||||||
|
<ReactionPickerLimited
|
||||||
|
chatId={renderedChatId}
|
||||||
|
loadAndPlay={isOpen}
|
||||||
|
scrollContainerRef={limitedScrollContainerRef}
|
||||||
|
onReactionSelect={handleToggleReaction}
|
||||||
|
selectedReactionIds={selectedReactionIds}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||||
|
const state = selectTabState(global);
|
||||||
|
const { chatId, messageId, position } = state.reactionPicker || {};
|
||||||
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
|
const message = chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
|
||||||
|
const isPrivateChat = chatId ? isUserId(chatId) : false;
|
||||||
|
const areSomeReactionsAllowed = chat?.fullInfo?.enabledReactions?.type === 'some';
|
||||||
|
const areCustomReactionsAllowed = chat?.fullInfo?.enabledReactions?.type === 'all'
|
||||||
|
&& chat?.fullInfo?.enabledReactions?.areCustomAllowed;
|
||||||
|
|
||||||
|
return {
|
||||||
|
message,
|
||||||
|
position,
|
||||||
|
withCustomReactions: chat?.isForbidden || areSomeReactionsAllowed
|
||||||
|
? false
|
||||||
|
: areCustomReactionsAllowed || isPrivateChat,
|
||||||
|
};
|
||||||
|
})(ReactionPicker));
|
||||||
|
|
||||||
|
function getTriggerElement(): HTMLElement | null {
|
||||||
|
return document.querySelector('body');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRootElement() {
|
||||||
|
return document.querySelector('body');
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
.root {
|
||||||
|
--emoji-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
height: auto;
|
||||||
|
max-height: 18rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
}
|
||||||
135
src/components/middle/message/ReactionPickerLimited.tsx
Normal file
135
src/components/middle/message/ReactionPickerLimited.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import type { RefObject } from 'react';
|
||||||
|
import React, { memo, useRef, useMemo } from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiReaction, ApiAvailableReaction, ApiChatReactions } from '../../../api/types';
|
||||||
|
|
||||||
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { getReactionUniqueKey, sortReactions } from '../../../global/helpers';
|
||||||
|
import { selectChat } from '../../../global/selectors';
|
||||||
|
|
||||||
|
import useWindowSize from '../../../hooks/useWindowSize';
|
||||||
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
|
||||||
|
import ReactionEmoji from '../../common/ReactionEmoji';
|
||||||
|
|
||||||
|
import styles from './ReactionPickerLimited.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
scrollContainerRef?: RefObject<HTMLDivElement>;
|
||||||
|
chatId: string;
|
||||||
|
loadAndPlay: boolean;
|
||||||
|
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||||
|
selectedReactionIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
enabledReactions?: ApiChatReactions;
|
||||||
|
availableReactions?: ApiAvailableReaction[];
|
||||||
|
topReactions: ApiReaction[];
|
||||||
|
canAnimate?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const REACTION_SIZE = 40;
|
||||||
|
const GRID_GAP_THRESHOLD = 600;
|
||||||
|
const MODAL_PADDING_SIZE_REM = 0.5;
|
||||||
|
const MODAL_MAX_HEIGHT_REM = 18;
|
||||||
|
const MODAL_MAX_WIDTH_REM = 26.25;
|
||||||
|
const GRID_GAP_DESKTOP_REM = 0.625;
|
||||||
|
const GRID_GAP_MOBILE_REM = 0.5;
|
||||||
|
|
||||||
|
const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||||
|
scrollContainerRef,
|
||||||
|
loadAndPlay,
|
||||||
|
enabledReactions,
|
||||||
|
availableReactions,
|
||||||
|
topReactions,
|
||||||
|
selectedReactionIds,
|
||||||
|
onReactionSelect,
|
||||||
|
}) => {
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
let containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
const { isTouchScreen } = useAppLayout();
|
||||||
|
if (scrollContainerRef) {
|
||||||
|
containerRef = scrollContainerRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allAvailableReactions = useMemo(() => {
|
||||||
|
if (!enabledReactions) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledReactions.type === 'all') {
|
||||||
|
return sortReactions((availableReactions || []).map(({ reaction }) => reaction), topReactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortReactions(enabledReactions.allowed, topReactions);
|
||||||
|
}, [availableReactions, enabledReactions, topReactions]);
|
||||||
|
|
||||||
|
const pickerHeight = useMemo(() => {
|
||||||
|
const pickerWidth = Math.min(MODAL_MAX_WIDTH_REM * REM, windowWidth);
|
||||||
|
const gapWidth = (windowWidth > GRID_GAP_THRESHOLD ? GRID_GAP_DESKTOP_REM : GRID_GAP_MOBILE_REM) * REM;
|
||||||
|
const availableWidth = pickerWidth - MODAL_PADDING_SIZE_REM * REM;
|
||||||
|
|
||||||
|
const itemsInRow = Math.floor((availableWidth + gapWidth) / (REACTION_SIZE + gapWidth));
|
||||||
|
const rowsCount = Math.ceil(allAvailableReactions.length / itemsInRow);
|
||||||
|
|
||||||
|
const pickerMaxHeight = rowsCount * REACTION_SIZE + (rowsCount + 1) * gapWidth + MODAL_PADDING_SIZE_REM * REM;
|
||||||
|
|
||||||
|
return Math.min(MODAL_MAX_HEIGHT_REM * REM, pickerMaxHeight);
|
||||||
|
}, [allAvailableReactions.length, windowWidth]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root} style={`height: ${pickerHeight}px`}>
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={buildClassName(styles.wrapper, 'no-selection', isTouchScreen ? 'no-scrollbar' : 'custom-scroll')}
|
||||||
|
>
|
||||||
|
<div className="symbol-set-container shared-canvas-container">
|
||||||
|
<canvas ref={sharedCanvasRef} className="shared-canvas" />
|
||||||
|
<canvas ref={sharedCanvasHqRef} className="shared-canvas" />
|
||||||
|
{allAvailableReactions.map((reaction) => {
|
||||||
|
const reactionId = getReactionUniqueKey(reaction);
|
||||||
|
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ReactionEmoji
|
||||||
|
key={reactionId}
|
||||||
|
reaction={reaction}
|
||||||
|
isSelected={isSelected}
|
||||||
|
loadAndPlay={loadAndPlay}
|
||||||
|
availableReactions={availableReactions}
|
||||||
|
onClick={onReactionSelect!}
|
||||||
|
sharedCanvasRef={sharedCanvasRef}
|
||||||
|
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const { availableReactions, topReactions } = global;
|
||||||
|
const { enabledReactions } = chat?.fullInfo || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabledReactions,
|
||||||
|
availableReactions,
|
||||||
|
topReactions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(ReactionPickerLimited));
|
||||||
@ -1,103 +1,122 @@
|
|||||||
.ReactionSelector {
|
.ReactionSelector {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 3rem;
|
height: 2.5rem;
|
||||||
background: var(--color-background);
|
|
||||||
min-width: 3rem;
|
min-width: 3rem;
|
||||||
max-width: calc(100% + 5rem);
|
max-width: calc(100% + 4rem);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow));
|
right: -2rem;
|
||||||
right: -3rem;
|
top: 0.5rem;
|
||||||
top: -3.5rem;
|
|
||||||
|
&--isRtl {
|
||||||
|
right: auto;
|
||||||
|
left: -3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--withBlur {
|
||||||
|
background: none;
|
||||||
|
filter: none;
|
||||||
|
|
||||||
|
.ReactionSelector__bubble-big,
|
||||||
|
.ReactionSelector__bubble-small,
|
||||||
|
.ReactionSelector__items-wrapper {
|
||||||
|
background: var(--color-background-compact-menu);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ReactionSelector__bubble-big,
|
||||||
|
.ReactionSelector__bubble-small,
|
||||||
|
.ReactionSelector__items-wrapper {
|
||||||
|
filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow));
|
||||||
|
|
||||||
|
body.is-safari & {
|
||||||
|
filter: none;
|
||||||
|
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__bubble-big {
|
&__bubble-big {
|
||||||
border: 0.5rem solid var(--color-background);
|
background: var(--color-background);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
right: 1.5rem;
|
right: 1.125rem;
|
||||||
bottom: -0.5rem;
|
bottom: -0.5rem;
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 0.5rem;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-radius: 0 0 50% 50%;
|
border-radius: 0 0 1rem 1rem;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
|
|
||||||
|
&--isRtl {
|
||||||
|
right: auto;
|
||||||
|
left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__bubble-small {
|
&__bubble-small {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
content: "";
|
content: "";
|
||||||
right: 1.25rem;
|
right: 1.125rem;
|
||||||
bottom: -1.25rem;
|
bottom: -1.25rem;
|
||||||
width: 0.5rem;
|
width: 0.5rem;
|
||||||
height: 0.5rem;
|
height: 0.5rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
|
||||||
|
|
||||||
body.is-safari & {
|
&--isRtl {
|
||||||
filter: none;
|
right: auto;
|
||||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
left: 2.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.is-safari &__bubble-small,
|
@media (max-width: 600px) {
|
||||||
body.is-safari &__bubble-big {
|
display: none;
|
||||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__items-wrapper {
|
&__items-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
|
background: var(--color-background);
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__items {
|
&__items {
|
||||||
padding: 0 1rem;
|
padding: 0 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: auto;
|
|
||||||
overflow: overlay;
|
|
||||||
overflow-y: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 3rem;
|
border-radius: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--compact {
|
&__show-more {
|
||||||
background: var(--color-background-compact-menu-reactions);
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
top: -2.5rem;
|
padding: 0;
|
||||||
}
|
margin-inline-start: 0.25rem;
|
||||||
|
margin-inline-end: -0.125rem;
|
||||||
&--compact &__items {
|
border-radius: 50%;
|
||||||
padding: 0 0.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
&--compact &__bubble-big {
|
|
||||||
border-color: var(--color-background-compact-menu-reactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--compact &__bubble-small {
|
|
||||||
background: var(--color-background-compact-menu-reactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__blocked-button {
|
|
||||||
width: 2rem !important;
|
|
||||||
height: 2rem !important;
|
|
||||||
margin-left: 0.5rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--compact &__blocked-button {
|
|
||||||
width: 1.5rem !important;
|
|
||||||
height: 1.5rem !important;
|
|
||||||
|
|
||||||
i {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useMemo, useRef,
|
memo, useCallback, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type {
|
import type {
|
||||||
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
|
import type { IAnchorPosition } from '../../../types';
|
||||||
|
|
||||||
import { getTouchY } from '../../../util/scrollLock';
|
|
||||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||||
import { IS_COMPACT_MENU } from '../../../util/windowEnvironment';
|
import {
|
||||||
import { isSameReaction, canSendReaction, getReactionUniqueKey } from '../../../global/helpers';
|
isSameReaction, canSendReaction, getReactionUniqueKey, sortReactions,
|
||||||
|
} from '../../../global/helpers';
|
||||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
import './ReactionSelector.scss';
|
import './ReactionSelector.scss';
|
||||||
|
|
||||||
@ -22,39 +24,37 @@ type OwnProps = {
|
|||||||
enabledReactions?: ApiChatReactions;
|
enabledReactions?: ApiChatReactions;
|
||||||
onToggleReaction: (reaction: ApiReaction) => void;
|
onToggleReaction: (reaction: ApiReaction) => void;
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
availableReactions?: ApiAvailableReaction[];
|
topReactions?: ApiReaction[];
|
||||||
|
allAvailableReactions?: ApiAvailableReaction[];
|
||||||
currentReactions?: ApiReactionCount[];
|
currentReactions?: ApiReactionCount[];
|
||||||
maxUniqueReactions?: number;
|
maxUniqueReactions?: number;
|
||||||
isReady?: boolean;
|
isReady?: boolean;
|
||||||
canBuyPremium?: boolean;
|
canBuyPremium?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
|
onShowMore: (position: IAnchorPosition) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const cn = createClassNameBuilder('ReactionSelector');
|
const cn = createClassNameBuilder('ReactionSelector');
|
||||||
|
const REACTIONS_AMOUNT = 6;
|
||||||
|
|
||||||
const ReactionSelector: FC<OwnProps> = ({
|
const ReactionSelector: FC<OwnProps> = ({
|
||||||
availableReactions,
|
allAvailableReactions,
|
||||||
|
topReactions,
|
||||||
enabledReactions,
|
enabledReactions,
|
||||||
currentReactions,
|
currentReactions,
|
||||||
maxUniqueReactions,
|
maxUniqueReactions,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
isReady,
|
isReady,
|
||||||
onToggleReaction,
|
onToggleReaction,
|
||||||
|
onShowMore,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const itemsScrollRef = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useHorizontalScroll(itemsScrollRef);
|
const { isTouchScreen } = useAppLayout();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const handleWheel = (e: React.WheelEvent | React.TouchEvent) => {
|
const availableReactions = useMemo(() => {
|
||||||
const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e);
|
const reactions = allAvailableReactions?.map((availableReaction) => {
|
||||||
|
|
||||||
if (deltaY && e.cancelable) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const reactionsToRender = useMemo(() => {
|
|
||||||
return availableReactions?.map((availableReaction) => {
|
|
||||||
if (availableReaction.isInactive) return undefined;
|
if (availableReaction.isInactive) return undefined;
|
||||||
if (!isPrivate && (!enabledReactions || !canSendReaction(availableReaction.reaction, enabledReactions))) {
|
if (!isPrivate && (!enabledReactions || !canSendReaction(availableReaction.reaction, enabledReactions))) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -64,8 +64,17 @@ const ReactionSelector: FC<OwnProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return availableReaction;
|
return availableReaction;
|
||||||
}) || [];
|
}).filter(Boolean) || [];
|
||||||
}, [availableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions]);
|
|
||||||
|
return sortReactions(reactions, topReactions);
|
||||||
|
}, [allAvailableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions, topReactions]);
|
||||||
|
|
||||||
|
const reactionsToRender = useMemo(() => {
|
||||||
|
return availableReactions.length === REACTIONS_AMOUNT + 1
|
||||||
|
? availableReactions
|
||||||
|
: availableReactions.slice(0, REACTIONS_AMOUNT);
|
||||||
|
}, [availableReactions]);
|
||||||
|
const withMoreButton = reactionsToRender.length < availableReactions.length;
|
||||||
|
|
||||||
const userReactionIndexes = useMemo(() => {
|
const userReactionIndexes = useMemo(() => {
|
||||||
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
const chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
||||||
@ -74,27 +83,40 @@ const ReactionSelector: FC<OwnProps> = ({
|
|||||||
)));
|
)));
|
||||||
}, [currentReactions, reactionsToRender]);
|
}, [currentReactions, reactionsToRender]);
|
||||||
|
|
||||||
|
const handleShowMoreClick = useCallback(() => {
|
||||||
|
const bound = ref.current?.getBoundingClientRect() || { x: 0, y: 0 };
|
||||||
|
onShowMore({
|
||||||
|
x: bound.x,
|
||||||
|
y: bound.y,
|
||||||
|
});
|
||||||
|
}, [onShowMore]);
|
||||||
|
|
||||||
if (!reactionsToRender.length) return undefined;
|
if (!reactionsToRender.length) return undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('&', IS_COMPACT_MENU && 'compact')} onWheelCapture={handleWheel} onTouchMove={handleWheel}>
|
<div className={cn('&', !isTouchScreen && 'withBlur', lang.isRtl && 'isRtl')} ref={ref}>
|
||||||
<div className={cn('bubble-big')} />
|
<div className={cn('bubble-small', lang.isRtl && 'isRtl')} />
|
||||||
<div className={cn('bubble-small')} />
|
|
||||||
<div className={cn('items-wrapper')}>
|
<div className={cn('items-wrapper')}>
|
||||||
<div className={cn('items', ['no-scrollbar'])} ref={itemsScrollRef}>
|
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
||||||
{reactionsToRender.map((reaction, i) => {
|
<div className={cn('items')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
if (!reaction) return undefined;
|
{reactionsToRender.map((reaction, i) => (
|
||||||
return (
|
<ReactionSelectorReaction
|
||||||
<ReactionSelectorReaction
|
key={getReactionUniqueKey(reaction.reaction)}
|
||||||
key={getReactionUniqueKey(reaction.reaction)}
|
isReady={isReady}
|
||||||
previewIndex={i}
|
onToggleReaction={onToggleReaction}
|
||||||
isReady={isReady}
|
reaction={reaction}
|
||||||
onToggleReaction={onToggleReaction}
|
chosen={userReactionIndexes.has(i)}
|
||||||
reaction={reaction}
|
/>
|
||||||
chosen={userReactionIndexes.has(i)}
|
))}
|
||||||
/>
|
{withMoreButton && (
|
||||||
);
|
<Button
|
||||||
})}
|
color="translucent"
|
||||||
|
className={cn('show-more')}
|
||||||
|
onClick={handleShowMoreClick}
|
||||||
|
>
|
||||||
|
<i className="icon-down" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,22 +1,11 @@
|
|||||||
.ReactionSelectorReaction {
|
.ReactionSelectorReaction {
|
||||||
margin-left: 0.5rem;
|
margin-inline-start: 0.25rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
min-height: 2rem;
|
min-height: 2rem;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0;
|
margin-inline-start: 0;
|
||||||
}
|
|
||||||
|
|
||||||
&__static {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-image: url('../../../assets/reaction-thumbs.png');
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: auto 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.AnimatedSticker {
|
.AnimatedSticker {
|
||||||
@ -36,8 +25,8 @@
|
|||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
width: 120%;
|
width: 2.375rem;
|
||||||
height: 120%;
|
height: 2.375rem;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: var(--color-background-compact-menu-hover);
|
background-color: var(--color-background-compact-menu-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import React, { memo } from '../../../lib/teact/teact';
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { ApiAvailableReaction, ApiReaction } from '../../../api/types';
|
import type { ApiAvailableReaction, ApiReaction } from '../../../api/types';
|
||||||
|
|
||||||
import { IS_COMPACT_MENU } from '../../../util/windowEnvironment';
|
|
||||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useMedia from '../../../hooks/useMedia';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
@ -12,11 +11,10 @@ import AnimatedSticker from '../../common/AnimatedSticker';
|
|||||||
|
|
||||||
import './ReactionSelectorReaction.scss';
|
import './ReactionSelectorReaction.scss';
|
||||||
|
|
||||||
const REACTION_SIZE = IS_COMPACT_MENU ? 24 : 32;
|
const REACTION_SIZE = 32;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
reaction: ApiAvailableReaction;
|
reaction: ApiAvailableReaction;
|
||||||
previewIndex: number;
|
|
||||||
isReady?: boolean;
|
isReady?: boolean;
|
||||||
chosen?: boolean;
|
chosen?: boolean;
|
||||||
onToggleReaction: (reaction: ApiReaction) => void;
|
onToggleReaction: (reaction: ApiReaction) => void;
|
||||||
@ -26,18 +24,16 @@ const cn = createClassNameBuilder('ReactionSelectorReaction');
|
|||||||
|
|
||||||
const ReactionSelectorReaction: FC<OwnProps> = ({
|
const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||||
reaction,
|
reaction,
|
||||||
previewIndex,
|
|
||||||
isReady,
|
isReady,
|
||||||
chosen,
|
chosen,
|
||||||
onToggleReaction,
|
onToggleReaction,
|
||||||
}) => {
|
}) => {
|
||||||
|
const mediaAppearData = useMedia(`sticker${reaction.appearAnimation?.id}`, !isReady);
|
||||||
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady);
|
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady);
|
||||||
|
|
||||||
const [isActivated, activate, deactivate] = useFlag();
|
|
||||||
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
||||||
|
|
||||||
const shouldRenderStatic = !isReady || !isAnimationLoaded;
|
const [isFirstPlay, , unmarkIsFirstPlay] = useFlag(true);
|
||||||
const shouldRenderAnimated = Boolean(isReady && mediaData);
|
const [isActivated, activate, deactivate] = useFlag();
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
onToggleReaction(reaction.reaction);
|
onToggleReaction(reaction.reaction);
|
||||||
@ -45,18 +41,23 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('&', IS_COMPACT_MENU && 'compact', chosen && 'chosen')}
|
className={cn('&', chosen && 'chosen')}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseEnter={isReady ? activate : undefined}
|
onMouseEnter={isReady && !isFirstPlay ? activate : undefined}
|
||||||
>
|
>
|
||||||
{shouldRenderStatic && (
|
{!isAnimationLoaded && (
|
||||||
<div
|
<AnimatedSticker
|
||||||
className={cn('static')}
|
key={reaction.appearAnimation?.id}
|
||||||
style={`background-position-x: ${previewIndex * -REACTION_SIZE}px;`}
|
tgsUrl={mediaAppearData}
|
||||||
|
play={isFirstPlay}
|
||||||
|
noLoop
|
||||||
|
size={REACTION_SIZE}
|
||||||
|
onEnded={unmarkIsFirstPlay}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{shouldRenderAnimated && (
|
{!isFirstPlay && (
|
||||||
<AnimatedSticker
|
<AnimatedSticker
|
||||||
|
key={reaction.selectAnimation?.id}
|
||||||
tgsUrl={mediaData}
|
tgsUrl={mediaData}
|
||||||
play={isActivated}
|
play={isActivated}
|
||||||
noLoop
|
noLoop
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import TopicIcon from '../common/TopicIcon';
|
|||||||
import InputText from '../ui/InputText';
|
import InputText from '../ui/InputText';
|
||||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker';
|
import CustomEmojiPicker from '../common/CustomEmojiPicker';
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
|
|
||||||
import styles from './ManageTopic.module.scss';
|
import styles from './ManageTopic.module.scss';
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import InputText from '../ui/InputText';
|
|||||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||||
import Spinner from '../ui/Spinner';
|
import Spinner from '../ui/Spinner';
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker';
|
import CustomEmojiPicker from '../common/CustomEmojiPicker';
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
|
|
||||||
import styles from './ManageTopic.module.scss';
|
import styles from './ManageTopic.module.scss';
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { fastRaf } from '../../util/schedulers';
|
|||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
@ -124,7 +124,7 @@ const ListItem: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||||
} = useContextMenuPosition(
|
} = useMenuPosition(
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
getTriggerElement,
|
getTriggerElement,
|
||||||
getRootElement,
|
getRootElement,
|
||||||
|
|||||||
@ -2,17 +2,18 @@ import type { RefObject } from 'react';
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
|
import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import { IS_BACKDROP_BLUR_SUPPORTED } from '../../util/windowEnvironment';
|
||||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
|
||||||
import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
|
|
||||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import buildStyle from '../../util/buildStyle';
|
import buildStyle from '../../util/buildStyle';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
|
||||||
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur';
|
||||||
import { IS_BACKDROP_BLUR_SUPPORTED, IS_COMPACT_MENU } from '../../util/windowEnvironment';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||||
|
import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
|
||||||
|
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||||
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
|
|
||||||
import Portal from './Portal';
|
import Portal from './Portal';
|
||||||
|
|
||||||
@ -82,6 +83,7 @@ const Menu: FC<OwnProps> = ({
|
|||||||
menuRef = ref;
|
menuRef = ref;
|
||||||
}
|
}
|
||||||
const backdropContainerRef = containerRef || menuRef;
|
const backdropContainerRef = containerRef || menuRef;
|
||||||
|
const { isTouchScreen } = useAppLayout();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
transitionClassNames,
|
transitionClassNames,
|
||||||
@ -135,7 +137,7 @@ const Menu: FC<OwnProps> = ({
|
|||||||
id={id}
|
id={id}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
'Menu no-selection',
|
'Menu no-selection',
|
||||||
!noCompact && IS_COMPACT_MENU && 'compact',
|
!noCompact && !isTouchScreen && 'compact',
|
||||||
!IS_BACKDROP_BLUR_SUPPORTED && 'no-blur',
|
!IS_BACKDROP_BLUR_SUPPORTED && 'no-blur',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, { useCallback } from '../../lib/teact/teact';
|
|||||||
import { IS_TEST } from '../../config';
|
import { IS_TEST } from '../../config';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import { IS_COMPACT_MENU } from '../../util/windowEnvironment';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
|
|
||||||
import './MenuItem.scss';
|
import './MenuItem.scss';
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
const { isTouchScreen } = useAppLayout();
|
||||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
if (disabled || !onClick) {
|
if (disabled || !onClick) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -73,7 +74,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
|||||||
className,
|
className,
|
||||||
disabled && 'disabled',
|
disabled && 'disabled',
|
||||||
destructive && 'destructive',
|
destructive && 'destructive',
|
||||||
IS_COMPACT_MENU && 'compact',
|
!isTouchScreen && 'compact',
|
||||||
withWrap && 'wrap',
|
withWrap && 'wrap',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,14 @@ export const PROFILE_SENSITIVE_AREA = 500;
|
|||||||
export const TOPIC_LIST_SENSITIVE_AREA = 600;
|
export const TOPIC_LIST_SENSITIVE_AREA = 600;
|
||||||
export const COMMON_CHATS_LIMIT = 100;
|
export const COMMON_CHATS_LIMIT = 100;
|
||||||
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
export const GROUP_CALL_PARTICIPANTS_LIMIT = 100;
|
||||||
|
|
||||||
|
// As in Telegram for Android
|
||||||
|
// https://github.com/DrKLO/Telegram/blob/51e9947527/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java#L7799
|
||||||
|
export const TOP_REACTIONS_LIMIT = 100;
|
||||||
|
|
||||||
|
// As in Telegram for Android
|
||||||
|
// https://github.com/DrKLO/Telegram/blob/51e9947527/TMessagesProj/src/main/java/org/telegram/messenger/MediaDataController.java#L7781
|
||||||
|
export const RECENT_REACTIONS_LIMIT = 50;
|
||||||
export const REACTION_LIST_LIMIT = 100;
|
export const REACTION_LIST_LIMIT = 100;
|
||||||
export const REACTION_UNREAD_SLICE = 100;
|
export const REACTION_UNREAD_SLICE = 100;
|
||||||
export const MENTION_UNREAD_SLICE = 100;
|
export const MENTION_UNREAD_SLICE = 100;
|
||||||
@ -172,6 +180,8 @@ export const RECENT_STICKERS_LIMIT = 20;
|
|||||||
export const RECENT_STATUS_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 TOP_SYMBOL_SET_ID = 'top';
|
||||||
|
export const POPULAR_SYMBOL_SET_ID = 'popular';
|
||||||
export const RECENT_SYMBOL_SET_ID = 'recent';
|
export const RECENT_SYMBOL_SET_ID = 'recent';
|
||||||
export const FAVORITE_SYMBOL_SET_ID = 'favorite';
|
export const FAVORITE_SYMBOL_SET_ID = 'favorite';
|
||||||
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import './ui/payments';
|
|||||||
import './ui/calls';
|
import './ui/calls';
|
||||||
import './ui/mediaViewer';
|
import './ui/mediaViewer';
|
||||||
import './ui/passcode';
|
import './ui/passcode';
|
||||||
|
import './ui/reactions';
|
||||||
|
|
||||||
import './api/initial';
|
import './api/initial';
|
||||||
import './api/chats';
|
import './api/chats';
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import * as mediaLoader from '../../../util/mediaLoader';
|
|
||||||
|
import type { ActionReturnType } from '../../types';
|
||||||
import { ApiMediaFormat } from '../../../api/types';
|
import { ApiMediaFormat } from '../../../api/types';
|
||||||
|
|
||||||
|
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
selectChatMessage, selectCurrentChat, selectTabState,
|
selectChatMessage, selectCurrentChat, selectTabState,
|
||||||
@ -13,12 +16,11 @@ import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions
|
|||||||
import {
|
import {
|
||||||
addChatMessagesById, addChats, addUsers, updateChatMessage,
|
addChatMessagesById, addChats, addUsers, updateChatMessage,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
|
||||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
|
||||||
import { isSameReaction, getUserReactions, isMessageLocal } from '../../helpers';
|
|
||||||
import type { ActionReturnType } from '../../types';
|
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
|
import * as mediaLoader from '../../../util/mediaLoader';
|
||||||
|
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
|
import { isSameReaction, getUserReactions, isMessageLocal } from '../../helpers';
|
||||||
|
|
||||||
const INTERACTION_RANDOM_OFFSET = 40;
|
const INTERACTION_RANDOM_OFFSET = 40;
|
||||||
|
|
||||||
@ -38,6 +40,9 @@ addActionHandler('loadAvailableReactions', async (global): Promise<void> => {
|
|||||||
if (availableReaction.centerIcon) {
|
if (availableReaction.centerIcon) {
|
||||||
mediaLoader.fetch(`sticker${availableReaction.centerIcon.id}`, ApiMediaFormat.BlobUrl);
|
mediaLoader.fetch(`sticker${availableReaction.centerIcon.id}`, ApiMediaFormat.BlobUrl);
|
||||||
}
|
}
|
||||||
|
if (availableReaction.appearAnimation) {
|
||||||
|
mediaLoader.fetch(`sticker${availableReaction.appearAnimation.id}`, ApiMediaFormat.BlobUrl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
@ -104,15 +109,20 @@ addActionHandler('sendDefaultReaction', (global, actions, payload): ActionReturn
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('toggleReaction', (global, actions, payload): ActionReturnType => {
|
addActionHandler('toggleReaction', async (global, actions, payload): Promise<void> => {
|
||||||
const { chatId, reaction, tabId = getCurrentTabId() } = payload;
|
const {
|
||||||
|
chatId,
|
||||||
|
reaction,
|
||||||
|
shouldAddToRecent,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
let { messageId } = payload;
|
let { messageId } = payload;
|
||||||
|
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
let message = selectChatMessage(global, chatId, messageId);
|
let message = selectChatMessage(global, chatId, messageId);
|
||||||
|
|
||||||
if (!chat || !message) {
|
if (!chat || !message) {
|
||||||
return undefined;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
|
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
|
||||||
@ -131,14 +141,10 @@ addActionHandler('toggleReaction', (global, actions, payload): ActionReturnType
|
|||||||
? userReactions.filter((userReaction) => !isSameReaction(userReaction, reaction)) : [...userReactions, reaction];
|
? userReactions.filter((userReaction) => !isSameReaction(userReaction, reaction)) : [...userReactions, reaction];
|
||||||
|
|
||||||
const limit = selectMaxUserReactions(global);
|
const limit = selectMaxUserReactions(global);
|
||||||
|
|
||||||
const reactions = newUserReactions.slice(-limit);
|
const reactions = newUserReactions.slice(-limit);
|
||||||
|
|
||||||
void callApi('sendReaction', { chat, messageId, reactions });
|
|
||||||
|
|
||||||
const { animationLevel } = global.settings.byKey;
|
const { animationLevel } = global.settings.byKey;
|
||||||
|
|
||||||
const tabState = selectTabState(global, tabId);
|
const tabState = selectTabState(global, tabId);
|
||||||
|
|
||||||
if (animationLevel === ANIMATION_LEVEL_MAX) {
|
if (animationLevel === ANIMATION_LEVEL_MAX) {
|
||||||
const newActiveReactions = hasReaction ? omit(tabState.activeReactions, [messageId]) : {
|
const newActiveReactions = hasReaction ? omit(tabState.activeReactions, [messageId]) : {
|
||||||
...tabState.activeReactions,
|
...tabState.activeReactions,
|
||||||
@ -155,7 +161,21 @@ addActionHandler('toggleReaction', (global, actions, payload): ActionReturnType
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return addMessageReaction(global, message, reactions);
|
global = addMessageReaction(global, message, reactions);
|
||||||
|
setGlobal(global);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await callApi('sendReaction', {
|
||||||
|
chat,
|
||||||
|
messageId,
|
||||||
|
reactions,
|
||||||
|
shouldAddToRecent,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
global = getGlobal();
|
||||||
|
global = addMessageReaction(global, message, userReactions);
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('stopActiveReaction', (global, actions, payload): ActionReturnType => {
|
addActionHandler('stopActiveReaction', (global, actions, payload): ActionReturnType => {
|
||||||
@ -395,3 +415,45 @@ addActionHandler('readAllReactions', (global, actions, payload): ActionReturnTyp
|
|||||||
unreadReactions: undefined,
|
unreadReactions: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadTopReactions', async (global): Promise<void> => {
|
||||||
|
const result = await callApi('fetchTopReactions', {});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
topReactions: result.reactions,
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('loadRecentReactions', async (global): Promise<void> => {
|
||||||
|
const result = await callApi('fetchRecentReactions', {});
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
recentReactions: result.reactions,
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('clearRecentReactions', async (global): Promise<void> => {
|
||||||
|
const result = await callApi('clearRecentReactions');
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
global = {
|
||||||
|
...global,
|
||||||
|
recentReactions: [],
|
||||||
|
};
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|||||||
@ -38,6 +38,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
actions.loadRecentStickers();
|
actions.loadRecentStickers();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'updateRecentReactions':
|
||||||
|
actions.loadRecentReactions();
|
||||||
|
break;
|
||||||
|
|
||||||
case 'updateRecentEmojiStatuses':
|
case 'updateRecentEmojiStatuses':
|
||||||
actions.loadRecentEmojiStatuses();
|
actions.loadRecentEmojiStatuses();
|
||||||
break;
|
break;
|
||||||
|
|||||||
58
src/global/actions/ui/reactions.ts
Normal file
58
src/global/actions/ui/reactions.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { addActionHandler } from '../../index';
|
||||||
|
|
||||||
|
import type { ActionReturnType } from '../../types';
|
||||||
|
|
||||||
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
|
import { selectTabState } from '../../selectors';
|
||||||
|
|
||||||
|
addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
return updateTabState(global, {
|
||||||
|
reactionPicker: {
|
||||||
|
chatId: id,
|
||||||
|
messageId: undefined,
|
||||||
|
position: undefined,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
reactionPicker: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('openReactionPicker', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
position,
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload!;
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
reactionPicker: {
|
||||||
|
chatId,
|
||||||
|
messageId,
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
const tabState = selectTabState(global, tabId);
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
reactionPicker: {
|
||||||
|
...tabState.reactionPicker,
|
||||||
|
messageId: undefined,
|
||||||
|
position: undefined,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
@ -334,6 +334,8 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
|
|||||||
'topInlineBots',
|
'topInlineBots',
|
||||||
'recentEmojis',
|
'recentEmojis',
|
||||||
'recentCustomEmojis',
|
'recentCustomEmojis',
|
||||||
|
'topReactions',
|
||||||
|
'recentReactions',
|
||||||
'push',
|
'push',
|
||||||
'serviceNotifications',
|
'serviceNotifications',
|
||||||
'attachmentSettings',
|
'attachmentSettings',
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type {
|
|||||||
ApiReaction,
|
ApiReaction,
|
||||||
ApiReactions,
|
ApiReactions,
|
||||||
ApiReactionCount,
|
ApiReactionCount,
|
||||||
|
ApiAvailableReaction,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { GlobalState } from '../types';
|
import type { GlobalState } from '../types';
|
||||||
|
|
||||||
@ -49,6 +50,21 @@ export function canSendReaction(reaction: ApiReaction, chatReactions: ApiChatRea
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortReactions<T extends ApiAvailableReaction | ApiReaction>(
|
||||||
|
reactions: T[],
|
||||||
|
topReactions?: ApiReaction[],
|
||||||
|
): T[] {
|
||||||
|
return reactions.slice().sort((left, right) => {
|
||||||
|
const reactionOne = left ? ('reaction' in left ? left.reaction : left) as ApiReaction : undefined;
|
||||||
|
const reactionTwo = right ? ('reaction' in right ? right.reaction : right) as ApiReaction : undefined;
|
||||||
|
const indexOne = topReactions?.findIndex((reaction) => isSameReaction(reaction, reactionOne)) || 0;
|
||||||
|
const indexTwo = topReactions?.findIndex((reaction) => isSameReaction(reaction, reactionTwo)) || 0;
|
||||||
|
return (
|
||||||
|
(indexOne > -1 ? indexOne : Infinity) - (indexTwo > -1 ? indexTwo : Infinity)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getUserReactions(message: ApiMessage): ApiReaction[] {
|
export function getUserReactions(message: ApiMessage): ApiReaction[] {
|
||||||
return message.reactions?.results?.filter((r): r is Required<ApiReactionCount> => isReactionChosen(r))
|
return message.reactions?.results?.filter((r): r is Required<ApiReactionCount> => isReactionChosen(r))
|
||||||
.sort((a, b) => a.chosenOrder - b.chosenOrder)
|
.sort((a, b) => a.chosenOrder - b.chosenOrder)
|
||||||
|
|||||||
@ -76,6 +76,8 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
|
|
||||||
recentEmojis: ['grinning', 'kissing_heart', 'christmas_tree', 'brain', 'trophy', 'duck', 'cherries'],
|
recentEmojis: ['grinning', 'kissing_heart', 'christmas_tree', 'brain', 'trophy', 'duck', 'cherries'],
|
||||||
recentCustomEmojis: ['5377305978079288312'],
|
recentCustomEmojis: ['5377305978079288312'],
|
||||||
|
topReactions: [],
|
||||||
|
recentReactions: [],
|
||||||
|
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: {},
|
setsById: {},
|
||||||
|
|||||||
@ -74,3 +74,10 @@ export function selectIsForumPanelOpen<T extends GlobalState>(
|
|||||||
tabState.globalSearch.query === undefined || tabState.globalSearch.isClosing
|
tabState.globalSearch.query === undefined || tabState.globalSearch.isClosing
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export function selectIsReactionPickerOpen<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
|
) {
|
||||||
|
const { reactionPicker } = selectTabState(global, tabId);
|
||||||
|
return Boolean(reactionPicker?.position);
|
||||||
|
}
|
||||||
|
|||||||
@ -68,6 +68,7 @@ import type {
|
|||||||
EmojiKeywords,
|
EmojiKeywords,
|
||||||
FocusDirection,
|
FocusDirection,
|
||||||
GlobalSearchContent,
|
GlobalSearchContent,
|
||||||
|
IAnchorPosition,
|
||||||
InlineBotSettings,
|
InlineBotSettings,
|
||||||
ISettings,
|
ISettings,
|
||||||
IThemeSettings,
|
IThemeSettings,
|
||||||
@ -246,6 +247,12 @@ export type TabState = {
|
|||||||
messageId: number;
|
messageId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
reactionPicker?: {
|
||||||
|
chatId?: string;
|
||||||
|
messageId?: number;
|
||||||
|
position?: IAnchorPosition;
|
||||||
|
};
|
||||||
|
|
||||||
inlineBots: {
|
inlineBots: {
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
byUsername: Record<string, false | InlineBotSettings>;
|
byUsername: Record<string, false | InlineBotSettings>;
|
||||||
@ -687,6 +694,8 @@ export type GlobalState = {
|
|||||||
|
|
||||||
recentEmojis: string[];
|
recentEmojis: string[];
|
||||||
recentCustomEmojis: string[];
|
recentCustomEmojis: string[];
|
||||||
|
topReactions: ApiReaction[];
|
||||||
|
recentReactions: ApiReaction[];
|
||||||
|
|
||||||
stickers: {
|
stickers: {
|
||||||
setsById: Record<string, ApiStickerSet>;
|
setsById: Record<string, ApiStickerSet>;
|
||||||
@ -1717,7 +1726,10 @@ export interface ActionPayloads {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Reactions
|
// Reactions
|
||||||
|
loadTopReactions: undefined;
|
||||||
|
loadRecentReactions: undefined;
|
||||||
loadAvailableReactions: undefined;
|
loadAvailableReactions: undefined;
|
||||||
|
clearRecentReactions: undefined;
|
||||||
|
|
||||||
loadMessageReactions: {
|
loadMessageReactions: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -1728,6 +1740,7 @@ export interface ActionPayloads {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
reaction: ApiReaction;
|
reaction: ApiReaction;
|
||||||
|
shouldAddToRecent?: boolean;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
|
||||||
setDefaultReaction: {
|
setDefaultReaction: {
|
||||||
@ -1748,6 +1761,13 @@ export interface ActionPayloads {
|
|||||||
reaction: ApiReaction;
|
reaction: ApiReaction;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
|
||||||
|
openReactionPicker: {
|
||||||
|
chatId: string;
|
||||||
|
messageId: number;
|
||||||
|
position: IAnchorPosition;
|
||||||
|
} & WithTabId;
|
||||||
|
closeReactionPicker: WithTabId | undefined;
|
||||||
|
|
||||||
// Media Viewer & Audio Player
|
// Media Viewer & Audio Player
|
||||||
openMediaViewer: {
|
openMediaViewer: {
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import { createCallbackManager } from '../util/callbacks';
|
|||||||
import { updateSizes } from '../util/windowSize';
|
import { updateSizes } from '../util/windowSize';
|
||||||
import useForceUpdate from './useForceUpdate';
|
import useForceUpdate from './useForceUpdate';
|
||||||
|
|
||||||
type MediaQueryCacheKey = 'mobile' | 'tablet' | 'landscape';
|
type MediaQueryCacheKey = 'mobile' | 'tablet' | 'landscape' | 'touch';
|
||||||
|
|
||||||
const mediaQueryCache = new Map<MediaQueryCacheKey, MediaQueryList>();
|
const mediaQueryCache = new Map<MediaQueryCacheKey, MediaQueryList>();
|
||||||
const callbacks = createCallbackManager();
|
const callbacks = createCallbackManager();
|
||||||
@ -19,6 +19,7 @@ const callbacks = createCallbackManager();
|
|||||||
let isMobile: boolean | undefined;
|
let isMobile: boolean | undefined;
|
||||||
let isTablet: boolean | undefined;
|
let isTablet: boolean | undefined;
|
||||||
let isLandscape: boolean | undefined;
|
let isLandscape: boolean | undefined;
|
||||||
|
let isTouchScreen: boolean | undefined;
|
||||||
|
|
||||||
export function getIsMobile() {
|
export function getIsMobile() {
|
||||||
return isMobile;
|
return isMobile;
|
||||||
@ -32,6 +33,7 @@ function handleMediaQueryChange() {
|
|||||||
isMobile = mediaQueryCache.get('mobile')?.matches || false;
|
isMobile = mediaQueryCache.get('mobile')?.matches || false;
|
||||||
isTablet = !isMobile && (mediaQueryCache.get('tablet')?.matches || false);
|
isTablet = !isMobile && (mediaQueryCache.get('tablet')?.matches || false);
|
||||||
isLandscape = mediaQueryCache.get('landscape')?.matches || false;
|
isLandscape = mediaQueryCache.get('landscape')?.matches || false;
|
||||||
|
isTouchScreen = mediaQueryCache.get('touch')?.matches || false;
|
||||||
updateSizes();
|
updateSizes();
|
||||||
callbacks.runCallbacks();
|
callbacks.runCallbacks();
|
||||||
}
|
}
|
||||||
@ -56,6 +58,10 @@ function initMediaQueryCache() {
|
|||||||
);
|
);
|
||||||
mediaQueryCache.set('landscape', landscapeQuery);
|
mediaQueryCache.set('landscape', landscapeQuery);
|
||||||
landscapeQuery.addEventListener('change', handleMediaQueryChange);
|
landscapeQuery.addEventListener('change', handleMediaQueryChange);
|
||||||
|
|
||||||
|
const isTouchScreenQuery = window.matchMedia('(pointer: coarse)');
|
||||||
|
mediaQueryCache.set('touch', isTouchScreenQuery);
|
||||||
|
isTouchScreenQuery.addEventListener('change', handleMediaQueryChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
initMediaQueryCache();
|
initMediaQueryCache();
|
||||||
@ -71,5 +77,6 @@ export default function useAppLayout() {
|
|||||||
isTablet,
|
isTablet,
|
||||||
isLandscape,
|
isLandscape,
|
||||||
isDesktop: !isMobile && !isTablet,
|
isDesktop: !isMobile && !isTablet,
|
||||||
|
isTouchScreen,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,9 @@ export default function useBoundsInSharedCanvas(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = container.classList.contains('sticker-set-cover') ? container : container.querySelector('img')!;
|
const target = container.classList.contains('sticker-set-cover') || container.classList.contains('sticker-reaction')
|
||||||
|
? container
|
||||||
|
: container.querySelector('img')!;
|
||||||
const targetBounds = target.getBoundingClientRect();
|
const targetBounds = target.getBoundingClientRect();
|
||||||
const canvasBounds = canvas.getBoundingClientRect();
|
const canvasBounds = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,10 @@ interface Layout {
|
|||||||
extraTopPadding?: number;
|
extraTopPadding?: number;
|
||||||
marginSides?: number;
|
marginSides?: number;
|
||||||
extraMarginTop?: number;
|
extraMarginTop?: number;
|
||||||
|
menuElMinWidth?: number;
|
||||||
|
shouldAvoidNegativePosition?: boolean;
|
||||||
withPortal?: boolean;
|
withPortal?: boolean;
|
||||||
|
isDense?: boolean; // Allows you to place the menu as close to the edges of the area as possible
|
||||||
}
|
}
|
||||||
|
|
||||||
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
|
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
|
||||||
@ -15,7 +18,7 @@ const EMPTY_RECT = {
|
|||||||
width: 0, left: 0, height: 0, top: 0,
|
width: 0, left: 0, height: 0, top: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function useContextMenuPosition(
|
export default function useMenuPosition(
|
||||||
anchor: IAnchorPosition | undefined,
|
anchor: IAnchorPosition | undefined,
|
||||||
getTriggerElement: () => HTMLElement | null,
|
getTriggerElement: () => HTMLElement | null,
|
||||||
getRootElement: () => HTMLElement | null,
|
getRootElement: () => HTMLElement | null,
|
||||||
@ -48,21 +51,25 @@ export default function useContextMenuPosition(
|
|||||||
extraTopPadding = 0,
|
extraTopPadding = 0,
|
||||||
marginSides = 0,
|
marginSides = 0,
|
||||||
extraMarginTop = 0,
|
extraMarginTop = 0,
|
||||||
|
menuElMinWidth = 0,
|
||||||
|
shouldAvoidNegativePosition = false,
|
||||||
withPortal = false,
|
withPortal = false,
|
||||||
|
isDense = false,
|
||||||
} = getLayout?.() || {};
|
} = getLayout?.() || {};
|
||||||
|
|
||||||
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
|
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
|
||||||
|
|
||||||
|
const { offsetWidth: menuElWidth, offsetHeight: menuElHeight } = menuEl || { offsetWidth: 0, offsetHeight: 0 };
|
||||||
const menuRect = menuEl ? {
|
const menuRect = menuEl ? {
|
||||||
width: menuEl.offsetWidth,
|
width: Math.max(menuElWidth, menuElMinWidth),
|
||||||
height: menuEl.offsetHeight + marginTop!,
|
height: menuElHeight + marginTop!,
|
||||||
} : EMPTY_RECT;
|
} : EMPTY_RECT;
|
||||||
|
|
||||||
const rootRect = rootEl ? rootEl.getBoundingClientRect() : EMPTY_RECT;
|
const rootRect = rootEl ? rootEl.getBoundingClientRect() : EMPTY_RECT;
|
||||||
|
|
||||||
let horizontalPosition: 'left' | 'right';
|
let horizontalPosition: 'left' | 'right';
|
||||||
let verticalPosition: 'top' | 'bottom';
|
let verticalPosition: 'top' | 'bottom';
|
||||||
if (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left) {
|
if (isDense || (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left)) {
|
||||||
x += 3;
|
x += 3;
|
||||||
horizontalPosition = 'left';
|
horizontalPosition = 'left';
|
||||||
} else if (x - menuRect.width - rootRect.left > 0) {
|
} else if (x - menuRect.width - rootRect.left > 0) {
|
||||||
@ -87,7 +94,7 @@ export default function useContextMenuPosition(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (y + menuRect.height < rootRect.height + rootRect.top) {
|
if (isDense || (y + menuRect.height < rootRect.height + rootRect.top)) {
|
||||||
verticalPosition = 'top';
|
verticalPosition = 'top';
|
||||||
} else {
|
} else {
|
||||||
verticalPosition = 'bottom';
|
verticalPosition = 'bottom';
|
||||||
@ -107,12 +114,23 @@ export default function useContextMenuPosition(
|
|||||||
x - triggerRect.left,
|
x - triggerRect.left,
|
||||||
rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX,
|
rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX,
|
||||||
);
|
);
|
||||||
const left = (horizontalPosition === 'left'
|
let left = (horizontalPosition === 'left'
|
||||||
? (withPortal
|
? (withPortal || shouldAvoidNegativePosition
|
||||||
? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, leftWithPossibleNegative)
|
? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, leftWithPossibleNegative)
|
||||||
: leftWithPossibleNegative)
|
: leftWithPossibleNegative)
|
||||||
: (x - triggerRect.left)) + addedXForPortalPositioning;
|
: (x - triggerRect.left)) + addedXForPortalPositioning;
|
||||||
const top = y - triggerRect.top + addedYForPortalPositioning;
|
let top = y - triggerRect.top + addedYForPortalPositioning;
|
||||||
|
|
||||||
|
if (isDense) {
|
||||||
|
left = Math.min(left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
||||||
|
top = Math.min(top, rootRect.height - menuRect.height - MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid hiding external parts of menus on mobile devices behind the edges of the screen (ReactionSelector for example)
|
||||||
|
const addedXForMenuPositioning = menuElMinWidth ? Math.max(0, (menuElMinWidth - menuElWidth) / 2) : 0;
|
||||||
|
if (left - addedXForMenuPositioning < 0 && shouldAvoidNegativePosition) {
|
||||||
|
left = addedXForMenuPositioning + MENU_POSITION_VISUAL_COMFORT_SPACE_PX;
|
||||||
|
}
|
||||||
|
|
||||||
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
|
const menuMaxHeight = rootRect.height - MENU_POSITION_BOTTOM_MARGIN - (marginTop || 0);
|
||||||
|
|
||||||
@ -1278,6 +1278,9 @@ messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.Transcrib
|
|||||||
messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;
|
messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;
|
||||||
messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;
|
messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;
|
||||||
messages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers;
|
messages.getFeaturedEmojiStickers#ecf6736 hash:long = messages.FeaturedStickers;
|
||||||
|
messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions;
|
||||||
|
messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions;
|
||||||
|
messages.clearRecentReactions#9dfeefb4 = Bool;
|
||||||
messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;
|
messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;
|
||||||
updates.getState#edd4882a = updates.State;
|
updates.getState#edd4882a = updates.State;
|
||||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||||
|
|||||||
@ -264,6 +264,9 @@
|
|||||||
"messages.getFeaturedEmojiStickers",
|
"messages.getFeaturedEmojiStickers",
|
||||||
"messages.readReactions",
|
"messages.readReactions",
|
||||||
"messages.getUnreadReactions",
|
"messages.getUnreadReactions",
|
||||||
|
"messages.getTopReactions",
|
||||||
|
"messages.getRecentReactions",
|
||||||
|
"messages.clearRecentReactions",
|
||||||
"messages.readMentions",
|
"messages.readMentions",
|
||||||
"messages.getUnreadMentions",
|
"messages.getUnreadMentions",
|
||||||
"help.getPremiumPromo",
|
"help.getPremiumPromo",
|
||||||
|
|||||||
@ -219,6 +219,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
|||||||
--z-ui-loader-mask: 2000;
|
--z-ui-loader-mask: 2000;
|
||||||
--z-notification: 1700;
|
--z-notification: 1700;
|
||||||
--z-confetti: 1600;
|
--z-confetti: 1600;
|
||||||
|
--z-reaction-picker: 1200;
|
||||||
--z-right-column: 900;
|
--z-right-column: 900;
|
||||||
--z-header-menu: 990;
|
--z-header-menu: 990;
|
||||||
--z-header-menu-backdrop: 980;
|
--z-header-menu-backdrop: 980;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type {
|
|||||||
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
||||||
ApiChatInviteImporter,
|
ApiChatInviteImporter,
|
||||||
ApiExportedInvite,
|
ApiExportedInvite,
|
||||||
ApiLanguage, ApiMessage, ApiStickerSet,
|
ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
|
|
||||||
export type TextPart = TeactNode;
|
export type TextPart = TeactNode;
|
||||||
@ -223,10 +223,10 @@ export enum SettingsScreens {
|
|||||||
DoNotTranslate,
|
DoNotTranslate,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
||||||
'id' | 'accessHash' | 'title' | 'count' | 'stickers' | 'hasThumbnail' | 'isLottie' | 'isVideos' | 'isEmoji' |
|
'id' | 'accessHash' | 'title' | 'count' | 'stickers' | 'hasThumbnail' | 'isLottie' | 'isVideos' | 'isEmoji' |
|
||||||
'installedDate' | 'isArchived'
|
'installedDate' | 'isArchived'
|
||||||
)>;
|
)> & { reactions?: ApiReaction[] };
|
||||||
|
|
||||||
export enum LeftColumnContent {
|
export enum LeftColumnContent {
|
||||||
ChatList,
|
ChatList,
|
||||||
|
|||||||
@ -109,7 +109,6 @@ if (IS_OPFS_SUPPORTED) {
|
|||||||
export const IS_OFFSET_PATH_SUPPORTED = CSS.supports('offset-rotate: 0deg');
|
export const IS_OFFSET_PATH_SUPPORTED = CSS.supports('offset-rotate: 0deg');
|
||||||
export const IS_BACKDROP_BLUR_SUPPORTED = CSS.supports('backdrop-filter: blur()')
|
export const IS_BACKDROP_BLUR_SUPPORTED = CSS.supports('backdrop-filter: blur()')
|
||||||
|| CSS.supports('-webkit-backdrop-filter: blur()');
|
|| CSS.supports('-webkit-backdrop-filter: blur()');
|
||||||
export const IS_COMPACT_MENU = !IS_TOUCH_ENV;
|
|
||||||
export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
export const IS_INSTALL_PROMPT_SUPPORTED = 'onbeforeinstallprompt' in window;
|
||||||
export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window;
|
export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window;
|
||||||
export const IS_OPEN_IN_NEW_TAB_SUPPORTED = IS_MULTITAB_SUPPORTED && !(IS_PWA && IS_MOBILE);
|
export const IS_OPEN_IN_NEW_TAB_SUPPORTED = IS_MULTITAB_SUPPORTED && !(IS_PWA && IS_MOBILE);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user