Reactions: Add maximum unique reactions limit, add optimistic UI for first reaction (#2205)
This commit is contained in:
parent
6657e23889
commit
2a0ad055f1
@ -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,
|
||||
|
||||
@ -174,6 +174,7 @@ export interface ApiAppConfig {
|
||||
isPremiumPurchaseBlocked: boolean;
|
||||
premiumPromoOrder: string[];
|
||||
defaultEmojiStatusesStickerSetId: string;
|
||||
maxUniqueReactions: number;
|
||||
limits: Record<ApiLimitType, readonly [number, number]>;
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,10 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
const chatIdRef = useRef<string>();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !isClosing) {
|
||||
chatIdRef.current = undefined;
|
||||
}
|
||||
|
||||
if (isClosing && !isOpen) {
|
||||
stopClosing();
|
||||
setChosenTab(undefined);
|
||||
@ -96,14 +100,14 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
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<OwnProps & StateProps> = ({
|
||||
const count = reactions?.results.find((l) => l.reaction === reaction)?.count;
|
||||
return (
|
||||
<Button
|
||||
key={reaction}
|
||||
className={buildClassName(chosenTab === reaction && 'chosen')}
|
||||
size="tiny"
|
||||
ripple
|
||||
@ -186,7 +191,7 @@ const ReactorListModal: FC<OwnProps & StateProps> = ({
|
||||
<Button
|
||||
className="confirm-dialog-button"
|
||||
isText
|
||||
onClick={closeReactorListModal}
|
||||
onClick={handleClose}
|
||||
>
|
||||
{lang('Close')}
|
||||
</Button>
|
||||
|
||||
@ -89,6 +89,7 @@ type StateProps = {
|
||||
canShowSeenBy?: boolean;
|
||||
enabledReactions?: string[];
|
||||
canScheduleUntilOnline?: boolean;
|
||||
maxUniqueReactions?: number;
|
||||
};
|
||||
|
||||
const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
@ -117,6 +118,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canRemoveReaction,
|
||||
canEdit,
|
||||
enabledReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isCurrentUserPremium,
|
||||
canForward,
|
||||
@ -402,6 +404,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canBuyPremium={canBuyPremium}
|
||||
isOpen={isMenuOpen}
|
||||
enabledReactions={enabledReactions}
|
||||
maxUniqueReactions={maxUniqueReactions}
|
||||
anchor={anchor}
|
||||
canShowReactionsCount={canShowReactionsCount}
|
||||
canShowReactionList={canShowReactionList}
|
||||
@ -488,7 +491,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
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<OwnProps>(
|
||||
activeDownloads,
|
||||
canShowSeenBy,
|
||||
enabledReactions: chat?.isForbidden ? undefined : chat?.fullInfo?.enabledReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isCurrentUserPremium,
|
||||
hasFullInfo: Boolean(chat?.fullInfo),
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
isPrivate,
|
||||
isCurrentUserPremium,
|
||||
enabledReactions,
|
||||
maxUniqueReactions,
|
||||
anchor,
|
||||
canSendNow,
|
||||
canReschedule,
|
||||
@ -171,6 +173,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
|
||||
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<OwnProps> = ({
|
||||
{canShowReactionList && (
|
||||
<ReactionSelector
|
||||
enabledReactions={enabledReactions}
|
||||
currentReactions={currentReactions}
|
||||
maxUniqueReactions={maxUniqueReactions}
|
||||
onSendReaction={onSendReaction!}
|
||||
isPrivate={isPrivate}
|
||||
availableReactions={availableReactions}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useLayoutEffect, useRef } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useLayoutEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiAvailableReaction } from '../../../api/types';
|
||||
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
@ -8,10 +10,8 @@ import useFlag from '../../../hooks/useFlag';
|
||||
import { getTouchY } from '../../../util/scrollLock';
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
import { IS_COMPACT_MENU } from '../../../util/environment';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import './ReactionSelector.scss';
|
||||
|
||||
@ -20,6 +20,8 @@ type OwnProps = {
|
||||
onSendReaction: (reaction: string, x: number, y: number) => 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<OwnProps> = ({
|
||||
availableReactions,
|
||||
enabledReactions,
|
||||
onSendReaction,
|
||||
currentReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
isReady,
|
||||
canBuyPremium,
|
||||
isCurrentUserPremium,
|
||||
onSendReaction,
|
||||
}) => {
|
||||
const { openPremiumModal } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const itemsScrollRef = useRef<HTMLDivElement>(null);
|
||||
const [isHorizontalScrollEnabled, enableHorizontalScroll] = useFlag(false);
|
||||
@ -55,7 +56,17 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className={cn('&', IS_COMPACT_MENU && 'compact')} onWheelCapture={handleWheel} onTouchMove={handleWheel}>
|
||||
@ -63,9 +74,8 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
<div className={cn('bubble-small')} />
|
||||
<div className={cn('items-wrapper')}>
|
||||
<div className={cn('items', ['no-scrollbar'])} ref={itemsScrollRef}>
|
||||
{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 (
|
||||
<ReactionSelectorReaction
|
||||
key={reaction.reaction}
|
||||
@ -76,23 +86,6 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{canBuyPremium && Boolean(
|
||||
availableReactions
|
||||
.filter((r) => r.isPremium && (!enabledReactions || enabledReactions.includes(r.reaction)))
|
||||
.length,
|
||||
) && (
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
className={cn('blocked-button')}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openPremiumModal({
|
||||
initialSection: 'unique_reactions',
|
||||
})}
|
||||
>
|
||||
<i className="icon-lock-badge" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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,
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user