Introduce Reaction Picker
This commit is contained in:
parent
c2d7234748
commit
22054c4416
@ -54,7 +54,9 @@ import {
|
||||
} from '../../../config';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { buildStickerFromDocument } from './symbols';
|
||||
import { buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromStripped } from './common';
|
||||
import {
|
||||
buildApiPhoto, buildApiPhotoSize, buildApiThumbnailFromPath, buildApiThumbnailFromStripped,
|
||||
} from './common';
|
||||
import { interpolateArray } from '../../../util/waveform';
|
||||
import { buildPeer } from '../gramjsBuilders';
|
||||
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 {
|
||||
const {
|
||||
selectAnimation, staticIcon, reaction, title,
|
||||
selectAnimation, staticIcon, reaction, title, appearAnimation,
|
||||
inactive, aroundAnimation, centerIcon, effectAnimation, activateAnimation,
|
||||
premium,
|
||||
} = availableReaction;
|
||||
|
||||
return {
|
||||
selectAnimation: buildApiDocument(selectAnimation),
|
||||
appearAnimation: buildApiDocument(appearAnimation),
|
||||
activateAnimation: buildApiDocument(activateAnimation),
|
||||
effectAnimation: buildApiDocument(effectAnimation),
|
||||
staticIcon: buildApiDocument(staticIcon),
|
||||
@ -609,11 +612,17 @@ export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | u
|
||||
id, size, mimeType, date, thumbs, attributes,
|
||||
} = 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 mediaSize: ApiDocument['mediaSize'] | undefined;
|
||||
const photoSize = thumbs && thumbs.find((s: any): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize);
|
||||
if (photoSize) {
|
||||
mediaSize = {
|
||||
width: photoSize.w,
|
||||
|
||||
@ -87,8 +87,8 @@ export {
|
||||
} from './calls';
|
||||
|
||||
export {
|
||||
getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList,
|
||||
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction,
|
||||
getAvailableReactions, sendReaction, sendEmojiInteraction, fetchMessageReactionsList, clearRecentReactions,
|
||||
setDefaultReaction, fetchMessageReactions, sendWatchingEmojiInteraction, fetchRecentReactions, fetchTopReactions,
|
||||
} from './reactions';
|
||||
|
||||
export {
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import type { ApiChat, ApiReaction } from '../../types';
|
||||
import { invokeRequest } from './client';
|
||||
import BigInt from 'big-integer';
|
||||
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 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 { buildApiAvailableReaction, buildApiReaction, buildMessagePeerReaction } from '../apiBuilders/messages';
|
||||
import { invokeRequest } from './client';
|
||||
import localDb from '../localDb';
|
||||
import { addEntitiesWithPhotosToLocalDb } from '../helpers';
|
||||
|
||||
export function sendWatchingEmojiInteraction({
|
||||
chat,
|
||||
@ -65,6 +68,9 @@ export async function getAvailableReactions() {
|
||||
if (reaction.aroundAnimation instanceof GramJs.Document) {
|
||||
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) {
|
||||
localDb.documents[String(reaction.centerIcon.id)] = reaction.centerIcon;
|
||||
}
|
||||
@ -74,15 +80,19 @@ export async function getAvailableReactions() {
|
||||
}
|
||||
|
||||
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({
|
||||
reaction: reactions?.map((r) => buildInputReaction(r)),
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
msgId: messageId,
|
||||
}), true);
|
||||
...(shouldAddToRecent && { addToRecent: true }),
|
||||
}), true, true);
|
||||
}
|
||||
|
||||
export function fetchMessageReactions({
|
||||
@ -134,3 +144,39 @@ export function setDefaultReaction({
|
||||
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' });
|
||||
} else if (update instanceof GramJs.UpdateRecentStickers) {
|
||||
onUpdate({ '@type': 'updateRecentStickers' });
|
||||
} else if (update instanceof GramJs.UpdateRecentReactions) {
|
||||
onUpdate({ '@type': 'updateRecentReactions' });
|
||||
} else if (update instanceof GramJs.UpdateMoveStickerSetToTop) {
|
||||
if (!update.masks) {
|
||||
onUpdate({
|
||||
|
||||
@ -459,6 +459,7 @@ export interface ApiReactionCount {
|
||||
|
||||
export interface ApiAvailableReaction {
|
||||
selectAnimation?: ApiDocument;
|
||||
appearAnimation?: ApiDocument;
|
||||
activateAnimation?: ApiDocument;
|
||||
effectAnimation?: ApiDocument;
|
||||
staticIcon?: ApiDocument;
|
||||
|
||||
@ -400,6 +400,10 @@ export type ApiUpdateRecentStickers = {
|
||||
'@type': 'updateRecentStickers';
|
||||
};
|
||||
|
||||
export type ApiUpdateRecentReactions = {
|
||||
'@type': 'updateRecentReactions';
|
||||
};
|
||||
|
||||
export type ApiUpdateMoveStickerSetToTop = {
|
||||
'@type': 'updateMoveStickerSetToTop';
|
||||
isCustomEmoji?: boolean;
|
||||
@ -638,7 +642,8 @@ export type ApiUpdate = (
|
||||
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
||||
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
||||
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
||||
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiRequestInitApi
|
||||
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses |
|
||||
ApiUpdateRecentReactions | ApiRequestInitApi
|
||||
);
|
||||
|
||||
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 HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
||||
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 PollModal } from '../components/middle/composer/PollModal';
|
||||
|
||||
@ -29,6 +29,7 @@ type OwnProps = {
|
||||
loopLimit?: number;
|
||||
style?: string;
|
||||
isBig?: boolean;
|
||||
noPlay?: boolean;
|
||||
withGridFix?: boolean;
|
||||
withSharedAnimation?: boolean;
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||
@ -48,6 +49,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
documentId,
|
||||
size = STICKER_SIZE,
|
||||
isBig,
|
||||
noPlay,
|
||||
className,
|
||||
loopLimit,
|
||||
style,
|
||||
@ -129,6 +131,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
sticker={customEmoji}
|
||||
isSmall={!isBig}
|
||||
size={size}
|
||||
noPlay={noPlay}
|
||||
customColor={customColor}
|
||||
thumbClassName={styles.thumb}
|
||||
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, {
|
||||
useState, useEffect, memo, useRef, useMemo, useCallback,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getGlobal, withGlobal } from '../../../global';
|
||||
} from '../../lib/teact/teact';
|
||||
import { getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiStickerSet, ApiSticker } from '../../../api/types';
|
||||
import type { StickerSetOrRecent } from '../../../types';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type {
|
||||
ApiStickerSet, ApiSticker, ApiReaction, ApiAvailableReaction,
|
||||
} from '../../api/types';
|
||||
import type { StickerSetOrReactionsSetOrRecent } from '../../types';
|
||||
|
||||
import {
|
||||
CHAT_STICKER_SET_ID,
|
||||
FAVORITE_SYMBOL_SET_ID,
|
||||
POPULAR_SYMBOL_SET_ID,
|
||||
PREMIUM_STICKER_SET_ID,
|
||||
RECENT_SYMBOL_SET_ID,
|
||||
SLIDE_TRANSITION_DURATION,
|
||||
STICKER_PICKER_MAX_SHARED_COVERS,
|
||||
STICKER_SIZE_PICKER_HEADER,
|
||||
} from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import fastSmoothScroll from '../../../util/fastSmoothScroll';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||
import { pickTruthy, unique } from '../../../util/iteratees';
|
||||
TOP_SYMBOL_SET_ID,
|
||||
} from '../../config';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import fastSmoothScrollHorizontal from '../../util/fastSmoothScrollHorizontal';
|
||||
import { pickTruthy, unique } from '../../util/iteratees';
|
||||
import { isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
selectIsAlwaysHighPriorityEmoji,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
} from '../../../global/selectors';
|
||||
} from '../../global/selectors';
|
||||
|
||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useAsyncRendering from '../right/hooks/useAsyncRendering';
|
||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
|
||||
import Loading from '../../ui/Loading';
|
||||
import Button from '../../ui/Button';
|
||||
import StickerButton from '../../common/StickerButton';
|
||||
import Loading from '../ui/Loading';
|
||||
import Button from '../ui/Button';
|
||||
import StickerButton from './StickerButton';
|
||||
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 = {
|
||||
scrollContainerRef?: RefObject<HTMLDivElement>;
|
||||
scrollHeaderRef?: RefObject<HTMLDivElement>;
|
||||
chatId?: string;
|
||||
className?: string;
|
||||
loadAndPlay: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
idPrefix?: string;
|
||||
withDefaultTopicIcons?: boolean;
|
||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
selectedReactionIds?: string[];
|
||||
isStatusPicker?: boolean;
|
||||
isReactionPicker?: boolean;
|
||||
onContextMenuOpen?: NoneToVoidFunction;
|
||||
onContextMenuClose?: NoneToVoidFunction;
|
||||
onContextMenuClick?: NoneToVoidFunction;
|
||||
@ -58,7 +71,10 @@ type StateProps = {
|
||||
customEmojisById?: Record<string, ApiSticker>;
|
||||
recentCustomEmojiIds?: string[];
|
||||
recentStatusEmojis?: ApiSticker[];
|
||||
topReactions?: ApiReaction[];
|
||||
recentReactions?: ApiReaction[];
|
||||
stickerSetsById: Record<string, ApiStickerSet>;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
addedCustomEmojiIds?: string[];
|
||||
defaultTopicIconsId?: string;
|
||||
defaultStatusIconsId?: string;
|
||||
@ -72,20 +88,37 @@ const SMOOTH_SCROLL_DISTANCE = 500;
|
||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||
const DEFAULT_ID_PREFIX = 'custom-emoji-set';
|
||||
const 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 CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
scrollContainerRef,
|
||||
scrollHeaderRef,
|
||||
className,
|
||||
loadAndPlay,
|
||||
addedCustomEmojiIds,
|
||||
customEmojisById,
|
||||
recentCustomEmojiIds,
|
||||
selectedReactionIds,
|
||||
recentStatusEmojis,
|
||||
stickerSetsById,
|
||||
topReactions,
|
||||
recentReactions,
|
||||
availableReactions,
|
||||
idPrefix = DEFAULT_ID_PREFIX,
|
||||
customEmojiFeaturedIds,
|
||||
canAnimate,
|
||||
isReactionPicker,
|
||||
isStatusPicker,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium,
|
||||
@ -93,20 +126,28 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
defaultTopicIconsId,
|
||||
defaultStatusIconsId,
|
||||
onCustomEmojiSelect,
|
||||
onReactionSelect,
|
||||
onContextMenuOpen,
|
||||
onContextMenuClose,
|
||||
onContextMenuClick,
|
||||
}) => {
|
||||
// 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
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
let headerRef = 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);
|
||||
if (scrollContainerRef) {
|
||||
containerRef = scrollContainerRef;
|
||||
}
|
||||
if (scrollHeaderRef) {
|
||||
headerRef = scrollHeaderRef;
|
||||
}
|
||||
|
||||
const [activeSetIndex, setActiveSetIndex] = useState<number>(0);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const recentCustomEmojis = useMemo(() => {
|
||||
return isStatusPicker
|
||||
@ -149,9 +190,43 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
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!];
|
||||
if (defaultStatusIconsPack.stickers?.length) {
|
||||
const stickers = (defaultStatusIconsPack.stickers || []).concat(recentCustomEmojis || []);
|
||||
@ -179,7 +254,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
title: lang('RecentStickers'),
|
||||
stickers: recentCustomEmojis,
|
||||
count: recentCustomEmojis.length,
|
||||
isEmoji: true as true,
|
||||
isEmoji: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -192,8 +267,9 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
...setsToDisplay,
|
||||
];
|
||||
}, [
|
||||
addedCustomEmojiIds, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||
customEmojiFeaturedIds, stickerSetsById, defaultStatusIconsId, lang, defaultTopicIconsId,
|
||||
addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||
customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions,
|
||||
defaultStatusIconsId, defaultTopicIconsId,
|
||||
]);
|
||||
|
||||
const noPopulatedSets = useMemo(() => (
|
||||
@ -201,10 +277,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
&& allSets.filter((set) => set.stickers?.length).length === 0
|
||||
), [allSets, areAddedLoaded]);
|
||||
|
||||
const canRenderContents = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||
const shouldRenderContents = areAddedLoaded && canRenderContents && !noPopulatedSets;
|
||||
const canRenderContent = useAsyncRendering([], SLIDE_TRANSITION_DURATION);
|
||||
const shouldRenderContent = areAddedLoaded && canRenderContent && !noPopulatedSets;
|
||||
|
||||
useHorizontalScroll(headerRef, !shouldRenderContents);
|
||||
useHorizontalScroll(headerRef, !(isMobile && shouldRenderContent));
|
||||
|
||||
// Scroll container and header when active set changes
|
||||
useEffect(() => {
|
||||
@ -232,7 +308,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
onCustomEmojiSelect(emoji);
|
||||
}, [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 buttonClassName = buildClassName(
|
||||
'symbol-set-button sticker-set-button',
|
||||
@ -242,25 +322,24 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
const withSharedCanvas = index < STICKER_PICKER_MAX_SHARED_COVERS;
|
||||
const isHq = selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSet as ApiStickerSet);
|
||||
|
||||
if (stickerSet.id === RECENT_SYMBOL_SET_ID
|
||||
|| stickerSet.id === FAVORITE_SYMBOL_SET_ID
|
||||
|| stickerSet.id === CHAT_STICKER_SET_ID
|
||||
|| stickerSet.id === PREMIUM_STICKER_SET_ID
|
||||
|| stickerSet.hasThumbnail
|
||||
|| !firstSticker
|
||||
) {
|
||||
if (stickerSet.id === TOP_SYMBOL_SET_ID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (STICKER_SET_IDS_WITH_COVER.has(stickerSet.id) || stickerSet.hasThumbnail || !firstSticker) {
|
||||
const isFaded = FADED_BUTTON_SET_IDS.has(stickerSet.id);
|
||||
return (
|
||||
<Button
|
||||
key={stickerSet.id}
|
||||
className={buttonClassName}
|
||||
ariaLabel={stickerSet.title}
|
||||
round
|
||||
faded={stickerSet.id === RECENT_SYMBOL_SET_ID || stickerSet.id === FAVORITE_SYMBOL_SET_ID}
|
||||
faded={isFaded}
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
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" />
|
||||
) : (
|
||||
<StickerSetCover
|
||||
@ -272,29 +351,29 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</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 (
|
||||
<div className={fullClassName}>
|
||||
{noPopulatedSets ? (
|
||||
@ -322,34 +401,43 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
ref={containerRef}
|
||||
className={buildClassName('StickerPicker-main no-selection', IS_TOUCH_ENV ? 'no-scrollbar' : 'custom-scroll')}
|
||||
>
|
||||
{allSets.map((stickerSet, i) => (
|
||||
<StickerSet
|
||||
key={stickerSet.id}
|
||||
stickerSet={stickerSet}
|
||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||
index={i}
|
||||
idPrefix={idPrefix}
|
||||
observeIntersection={observeIntersection}
|
||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isStatusPicker={isStatusPicker}
|
||||
shouldHideRecentHeader={withDefaultTopicIcons || isStatusPicker}
|
||||
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
withDefaultStatusIcon={isStatusPicker && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
onStickerSelect={handleEmojiSelect}
|
||||
onContextMenuOpen={onContextMenuOpen}
|
||||
onContextMenuClose={onContextMenuClose}
|
||||
onContextMenuClick={onContextMenuClick}
|
||||
/>
|
||||
))}
|
||||
{allSets.map((stickerSet, i) => {
|
||||
const shouldHideHeader = stickerSet.id === TOP_SYMBOL_SET_ID
|
||||
|| (stickerSet.id === RECENT_SYMBOL_SET_ID && (withDefaultTopicIcons || isStatusPicker));
|
||||
|
||||
return (
|
||||
<StickerSet
|
||||
key={stickerSet.id}
|
||||
stickerSet={stickerSet}
|
||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||
index={i}
|
||||
idPrefix={idPrefix}
|
||||
observeIntersection={observeIntersection}
|
||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isStatusPicker={isStatusPicker}
|
||||
isReactionPicker={isReactionPicker}
|
||||
shouldHideHeader={shouldHideHeader}
|
||||
withDefaultTopicIcon={withDefaultTopicIcons && stickerSet.id === RECENT_SYMBOL_SET_ID}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, isStatusPicker }): StateProps => {
|
||||
(global, { chatId, isStatusPicker, isReactionPicker }): StateProps => {
|
||||
const {
|
||||
stickers: {
|
||||
setsById: stickerSetsById,
|
||||
@ -362,6 +450,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
},
|
||||
},
|
||||
recentCustomEmojis: recentCustomEmojiIds,
|
||||
recentReactions,
|
||||
topReactions,
|
||||
} = global;
|
||||
|
||||
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
|
||||
@ -378,6 +468,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
customEmojiFeaturedIds,
|
||||
defaultTopicIconsId: global.defaultTopicIconsId,
|
||||
defaultStatusIconsId: global.defaultStatusIconsId,
|
||||
topReactions: isReactionPicker ? topReactions : undefined,
|
||||
recentReactions: isReactionPicker ? recentReactions : undefined,
|
||||
availableReactions: isReactionPicker ? global.availableReactions : undefined,
|
||||
};
|
||||
},
|
||||
)(CustomEmojiPicker));
|
||||
@ -16,7 +16,7 @@ import useMedia from '../../hooks/useMedia';
|
||||
import useBuffering from '../../hooks/useBuffering';
|
||||
import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
|
||||
import Spinner from '../ui/Spinner';
|
||||
@ -83,7 +83,7 @@ const GifButton: FC<OwnProps> = ({
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
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;
|
||||
|
||||
&.custom-emoji {
|
||||
color: var(--color-primary);
|
||||
|
||||
width: var(--custom-emoji-size);
|
||||
height: var(--custom-emoji-size);
|
||||
margin: 0.3125rem;
|
||||
@ -63,6 +65,10 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
}
|
||||
|
||||
&.interactive {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@ -15,7 +15,7 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useDynamicColorListener from '../../hooks/useDynamicColorListener';
|
||||
|
||||
import StickerView from './StickerView';
|
||||
@ -35,6 +35,7 @@ type OwnProps<T> = {
|
||||
isSavedMessages?: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
canViewSet?: boolean;
|
||||
isSelected?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||
observeIntersection: ObserveFn;
|
||||
@ -68,6 +69,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
isStatusPicker,
|
||||
canViewSet,
|
||||
observeIntersection,
|
||||
isSelected,
|
||||
isCurrentUserPremium,
|
||||
noShowPremium,
|
||||
sharedCanvasRef,
|
||||
@ -123,7 +125,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
@ -205,8 +207,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
const fullClassName = buildClassName(
|
||||
'StickerButton',
|
||||
onClick && 'interactive',
|
||||
isSelected && 'selected',
|
||||
isCustomEmoji && 'custom-emoji',
|
||||
`sticker-button-${id}`,
|
||||
className,
|
||||
);
|
||||
|
||||
|
||||
@ -1,36 +1,43 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiSticker } from '../../../api/types';
|
||||
import type { StickerSetOrRecent } from '../../../types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiAvailableReaction, ApiReaction, ApiSticker } from '../../api/types';
|
||||
import type { StickerSetOrReactionsSetOrRecent } from '../../types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import {
|
||||
DEFAULT_STATUS_ICON_ID,
|
||||
DEFAULT_TOPIC_ICON_STICKER_ID,
|
||||
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
|
||||
} from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
|
||||
EMOJI_SIZE_PICKER,
|
||||
FAVORITE_SYMBOL_SET_ID,
|
||||
POPULAR_SYMBOL_SET_ID,
|
||||
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 useFlag from '../../../hooks/useFlag';
|
||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
import useResizeObserver from '../../hooks/useResizeObserver';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useWindowSize from '../../hooks/useWindowSize';
|
||||
|
||||
import StickerButton from '../../common/StickerButton';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import Button from '../../ui/Button';
|
||||
import StickerButton from './StickerButton';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
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 = {
|
||||
stickerSet: StickerSetOrRecent;
|
||||
stickerSet: StickerSetOrReactionsSetOrRecent;
|
||||
loadAndPlay: boolean;
|
||||
index: number;
|
||||
idPrefix?: string;
|
||||
@ -38,12 +45,16 @@ type OwnProps = {
|
||||
favoriteStickers?: ApiSticker[];
|
||||
isSavedMessages?: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
isReactionPicker?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
shouldHideRecentHeader?: boolean;
|
||||
shouldHideHeader?: boolean;
|
||||
selectedReactionIds?: string[];
|
||||
withDefaultTopicIcon?: boolean;
|
||||
withDefaultStatusIcon?: boolean;
|
||||
observeIntersection: ObserveFn;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
onStickerUnfave?: (sticker: ApiSticker) => void;
|
||||
onStickerFave?: (sticker: ApiSticker) => void;
|
||||
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
||||
@ -53,6 +64,10 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
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> = ({
|
||||
stickerSet,
|
||||
@ -61,13 +76,17 @@ const StickerSet: FC<OwnProps> = ({
|
||||
idPrefix,
|
||||
shouldRender,
|
||||
favoriteStickers,
|
||||
availableReactions,
|
||||
isSavedMessages,
|
||||
isStatusPicker,
|
||||
isReactionPicker,
|
||||
isCurrentUserPremium,
|
||||
shouldHideRecentHeader,
|
||||
shouldHideHeader,
|
||||
withDefaultTopicIcon,
|
||||
selectedReactionIds,
|
||||
withDefaultStatusIcon,
|
||||
observeIntersection,
|
||||
onReactionSelect,
|
||||
onStickerSelect,
|
||||
onStickerUnfave,
|
||||
onStickerFave,
|
||||
@ -79,6 +98,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
const {
|
||||
clearRecentStickers,
|
||||
clearRecentCustomEmoji,
|
||||
clearRecentReactions,
|
||||
openPremiumModal,
|
||||
toggleStickerSet,
|
||||
loadStickers,
|
||||
@ -93,10 +113,11 @@ const StickerSet: FC<OwnProps> = ({
|
||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK);
|
||||
const [itemsPerRow, setItemsPerRow] = useState(getItemsPerRowFallback(windowWidth));
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
@ -106,17 +127,22 @@ const StickerSet: FC<OwnProps> = ({
|
||||
const emojiMarginPx = isMobile ? 8 : 10;
|
||||
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID;
|
||||
const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
|
||||
const isPopular = stickerSet.id === POPULAR_SYMBOL_SET_ID;
|
||||
const isEmoji = stickerSet.isEmoji;
|
||||
const isPremiumSet = !isRecent && selectIsSetPremium(stickerSet);
|
||||
|
||||
const handleClearRecent = useCallback(() => {
|
||||
if (isEmoji) {
|
||||
if (isReactionPicker) {
|
||||
clearRecentReactions();
|
||||
} else if (isEmoji) {
|
||||
clearRecentCustomEmoji();
|
||||
} else {
|
||||
clearRecentStickers();
|
||||
}
|
||||
closeConfirmModal();
|
||||
}, [clearRecentCustomEmoji, clearRecentStickers, closeConfirmModal, isEmoji]);
|
||||
}, [
|
||||
clearRecentCustomEmoji, clearRecentReactions, clearRecentStickers, closeConfirmModal, isEmoji, isReactionPicker,
|
||||
]);
|
||||
|
||||
const handleAddClick = useCallback(() => {
|
||||
if (isPremiumSet && !isCurrentUserPremium) {
|
||||
@ -156,10 +182,12 @@ const StickerSet: FC<OwnProps> = ({
|
||||
const margin = isEmoji ? emojiMarginPx : stickerMarginPx;
|
||||
|
||||
const calculateItemsPerRow = useCallback((width: number) => {
|
||||
if (!width) return ITEMS_PER_ROW_FALLBACK;
|
||||
if (!width) {
|
||||
return getItemsPerRowFallback(windowWidth);
|
||||
}
|
||||
|
||||
return Math.floor(width / (itemSize + margin));
|
||||
}, [itemSize, margin]);
|
||||
}, [itemSize, margin, windowWidth]);
|
||||
|
||||
const handleResize = useCallback((entry: ResizeObserverEntry) => {
|
||||
setItemsPerRow(calculateItemsPerRow(entry.contentRect.width));
|
||||
@ -172,7 +200,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
}, [calculateItemsPerRow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isIntersecting && !stickerSet.stickers?.length && stickerSet.accessHash) {
|
||||
if (isIntersecting && !stickerSet.stickers?.length && !stickerSet.reactions?.length && stickerSet.accessHash) {
|
||||
loadStickers({
|
||||
stickerSetInfo: {
|
||||
id: stickerSet.id,
|
||||
@ -186,7 +214,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
|
||||
|
||||
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 itemsBeforeCutout = itemsPerRow * 3 - 1;
|
||||
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 height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * (itemSize + margin);
|
||||
|
||||
const shouldHideHeader = isRecent && shouldHideRecentHeader;
|
||||
|
||||
const favoriteStickerIdsSet = useMemo(() => (
|
||||
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
|
||||
), [favoriteStickers]);
|
||||
@ -218,7 +244,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
{isRecent && (
|
||||
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
|
||||
)}
|
||||
{!isRecent && isEmoji && !isInstalled && (
|
||||
{!isRecent && isEmoji && !isInstalled && !isPopular && (
|
||||
<Button
|
||||
className="symbol-set-add-button"
|
||||
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
|
||||
@ -262,14 +288,33 @@ const StickerSet: FC<OwnProps> = ({
|
||||
<i className="icon-premium" />
|
||||
</Button>
|
||||
)}
|
||||
{shouldRender && stickerSet.stickers && stickerSet.stickers
|
||||
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
|
||||
{shouldRender && stickerSet.reactions?.map((reaction) => {
|
||||
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) => {
|
||||
const isHqEmoji = (isRecent || isFavorite)
|
||||
&& selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo);
|
||||
const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji
|
||||
? sharedCanvasHqRef
|
||||
: sharedCanvasRef;
|
||||
const reactionId = sticker.isCustomEmoji ? sticker.id : sticker.emoji;
|
||||
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
|
||||
|
||||
return (
|
||||
<StickerButton
|
||||
@ -285,6 +330,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
sharedCanvasRef={canvasRef}
|
||||
onClick={onStickerSelect}
|
||||
clickArg={sticker}
|
||||
isSelected={isSelected}
|
||||
onUnfaveClick={isFavorite && favoriteStickerIdsSet?.has(sticker.id) ? onStickerUnfave : undefined}
|
||||
onFaveClick={!favoriteStickerIdsSet?.has(sticker.id) ? onStickerFave : undefined}
|
||||
onRemoveRecentClick={isRecent ? onStickerRemoveRecent : undefined}
|
||||
@ -309,7 +355,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
|
||||
{isRecent && (
|
||||
<ConfirmDialog
|
||||
text={lang('ClearRecentStickersAlertMessage')}
|
||||
text={lang(isReactionPicker ? 'ClearRecentReactionsAlertMessage' : 'ClearRecentStickersAlertMessage')}
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={closeConfirmModal}
|
||||
confirmHandler={handleClearRecent}
|
||||
@ -321,3 +367,11 @@ const StickerSet: FC<OwnProps> = ({
|
||||
};
|
||||
|
||||
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 telegramLogoPath from '../../assets/telegram-logo.svg';
|
||||
import reactionThumbsPath from '../../assets/reaction-thumbs.png';
|
||||
import lockPreviewPath from '../../assets/lock.png';
|
||||
import monkeyPath from '../../assets/monkey.svg';
|
||||
import spoilerMaskPath from '../../assets/spoilers/mask.svg';
|
||||
@ -81,7 +80,6 @@ const preloadTasks = {
|
||||
loadModule(Bundles.Main)
|
||||
.then(preloadFonts),
|
||||
preloadAvatars(),
|
||||
preloadImage(reactionThumbsPath),
|
||||
preloadImage(spoilerMaskPath),
|
||||
]),
|
||||
authPhoneNumber: () => Promise.all([
|
||||
|
||||
@ -11,7 +11,7 @@ import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import Menu from '../../ui/Menu';
|
||||
import Portal from '../../ui/Portal';
|
||||
import CustomEmojiPicker from '../../middle/composer/CustomEmojiPicker';
|
||||
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||
|
||||
import styles from './StatusPickerMenu.module.scss';
|
||||
|
||||
@ -35,6 +35,11 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
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 [isContextMenuShown, markContextMenuShown, unmarkContextMenuShown] = useFlag();
|
||||
useEffect(() => {
|
||||
@ -47,6 +52,15 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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) => {
|
||||
onEmojiStatusSelect(sticker);
|
||||
onClose();
|
||||
@ -62,11 +76,14 @@ const StatusPickerMenu: FC<OwnProps & StateProps> = ({
|
||||
onClose={onClose}
|
||||
transformOriginX={transformOriginX.current}
|
||||
noCloseOnBackdrop={isContextMenuShown}
|
||||
onCloseAnimationEnd={handleResetScrollPosition}
|
||||
>
|
||||
<CustomEmojiPicker
|
||||
idPrefix="status-emoji-set-"
|
||||
loadAndPlay={isOpen}
|
||||
isStatusPicker
|
||||
scrollHeaderRef={scrollHeaderRef}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
onContextMenuOpen={markContextMenuShown}
|
||||
onContextMenuClose={unmarkContextMenuShown}
|
||||
onCustomEmojiSelect={handleEmojiSelect}
|
||||
|
||||
@ -26,7 +26,7 @@ import {
|
||||
selectIsMediaViewerOpen,
|
||||
selectIsRightColumnShown,
|
||||
selectIsServiceChatReady,
|
||||
selectUser,
|
||||
selectUser, selectIsReactionPickerOpen,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||
@ -47,6 +47,7 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
|
||||
import useInterval from '../../hooks/useInterval';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useTimeout from '../../hooks/useTimeout';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
|
||||
import StickerSetModal from '../common/StickerSetModal.async';
|
||||
import UnreadCount from '../common/UnreadCounter';
|
||||
@ -81,6 +82,7 @@ import DeleteFolderDialog from './DeleteFolderDialog.async';
|
||||
import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
||||
import DraftRecipientPicker from './DraftRecipientPicker.async';
|
||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
||||
|
||||
import './Main.scss';
|
||||
|
||||
@ -131,11 +133,13 @@ type StateProps = {
|
||||
deleteFolderDialogId?: number;
|
||||
isPaymentModalOpen?: boolean;
|
||||
isReceiptModalOpen?: boolean;
|
||||
isReactionPickerOpen: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const APP_OUTDATED_TIMEOUT_MS = 5 * 60 * 1000; // 5 min
|
||||
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
|
||||
let DEBUG_isLogged = false;
|
||||
@ -181,6 +185,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
isPremiumModalOpen,
|
||||
isPaymentModalOpen,
|
||||
isReceiptModalOpen,
|
||||
isReactionPickerOpen,
|
||||
isCurrentUserPremium,
|
||||
deleteFolderDialogId,
|
||||
isMasterTab,
|
||||
@ -219,6 +224,9 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
toggleLeftColumn,
|
||||
loadRecentEmojiStatuses,
|
||||
updatePageTitle,
|
||||
loadTopReactions,
|
||||
loadRecentReactions,
|
||||
loadFeaturedEmojiStickers,
|
||||
} = getActions();
|
||||
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
@ -232,6 +240,9 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
void loadBundle(Bundles.Calls);
|
||||
}, 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
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -266,19 +277,26 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
loadContactList();
|
||||
loadPremiumGifts();
|
||||
loadDefaultTopicIcons();
|
||||
loadDefaultStatusIcons();
|
||||
checkAppVersion();
|
||||
if (isCurrentUserPremium) {
|
||||
loadRecentEmojiStatuses();
|
||||
}
|
||||
loadTopReactions();
|
||||
loadRecentReactions();
|
||||
loadFeaturedEmojiStickers();
|
||||
}
|
||||
}, [
|
||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
|
||||
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons,
|
||||
loadDefaultStatusIcons, loadRecentEmojiStatuses, isCurrentUserPremium, isMasterTab, initMain,
|
||||
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons, loadTopReactions,
|
||||
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
|
||||
useEffect(() => {
|
||||
if (lastSyncTime && isMasterTab) {
|
||||
@ -502,6 +520,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||
<DeleteFolderDialog deleteFolderDialogId={deleteFolderDialogId} />
|
||||
<ReactionPicker isOpen={isReactionPickerOpen} shouldLoad={shouldLoadReactionPicker} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -559,6 +578,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
||||
isMediaViewerOpen: selectIsMediaViewerOpen(global),
|
||||
isForwardModalOpen: selectIsForwardModalOpen(global),
|
||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||
hasNotifications: Boolean(notifications.length),
|
||||
hasDialogs: Boolean(dialogs.length),
|
||||
audioMessage,
|
||||
|
||||
@ -5,10 +5,10 @@ import type { ISettings } from '../../../types';
|
||||
import type { ApiDocument } 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 buildClassName from '../../../util/buildClassName';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
|
||||
import styles from './AttachBotIcon.module.scss';
|
||||
|
||||
@ -25,6 +25,7 @@ const COLOR_REPLACE_PATTERN = /#fff/gi;
|
||||
const AttachBotIcon: FC<OwnProps> = ({
|
||||
icon, theme,
|
||||
}) => {
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
const mediaData = useMedia(getDocumentMediaHash(icon), false, ApiMediaFormat.Text);
|
||||
|
||||
const iconSvg = useMemo(() => {
|
||||
@ -42,8 +43,8 @@ const AttachBotIcon: FC<OwnProps> = ({
|
||||
}, [mediaData, theme]);
|
||||
|
||||
return (
|
||||
<i className={buildClassName(styles.root, IS_COMPACT_MENU && styles.compact)}>
|
||||
<img src={iconSvg} alt="" className={buildClassName(styles.image, IS_COMPACT_MENU && styles.compact)} />
|
||||
<i className={buildClassName(styles.root, !isTouchScreen && styles.compact)}>
|
||||
<img src={iconSvg} alt="" className={buildClassName(styles.image, !isTouchScreen && styles.compact)} />
|
||||
</i>
|
||||
);
|
||||
};
|
||||
|
||||
@ -29,7 +29,7 @@ import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import EmbeddedMessage from '../../common/EmbeddedMessage';
|
||||
@ -141,7 +141,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
|
||||
@ -107,7 +107,3 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.CustomEmojiPicker {
|
||||
--emoji-size: 2.5rem;
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
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 {
|
||||
@ -33,7 +33,7 @@ import Avatar from '../../common/Avatar';
|
||||
import Loading from '../../ui/Loading';
|
||||
import Button from '../../ui/Button';
|
||||
import StickerButton from '../../common/StickerButton';
|
||||
import StickerSet from './StickerSet';
|
||||
import StickerSet from '../../common/StickerSet';
|
||||
import StickerSetCover from './StickerSetCover';
|
||||
import PremiumIcon from '../../common/PremiumIcon';
|
||||
|
||||
@ -263,7 +263,7 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
removeRecentSticker({ sticker });
|
||||
}, [removeRecentSticker]);
|
||||
|
||||
function renderCover(stickerSet: StickerSetOrRecent, index: number) {
|
||||
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
||||
const firstSticker = stickerSet.stickers?.[0];
|
||||
const buttonClassName = buildClassName(
|
||||
'symbol-set-button sticker-set-button',
|
||||
|
||||
@ -236,10 +236,19 @@
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
border-radius: 50%;
|
||||
padding: 0.25rem;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&-container {
|
||||
text-align: initial;
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
}
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-button {
|
||||
@ -253,29 +262,30 @@
|
||||
@include while-transition() {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.symbol-set-container {
|
||||
display: grid;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4rem));
|
||||
grid-gap: 0.625rem;
|
||||
padding: 0.3125rem;
|
||||
.symbol-set-container {
|
||||
display: grid !important;
|
||||
justify-content: space-between;
|
||||
grid-template-columns: repeat(auto-fill, var(--emoji-size, 4rem));
|
||||
grid-gap: 0.625rem;
|
||||
padding: 0.3125rem;
|
||||
text-align: initial;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
|
||||
&:not(.shown) {
|
||||
display: block;
|
||||
}
|
||||
&:not(.shown) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.closing {
|
||||
transition: none;
|
||||
}
|
||||
&.closing {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
> .EmojiButton,
|
||||
> .StickerButton {
|
||||
margin: 0;
|
||||
}
|
||||
> .EmojiButton,
|
||||
> .StickerButton {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ import Button from '../../ui/Button';
|
||||
import Menu from '../../ui/Menu';
|
||||
import Transition from '../../ui/Transition';
|
||||
import EmojiPicker from './EmojiPicker';
|
||||
import CustomEmojiPicker from './CustomEmojiPicker';
|
||||
import CustomEmojiPicker from '../../common/CustomEmojiPicker';
|
||||
import StickerPicker from './StickerPicker';
|
||||
import GifPicker from './GifPicker';
|
||||
import SymbolMenuFooter, { SYMBOL_MENU_TAB_TITLES, SymbolMenuTabs } from './SymbolMenuFooter';
|
||||
@ -100,7 +100,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
transformOriginY,
|
||||
style,
|
||||
}) => {
|
||||
const { loadPremiumSetStickers, loadFeaturedEmojiStickers } = getActions();
|
||||
const { loadPremiumSetStickers } = getActions();
|
||||
const [activeTab, setActiveTab] = useState<number>(0);
|
||||
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||
const [recentCustomEmojis, setRecentCustomEmojis] = useState<string[]>([]);
|
||||
@ -124,12 +124,10 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
}, [canSendPlainText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastSyncTime) return;
|
||||
if (isCurrentUserPremium) {
|
||||
if (lastSyncTime && isCurrentUserPremium) {
|
||||
loadPremiumSetStickers();
|
||||
}
|
||||
loadFeaturedEmojiStickers();
|
||||
}, [isCurrentUserPremium, lastSyncTime, loadFeaturedEmojiStickers, loadPremiumSetStickers]);
|
||||
}, [isCurrentUserPremium, lastSyncTime, loadPremiumSetStickers]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
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 buildClassName from '../../../util/buildClassName';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
@ -146,7 +146,7 @@ const SymbolMenuButton: FC<OwnProps> = ({
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
|
||||
@ -61,13 +61,14 @@ export type OwnProps = {
|
||||
messageListType: MessageListType;
|
||||
noReplies?: boolean;
|
||||
detectedLanguage?: string;
|
||||
onClose: () => void;
|
||||
onCloseAnimationEnd: () => void;
|
||||
repliesThreadInfo?: ApiThreadInfo;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
topReactions?: ApiReaction[];
|
||||
customEmojiSetsInfo?: ApiStickerSetInfo[];
|
||||
customEmojiSets?: ApiStickerSet[];
|
||||
noOptions?: boolean;
|
||||
@ -106,8 +107,11 @@ type StateProps = {
|
||||
threadId?: number;
|
||||
};
|
||||
|
||||
const REACTION_PICKER_APPEARANCE_DURATION_MS = 250;
|
||||
|
||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
availableReactions,
|
||||
topReactions,
|
||||
isOpen,
|
||||
messageListType,
|
||||
chatUsername,
|
||||
@ -182,11 +186,13 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
requestMessageTranslation,
|
||||
showOriginalMessage,
|
||||
openMessageLanguageModal,
|
||||
openReactionPicker,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const { transitionClassNames } = useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [noAnimationOnClose, setNoAnimationOnClose] = useState(false);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||
const [isPinModalOpen, setIsPinModalOpen] = useState(false);
|
||||
@ -255,7 +261,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
setIsReportModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
const closeMenu = useCallback((noCloseAnimation = false) => {
|
||||
setNoAnimationOnClose(noCloseAnimation);
|
||||
setIsMenuOpen(false);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
@ -409,11 +416,18 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleToggleReaction = useCallback((reaction: ApiReaction) => {
|
||||
toggleReaction({
|
||||
chatId: message.chatId, messageId: message.id, reaction,
|
||||
chatId: message.chatId, messageId: message.id, reaction, shouldAddToRecent: true,
|
||||
});
|
||||
closeMenu();
|
||||
}, [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(() => {
|
||||
requestMessageTranslation({
|
||||
chatId: message.chatId,
|
||||
@ -453,6 +467,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
<div className={buildClassName('ContextMenuContainer', transitionClassNames)}>
|
||||
<MessageContextMenu
|
||||
availableReactions={availableReactions}
|
||||
topReactions={topReactions}
|
||||
message={message}
|
||||
isPrivate={isPrivate}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
@ -488,6 +503,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canSelectLanguage={canSelectLanguage}
|
||||
hasCustomEmoji={hasCustomEmoji}
|
||||
customEmojiSets={customEmojiSets}
|
||||
noTransition={noAnimationOnClose}
|
||||
isDownloading={isDownloading}
|
||||
seenByRecentUsers={seenByRecentUsers}
|
||||
noReplies={noReplies}
|
||||
@ -515,6 +531,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
onShowSeenBy={handleOpenSeenByModal}
|
||||
onToggleReaction={handleToggleReaction}
|
||||
onShowReactors={handleOpenReactorListModal}
|
||||
onReactionPickerOpen={handleReactionPickerOpen}
|
||||
onTranslate={handleTranslate}
|
||||
onShowOriginal={handleShowOriginal}
|
||||
onSelectLanguage={handleSelectLanguage}
|
||||
@ -617,6 +634,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
availableReactions: global.availableReactions,
|
||||
topReactions: global.topReactions,
|
||||
noOptions,
|
||||
canSendNow: isScheduled,
|
||||
canReschedule: isScheduled,
|
||||
|
||||
@ -21,9 +21,21 @@
|
||||
}
|
||||
|
||||
&.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 {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import { getMessageCopyOptions } from './helpers/copyOptions';
|
||||
import { disableScrolling, enableScrolling } from '../../../util/scrollLock';
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
@ -23,7 +24,7 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useContextMenuPosition from '../../../hooks/useContextMenuPosition';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
|
||||
@ -38,6 +39,7 @@ import './MessageContextMenu.scss';
|
||||
|
||||
type OwnProps = {
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
topReactions?: ApiReaction[];
|
||||
isOpen: boolean;
|
||||
anchor: IAnchorPosition;
|
||||
message: ApiMessage | ApiSponsoredMessage;
|
||||
@ -76,44 +78,48 @@ type OwnProps = {
|
||||
noReplies?: boolean;
|
||||
hasCustomEmoji?: boolean;
|
||||
customEmojiSets?: ApiStickerSet[];
|
||||
onReply?: () => void;
|
||||
noTransition?: boolean;
|
||||
onReply?: NoneToVoidFunction;
|
||||
onOpenThread?: VoidFunction;
|
||||
onEdit?: () => void;
|
||||
onPin?: () => void;
|
||||
onUnpin?: () => void;
|
||||
onForward?: () => void;
|
||||
onDelete?: () => void;
|
||||
onReport?: () => void;
|
||||
onFaveSticker?: () => void;
|
||||
onUnfaveSticker?: () => void;
|
||||
onSelect?: () => void;
|
||||
onSend?: () => void;
|
||||
onReschedule?: () => void;
|
||||
onClose: () => void;
|
||||
onCloseAnimationEnd?: () => void;
|
||||
onCopyLink?: () => void;
|
||||
onEdit?: NoneToVoidFunction;
|
||||
onPin?: NoneToVoidFunction;
|
||||
onUnpin?: NoneToVoidFunction;
|
||||
onForward?: NoneToVoidFunction;
|
||||
onDelete?: NoneToVoidFunction;
|
||||
onReport?: NoneToVoidFunction;
|
||||
onFaveSticker?: NoneToVoidFunction;
|
||||
onUnfaveSticker?: NoneToVoidFunction;
|
||||
onSelect?: NoneToVoidFunction;
|
||||
onSend?: NoneToVoidFunction;
|
||||
onReschedule?: NoneToVoidFunction;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||
onCopyLink?: NoneToVoidFunction;
|
||||
onCopyMessages?: (messageIds: number[]) => void;
|
||||
onCopyNumber?: () => void;
|
||||
onDownload?: () => void;
|
||||
onSaveGif?: () => void;
|
||||
onCancelVote?: () => void;
|
||||
onClosePoll?: () => void;
|
||||
onShowSeenBy?: () => void;
|
||||
onShowReactors?: () => void;
|
||||
onAboutAds?: () => void;
|
||||
onSponsoredHide?: () => void;
|
||||
onTranslate?: () => void;
|
||||
onShowOriginal?: () => void;
|
||||
onSelectLanguage?: () => void;
|
||||
onCopyNumber?: NoneToVoidFunction;
|
||||
onDownload?: NoneToVoidFunction;
|
||||
onSaveGif?: NoneToVoidFunction;
|
||||
onCancelVote?: NoneToVoidFunction;
|
||||
onClosePoll?: NoneToVoidFunction;
|
||||
onShowSeenBy?: NoneToVoidFunction;
|
||||
onShowReactors?: NoneToVoidFunction;
|
||||
onAboutAds?: NoneToVoidFunction;
|
||||
onSponsoredHide?: NoneToVoidFunction;
|
||||
onTranslate?: NoneToVoidFunction;
|
||||
onShowOriginal?: NoneToVoidFunction;
|
||||
onSelectLanguage?: NoneToVoidFunction;
|
||||
onToggleReaction?: (reaction: ApiReaction) => void;
|
||||
onReactionPickerOpen?: (position: IAnchorPosition) => void;
|
||||
};
|
||||
|
||||
const SCROLLBAR_WIDTH = 10;
|
||||
const REACTION_BUBBLE_EXTRA_WIDTH = 32;
|
||||
const REACTION_SELECTOR_WIDTH_REM = 19.25;
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
const MessageContextMenu: FC<OwnProps> = ({
|
||||
availableReactions,
|
||||
topReactions,
|
||||
isOpen,
|
||||
message,
|
||||
isPrivate,
|
||||
@ -152,6 +158,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
seenByRecentUsers,
|
||||
hasCustomEmoji,
|
||||
customEmojiSets,
|
||||
noTransition,
|
||||
onReply,
|
||||
onOpenThread,
|
||||
onEdit,
|
||||
@ -179,6 +186,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
onCopyMessages,
|
||||
onAboutAds,
|
||||
onSponsoredHide,
|
||||
onReactionPickerOpen,
|
||||
onTranslate,
|
||||
onShowOriginal,
|
||||
onSelectLanguage,
|
||||
@ -255,6 +263,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
extraTopPadding: (document.querySelector<HTMLElement>('.MiddleHeader')!).offsetHeight,
|
||||
marginSides: withReactions ? REACTION_BUBBLE_EXTRA_WIDTH : undefined,
|
||||
extraMarginTop: extraHeightPinned + extraHeightAudioPlayer,
|
||||
shouldAvoidNegativePosition: true,
|
||||
menuElMinWidth: withReactions && isMobile ? REACTION_SELECTOR_WIDTH_REM * REM : undefined,
|
||||
};
|
||||
}, [isMobile, withReactions]);
|
||||
|
||||
@ -271,7 +281,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style, menuStyle, withScroll,
|
||||
} = useContextMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||
} = useMenuPosition(anchor, getTriggerElement, getRootElement, getMenuElement, getLayout);
|
||||
|
||||
useEffect(() => {
|
||||
disableScrolling(withScroll ? scrollableRef.current : undefined, '.ReactionSelector');
|
||||
@ -292,20 +302,23 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
className={buildClassName(
|
||||
'MessageContextMenu', 'fluid', withReactions && 'with-reactions',
|
||||
)}
|
||||
shouldSkipTransition={noTransition}
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
>
|
||||
{withReactions && (
|
||||
<ReactionSelector
|
||||
enabledReactions={enabledReactions}
|
||||
topReactions={topReactions}
|
||||
allAvailableReactions={availableReactions}
|
||||
currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined}
|
||||
maxUniqueReactions={maxUniqueReactions}
|
||||
onToggleReaction={onToggleReaction!}
|
||||
isPrivate={isPrivate}
|
||||
availableReactions={availableReactions}
|
||||
isReady={isReady}
|
||||
canBuyPremium={canBuyPremium}
|
||||
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 {
|
||||
position: absolute;
|
||||
height: 3rem;
|
||||
background: var(--color-background);
|
||||
height: 2.5rem;
|
||||
min-width: 3rem;
|
||||
max-width: calc(100% + 5rem);
|
||||
max-width: calc(100% + 4rem);
|
||||
z-index: 100;
|
||||
border-radius: 3rem;
|
||||
filter: drop-shadow(0 0.25rem 0.125rem var(--color-default-shadow));
|
||||
right: -3rem;
|
||||
top: -3.5rem;
|
||||
right: -2rem;
|
||||
top: 0.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 {
|
||||
border: 0.5rem solid var(--color-background);
|
||||
background: var(--color-background);
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
right: 1.5rem;
|
||||
right: 1.125rem;
|
||||
bottom: -0.5rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
height: 0.5rem;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-radius: 0 0 50% 50%;
|
||||
border-radius: 0 0 1rem 1rem;
|
||||
z-index: -1;
|
||||
|
||||
&--isRtl {
|
||||
right: auto;
|
||||
left: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__bubble-small {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: "";
|
||||
right: 1.25rem;
|
||||
right: 1.125rem;
|
||||
bottom: -1.25rem;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-background);
|
||||
}
|
||||
|
||||
body.is-safari & {
|
||||
filter: none;
|
||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||
}
|
||||
&--isRtl {
|
||||
right: auto;
|
||||
left: 2.125rem;
|
||||
}
|
||||
|
||||
body.is-safari &__bubble-small,
|
||||
body.is-safari &__bubble-big {
|
||||
box-shadow: 0 0.25rem 0.125rem var(--color-default-shadow);
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__items-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 3rem;
|
||||
background: var(--color-background);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
&__items {
|
||||
padding: 0 1rem;
|
||||
padding: 0 0.5rem;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
overflow: overlay;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
&--compact {
|
||||
background: var(--color-background-compact-menu-reactions);
|
||||
&__show-more {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
top: -2.5rem;
|
||||
}
|
||||
|
||||
&--compact &__items {
|
||||
padding: 0 0.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;
|
||||
}
|
||||
padding: 0;
|
||||
margin-inline-start: 0.25rem;
|
||||
margin-inline-end: -0.125rem;
|
||||
border-radius: 50%;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import React, {
|
||||
memo, useMemo, useRef,
|
||||
memo, useCallback, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
import { getTouchY } from '../../../util/scrollLock';
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import { IS_COMPACT_MENU } from '../../../util/windowEnvironment';
|
||||
import { isSameReaction, canSendReaction, getReactionUniqueKey } from '../../../global/helpers';
|
||||
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import {
|
||||
isSameReaction, canSendReaction, getReactionUniqueKey, sortReactions,
|
||||
} from '../../../global/helpers';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import './ReactionSelector.scss';
|
||||
|
||||
@ -22,39 +24,37 @@ type OwnProps = {
|
||||
enabledReactions?: ApiChatReactions;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
isPrivate?: boolean;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
topReactions?: ApiReaction[];
|
||||
allAvailableReactions?: ApiAvailableReaction[];
|
||||
currentReactions?: ApiReactionCount[];
|
||||
maxUniqueReactions?: number;
|
||||
isReady?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
onShowMore: (position: IAnchorPosition) => void;
|
||||
};
|
||||
|
||||
const cn = createClassNameBuilder('ReactionSelector');
|
||||
const REACTIONS_AMOUNT = 6;
|
||||
|
||||
const ReactionSelector: FC<OwnProps> = ({
|
||||
availableReactions,
|
||||
allAvailableReactions,
|
||||
topReactions,
|
||||
enabledReactions,
|
||||
currentReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isReady,
|
||||
onToggleReaction,
|
||||
onShowMore,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const itemsScrollRef = useRef<HTMLDivElement>(null);
|
||||
useHorizontalScroll(itemsScrollRef);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
const lang = useLang();
|
||||
|
||||
const handleWheel = (e: React.WheelEvent | React.TouchEvent) => {
|
||||
const deltaY = 'deltaY' in e ? e.deltaY : getTouchY(e);
|
||||
|
||||
if (deltaY && e.cancelable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const reactionsToRender = useMemo(() => {
|
||||
return availableReactions?.map((availableReaction) => {
|
||||
const availableReactions = useMemo(() => {
|
||||
const reactions = allAvailableReactions?.map((availableReaction) => {
|
||||
if (availableReaction.isInactive) return undefined;
|
||||
if (!isPrivate && (!enabledReactions || !canSendReaction(availableReaction.reaction, enabledReactions))) {
|
||||
return undefined;
|
||||
@ -64,8 +64,17 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
return availableReaction;
|
||||
}) || [];
|
||||
}, [availableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions]);
|
||||
}).filter(Boolean) || [];
|
||||
|
||||
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 chosenReactions = currentReactions?.filter(({ chosenOrder }) => chosenOrder !== undefined) || [];
|
||||
@ -74,27 +83,40 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
)));
|
||||
}, [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;
|
||||
|
||||
return (
|
||||
<div className={cn('&', IS_COMPACT_MENU && 'compact')} onWheelCapture={handleWheel} onTouchMove={handleWheel}>
|
||||
<div className={cn('bubble-big')} />
|
||||
<div className={cn('bubble-small')} />
|
||||
<div className={cn('&', !isTouchScreen && 'withBlur', lang.isRtl && 'isRtl')} ref={ref}>
|
||||
<div className={cn('bubble-small', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items-wrapper')}>
|
||||
<div className={cn('items', ['no-scrollbar'])} ref={itemsScrollRef}>
|
||||
{reactionsToRender.map((reaction, i) => {
|
||||
if (!reaction) return undefined;
|
||||
return (
|
||||
<ReactionSelectorReaction
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
previewIndex={i}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className={cn('bubble-big', lang.isRtl && 'isRtl')} />
|
||||
<div className={cn('items')} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{reactionsToRender.map((reaction, i) => (
|
||||
<ReactionSelectorReaction
|
||||
key={getReactionUniqueKey(reaction.reaction)}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
reaction={reaction}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
/>
|
||||
))}
|
||||
{withMoreButton && (
|
||||
<Button
|
||||
color="translucent"
|
||||
className={cn('show-more')}
|
||||
onClick={handleShowMoreClick}
|
||||
>
|
||||
<i className="icon-down" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,22 +1,11 @@
|
||||
.ReactionSelectorReaction {
|
||||
margin-left: 0.5rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
position: relative;
|
||||
min-width: 2rem;
|
||||
min-height: 2rem;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 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%;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
.AnimatedSticker {
|
||||
@ -36,8 +25,8 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
width: 2.375rem;
|
||||
height: 2.375rem;
|
||||
border-radius: 50%;
|
||||
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 { ApiAvailableReaction, ApiReaction } from '../../../api/types';
|
||||
|
||||
import { IS_COMPACT_MENU } from '../../../util/windowEnvironment';
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
@ -12,11 +11,10 @@ import AnimatedSticker from '../../common/AnimatedSticker';
|
||||
|
||||
import './ReactionSelectorReaction.scss';
|
||||
|
||||
const REACTION_SIZE = IS_COMPACT_MENU ? 24 : 32;
|
||||
const REACTION_SIZE = 32;
|
||||
|
||||
type OwnProps = {
|
||||
reaction: ApiAvailableReaction;
|
||||
previewIndex: number;
|
||||
isReady?: boolean;
|
||||
chosen?: boolean;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
@ -26,18 +24,16 @@ const cn = createClassNameBuilder('ReactionSelectorReaction');
|
||||
|
||||
const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
reaction,
|
||||
previewIndex,
|
||||
isReady,
|
||||
chosen,
|
||||
onToggleReaction,
|
||||
}) => {
|
||||
const mediaAppearData = useMedia(`sticker${reaction.appearAnimation?.id}`, !isReady);
|
||||
const mediaData = useMedia(`document${reaction.selectAnimation?.id}`, !isReady);
|
||||
|
||||
const [isActivated, activate, deactivate] = useFlag();
|
||||
const [isAnimationLoaded, markAnimationLoaded] = useFlag();
|
||||
|
||||
const shouldRenderStatic = !isReady || !isAnimationLoaded;
|
||||
const shouldRenderAnimated = Boolean(isReady && mediaData);
|
||||
const [isFirstPlay, , unmarkIsFirstPlay] = useFlag(true);
|
||||
const [isActivated, activate, deactivate] = useFlag();
|
||||
|
||||
function handleClick() {
|
||||
onToggleReaction(reaction.reaction);
|
||||
@ -45,18 +41,23 @@ const ReactionSelectorReaction: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('&', IS_COMPACT_MENU && 'compact', chosen && 'chosen')}
|
||||
className={cn('&', chosen && 'chosen')}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={isReady ? activate : undefined}
|
||||
onMouseEnter={isReady && !isFirstPlay ? activate : undefined}
|
||||
>
|
||||
{shouldRenderStatic && (
|
||||
<div
|
||||
className={cn('static')}
|
||||
style={`background-position-x: ${previewIndex * -REACTION_SIZE}px;`}
|
||||
{!isAnimationLoaded && (
|
||||
<AnimatedSticker
|
||||
key={reaction.appearAnimation?.id}
|
||||
tgsUrl={mediaAppearData}
|
||||
play={isFirstPlay}
|
||||
noLoop
|
||||
size={REACTION_SIZE}
|
||||
onEnded={unmarkIsFirstPlay}
|
||||
/>
|
||||
)}
|
||||
{shouldRenderAnimated && (
|
||||
{!isFirstPlay && (
|
||||
<AnimatedSticker
|
||||
key={reaction.selectAnimation?.id}
|
||||
tgsUrl={mediaData}
|
||||
play={isActivated}
|
||||
noLoop
|
||||
|
||||
@ -21,7 +21,7 @@ import TopicIcon from '../common/TopicIcon';
|
||||
import InputText from '../ui/InputText';
|
||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker';
|
||||
import CustomEmojiPicker from '../common/CustomEmojiPicker';
|
||||
import Transition from '../ui/Transition';
|
||||
|
||||
import styles from './ManageTopic.module.scss';
|
||||
|
||||
@ -20,7 +20,7 @@ import InputText from '../ui/InputText';
|
||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import Loading from '../ui/Loading';
|
||||
import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker';
|
||||
import CustomEmojiPicker from '../common/CustomEmojiPicker';
|
||||
import Transition from '../ui/Transition';
|
||||
|
||||
import styles from './ManageTopic.module.scss';
|
||||
|
||||
@ -7,7 +7,7 @@ import { fastRaf } from '../../util/schedulers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useContextMenuPosition from '../../hooks/useContextMenuPosition';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
@ -124,7 +124,7 @@ const ListItem: FC<OwnProps> = ({
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useContextMenuPosition(
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
|
||||
@ -2,17 +2,18 @@ import type { RefObject } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
import useVirtualBackdrop from '../../hooks/useVirtualBackdrop';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import { IS_BACKDROP_BLUR_SUPPORTED } from '../../util/windowEnvironment';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
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';
|
||||
|
||||
@ -82,6 +83,7 @@ const Menu: FC<OwnProps> = ({
|
||||
menuRef = ref;
|
||||
}
|
||||
const backdropContainerRef = containerRef || menuRef;
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
|
||||
const {
|
||||
transitionClassNames,
|
||||
@ -135,7 +137,7 @@ const Menu: FC<OwnProps> = ({
|
||||
id={id}
|
||||
className={buildClassName(
|
||||
'Menu no-selection',
|
||||
!noCompact && IS_COMPACT_MENU && 'compact',
|
||||
!noCompact && !isTouchScreen && 'compact',
|
||||
!IS_BACKDROP_BLUR_SUPPORTED && 'no-blur',
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -4,7 +4,7 @@ import React, { useCallback } from '../../lib/teact/teact';
|
||||
import { IS_TEST } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { IS_COMPACT_MENU } from '../../util/windowEnvironment';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
|
||||
import './MenuItem.scss';
|
||||
|
||||
@ -42,6 +42,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
||||
} = props;
|
||||
|
||||
const lang = useLang();
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (disabled || !onClick) {
|
||||
e.stopPropagation();
|
||||
@ -73,7 +74,7 @@ const MenuItem: FC<MenuItemProps> = (props) => {
|
||||
className,
|
||||
disabled && 'disabled',
|
||||
destructive && 'destructive',
|
||||
IS_COMPACT_MENU && 'compact',
|
||||
!isTouchScreen && 'compact',
|
||||
withWrap && 'wrap',
|
||||
);
|
||||
|
||||
|
||||
@ -79,6 +79,14 @@ export const PROFILE_SENSITIVE_AREA = 500;
|
||||
export const TOPIC_LIST_SENSITIVE_AREA = 600;
|
||||
export const COMMON_CHATS_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_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 EMOJI_STATUS_LOOP_LIMIT = 2;
|
||||
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 FAVORITE_SYMBOL_SET_ID = 'favorite';
|
||||
export const CHAT_STICKER_SET_ID = 'chatStickers';
|
||||
|
||||
@ -11,6 +11,7 @@ import './ui/payments';
|
||||
import './ui/calls';
|
||||
import './ui/mediaViewer';
|
||||
import './ui/passcode';
|
||||
import './ui/reactions';
|
||||
|
||||
import './api/initial';
|
||||
import './api/chats';
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
|
||||
import type { ActionReturnType } from '../../types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage, selectCurrentChat, selectTabState,
|
||||
@ -13,12 +16,11 @@ import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions
|
||||
import {
|
||||
addChatMessagesById, addChats, addUsers, updateChatMessage,
|
||||
} 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 * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { isSameReaction, getUserReactions, isMessageLocal } from '../../helpers';
|
||||
|
||||
const INTERACTION_RANDOM_OFFSET = 40;
|
||||
|
||||
@ -38,6 +40,9 @@ addActionHandler('loadAvailableReactions', async (global): Promise<void> => {
|
||||
if (availableReaction.centerIcon) {
|
||||
mediaLoader.fetch(`sticker${availableReaction.centerIcon.id}`, ApiMediaFormat.BlobUrl);
|
||||
}
|
||||
if (availableReaction.appearAnimation) {
|
||||
mediaLoader.fetch(`sticker${availableReaction.appearAnimation.id}`, ApiMediaFormat.BlobUrl);
|
||||
}
|
||||
});
|
||||
|
||||
global = getGlobal();
|
||||
@ -104,15 +109,20 @@ addActionHandler('sendDefaultReaction', (global, actions, payload): ActionReturn
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('toggleReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, reaction, tabId = getCurrentTabId() } = payload;
|
||||
addActionHandler('toggleReaction', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chatId,
|
||||
reaction,
|
||||
shouldAddToRecent,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
let { messageId } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
let message = selectChatMessage(global, chatId, messageId);
|
||||
|
||||
if (!chat || !message) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
const limit = selectMaxUserReactions(global);
|
||||
|
||||
const reactions = newUserReactions.slice(-limit);
|
||||
|
||||
void callApi('sendReaction', { chat, messageId, reactions });
|
||||
|
||||
const { animationLevel } = global.settings.byKey;
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
if (animationLevel === ANIMATION_LEVEL_MAX) {
|
||||
const newActiveReactions = hasReaction ? omit(tabState.activeReactions, [messageId]) : {
|
||||
...tabState.activeReactions,
|
||||
@ -155,7 +161,21 @@ addActionHandler('toggleReaction', (global, actions, payload): ActionReturnType
|
||||
}, 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 => {
|
||||
@ -395,3 +415,45 @@ addActionHandler('readAllReactions', (global, actions, payload): ActionReturnTyp
|
||||
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();
|
||||
break;
|
||||
|
||||
case 'updateRecentReactions':
|
||||
actions.loadRecentReactions();
|
||||
break;
|
||||
|
||||
case 'updateRecentEmojiStatuses':
|
||||
actions.loadRecentEmojiStatuses();
|
||||
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',
|
||||
'recentEmojis',
|
||||
'recentCustomEmojis',
|
||||
'topReactions',
|
||||
'recentReactions',
|
||||
'push',
|
||||
'serviceNotifications',
|
||||
'attachmentSettings',
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
ApiReaction,
|
||||
ApiReactions,
|
||||
ApiReactionCount,
|
||||
ApiAvailableReaction,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
@ -49,6 +50,21 @@ export function canSendReaction(reaction: ApiReaction, chatReactions: ApiChatRea
|
||||
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[] {
|
||||
return message.reactions?.results?.filter((r): r is Required<ApiReactionCount> => isReactionChosen(r))
|
||||
.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'],
|
||||
recentCustomEmojis: ['5377305978079288312'],
|
||||
topReactions: [],
|
||||
recentReactions: [],
|
||||
|
||||
stickers: {
|
||||
setsById: {},
|
||||
|
||||
@ -74,3 +74,10 @@ export function selectIsForumPanelOpen<T extends GlobalState>(
|
||||
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,
|
||||
FocusDirection,
|
||||
GlobalSearchContent,
|
||||
IAnchorPosition,
|
||||
InlineBotSettings,
|
||||
ISettings,
|
||||
IThemeSettings,
|
||||
@ -246,6 +247,12 @@ export type TabState = {
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
reactionPicker?: {
|
||||
chatId?: string;
|
||||
messageId?: number;
|
||||
position?: IAnchorPosition;
|
||||
};
|
||||
|
||||
inlineBots: {
|
||||
isLoading: boolean;
|
||||
byUsername: Record<string, false | InlineBotSettings>;
|
||||
@ -687,6 +694,8 @@ export type GlobalState = {
|
||||
|
||||
recentEmojis: string[];
|
||||
recentCustomEmojis: string[];
|
||||
topReactions: ApiReaction[];
|
||||
recentReactions: ApiReaction[];
|
||||
|
||||
stickers: {
|
||||
setsById: Record<string, ApiStickerSet>;
|
||||
@ -1717,7 +1726,10 @@ export interface ActionPayloads {
|
||||
};
|
||||
|
||||
// Reactions
|
||||
loadTopReactions: undefined;
|
||||
loadRecentReactions: undefined;
|
||||
loadAvailableReactions: undefined;
|
||||
clearRecentReactions: undefined;
|
||||
|
||||
loadMessageReactions: {
|
||||
chatId: string;
|
||||
@ -1728,6 +1740,7 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
reaction: ApiReaction;
|
||||
shouldAddToRecent?: boolean;
|
||||
} & WithTabId;
|
||||
|
||||
setDefaultReaction: {
|
||||
@ -1748,6 +1761,13 @@ export interface ActionPayloads {
|
||||
reaction: ApiReaction;
|
||||
} & WithTabId;
|
||||
|
||||
openReactionPicker: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
position: IAnchorPosition;
|
||||
} & WithTabId;
|
||||
closeReactionPicker: WithTabId | undefined;
|
||||
|
||||
// Media Viewer & Audio Player
|
||||
openMediaViewer: {
|
||||
chatId?: string;
|
||||
|
||||
@ -11,7 +11,7 @@ import { createCallbackManager } from '../util/callbacks';
|
||||
import { updateSizes } from '../util/windowSize';
|
||||
import useForceUpdate from './useForceUpdate';
|
||||
|
||||
type MediaQueryCacheKey = 'mobile' | 'tablet' | 'landscape';
|
||||
type MediaQueryCacheKey = 'mobile' | 'tablet' | 'landscape' | 'touch';
|
||||
|
||||
const mediaQueryCache = new Map<MediaQueryCacheKey, MediaQueryList>();
|
||||
const callbacks = createCallbackManager();
|
||||
@ -19,6 +19,7 @@ const callbacks = createCallbackManager();
|
||||
let isMobile: boolean | undefined;
|
||||
let isTablet: boolean | undefined;
|
||||
let isLandscape: boolean | undefined;
|
||||
let isTouchScreen: boolean | undefined;
|
||||
|
||||
export function getIsMobile() {
|
||||
return isMobile;
|
||||
@ -32,6 +33,7 @@ function handleMediaQueryChange() {
|
||||
isMobile = mediaQueryCache.get('mobile')?.matches || false;
|
||||
isTablet = !isMobile && (mediaQueryCache.get('tablet')?.matches || false);
|
||||
isLandscape = mediaQueryCache.get('landscape')?.matches || false;
|
||||
isTouchScreen = mediaQueryCache.get('touch')?.matches || false;
|
||||
updateSizes();
|
||||
callbacks.runCallbacks();
|
||||
}
|
||||
@ -56,6 +58,10 @@ function initMediaQueryCache() {
|
||||
);
|
||||
mediaQueryCache.set('landscape', landscapeQuery);
|
||||
landscapeQuery.addEventListener('change', handleMediaQueryChange);
|
||||
|
||||
const isTouchScreenQuery = window.matchMedia('(pointer: coarse)');
|
||||
mediaQueryCache.set('touch', isTouchScreenQuery);
|
||||
isTouchScreenQuery.addEventListener('change', handleMediaQueryChange);
|
||||
}
|
||||
|
||||
initMediaQueryCache();
|
||||
@ -71,5 +77,6 @@ export default function useAppLayout() {
|
||||
isTablet,
|
||||
isLandscape,
|
||||
isDesktop: !isMobile && !isTablet,
|
||||
isTouchScreen,
|
||||
};
|
||||
}
|
||||
|
||||
@ -31,7 +31,9 @@ export default function useBoundsInSharedCanvas(
|
||||
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 canvasBounds = canvas.getBoundingClientRect();
|
||||
|
||||
|
||||
@ -6,7 +6,10 @@ interface Layout {
|
||||
extraTopPadding?: number;
|
||||
marginSides?: number;
|
||||
extraMarginTop?: number;
|
||||
menuElMinWidth?: number;
|
||||
shouldAvoidNegativePosition?: 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;
|
||||
@ -15,7 +18,7 @@ const EMPTY_RECT = {
|
||||
width: 0, left: 0, height: 0, top: 0,
|
||||
};
|
||||
|
||||
export default function useContextMenuPosition(
|
||||
export default function useMenuPosition(
|
||||
anchor: IAnchorPosition | undefined,
|
||||
getTriggerElement: () => HTMLElement | null,
|
||||
getRootElement: () => HTMLElement | null,
|
||||
@ -48,21 +51,25 @@ export default function useContextMenuPosition(
|
||||
extraTopPadding = 0,
|
||||
marginSides = 0,
|
||||
extraMarginTop = 0,
|
||||
menuElMinWidth = 0,
|
||||
shouldAvoidNegativePosition = false,
|
||||
withPortal = false,
|
||||
isDense = false,
|
||||
} = getLayout?.() || {};
|
||||
|
||||
const marginTop = menuEl ? parseInt(getComputedStyle(menuEl).marginTop, 10) + extraMarginTop : undefined;
|
||||
|
||||
const { offsetWidth: menuElWidth, offsetHeight: menuElHeight } = menuEl || { offsetWidth: 0, offsetHeight: 0 };
|
||||
const menuRect = menuEl ? {
|
||||
width: menuEl.offsetWidth,
|
||||
height: menuEl.offsetHeight + marginTop!,
|
||||
width: Math.max(menuElWidth, menuElMinWidth),
|
||||
height: menuElHeight + marginTop!,
|
||||
} : EMPTY_RECT;
|
||||
|
||||
const rootRect = rootEl ? rootEl.getBoundingClientRect() : EMPTY_RECT;
|
||||
|
||||
let horizontalPosition: 'left' | 'right';
|
||||
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;
|
||||
horizontalPosition = 'left';
|
||||
} 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';
|
||||
} else {
|
||||
verticalPosition = 'bottom';
|
||||
@ -107,12 +114,23 @@ export default function useContextMenuPosition(
|
||||
x - triggerRect.left,
|
||||
rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX,
|
||||
);
|
||||
const left = (horizontalPosition === 'left'
|
||||
? (withPortal
|
||||
let left = (horizontalPosition === 'left'
|
||||
? (withPortal || shouldAvoidNegativePosition
|
||||
? Math.max(MENU_POSITION_VISUAL_COMFORT_SPACE_PX, leftWithPossibleNegative)
|
||||
: leftWithPossibleNegative)
|
||||
: (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);
|
||||
|
||||
@ -1278,6 +1278,9 @@ messages.transcribeAudio#269e9a49 peer:InputPeer msg_id:int = messages.Transcrib
|
||||
messages.getCustomEmojiDocuments#d9ab0f54 document_id:Vector<long> = Vector<Document>;
|
||||
messages.getEmojiStickers#fbfca18f hash:long = messages.AllStickers;
|
||||
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;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
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.readReactions",
|
||||
"messages.getUnreadReactions",
|
||||
"messages.getTopReactions",
|
||||
"messages.getRecentReactions",
|
||||
"messages.clearRecentReactions",
|
||||
"messages.readMentions",
|
||||
"messages.getUnreadMentions",
|
||||
"help.getPremiumPromo",
|
||||
|
||||
@ -219,6 +219,7 @@ $color-message-reaction-own-hover: #b5e0a4;
|
||||
--z-ui-loader-mask: 2000;
|
||||
--z-notification: 1700;
|
||||
--z-confetti: 1600;
|
||||
--z-reaction-picker: 1200;
|
||||
--z-right-column: 900;
|
||||
--z-header-menu: 990;
|
||||
--z-header-menu-backdrop: 980;
|
||||
|
||||
@ -3,7 +3,7 @@ import type {
|
||||
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
||||
ApiChatInviteImporter,
|
||||
ApiExportedInvite,
|
||||
ApiLanguage, ApiMessage, ApiStickerSet,
|
||||
ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet,
|
||||
} from '../api/types';
|
||||
|
||||
export type TextPart = TeactNode;
|
||||
@ -223,10 +223,10 @@ export enum SettingsScreens {
|
||||
DoNotTranslate,
|
||||
}
|
||||
|
||||
export type StickerSetOrRecent = Pick<ApiStickerSet, (
|
||||
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
||||
'id' | 'accessHash' | 'title' | 'count' | 'stickers' | 'hasThumbnail' | 'isLottie' | 'isVideos' | 'isEmoji' |
|
||||
'installedDate' | 'isArchived'
|
||||
)>;
|
||||
)> & { reactions?: ApiReaction[] };
|
||||
|
||||
export enum LeftColumnContent {
|
||||
ChatList,
|
||||
|
||||
@ -109,7 +109,6 @@ if (IS_OPFS_SUPPORTED) {
|
||||
export const IS_OFFSET_PATH_SUPPORTED = CSS.supports('offset-rotate: 0deg');
|
||||
export const IS_BACKDROP_BLUR_SUPPORTED = CSS.supports('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_MULTITAB_SUPPORTED = 'BroadcastChannel' in window;
|
||||
export const IS_OPEN_IN_NEW_TAB_SUPPORTED = IS_MULTITAB_SUPPORTED && !(IS_PWA && IS_MOBILE);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user