diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index 4f842534d..57e26b67f 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -25,6 +25,8 @@ interface GramJsAppConfig extends LimitsConfig { reactions_uniq_max: number; chat_read_mark_size_threshold: number; chat_read_mark_expire_period: number; + reactions_user_max_default: number; + reactions_user_max_premium: number; autologin_domains: string[]; autologin_token: string; url_auth_domains: string[]; @@ -77,6 +79,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue): ApiAppConfig { premiumPromoOrder: appConfig.premium_promo_order, isPremiumPurchaseBlocked: appConfig.premium_purchase_blocked, defaultEmojiStatusesStickerSetId: appConfig.default_emoji_statuses_stickerset_id, + maxUserReactionsDefault: appConfig.reactions_user_max_default, + maxUserReactionsPremium: appConfig.reactions_user_max_premium, limits: { uploadMaxFileparts: getLimit(appConfig, 'upload_max_fileparts', 'uploadMaxFileparts'), stickersFaved: getLimit(appConfig, 'stickers_faved_limit', 'stickersFaved'), diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 76ba95b8f..52812738e 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -12,6 +12,7 @@ import type { ApiChatInviteImporter, ApiChatSettings, ApiSendAsPeerId, + ApiChatReactions, } from '../../types'; import { pick, pickTruthy } from '../../../util/iteratees'; import { @@ -462,14 +463,18 @@ export function buildApiChatSettings({ }; } -export function buildApiChatReactions(availableReactions?: GramJs.TypeChatReactions): string[] | undefined { - if (availableReactions instanceof GramJs.ChatReactionsAll) { - // TODO Hack before custom reactions are implemented - // eslint-disable-next-line max-len - return ['๐Ÿ‘', '๐Ÿ‘Ž', 'โค', '๐Ÿ”ฅ', '๐Ÿฅฐ', '๐Ÿ‘', '๐Ÿ˜', '๐Ÿค”', '๐Ÿคฏ', '๐Ÿ˜ฑ', '๐Ÿคฌ', '๐Ÿ˜ข', '๐ŸŽ‰', '๐Ÿคฉ', '๐Ÿคฎ', '๐Ÿ’ฉ', '๐Ÿ™', '๐Ÿ‘Œ', '๐Ÿ•Š', '๐Ÿคก', '๐Ÿฅฑ', '๐Ÿฅด', '๐Ÿ˜', '๐Ÿณ', 'โคโ€๐Ÿ”ฅ', '๐ŸŒš', '๐ŸŒญ', '๐Ÿ’ฏ', '๐Ÿคฃ', 'โšก', '๐ŸŒ', '๐Ÿ†', '๐Ÿ’”', '๐Ÿคจ', '๐Ÿ˜', '๐Ÿ“', '๐Ÿพ', '๐Ÿ’‹', '๐Ÿ–•', '๐Ÿ˜ˆ', '๐Ÿ˜ด', '๐Ÿ˜ญ', '๐Ÿค“', '๐Ÿ‘ป', '๐Ÿ‘จโ€๐Ÿ’ป', '๐Ÿ‘€', '๐ŸŽƒ', '๐Ÿ™ˆ', '๐Ÿ˜‡', '๐Ÿ˜จ', '๐Ÿค', 'โœ๏ธ', '๐Ÿค—', '๐Ÿซก']; +export function buildApiChatReactions(chatReactions?: GramJs.TypeChatReactions): ApiChatReactions | undefined { + if (chatReactions instanceof GramJs.ChatReactionsAll) { + return { + type: 'all', + areCustomAllowed: chatReactions.allowCustom, + }; } - if (availableReactions instanceof GramJs.ChatReactionsSome) { - return availableReactions.reactions.map(buildApiReaction).filter(Boolean); + if (chatReactions instanceof GramJs.ChatReactionsSome) { + return { + type: 'some', + allowed: chatReactions.reactions.map(buildApiReaction).filter(Boolean), + }; } return undefined; diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 964ee54b4..62c49bfa7 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -34,6 +34,8 @@ import type { ApiWebDocument, ApiMessageEntityDefault, ApiMessageExtendedMediaPreview, + ApiReaction, + ApiReactionEmoji, } from '../../types'; import { ApiMessageEntityTypes, @@ -223,20 +225,30 @@ export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiRe return { canSeeList, - results: results.map(buildReactionCount).filter(Boolean), + results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator), recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean), }; } +function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) { + const diff = b.count - a.count; + if (diff) return diff; + if (a.chosenOrder !== undefined && b.chosenOrder !== undefined) { + return a.chosenOrder - b.chosenOrder; + } + if (a.chosenOrder !== undefined) return 1; + if (b.chosenOrder !== undefined) return -1; + return 0; +} + function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined { const { chosenOrder, count, reaction } = reactionCount; - // TODO: Add custom reactions support const apiReaction = buildApiReaction(reaction); if (!apiReaction) return undefined; return { - isChosen: chosenOrder !== undefined, // TODO: Add custom reactions support + chosenOrder, count, reaction: apiReaction, }; @@ -247,7 +259,6 @@ export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReactio peerId, reaction, big, unread, } = userReaction; - // TODO: Add custom reactions support const apiReaction = buildApiReaction(reaction); if (!apiReaction) return undefined; @@ -259,11 +270,19 @@ export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReactio }; } -export function buildApiReaction(reaction: GramJs.TypeReaction): string | undefined { +export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | undefined { if (reaction instanceof GramJs.ReactionEmoji) { - return reaction.emoticon; + return { + emoticon: reaction.emoticon, + }; } - // TODO: Add custom reactions support + + if (reaction instanceof GramJs.ReactionCustomEmoji) { + return { + documentId: reaction.documentId.toString(), + }; + } + return undefined; } @@ -281,7 +300,7 @@ export function buildApiAvailableReaction(availableReaction: GramJs.AvailableRea staticIcon: buildApiDocument(staticIcon), aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined, centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined, - reaction, + reaction: { emoticon: reaction } as ApiReactionEmoji, title, isInactive: inactive, isPremium: premium, diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index a5d8e2ac0..02f125cc7 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -6,7 +6,7 @@ import type { } from '../../types'; import type { ApiPrivacySettings, ApiPrivacyKey, PrivacyVisibility } from '../../../types'; -import { buildApiDocument } from './messages'; +import { buildApiDocument, buildApiReaction } from './messages'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { pick } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; @@ -224,8 +224,7 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA } export function buildApiConfig(config: GramJs.Config): ApiConfig { - const defaultReaction = config.reactionsDefault - && 'emoticon' in config.reactionsDefault ? config.reactionsDefault.emoticon : undefined; + const defaultReaction = config.reactionsDefault && buildApiReaction(config.reactionsDefault); return { expiresAt: config.expires, gifSearchUsername: config.gifSearchUsername, diff --git a/src/api/gramjs/apiBuilders/symbols.ts b/src/api/gramjs/apiBuilders/symbols.ts index cddc3f8d4..9e836b74b 100644 --- a/src/api/gramjs/apiBuilders/symbols.ts +++ b/src/api/gramjs/apiBuilders/symbols.ts @@ -171,7 +171,7 @@ export function buildStickerSetCovered(coveredStickerSet: GramJs.TypeStickerSetC export function buildApiEmojiInteraction(json: GramJsEmojiInteraction): ApiEmojiInteraction { return { - timestamps: json.a.map((l) => l.t), + timestamps: json.a.map(({ t }) => t), }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 22c0d7959..d95c7c3db 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -20,6 +20,8 @@ import type { ApiThemeParameters, ApiPoll, ApiRequestInputInvoice, + ApiChatReactions, + ApiReaction, } from '../../types'; import { ApiMessageEntityTypes, @@ -547,15 +549,34 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) { } } -export function buildInputReaction(reaction?: string) { - if (!reaction) return new GramJs.ReactionEmpty(); - return new GramJs.ReactionEmoji({ - emoticon: reaction, - }); +export function buildInputReaction(reaction?: ApiReaction) { + if (reaction && 'emoticon' in reaction) { + return new GramJs.ReactionEmoji({ + emoticon: reaction.emoticon, + }); + } + + if (reaction && 'documentId' in reaction) { + return new GramJs.ReactionCustomEmoji({ + documentId: BigInt(reaction.documentId), + }); + } + + return new GramJs.ReactionEmpty(); } -export function buildInputChatReactions(chatReactions: string[]) { - return new GramJs.ChatReactionsSome({ - reactions: chatReactions.map(buildInputReaction), - }); +export function buildInputChatReactions(chatReactions?: ApiChatReactions) { + if (chatReactions?.type === 'all') { + return new GramJs.ChatReactionsAll({ + allowCustom: chatReactions.areCustomAllowed, + }); + } + + if (chatReactions?.type === 'some') { + return new GramJs.ChatReactionsSome({ + reactions: chatReactions.allowed.map(buildInputReaction), + }); + } + + return new GramJs.ChatReactionsNone(); } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 8592512ba..ca9a9bad0 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -12,7 +12,7 @@ import type { ApiChatBannedRights, ApiChatAdminRights, ApiGroupCall, - ApiUserStatus, ApiPhoto, + ApiUserStatus, ApiPhoto, ApiChatReactions, } from '../../types'; import { @@ -1266,7 +1266,7 @@ export async function importChatInvite({ hash }: { hash: string }) { export function setChatEnabledReactions({ chat, enabledReactions, }: { - chat: ApiChat; enabledReactions: string[]; + chat: ApiChat; enabledReactions?: ApiChatReactions; }) { return invokeRequest(new GramJs.messages.SetChatAvailableReactions({ peer: buildInputPeer(chat.id, chat.accessHash), diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index ce2a18d24..354631586 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -42,7 +42,7 @@ export { faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet, searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects, removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets, - fetchFeaturedEmojiStickers, + fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, } from './symbols'; export { diff --git a/src/api/gramjs/methods/reactions.ts b/src/api/gramjs/methods/reactions.ts index 00f66e981..6db6382ad 100644 --- a/src/api/gramjs/methods/reactions.ts +++ b/src/api/gramjs/methods/reactions.ts @@ -1,4 +1,4 @@ -import type { ApiChat } from '../../types'; +import type { ApiChat, ApiReaction } from '../../types'; import { invokeRequest } from './client'; import { Api as GramJs } from '../../../lib/gramjs'; import { buildInputPeer, buildInputReaction } from '../gramjsBuilders'; @@ -74,12 +74,12 @@ export async function getAvailableReactions() { } export function sendReaction({ - chat, messageId, reaction, + chat, messageId, reactions, }: { - chat: ApiChat; messageId: number; reaction?: string; + chat: ApiChat; messageId: number; reactions?: ApiReaction[]; }) { return invokeRequest(new GramJs.messages.SendReaction({ - ...(reaction && { reaction: [buildInputReaction(reaction)] }), + reaction: reactions?.map((r) => buildInputReaction(r)), peer: buildInputPeer(chat.id, chat.accessHash), msgId: messageId, }), true); @@ -99,7 +99,7 @@ export function fetchMessageReactions({ export async function fetchMessageReactionsList({ chat, messageId, reaction, offset, }: { - chat: ApiChat; messageId: number; reaction?: string; offset?: string; + chat: ApiChat; messageId: number; reaction?: ApiReaction; offset?: string; }) { const result = await invokeRequest(new GramJs.messages.GetMessageReactionsList({ peer: buildInputPeer(chat.id, chat.accessHash), @@ -128,7 +128,7 @@ export async function fetchMessageReactionsList({ export function setDefaultReaction({ reaction, }: { - reaction: string; + reaction: ApiReaction; }) { return invokeRequest(new GramJs.messages.SetDefaultReaction({ reaction: buildInputReaction(reaction), diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index 9271486d7..8b384e42d 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -218,6 +218,21 @@ export async function fetchAnimatedEmojiEffects() { }; } +export async function fetchGenericEmojiEffects() { + const result = await invokeRequest(new GramJs.messages.GetStickerSet({ + stickerset: new GramJs.InputStickerSetEmojiGenericAnimations(), + })); + + if (!(result instanceof GramJs.messages.StickerSet)) { + return undefined; + } + + return { + set: buildStickerSet(result.set), + stickers: processStickerResult(result.documents), + }; +} + export async function fetchPremiumGifts() { const result = await invokeRequest(new GramJs.messages.GetStickerSet({ stickerset: new GramJs.InputStickerSetPremiumGifts(), diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index dca2be7eb..3ce421db3 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -1,4 +1,6 @@ -import type { ApiMessage, ApiPhoto, ApiStickerSet } from './messages'; +import type { + ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet, +} from './messages'; import type { ApiBotCommand } from './bots'; import type { ApiChatInviteImporter } from './misc'; import type { ApiFakeType, ApiUsername } from './users'; @@ -101,7 +103,7 @@ export interface ApiChatFullInfo { }; linkedChatId?: string; botCommands?: ApiBotCommand[]; - enabledReactions?: string[]; + enabledReactions?: ApiChatReactions; sendAsId?: string; canViewStatistics?: boolean; recentRequesterIds?: string[]; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 6e59436f1..524e36ad4 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -436,15 +436,15 @@ export interface ApiReactions { export interface ApiUserReaction { userId: string; - reaction: string; + reaction: ApiReaction; isBig?: boolean; isUnread?: boolean; } export interface ApiReactionCount { - isChosen?: boolean; + chosenOrder?: number; count: number; - reaction: string; + reaction: ApiReaction; } export interface ApiAvailableReaction { @@ -454,12 +454,34 @@ export interface ApiAvailableReaction { staticIcon?: ApiDocument; centerIcon?: ApiDocument; aroundAnimation?: ApiDocument; - reaction: string; + reaction: ApiReactionEmoji; title: string; isInactive?: boolean; isPremium?: boolean; } +type ApiChatReactionsAll = { + type: 'all'; + areCustomAllowed?: true; +}; + +type ApiChatReactionsSome = { + type: 'some'; + allowed: ApiReaction[]; +}; + +export type ApiChatReactions = ApiChatReactionsAll | ApiChatReactionsSome; + +export type ApiReactionEmoji = { + emoticon: string; +}; + +export type ApiReactionCustomEmoji = { + documentId: string; +}; + +export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji; + export interface ApiThreadInfo { threadId: number; chatId: string; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index c56491b7f..f3db42632 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -1,4 +1,4 @@ -import type { ApiDocument, ApiPhoto } from './messages'; +import type { ApiDocument, ApiPhoto, ApiReaction } from './messages'; import type { ApiUser } from './users'; import type { ApiLimitType } from '../../global/types'; @@ -174,12 +174,14 @@ export interface ApiAppConfig { premiumPromoOrder: string[]; defaultEmojiStatusesStickerSetId: string; maxUniqueReactions: number; + maxUserReactionsDefault: number; + maxUserReactionsPremium: number; limits: Record; } export interface ApiConfig { expiresAt: number; - defaultReaction?: string; + defaultReaction?: ApiReaction; gifSearchUsername?: string; maxGroupSize: number; } diff --git a/src/components/common/CustomEmoji.module.scss b/src/components/common/CustomEmoji.module.scss index d4e0ea149..b78365d5e 100644 --- a/src/components/common/CustomEmoji.module.scss +++ b/src/components/common/CustomEmoji.module.scss @@ -29,7 +29,7 @@ } .root, .media, .thumb { - border-radius: 0 !important; + border-radius: var(--custom-emoji-border-radius) !important; } .highlightCatch { diff --git a/src/components/common/ReactionStaticEmoji.tsx b/src/components/common/ReactionStaticEmoji.tsx index 781815580..63a55b925 100644 --- a/src/components/common/ReactionStaticEmoji.tsx +++ b/src/components/common/ReactionStaticEmoji.tsx @@ -1,35 +1,62 @@ -import type { RefObject } from 'react'; -import type { FC } from '../../lib/teact/teact'; -import React, { memo } from '../../lib/teact/teact'; -import { getGlobal } from '../../global'; +import React, { memo, useMemo } 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 { ApiMediaFormat } from '../../api/types'; -import useMedia from '../../hooks/useMedia'; import buildClassName from '../../util/buildClassName'; +import { isSameReaction } from '../../global/helpers'; +import useMediaTransition from '../../hooks/useMediaTransition'; +import useMedia from '../../hooks/useMedia'; + +import CustomEmoji from './CustomEmoji'; + +import blankUrl from '../../assets/blank.png'; import './ReactionStaticEmoji.scss'; type OwnProps = { - reaction: string; - ref?: RefObject; + reaction: ApiReaction; + availableReactions?: ApiAvailableReaction[]; className?: string; + size?: number; + observeIntersection?: ObserveFn; }; const ReactionStaticEmoji: FC = ({ reaction, - ref, + availableReactions, className, + size, + observeIntersection, }) => { - const staticIconId = getGlobal().availableReactions?.find((l) => l.reaction === reaction)?.staticIcon?.id; + const isCustom = 'documentId' in reaction; + const availableReaction = useMemo(() => ( + availableReactions?.find((available) => isSameReaction(available.reaction, reaction)) + ), [availableReactions, reaction]); + const staticIconId = availableReaction?.staticIcon?.id; const mediaData = useMedia(`document${staticIconId}`, !staticIconId, ApiMediaFormat.BlobUrl); + const transitionClassNames = useMediaTransition(mediaData); + + if (isCustom) { + return ( + + ); + } + return ( ); }; diff --git a/src/components/common/hooks/useCustomEmoji.ts b/src/components/common/hooks/useCustomEmoji.ts index 2fb47018d..de1cedbe9 100644 --- a/src/components/common/hooks/useCustomEmoji.ts +++ b/src/components/common/hooks/useCustomEmoji.ts @@ -7,19 +7,22 @@ import { addCustomEmojiCallback, removeCustomEmojiCallback } from '../../../util import useEnsureCustomEmoji from '../../../hooks/useEnsureCustomEmoji'; -export default function useCustomEmoji(documentId: string) { - const [customEmoji, setCustomEmoji] = useState(getGlobal().customEmojis.byId[documentId]); +export default function useCustomEmoji(documentId?: string) { + const [customEmoji, setCustomEmoji] = useState( + documentId ? getGlobal().customEmojis.byId[documentId] : undefined, + ); useEnsureCustomEmoji(documentId); const handleGlobalChange = useCallback(() => { + if (!documentId) return; setCustomEmoji(getGlobal().customEmojis.byId[documentId]); }, [documentId]); useEffect(handleGlobalChange, [documentId, handleGlobalChange]); useEffect(() => { - if (customEmoji) return undefined; + if (customEmoji || !documentId) return undefined; addCustomEmojiCallback(handleGlobalChange, documentId); diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index c505fd9eb..f0a2ded16 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -357,9 +357,7 @@ } .SettingsDefaultReaction { - .ReactionStaticEmoji { - width: 1.5rem; - height: 1.5rem; + .current-default-reaction { margin-inline-end: 2rem; } } diff --git a/src/components/left/settings/SettingsQuickReaction.tsx b/src/components/left/settings/SettingsQuickReaction.tsx index 029ff9a51..bd45047dc 100644 --- a/src/components/left/settings/SettingsQuickReaction.tsx +++ b/src/components/left/settings/SettingsQuickReaction.tsx @@ -1,11 +1,9 @@ -import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useCallback } from '../../../lib/teact/teact'; +import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { ApiAvailableReaction } from '../../../api/types'; -import { selectIsCurrentUserPremium } from '../../../global/selectors'; - import useHistoryBack from '../../../hooks/useHistoryBack'; import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; @@ -18,14 +16,12 @@ type OwnProps = { type StateProps = { availableReactions?: ApiAvailableReaction[]; - isPremium?: boolean; selectedReaction?: string; }; const SettingsQuickReaction: FC = ({ isActive, availableReactions, - isPremium, selectedReaction, onReset, }) => { @@ -36,17 +32,23 @@ const SettingsQuickReaction: FC = ({ onBack: onReset, }); - const options = availableReactions?.filter((l) => ( - !(l.isInactive || (!isPremium && l.isPremium)) - )).map((l) => { - return { - label: <>{l.title}, - value: l.reaction, - }; - }) || []; + const options = useMemo(() => ( + (availableReactions || []).filter((availableReaction) => !availableReaction.isInactive) + .map((availableReaction) => ({ + label: ( + <> + + {availableReaction.title} + + ), + value: availableReaction.reaction.emoticon, + })) + ), [availableReactions]); const handleChange = useCallback((reaction: string) => { - setDefaultReaction({ reaction }); + setDefaultReaction({ + reaction: { emoticon: reaction }, + }); }, [setDefaultReaction]); return ( @@ -64,12 +66,10 @@ const SettingsQuickReaction: FC = ({ export default memo(withGlobal( (global) => { const { availableReactions, config } = global; - const isPremium = selectIsCurrentUserPremium(global); return { availableReactions, selectedReaction: config?.defaultReaction, - isPremium, }; }, )(SettingsQuickReaction)); diff --git a/src/components/left/settings/SettingsStickers.tsx b/src/components/left/settings/SettingsStickers.tsx index 3fb44a7c8..f726d2461 100644 --- a/src/components/left/settings/SettingsStickers.tsx +++ b/src/components/left/settings/SettingsStickers.tsx @@ -6,10 +6,16 @@ import { getActions, withGlobal } from '../../../global'; import type { FC } from '../../../lib/teact/teact'; import { SettingsScreens } from '../../../types'; import type { ISettings } from '../../../types'; -import type { ApiSticker, ApiStickerSet } from '../../../api/types'; +import type { + ApiAvailableReaction, + ApiReaction, + ApiSticker, + ApiStickerSet, +} from '../../../api/types'; import renderText from '../../common/helpers/renderText'; import { pick } from '../../../util/iteratees'; +import { REM } from '../../common/helpers/mediaDimensions'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import useHistoryBack from '../../../hooks/useHistoryBack'; @@ -20,6 +26,8 @@ import Checkbox from '../../ui/Checkbox'; import ListItem from '../../ui/ListItem'; import StickerSetCard from '../../common/StickerSetCard'; +const DEFAULT_REACTION_SIZE = 1.5 * REM; + type OwnProps = { isActive?: boolean; onScreenSelect: (screen: SettingsScreens) => void; @@ -34,7 +42,8 @@ type StateProps = addedSetIds?: string[]; customEmojiSetIds?: string[]; stickerSetsById: Record; - defaultReaction?: string; + defaultReaction?: ApiReaction; + availableReactions?: ApiAvailableReaction[]; }; const SettingsStickers: FC = ({ @@ -45,6 +54,7 @@ const SettingsStickers: FC = ({ defaultReaction, shouldSuggestStickers, shouldLoopStickers, + availableReactions, onReset, onScreenSelect, }) => { @@ -109,7 +119,12 @@ const SettingsStickers: FC = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => onScreenSelect(SettingsScreens.QuickReaction)} > - +
{lang('DoubleTapSetting')}
)} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index cf75e59a6..50502b280 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -191,6 +191,7 @@ const Main: FC = ({ loadAttachBots, loadContactList, loadCustomEmojis, + loadGenericEmojiEffects, closePaymentModal, clearReceipt, checkAppVersion, @@ -213,6 +214,7 @@ const Main: FC = ({ loadAppConfig(); loadAvailableReactions(); loadAnimatedEmojis(); + loadGenericEmojiEffects(); loadNotificationSettings(); loadNotificationExceptions(); loadTopInlineBots(); @@ -225,7 +227,7 @@ const Main: FC = ({ }, [ lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings, loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList, - loadPremiumGifts, checkAppVersion, loadConfig, + loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, ]); // Language-based API calls diff --git a/src/components/main/premium/PremiumFeatureModal.tsx b/src/components/main/premium/PremiumFeatureModal.tsx index 6c6537e8f..7030890d4 100644 --- a/src/components/main/premium/PremiumFeatureModal.tsx +++ b/src/components/main/premium/PremiumFeatureModal.tsx @@ -17,7 +17,6 @@ import { formatCurrency } from '../../../util/formatCurrency'; import Button from '../../ui/Button'; import PremiumLimitPreview from './common/PremiumLimitPreview'; import PremiumFeaturePreviewVideo from './previews/PremiumFeaturePreviewVideo'; -import PremiumFeaturePreviewReactions from './previews/PremiumFeaturePreviewReactions'; import SliderDots from '../../common/SliderDots'; import PremiumFeaturePreviewStickers from './previews/PremiumFeaturePreviewStickers'; @@ -25,7 +24,7 @@ import styles from './PremiumFeatureModal.module.scss'; export const PREMIUM_FEATURE_TITLES: Record = { double_limits: 'PremiumPreviewLimits', - unique_reactions: 'PremiumPreviewReactions', + infinite_reactions: 'PremiumPreviewReactions2', premium_stickers: 'PremiumPreviewStickers', animated_emoji: 'PremiumPreviewEmoji', no_ads: 'PremiumPreviewNoAds', @@ -39,7 +38,7 @@ export const PREMIUM_FEATURE_TITLES: Record = { export const PREMIUM_FEATURE_DESCRIPTIONS: Record = { double_limits: 'PremiumPreviewLimitsDescription', - unique_reactions: 'PremiumPreviewReactionsDescription', + infinite_reactions: 'PremiumPreviewReactions2Description', premium_stickers: 'PremiumPreviewStickersDescription', no_ads: 'PremiumPreviewNoAdsDescription', animated_emoji: 'PremiumPreviewEmojiDescription', @@ -57,7 +56,7 @@ export const PREMIUM_FEATURE_SECTIONS = [ 'faster_download', 'voice_to_text', 'no_ads', - 'unique_reactions', + 'infinite_reactions', 'premium_stickers', 'animated_emoji', 'advanced_chat_management', @@ -242,21 +241,6 @@ const PremiumFeatureModal: FC = ({ ); } - if (section === 'unique_reactions') { - return ( -
-
- -
-

- {lang(PREMIUM_FEATURE_TITLES.unique_reactions)} -

-
- {renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS.unique_reactions), ['br'])} -
-
- ); - } if (section === 'premium_stickers') { return ( diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx index db5e7bddd..73919357e 100644 --- a/src/components/main/premium/PremiumMainModal.tsx +++ b/src/components/main/premium/PremiumMainModal.tsx @@ -48,7 +48,7 @@ const LIMIT_ACCOUNTS = 4; const PREMIUM_FEATURE_COLOR_ICONS: Record = { double_limits: PremiumLimits, - unique_reactions: PremiumReactions, + infinite_reactions: PremiumReactions, premium_stickers: PremiumStickers, animated_emoji: PremiumEmoji, no_ads: PremiumAds, diff --git a/src/components/main/premium/previews/PremiumFeaturePreviewReactions.module.scss b/src/components/main/premium/previews/PremiumFeaturePreviewReactions.module.scss deleted file mode 100644 index d4c44a624..000000000 --- a/src/components/main/premium/previews/PremiumFeaturePreviewReactions.module.scss +++ /dev/null @@ -1,34 +0,0 @@ -.root { - display: flex; - align-items: center; - justify-content: center; - height: 100%; -} - -.sticker { - --x: 0px; - --y: 0px; - --scale: 0; - transition: 0.25s ease-in-out transform, 0.25s ease-in-out opacity; - position: absolute; - transform: - translate(var(--x), var(--y)) - scale(var(--scale)); - opacity: var(--scale); - - canvas { - width: 100% !important; - height: 100% !important; - } -} - -.effect-sticker { - composes: sticker; - z-index: 2; - pointer-events: none; - - canvas { - width: 100% !important; - height: 100% !important; - } -} diff --git a/src/components/main/premium/previews/PremiumFeaturePreviewReactions.tsx b/src/components/main/premium/previews/PremiumFeaturePreviewReactions.tsx deleted file mode 100644 index c962472fd..000000000 --- a/src/components/main/premium/previews/PremiumFeaturePreviewReactions.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import type { FC } from '../../../../lib/teact/teact'; -import React, { - memo, useCallback, useEffect, useRef, useState, -} from '../../../../lib/teact/teact'; -import { withGlobal } from '../../../../global'; - -import type { GlobalState } from '../../../../global/types'; -import type { ApiAvailableReaction } from '../../../../api/types'; - -import cycleRestrict from '../../../../util/cycleRestrict'; -import useMedia from '../../../../hooks/useMedia'; -import useInterval from '../../../../hooks/useInterval'; -import useFlag from '../../../../hooks/useFlag'; - -import AnimatedSticker from '../../../common/AnimatedSticker'; - -import styles from './PremiumFeaturePreviewReactions.module.scss'; - -type OwnProps = { - isActive: boolean; -}; - -type StateProps = { - availableReactions: GlobalState['availableReactions']; -}; - -const EMOJI_SIZE_MULTIPLIER = 0.2; -const EFFECT_SIZE_MULTIPLIER = 0.6; -const ROTATE_INTERVAL = 3000; -const CLICK_DELAY = 4000; -const MAX_EMOJIS = 15; - -const AnimatedCircleReaction: FC<{ - size: number; - realIndex: number; - reaction: ApiAvailableReaction; - index: number; - maxLength: number; - handleClick: (index: number) => void; - isActivated: boolean; - canPlay: boolean; -}> = ({ - size, realIndex, isActivated, canPlay, - reaction, index, maxLength, handleClick, -}) => { - const mediaData = useMedia(`document${reaction.activateAnimation?.id}`); - const mediaDataAround = useMedia(`document${reaction.aroundAnimation?.id}`); - const [isAnimated, animate, inanimate] = useFlag(isActivated); - const [isEffectEnded, markEffectEnded, unmarkEffectEnded] = useFlag(false); - - const circleSize = (size - size * EMOJI_SIZE_MULTIPLIER) / 2; - - const t = index / maxLength; - const angle = t * (Math.PI * 2); - const totalAngle = angle - (Math.PI / 6) * Math.cos(angle); - const scaleNotFull = 0.2 + (0.7 * (Math.sin(totalAngle) + 1)) / 2; - const scale = scaleNotFull > 0.85 ? 1 : scaleNotFull; - - const x = Math.cos(totalAngle) * circleSize; - const y = Math.sin(totalAngle) * circleSize * 0.6; - - const handleClickEmoji = useCallback(() => { - handleClick(realIndex); - }, [handleClick, realIndex]); - - useEffect(() => { - if (isActivated) { - animate(); - unmarkEffectEnded(); - } - }, [isActivated, animate, unmarkEffectEnded]); - - return ( - <> - {isActivated && !isEffectEnded && ( - - )} - - - ); -}; -const PremiumFeaturePreviewReactions: FC = ({ - availableReactions, isActive, -}) => { - // eslint-disable-next-line no-null/no-null - const containerRef = useRef(null); - const [isIntervalPaused, pauseInterval, unpauseInterval] = useFlag(); - const lastUnpauseTimeout = useRef(); - const [offset, setOffset] = useState(0); - const [size, setSize] = useState(0); - - const renderedReactions = availableReactions?.filter((l) => l.isPremium)?.slice(0, MAX_EMOJIS) || []; - - useInterval(() => { - setOffset((current) => cycleRestrict(renderedReactions.length, current + 1)); - }, isIntervalPaused || !isActive ? undefined : ROTATE_INTERVAL); - - const handleClickEmoji = useCallback((i: number) => { - setOffset(i); - pauseInterval(); - if (lastUnpauseTimeout.current) clearTimeout(lastUnpauseTimeout.current); - lastUnpauseTimeout.current = setTimeout(() => { - unpauseInterval(); - }, CLICK_DELAY); - }, [pauseInterval, unpauseInterval]); - - useEffect(() => { - const container = containerRef.current; - if (!container) return; - - setSize(container.closest('.modal-dialog')!.clientWidth); - }, []); - - return ( -
- {renderedReactions.map((l, i) => { - return ( - - ); - })} -
- ); -}; - -export default memo(withGlobal( - (global): StateProps => { - return { - availableReactions: global.availableReactions, - }; - }, -)(PremiumFeaturePreviewReactions)); diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index b8c7f165c..02cd4c8fb 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -242,7 +242,7 @@ const MessageList: FC = ({ if (!messageIds || !messagesById) { return; } - const ids = messageIds.filter((l) => messagesById[l]?.reactions); + const ids = messageIds.filter((id) => messagesById[id]?.reactions); if (!ids.length) return; @@ -592,7 +592,6 @@ const MessageList: FC = ({ isViewportNewest={Boolean(isViewportNewest)} isUnread={Boolean(firstUnreadId)} withUsers={withUsers} - areReactionsInMeta={isPrivate} noAvatars={noAvatars} containerRef={containerRef} anchorIdRef={anchorIdRef} diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 9e9475c47..820ef0596 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -39,7 +39,6 @@ interface OwnProps { threadId: number; type: MessageListType; isReady: boolean; - areReactionsInMeta: boolean; isScrollingRef: { current: boolean | undefined }; isScrollPatchNeededRef: { current: boolean | undefined }; threadTopMessageId: number | undefined; @@ -60,7 +59,6 @@ const MessageListContent: FC = ({ isViewportNewest, isUnread, withUsers, - areReactionsInMeta, noAvatars, containerRef, anchorIdRef, @@ -202,7 +200,6 @@ const MessageListContent: FC = ({ noAvatars={noAvatars} withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)} withSenderName={position.isFirstInGroup && withUsers && !isOwn} - areReactionsInMeta={areReactionsInMeta} threadId={threadId} messageListType={type} noComments={hasLinkedChat === false} diff --git a/src/components/middle/ReactorListModal.scss b/src/components/middle/ReactorListModal.scss index a66a51df6..82283c2d1 100644 --- a/src/components/middle/ReactorListModal.scss +++ b/src/components/middle/ReactorListModal.scss @@ -11,6 +11,12 @@ margin-bottom: 0.5rem; } + .icon-heart { + width: 1.125rem; + height: 1.125rem; + margin-right: 0.25rem; + } + .reaction-filter-emoji { margin-right: 0.25rem; } diff --git a/src/components/middle/ReactorListModal.tsx b/src/components/middle/ReactorListModal.tsx index 45d39662c..f83027dac 100644 --- a/src/components/middle/ReactorListModal.tsx +++ b/src/components/middle/ReactorListModal.tsx @@ -4,17 +4,19 @@ import React, { } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; -import type { ApiMessage } from '../../api/types'; +import type { ApiAvailableReaction, ApiMessage, ApiReaction } from '../../api/types'; import type { AnimationLevel } from '../../types'; import { LoadMoreDirection } from '../../types'; -import useLang from '../../hooks/useLang'; import { selectChatMessage } from '../../global/selectors'; -import useInfiniteScroll from '../../hooks/useInfiniteScroll'; -import useFlag from '../../hooks/useFlag'; import buildClassName from '../../util/buildClassName'; import { formatIntegerCompact } from '../../util/textFormat'; import { unique } from '../../util/iteratees'; +import { isSameReaction, getReactionUniqueKey } from '../../global/helpers'; + +import useLang from '../../hooks/useLang'; +import useInfiniteScroll from '../../hooks/useInfiniteScroll'; +import useFlag from '../../hooks/useFlag'; import InfiniteScroll from '../ui/InfiniteScroll'; import Modal from '../ui/Modal'; @@ -37,6 +39,7 @@ export type StateProps = Pick = ({ @@ -47,6 +50,7 @@ const ReactorListModal: FC = ({ messageId, seenByUserIds, animationLevel, + availableReactions, }) => { const { loadReactors, @@ -59,7 +63,7 @@ const ReactorListModal: FC = ({ const lang = useLang(); const [isClosing, startClosing, stopClosing] = useFlag(false); - const [chosenTab, setChosenTab] = useState(undefined); + const [chosenTab, setChosenTab] = useState(undefined); const canShowFilters = reactors && reactions && reactors.count >= MIN_REACTIONS_COUNT_FOR_FILTERS && reactions.results.length > 1; const chatIdRef = useRef(); @@ -99,14 +103,22 @@ const ReactorListModal: FC = ({ }, [chatId, loadReactors, messageId]); const allReactions = useMemo(() => { - return reactors?.reactions ? unique(reactors.reactions.map((l) => l.reaction)) : []; + const uniqueReactions: ApiReaction[] = []; + reactors?.reactions?.forEach(({ reaction }) => { + if (!uniqueReactions.some((r) => isSameReaction(r, reaction))) { + uniqueReactions.push(reaction); + } + }); + return uniqueReactions; }, [reactors]); const userIds = useMemo(() => { if (chosenTab) { - return reactors?.reactions.filter((l) => l.reaction === chosenTab).map((l) => l.userId); + return reactors?.reactions + .filter(({ reaction }) => isSameReaction(reaction, chosenTab)) + .map(({ userId }) => userId); } - return unique(reactors?.reactions.map((l) => l.userId).concat(seenByUserIds || []) || []); + return unique(reactors?.reactions.map(({ userId }) => userId).concat(seenByUserIds || []) || []); }, [chosenTab, reactors, seenByUserIds]); const [viewportIds, getMore] = useInfiniteScroll( @@ -138,17 +150,22 @@ const ReactorListModal: FC = ({ {reactors?.count && formatIntegerCompact(reactors.count)} {allReactions.map((reaction) => { - const count = reactions?.results.find((l) => l.reaction === reaction)?.count; + const count = reactions?.results + .find((reactionsCount) => isSameReaction(reactionsCount.reaction, reaction))?.count; return ( ); @@ -166,19 +183,26 @@ const ReactorListModal: FC = ({ {viewportIds?.flatMap( (userId) => { const user = usersById[userId]; - const userReactions = reactors?.reactions.filter((l) => l.userId === userId); + const userReactions = reactors?.reactions.filter((reactor) => reactor.userId === userId); const items: React.ReactNode[] = []; userReactions?.forEach((r) => { + if (chosenTab && !isSameReaction(r.reaction, chosenTab)) return; items.push( handleClick(userId)} > - {r.reaction && } + {r.reaction && ( + + )} , ); }); diff --git a/src/components/middle/composer/StickerPicker.tsx b/src/components/middle/composer/StickerPicker.tsx index 2b348a130..57e5f9a22 100644 --- a/src/components/middle/composer/StickerPicker.tsx +++ b/src/components/middle/composer/StickerPicker.tsx @@ -160,7 +160,7 @@ const StickerPicker: FC = ({ if (isCurrentUserPremium) { const addedPremiumStickers = existingAddedSetIds - .map((l) => l.stickers?.filter((sticker) => sticker.hasEffect)) + .map(({ stickers }) => stickers?.filter((sticker) => sticker.hasEffect)) .flat() .filter(Boolean); diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 0442e1b81..695a93e4e 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -6,7 +6,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global'; import type { MessageListType } from '../../../global/types'; import type { - ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet, + ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet, ApiChatReactions, ApiReaction, } from '../../../api/types'; import type { IAlbum, IAnchorPosition } from '../../../types'; @@ -28,7 +28,6 @@ import { } from '../../../global/helpers'; import { SERVICE_NOTIFICATIONS_USER_ID, TME_LINK_PREFIX } from '../../../config'; import buildClassName from '../../../util/buildClassName'; -import { REM } from '../../common/helpers/mediaDimensions'; import { copyTextToClipboard } from '../../../util/clipboard'; import useShowTransition from '../../../hooks/useShowTransition'; @@ -42,8 +41,6 @@ import PinMessageModal from '../../common/PinMessageModal'; import MessageContextMenu from './MessageContextMenu'; import ConfirmDialog from '../../ui/ConfirmDialog'; -const START_SIZE = 2 * REM; - export type OwnProps = { isOpen: boolean; chatUsername?: string; @@ -67,7 +64,6 @@ type StateProps = { canShowReactionsCount?: boolean; canBuyPremium?: boolean; canShowReactionList?: boolean; - canRemoveReaction?: boolean; canUnpin?: boolean; canDelete?: boolean; canReport?: boolean; @@ -87,7 +83,7 @@ type StateProps = { canClosePoll?: boolean; activeDownloads: number[]; canShowSeenBy?: boolean; - enabledReactions?: string[]; + enabledReactions?: ApiChatReactions; canScheduleUntilOnline?: boolean; maxUniqueReactions?: number; }; @@ -115,7 +111,6 @@ const ContextMenuContainer: FC = ({ canReport, canShowReactionsCount, canShowReactionList, - canRemoveReaction, canEdit, enabledReactions, maxUniqueReactions, @@ -150,7 +145,6 @@ const ContextMenuContainer: FC = ({ cancelMessageMediaDownload, loadSeenBy, openSeenByModal, - sendReaction, openReactorListModal, loadFullChat, loadReactors, @@ -159,6 +153,7 @@ const ContextMenuContainer: FC = ({ loadStickers, cancelPollVote, closePoll, + toggleReaction, } = getActions(); const lang = useLang(); @@ -376,12 +371,12 @@ const ContextMenuContainer: FC = ({ closeMenu(); }, [closeMenu, message, saveGif]); - const handleSendReaction = useCallback((reaction: string | undefined, x: number, y: number) => { - sendReaction({ - chatId: message.chatId, messageId: message.id, reaction, x, y, startSize: START_SIZE, + const handleToggleReaction = useCallback((reaction: ApiReaction) => { + toggleReaction({ + chatId: message.chatId, messageId: message.id, reaction, }); closeMenu(); - }, [closeMenu, message.chatId, message.id, sendReaction]); + }, [closeMenu, message, toggleReaction]); const reportMessageIds = useMemo(() => (album ? album.messages : [message]).map(({ id }) => id), [album, message]); @@ -408,7 +403,6 @@ const ContextMenuContainer: FC = ({ anchor={anchor} canShowReactionsCount={canShowReactionsCount} canShowReactionList={canShowReactionList} - canRemoveReaction={canRemoveReaction} canSendNow={canSendNow} canReschedule={canReschedule} canReply={canReply} @@ -453,7 +447,7 @@ const ContextMenuContainer: FC = ({ onCancelVote={handleCancelVote} onClosePoll={openClosePollDialog} onShowSeenBy={handleOpenSeenByModal} - onSendReaction={handleSendReaction} + onToggleReaction={handleToggleReaction} onShowReactors={handleOpenReactorListModal} /> ( const isAction = isActionMessage(message); const canShowReactionsCount = !isLocal && !isChannel && !isScheduled && !isAction && !isPrivate && message.reactions && !areReactionsEmpty(message.reactions) && message.reactions.canSeeList; - const canRemoveReaction = isPrivate && message.reactions?.results?.some((l) => l.isChosen); const isProtected = selectIsMessageProtected(global, message); const canCopyNumber = Boolean(message.content.contact); const isCurrentUserPremium = selectIsCurrentUserPremium(global); @@ -569,7 +562,6 @@ export default memo(withGlobal( hasFullInfo: Boolean(chat?.fullInfo), canShowReactionsCount, canShowReactionList: !isLocal && !isAction && !isScheduled && chat?.id !== SERVICE_NOTIFICATIONS_USER_ID, - canRemoveReaction, canBuyPremium: !isCurrentUserPremium && !selectIsPremiumPurchaseBlocked(global), customEmojiSetsInfo, customEmojiSets, diff --git a/src/components/middle/message/CustomReactionAnimation.module.scss b/src/components/middle/message/CustomReactionAnimation.module.scss new file mode 100644 index 000000000..20e194e5d --- /dev/null +++ b/src/components/middle/message/CustomReactionAnimation.module.scss @@ -0,0 +1,35 @@ +.root { + position: absolute; + z-index: 10; +} + +.particle { + position: absolute; + width: 1rem; + height: 1rem; + border-radius: 0.25rem; + offset-path: var(--offset-path); + offset-rotate: 0deg; + animation: 1.5s particle ease-out; +} + +@keyframes particle { + 0% { + offset-distance: 0%; + transform: scale(1); + } + + 50% { + transform: scale(1.25); + } + + 75% { + opacity: 1; + } + + 100% { + offset-distance: 100%; + opacity: 0; + transform: scale(1); + } +} diff --git a/src/components/middle/message/CustomReactionAnimation.tsx b/src/components/middle/message/CustomReactionAnimation.tsx new file mode 100644 index 000000000..22f6fbc93 --- /dev/null +++ b/src/components/middle/message/CustomReactionAnimation.tsx @@ -0,0 +1,56 @@ +import React, { memo, useMemo } from '../../../lib/teact/teact'; + +import type { FC } from '../../../lib/teact/teact'; +import type { ApiReactionCustomEmoji } from '../../../api/types'; + +import { getStickerPreviewHash } from '../../../global/helpers'; +import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/environment'; +import useMedia from '../../../hooks/useMedia'; + +import styles from './CustomReactionAnimation.module.scss'; + +type OwnProps = { + reaction: ApiReactionCustomEmoji; +}; + +const EFFECT_AMOUNT = 7; + +const CustomReactionAnimation: FC = ({ + reaction, +}) => { + const stickerHash = getStickerPreviewHash(reaction.documentId); + + const previewMediaData = useMedia(stickerHash); + + const paths: string[] = useMemo(() => { + if (!IS_OFFSET_PATH_SUPPORTED) return []; + return Array.from({ length: EFFECT_AMOUNT }).map(() => generateRandomDropPath()); + }, []); + + if (!previewMediaData) return undefined; + + return ( +
+ {paths.map((path) => { + const style = `--offset-path: path('${path}');`; + return ( + + ); + })} +
+ ); +}; + +export default memo(CustomReactionAnimation); + +function generateRandomDropPath() { + const x = (10 + Math.random() * 60) * (Math.random() > 0.5 ? 1 : -1); + const y = 20 + Math.random() * 80; + + return `M 0 0 C 0 0 ${x} ${-y - 20} ${x} ${y}`; +} diff --git a/src/components/middle/message/Message.scss b/src/components/middle/message/Message.scss index c7063b45d..90a2ddc28 100644 --- a/src/components/middle/message/Message.scss +++ b/src/components/middle/message/Message.scss @@ -72,6 +72,8 @@ } .quick-reaction { + --custom-emoji-size: 2rem; + cursor: pointer; position: absolute; right: -0.5rem; @@ -79,7 +81,7 @@ display: flex; align-items: center; justify-content: center; - transform: scale(1); + transform: scale(0.75); opacity: 0; transition: transform 0.2s ease-out, opacity 0.2s ease-out; transition-delay: 0.2s; @@ -90,16 +92,12 @@ &:hover { transition-delay: unset; - transform: scale(1.4); - } - - .ReactionStaticEmoji { - width: 1.125rem; + transform: scale(1); } } &.last-in-list .quick-reaction:hover { - transform: translateY(-0.1875rem) scale(1.4); + transform: translateY(-0.1875rem) scale(1); } &.own .quick-reaction { diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index a6872f459..feb323d5a 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -19,6 +19,8 @@ import type { ApiAvailableReaction, ApiChatMember, ApiUsername, + ApiReaction, + ApiStickerSet, } from '../../../api/types'; import type { AnimationLevel, FocusDirection, IAlbum, ISettings, @@ -83,7 +85,11 @@ import { import buildClassName from '../../../util/buildClassName'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; -import { calculateDimensionsForMessageMedia, ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions'; +import { + calculateDimensionsForMessageMedia, + REM, + ROUND_VIDEO_DIMENSIONS_PX, +} from '../../common/helpers/mediaDimensions'; import { buildContentClassName } from './helpers/buildContentClassName'; import { getMinMediaWidth, calculateMediaDimensions } from './helpers/mediaDimensions'; import { calculateAlbumLayout } from './helpers/calculateAlbumLayout'; @@ -153,7 +159,6 @@ type OwnProps = noAvatars?: boolean; withAvatar?: boolean; withSenderName?: boolean; - areReactionsInMeta?: boolean; threadId: number; messageListType: MessageListType; noComments: boolean; @@ -194,6 +199,7 @@ type StateProps = { highlight?: string; animatedEmoji?: string; animatedCustomEmoji?: string; + genericEffects?: ApiStickerSet; isInSelectMode?: boolean; isSelected?: boolean; isGroupSelected?: boolean; @@ -207,8 +213,8 @@ type StateProps = { threadInfo?: ApiThreadInfo; reactionMessage?: ApiMessage; availableReactions?: ApiAvailableReaction[]; - defaultReaction?: string; - activeReaction?: ActiveReaction; + defaultReaction?: ApiReaction; + activeReactions?: ActiveReaction[]; activeEmojiInteractions?: ActiveEmojiInteraction[]; hasUnreadReaction?: boolean; isTranscribing?: boolean; @@ -226,7 +232,6 @@ type MetaPosition = type ReactionsPosition = 'inside' | 'outside' - | 'in-meta' | 'none'; const NBSP = '\u00A0'; @@ -236,6 +241,7 @@ const APPENDIX_OWN = { __html: '' }; const APPEARANCE_DELAY = 10; const NO_MEDIA_CORNERS_THRESHOLD = 18; +const QUICK_REACTION_SIZE = 2 * REM; const Message: FC = ({ message, @@ -247,7 +253,6 @@ const Message: FC = ({ noAvatars, withAvatar, withSenderName, - areReactionsInMeta, noComments, appearanceOrder, isFirstInGroup, @@ -288,6 +293,7 @@ const Message: FC = ({ highlight, animatedEmoji, animatedCustomEmoji, + genericEffects, isInSelectMode, isSelected, isGroupSelected, @@ -295,7 +301,7 @@ const Message: FC = ({ reactionMessage, availableReactions, defaultReaction, - activeReaction, + activeReactions, activeEmojiInteractions, messageListType, isPinnedList, @@ -502,7 +508,7 @@ const Message: FC = ({ Boolean(message.inlineButtons) && 'has-inline-buttons', isSwiped && 'is-swiped', transitionClassNames, - (Boolean(activeReaction) || hasActiveStickerEffect) && 'has-active-reaction', + (Boolean(activeReactions) || hasActiveStickerEffect) && 'has-active-reaction', ); const { @@ -545,9 +551,7 @@ const Message: FC = ({ } let reactionsPosition!: ReactionsPosition; - if (areReactionsInMeta) { - reactionsPosition = 'in-meta'; - } else if (hasReactions) { + if (hasReactions) { if (isCustomShape || ((photo || video) && !hasText)) { reactionsPosition = 'outside'; } else if (asForwarded) { @@ -655,13 +659,10 @@ const Message: FC = ({ const meta = ( ); @@ -672,10 +673,12 @@ const Message: FC = ({ return ( ); } @@ -1100,10 +1103,15 @@ const Message: FC = ({ )} {withQuickReactionButton && (
- +
)} @@ -1114,8 +1122,10 @@ const Message: FC = ({ )} @@ -1260,7 +1270,7 @@ export default memo(withGlobal( threadInfo: actualThreadInfo, availableReactions: global.availableReactions, defaultReaction: isMessageLocal(message) ? undefined : selectDefaultReaction(global, chatId), - activeReaction: reactionMessage && global.activeReactions[reactionMessage.id], + activeReactions: reactionMessage && global.activeReactions[reactionMessage.id], activeEmojiInteractions: global.activeEmojiInteractions, ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(typeof uploadProgress === 'number' && { uploadProgress }), @@ -1271,6 +1281,7 @@ export default memo(withGlobal( isPremium: selectIsCurrentUserPremium(global), animationLevel: global.settings.byKey.animationLevel, senderAdminMember, + genericEffects: global.genericEmojiEffects, }; }, )(Message)); diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index beecb532c..cd8a9bcea 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -1,11 +1,11 @@ import React, { - memo, useMemo, useCallback, useEffect, useRef, + memo, useCallback, useEffect, useRef, } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { FC } from '../../../lib/teact/teact'; import type { - ApiAvailableReaction, ApiMessage, ApiSponsoredMessage, ApiStickerSet, ApiUser, + ApiAvailableReaction, ApiChatReactions, ApiMessage, ApiReaction, ApiSponsoredMessage, ApiStickerSet, ApiUser, } from '../../../api/types'; import type { IAnchorPosition } from '../../../types'; @@ -35,7 +35,7 @@ type OwnProps = { anchor: IAnchorPosition; message: ApiMessage | ApiSponsoredMessage; canSendNow?: boolean; - enabledReactions?: string[]; + enabledReactions?: ApiChatReactions; maxUniqueReactions?: number; canReschedule?: boolean; canReply?: boolean; @@ -45,7 +45,6 @@ type OwnProps = { canReport?: boolean; canShowReactionsCount?: boolean; canShowReactionList?: boolean; - canRemoveReaction?: boolean; canBuyPremium?: boolean; canEdit?: boolean; canForward?: boolean; @@ -90,7 +89,7 @@ type OwnProps = { onShowReactors?: () => void; onAboutAds?: () => void; onSponsoredHide?: () => void; - onSendReaction?: (reaction: string | undefined, x: number, y: number) => void; + onToggleReaction?: (reaction: ApiReaction) => void; }; const SCROLLBAR_WIDTH = 10; @@ -128,7 +127,6 @@ const MessageContextMenu: FC = ({ isDownloading, canShowSeenBy, canShowReactionsCount, - canRemoveReaction, canShowReactionList, seenByRecentUsers, hasCustomEmoji, @@ -155,7 +153,7 @@ const MessageContextMenu: FC = ({ onClosePoll, onShowSeenBy, onShowReactors, - onSendReaction, + onToggleReaction, onCopyMessages, onAboutAds, onSponsoredHide, @@ -166,18 +164,13 @@ const MessageContextMenu: FC = ({ // eslint-disable-next-line no-null/no-null const scrollableRef = useRef(null); const lang = useLang(); - const noReactions = !isPrivate && !enabledReactions?.length; + const noReactions = !isPrivate && !enabledReactions; const withReactions = canShowReactionList && !noReactions; const isSponsoredMessage = !('id' in message); const messageId = !isSponsoredMessage ? message.id : ''; const [isReady, markIsReady, unmarkIsReady] = useFlag(); - const currentReactions = useMemo(() => { - if (isSponsoredMessage) return undefined; - return message.reactions?.results.map((reaction) => reaction.reaction); - }, [isSponsoredMessage, message]); - const handleAfterCopy = useCallback(() => { showNotification({ message: lang('Share.Link.Copied'), @@ -239,10 +232,6 @@ const MessageContextMenu: FC = ({ }; }, [withReactions]); - const handleRemoveReaction = useCallback(() => { - onSendReaction!(undefined, 0, 0); - }, [onSendReaction]); - useEffect(() => { if (!isOpen) { unmarkIsReady(); @@ -280,12 +269,12 @@ const MessageContextMenu: FC = ({ onClose={onClose} onCloseAnimationEnd={onCloseAnimationEnd} > - {canShowReactionList && ( + {withReactions && ( = ({ style={menuStyle} ref={scrollableRef} > - {canRemoveReaction && Remove Reaction} {canSendNow && {lang('MessageScheduleSend')}} {canReschedule && ( {lang('MessageScheduleEditTime')} diff --git a/src/components/middle/message/MessageMeta.scss b/src/components/middle/message/MessageMeta.scss index 34722884c..c401888cc 100644 --- a/src/components/middle/message/MessageMeta.scss +++ b/src/components/middle/message/MessageMeta.scss @@ -13,12 +13,6 @@ max-width: 100%; user-select: none; - .ReactionAnimatedEmoji { - width: 1rem; - height: 1rem; - margin-right: 0.25rem; - } - .message-time, .message-imported, .message-signature, diff --git a/src/components/middle/message/MessageMeta.tsx b/src/components/middle/message/MessageMeta.tsx index 0084636db..dead41eae 100644 --- a/src/components/middle/message/MessageMeta.tsx +++ b/src/components/middle/message/MessageMeta.tsx @@ -3,7 +3,6 @@ import React, { memo, useMemo } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiAvailableReaction, ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types'; -import type { ActiveReaction } from '../../../global/types'; import { formatDateTimeToString, formatTime } from '../../../util/dateFormat'; import { formatIntegerCompact } from '../../../util/textFormat'; @@ -14,32 +13,29 @@ import useFlag from '../../../hooks/useFlag'; import buildClassName from '../../../util/buildClassName'; import MessageOutgoingStatus from '../../common/MessageOutgoingStatus'; -import ReactionAnimatedEmoji from './ReactionAnimatedEmoji'; import './MessageMeta.scss'; type OwnProps = { message: ApiMessage; - reactionMessage?: ApiMessage; - withReactions?: boolean; withReactionOffset?: boolean; outgoingStatus?: ApiMessageOutgoingStatus; signature?: string; - onClick: (e: React.MouseEvent) => void; - activeReaction?: ActiveReaction; availableReactions?: ApiAvailableReaction[]; + onClick: (e: React.MouseEvent) => void; }; const MessageMeta: FC = ({ - message, outgoingStatus, signature, onClick, withReactions, - activeReaction, withReactionOffset, availableReactions, - reactionMessage, + message, + outgoingStatus, + signature, + withReactionOffset, + onClick, }) => { const { showNotification } = getActions(); const lang = useLang(); const [isActivated, markActivated] = useFlag(); - const reactions = withReactions && reactionMessage?.reactions?.results.filter((l) => l.count > 0); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -80,14 +76,6 @@ const MessageMeta: FC = ({ onClick={onClick} data-ignore-on-paste > - {reactions && reactions.map((l) => ( - - ))} {Boolean(message.views) && ( <> diff --git a/src/components/middle/message/ReactionAnimatedEmoji.module.scss b/src/components/middle/message/ReactionAnimatedEmoji.module.scss new file mode 100644 index 000000000..6249f799a --- /dev/null +++ b/src/components/middle/message/ReactionAnimatedEmoji.module.scss @@ -0,0 +1,45 @@ +.root { + --custom-emoji-border-radius: 0.25rem; + + position: relative; + display: flex; + align-items: center; + justify-content: center; + + width: 1.125rem; + height: 1.125rem; + margin-right: 0.25rem; + + z-index: 2; + + &.is-custom-emoji { + margin-right: 0.375rem; + } +} + +.animated-icon, .effect { + position: fixed; + top: -0.375rem; + left: -0.375rem; + pointer-events: none; + + &.effect { + top: -2.5rem; + left: -2.5rem; + } + + &:not(:global(.open)) { + opacity: 1 !important; + } + + &:global(.closing) { + opacity: 0 !important; + } +} + +.animating { + // Fix for redundant scroll on iOS + transform: translateZ(0); + // Fix for redundant scroll in Firefox + contain: layout; +} diff --git a/src/components/middle/message/ReactionAnimatedEmoji.scss b/src/components/middle/message/ReactionAnimatedEmoji.scss deleted file mode 100644 index 7c5511642..000000000 --- a/src/components/middle/message/ReactionAnimatedEmoji.scss +++ /dev/null @@ -1,52 +0,0 @@ -.ReactionAnimatedEmoji { - position: relative; - display: flex; - align-items: center; - justify-content: center; - - &.is-animating { - // Fix for redundant scroll on iOS - transform: translateZ(0); - // Fix for redundant scroll in Firefox - contain: layout; - } - - .AnimatedSticker { - position: fixed; - top: -0.375rem; - left: -0.375rem; - pointer-events: none; - - &.effect { - top: -2.5rem; - left: -2.5rem; - } - - &:not(.open) { - opacity: 1 !important; - } - - &.closing { - opacity: 0 !important; - } - } - - &.in-meta { - .AnimatedSticker { - top: -0.4375rem; - left: -0.4375rem; - - &.effect { - top: -2.5625rem; - left: -2.5625rem; - } - - // Fix for weird positioning in Chrome - canvas { - position: absolute; - left: 0; - top: 0; - } - } - } -} diff --git a/src/components/middle/message/ReactionAnimatedEmoji.tsx b/src/components/middle/message/ReactionAnimatedEmoji.tsx index 85d556671..a0bca089c 100644 --- a/src/components/middle/message/ReactionAnimatedEmoji.tsx +++ b/src/components/middle/message/ReactionAnimatedEmoji.tsx @@ -1,41 +1,87 @@ -import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useCallback } from '../../../lib/teact/teact'; +import React, { + memo, useCallback, useMemo, useRef, +} from '../../../lib/teact/teact'; import { getActions } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; import type { ActiveReaction } from '../../../global/types'; -import type { ApiAvailableReaction } from '../../../api/types'; +import type { ApiAvailableReaction, ApiReaction, ApiStickerSet } from '../../../api/types'; +import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import buildClassName from '../../../util/buildClassName'; +import { isSameReaction } from '../../../global/helpers'; +import { REM } from '../../common/helpers/mediaDimensions'; + import useMedia from '../../../hooks/useMedia'; import useShowTransition from '../../../hooks/useShowTransition'; import useFlag from '../../../hooks/useFlag'; +import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; +import useCustomEmoji from '../../common/hooks/useCustomEmoji'; +import CustomEmoji from '../../common/CustomEmoji'; import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; import AnimatedSticker from '../../common/AnimatedSticker'; +import CustomReactionAnimation from './CustomReactionAnimation'; -import './ReactionAnimatedEmoji.scss'; +import styles from './ReactionAnimatedEmoji.module.scss'; type OwnProps = { - reaction: string; - activeReaction?: ActiveReaction; - isInMeta?: boolean; + reaction: ApiReaction; + activeReactions?: ActiveReaction[]; availableReactions?: ApiAvailableReaction[]; + genericEffects?: ApiStickerSet; + observeIntersection?: ObserveFn; }; -const CENTER_ICON_SIZE = 30; -const EFFECT_SIZE = 100; +const CENTER_ICON_SIZE = 1.875 * REM; +const EFFECT_SIZE = 6.25 * REM; const ReactionAnimatedEmoji: FC = ({ reaction, - activeReaction, - isInMeta, + genericEffects, + activeReactions, availableReactions, + observeIntersection, }) => { const { stopActiveReaction } = getActions(); - const availableReaction = availableReactions?.find((r) => r.reaction === reaction); + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + + const isCustom = 'documentId' in reaction; + + const availableReaction = useMemo(() => ( + availableReactions?.find((r) => isSameReaction(r.reaction, reaction)) + ), [availableReactions, reaction]); const centerIconId = availableReaction?.centerIcon?.id; - const effectId = availableReaction?.aroundAnimation?.id; + + const customEmoji = useCustomEmoji(isCustom ? reaction.documentId : undefined); + + const assignedEffectId = useMemo(() => { + if (!isCustom) return availableReaction?.aroundAnimation?.id; + + if (!customEmoji) return undefined; + const assignedId = availableReactions?.find((available) => available.reaction.emoticon === customEmoji.emoji) + ?.aroundAnimation?.id; + return assignedId; + }, [availableReaction, availableReactions, customEmoji, isCustom]); + + const effectId = useMemo(() => { + if (assignedEffectId) { + return assignedEffectId; + } + + if (!genericEffects?.stickers) { + return undefined; + } + + const { stickers } = genericEffects; + const randomIndex = Math.floor(Math.random() * stickers.length); + + return stickers[randomIndex].id; + }, [assignedEffectId, genericEffects]); + + const isIntersecting = useIsIntersecting(ref, observeIntersection); const mediaHashCenterIcon = centerIconId && `sticker${centerIconId}`; const mediaHashEffect = effectId && `sticker${effectId}`; @@ -43,51 +89,67 @@ const ReactionAnimatedEmoji: FC = ({ const mediaDataCenterIcon = useMedia(mediaHashCenterIcon, !centerIconId); const mediaDataEffect = useMedia(mediaHashEffect, !effectId); - const shouldPlay = Boolean(activeReaction?.reaction === reaction && mediaDataCenterIcon && mediaDataEffect); + const activeReaction = useMemo(() => ( + activeReactions?.find((active) => isSameReaction(active.reaction, reaction)) + ), [activeReactions, reaction]); + + const shouldPlay = Boolean(activeReaction && (isCustom || mediaDataCenterIcon) && mediaDataEffect); const { shouldRender: shouldRenderAnimation, transitionClassNames: animationClassNames, } = useShowTransition(shouldPlay, undefined, true, 'slow'); const handleEnded = useCallback(() => { - stopActiveReaction({ messageId: activeReaction?.messageId, reaction }); + if (!activeReaction?.messageId) return; + stopActiveReaction({ messageId: activeReaction.messageId, reaction }); }, [activeReaction?.messageId, reaction, stopActiveReaction]); const [isAnimationLoaded, markAnimationLoaded, unmarkAnimationLoaded] = useFlag(); - const shouldRenderStatic = !shouldPlay || !isAnimationLoaded; + const shouldRenderStatic = !isCustom && (!shouldPlay || !isAnimationLoaded); const className = buildClassName( - 'ReactionAnimatedEmoji', - isInMeta && 'in-meta', - shouldRenderAnimation && 'is-animating', + styles.root, + shouldRenderAnimation && styles.animating, + isCustom && styles.isCustomEmoji, ); return ( -
- {shouldRenderStatic && } +
+ {shouldRenderStatic && } + {isCustom && ( + + )} {shouldRenderAnimation && ( <> - + {isCustom ? ( + !assignedEffectId && isIntersecting && + ) : ( + + )} )}
diff --git a/src/components/middle/message/ReactionButton.tsx b/src/components/middle/message/ReactionButton.tsx index 21d2f5392..35ba0e847 100644 --- a/src/components/middle/message/ReactionButton.tsx +++ b/src/components/middle/message/ReactionButton.tsx @@ -1,14 +1,16 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import { getActions, getGlobal } from '../../../global'; +import type { FC } from '../../../lib/teact/teact'; +import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { - ApiAvailableReaction, ApiMessage, ApiReactionCount, ApiUser, + ApiAvailableReaction, ApiMessage, ApiReactionCount, ApiStickerSet, ApiUser, } from '../../../api/types'; import type { ActiveReaction } from '../../../global/types'; import buildClassName from '../../../util/buildClassName'; import { formatIntegerCompact } from '../../../util/textFormat'; +import { isSameReaction, isReactionChosen } from '../../../global/helpers'; import Button from '../../ui/Button'; import Avatar from '../../common/Avatar'; @@ -17,25 +19,28 @@ import AnimatedCounter from '../../common/AnimatedCounter'; import './Reactions.scss'; -const MAX_REACTORS_AVATARS = 3; - const ReactionButton: FC<{ reaction: ApiReactionCount; message: ApiMessage; - activeReaction?: ActiveReaction; + activeReactions?: ActiveReaction[]; availableReactions?: ApiAvailableReaction[]; + withRecentReactors?: boolean; + genericEffects?: ApiStickerSet; + observeIntersection?: ObserveFn; }> = ({ reaction, message, - activeReaction, + activeReactions, availableReactions, + withRecentReactors, + genericEffects, + observeIntersection, }) => { - const { sendReaction } = getActions(); - + const { toggleReaction } = getActions(); const { recentReactions } = message.reactions!; const recentReactors = useMemo(() => { - if (!recentReactions || reaction.count > MAX_REACTORS_AVATARS) { + if (!withRecentReactors || !recentReactions) { return undefined; } @@ -43,29 +48,31 @@ const ReactionButton: FC<{ const usersById = getGlobal().users.byId; return recentReactions - .filter((recentReaction) => recentReaction.reaction === reaction.reaction) + .filter((recentReaction) => isSameReaction(recentReaction.reaction, reaction.reaction)) .map((recentReaction) => usersById[recentReaction.userId]) .filter(Boolean) as ApiUser[]; - }, [reaction, recentReactions]); + }, [reaction.reaction, recentReactions, withRecentReactors]); const handleClick = useCallback(() => { - sendReaction({ - reaction: reaction.isChosen ? undefined : reaction.reaction, + toggleReaction({ + reaction: reaction.reaction, chatId: message.chatId, messageId: message.id, }); - }, [message, reaction, sendReaction]); + }, [message, reaction, toggleReaction]); return (