From 2a0ad055f1c21d6c88a79865df2b0122cac76b9f Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Thu, 15 Dec 2022 19:19:27 +0100 Subject: [PATCH] Reactions: Add maximum unique reactions limit, add optimistic UI for first reaction (#2205) --- src/api/gramjs/apiBuilders/appConfig.ts | 1 + src/api/types/misc.ts | 1 + src/components/middle/ReactorListModal.tsx | 11 ++-- .../middle/message/ContextMenuContainer.tsx | 6 ++- .../middle/message/MessageContextMenu.tsx | 13 ++++- .../middle/message/ReactionSelector.tsx | 51 ++++++++----------- src/global/actions/api/reactions.ts | 11 +--- src/global/reducers/reactions.ts | 6 +-- 8 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index a87b7e450..9107dc064 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -73,6 +73,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue): ApiAppConfig { autologinDomains: appConfig.autologin_domains || [], autologinToken: appConfig.autologin_token || '', urlAuthDomains: appConfig.url_auth_domains || [], + maxUniqueReactions: appConfig.reactions_uniq_max, premiumBotUsername: appConfig.premium_bot_username, premiumInvoiceSlug: appConfig.premium_invoice_slug, premiumPromoOrder: appConfig.premium_promo_order, diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 70581c66d..6d8e50128 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -174,6 +174,7 @@ export interface ApiAppConfig { isPremiumPurchaseBlocked: boolean; premiumPromoOrder: string[]; defaultEmojiStatusesStickerSetId: string; + maxUniqueReactions: number; limits: Record; } diff --git a/src/components/middle/ReactorListModal.tsx b/src/components/middle/ReactorListModal.tsx index 08f30f92c..45d39662c 100644 --- a/src/components/middle/ReactorListModal.tsx +++ b/src/components/middle/ReactorListModal.tsx @@ -65,6 +65,10 @@ const ReactorListModal: FC = ({ const chatIdRef = useRef(); useEffect(() => { + if (isOpen && !isClosing) { + chatIdRef.current = undefined; + } + if (isClosing && !isOpen) { stopClosing(); setChosenTab(undefined); @@ -96,14 +100,14 @@ const ReactorListModal: FC = ({ const allReactions = useMemo(() => { return reactors?.reactions ? unique(reactors.reactions.map((l) => l.reaction)) : []; - }, [reactors?.reactions]); + }, [reactors]); const userIds = useMemo(() => { if (chosenTab) { return reactors?.reactions.filter((l) => l.reaction === chosenTab).map((l) => l.userId); } return unique(reactors?.reactions.map((l) => l.userId).concat(seenByUserIds || []) || []); - }, [chosenTab, reactors?.reactions, seenByUserIds]); + }, [chosenTab, reactors, seenByUserIds]); const [viewportIds, getMore] = useInfiniteScroll( handleLoadMore, userIds, reactors && reactors.nextOffset === undefined, @@ -137,6 +141,7 @@ const ReactorListModal: FC = ({ const count = reactions?.results.find((l) => l.reaction === reaction)?.count; return ( diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 3131d8c82..0442e1b81 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -89,6 +89,7 @@ type StateProps = { canShowSeenBy?: boolean; enabledReactions?: string[]; canScheduleUntilOnline?: boolean; + maxUniqueReactions?: number; }; const ContextMenuContainer: FC = ({ @@ -117,6 +118,7 @@ const ContextMenuContainer: FC = ({ canRemoveReaction, canEdit, enabledReactions, + maxUniqueReactions, isPrivate, isCurrentUserPremium, canForward, @@ -402,6 +404,7 @@ const ContextMenuContainer: FC = ({ canBuyPremium={canBuyPremium} isOpen={isMenuOpen} enabledReactions={enabledReactions} + maxUniqueReactions={maxUniqueReactions} anchor={anchor} canShowReactionsCount={canShowReactionsCount} canShowReactionList={canShowReactionList} @@ -488,7 +491,7 @@ export default memo(withGlobal( const { threadId } = selectCurrentMessageList(global) || {}; const activeDownloads = selectActiveDownloadIds(global, message.chatId); const chat = selectChat(global, message.chatId); - const { seenByExpiresAt, seenByMaxChatMembers } = global.appConfig || {}; + const { seenByExpiresAt, seenByMaxChatMembers, maxUniqueReactions } = global.appConfig || {}; const { noOptions, canReply, @@ -560,6 +563,7 @@ export default memo(withGlobal( activeDownloads, canShowSeenBy, enabledReactions: chat?.isForbidden ? undefined : chat?.fullInfo?.enabledReactions, + maxUniqueReactions, isPrivate, isCurrentUserPremium, hasFullInfo: Boolean(chat?.fullInfo), diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index 72853eefc..beecb532c 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -1,9 +1,9 @@ -import type { FC } from '../../../lib/teact/teact'; import React, { - memo, useCallback, useEffect, useRef, + memo, useMemo, 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, } from '../../../api/types'; @@ -36,6 +36,7 @@ type OwnProps = { message: ApiMessage | ApiSponsoredMessage; canSendNow?: boolean; enabledReactions?: string[]; + maxUniqueReactions?: number; canReschedule?: boolean; canReply?: boolean; canPin?: boolean; @@ -103,6 +104,7 @@ const MessageContextMenu: FC = ({ isPrivate, isCurrentUserPremium, enabledReactions, + maxUniqueReactions, anchor, canSendNow, canReschedule, @@ -171,6 +173,11 @@ const MessageContextMenu: FC = ({ 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'), @@ -276,6 +283,8 @@ const MessageContextMenu: FC = ({ {canShowReactionList && ( void; isPrivate?: boolean; availableReactions?: ApiAvailableReaction[]; + currentReactions?: string[]; + maxUniqueReactions?: number; isReady?: boolean; canBuyPremium?: boolean; isCurrentUserPremium?: boolean; @@ -30,13 +32,12 @@ const cn = createClassNameBuilder('ReactionSelector'); const ReactionSelector: FC = ({ availableReactions, enabledReactions, - onSendReaction, + currentReactions, + maxUniqueReactions, isPrivate, isReady, - canBuyPremium, - isCurrentUserPremium, + onSendReaction, }) => { - const { openPremiumModal } = getActions(); // eslint-disable-next-line no-null/no-null const itemsScrollRef = useRef(null); const [isHorizontalScrollEnabled, enableHorizontalScroll] = useFlag(false); @@ -55,7 +56,17 @@ const ReactionSelector: FC = ({ } }; - if ((!isPrivate && !enabledReactions?.length) || !availableReactions) return undefined; + const reactionsToRender = useMemo(() => { + return availableReactions?.map((reaction) => { + if (reaction.isInactive) return undefined; + if (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction))) return undefined; + if (maxUniqueReactions && currentReactions && currentReactions.length >= maxUniqueReactions + && !currentReactions.includes(reaction.reaction)) return undefined; + return reaction; + }) || []; + }, [availableReactions, currentReactions, enabledReactions, isPrivate, maxUniqueReactions]); + + if (!reactionsToRender.length) return undefined; return (
@@ -63,9 +74,8 @@ const ReactionSelector: FC = ({
- {availableReactions?.map((reaction, i) => { - if (reaction.isInactive || (reaction.isPremium && !isCurrentUserPremium) - || (!isPrivate && (!enabledReactions || !enabledReactions.includes(reaction.reaction)))) return undefined; + {reactionsToRender.map((reaction, i) => { + if (!reaction) return undefined; return ( = ({ /> ); })} - {canBuyPremium && Boolean( - availableReactions - .filter((r) => r.isPremium && (!enabledReactions || enabledReactions.includes(r.reaction))) - .length, - ) && ( - - )}
diff --git a/src/global/actions/api/reactions.ts b/src/global/actions/api/reactions.ts index 3d2f12fcd..35a03c8b7 100644 --- a/src/global/actions/api/reactions.ts +++ b/src/global/actions/api/reactions.ts @@ -224,17 +224,8 @@ addActionHandler('loadReactors', async (global, actions, payload) => { global = addUsers(global, buildCollectionByKey(result.users, 'id')); } - const { nextOffset, count, reactions } = result; - setGlobal(updateChatMessage(global, chatId, messageId, { - reactors: { - nextOffset, - count, - reactions: [ - ...(message.reactors?.reactions || []), - ...reactions, - ], - }, + reactors: result, })); }); diff --git a/src/global/reducers/reactions.ts b/src/global/reducers/reactions.ts index aebdc4dad..716a1534a 100644 --- a/src/global/reducers/reactions.ts +++ b/src/global/reducers/reactions.ts @@ -36,11 +36,7 @@ export function subtractXForEmojiInteraction(global: GlobalState, x: number) { } export function addMessageReaction(global: GlobalState, chatId: string, messageId: number, reaction: string) { - const { reactions } = selectChatMessage(global, chatId, messageId) || {}; - - if (!reactions) { - return global; - } + const reactions = selectChatMessage(global, chatId, messageId)?.reactions || { results: [] }; // Update UI without waiting for server response let results = reactions.results.map((l) => (l.reaction === reaction