397 lines
12 KiB
TypeScript
397 lines
12 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import React, {
|
|
memo, useEffect, useMemo, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
|
|
|
import type {
|
|
ApiChat, ApiMessage, ApiPaidReactionPrivacyType,
|
|
ApiPeer,
|
|
ApiSendAsPeerId,
|
|
ApiStarsAmount, ApiUser,
|
|
} from '../../../api/types';
|
|
import type { TabState } from '../../../global/types';
|
|
import type { CustomPeer } from '../../../types';
|
|
|
|
import { STARS_ICON_PLACEHOLDER } from '../../../config';
|
|
import { getPeerTitle } from '../../../global/helpers';
|
|
import { isApiPeerUser } from '../../../global/helpers/peers';
|
|
import {
|
|
selectChat, selectChatMessage, selectPeer, selectUser,
|
|
} from '../../../global/selectors';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { formatInteger } from '../../../util/textFormat';
|
|
import renderText from '../../common/helpers/renderText';
|
|
|
|
import useAppLayout from '../../../hooks/useAppLayout';
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
|
|
import Avatar from '../../common/Avatar';
|
|
import FullNameTitle from '../../common/FullNameTitle';
|
|
import Icon from '../../common/icons/Icon';
|
|
import PeerBadge from '../../common/PeerBadge';
|
|
import SafeLink from '../../common/SafeLink';
|
|
import Button from '../../ui/Button';
|
|
import Checkbox from '../../ui/Checkbox';
|
|
import DropdownMenu from '../../ui/DropdownMenu';
|
|
import MenuItem from '../../ui/MenuItem';
|
|
import Modal from '../../ui/Modal';
|
|
import Separator from '../../ui/Separator';
|
|
import BalanceBlock from '../stars/BalanceBlock';
|
|
import StarSlider from './StarSlider';
|
|
|
|
import styles from './PaidReactionModal.module.scss';
|
|
|
|
export type OwnProps = {
|
|
modal: TabState['paidReactionModal'];
|
|
};
|
|
|
|
type StateProps = {
|
|
message?: ApiMessage;
|
|
chat?: ApiChat;
|
|
maxAmount: number;
|
|
starBalance?: ApiStarsAmount;
|
|
defaultPrivacy?: ApiPaidReactionPrivacyType;
|
|
sendPaidReactionsAsPeerIds?: ApiSendAsPeerId[];
|
|
currentUserId: string;
|
|
currentUser: ApiUser;
|
|
};
|
|
|
|
type ReactorData = {
|
|
amount: number;
|
|
localAmount: number;
|
|
isMy?: boolean;
|
|
isAnonymous?: boolean;
|
|
user?: ApiPeer;
|
|
};
|
|
|
|
const MAX_TOP_REACTORS = 3;
|
|
const DEFAULT_STARS_AMOUNT = 50;
|
|
const MAX_REACTION_AMOUNT = 2500;
|
|
const ANONYMOUS_PEER: CustomPeer = {
|
|
avatarIcon: 'author-hidden',
|
|
customPeerAvatarColor: '#9eaab5',
|
|
isCustomPeer: true,
|
|
titleKey: 'StarsReactionAnonymous',
|
|
};
|
|
|
|
const PaidReactionModal = ({
|
|
modal,
|
|
chat,
|
|
message,
|
|
maxAmount,
|
|
starBalance,
|
|
defaultPrivacy,
|
|
sendPaidReactionsAsPeerIds,
|
|
currentUserId,
|
|
currentUser,
|
|
}: OwnProps & StateProps) => {
|
|
const { closePaidReactionModal, addLocalPaidReaction, loadSendPaidReactionsAs } = getActions();
|
|
|
|
const [starsAmount, setStarsAmount] = useState(DEFAULT_STARS_AMOUNT);
|
|
const [isTouched, markTouched, unmarkTouched] = useFlag();
|
|
const [shouldSendAsAnonymous, setShouldSendAsAnonymous] = useState(true);
|
|
const [sendAsPeerId, setSendAsPeerId] = useState(currentUserId);
|
|
|
|
const chatId = chat?.id;
|
|
|
|
const senderPeer = sendAsPeerId ? (selectPeer(getGlobal(), sendAsPeerId)) : currentUser;
|
|
|
|
const oldLang = useOldLang();
|
|
const { isMobile } = useAppLayout();
|
|
const lang = useLang();
|
|
|
|
const handleShowInTopSendersChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setShouldSendAsAnonymous(!e.target.checked);
|
|
});
|
|
|
|
const handleAmountChange = useLastCallback((value: number) => {
|
|
setStarsAmount(value);
|
|
markTouched();
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (chatId && !sendPaidReactionsAsPeerIds) {
|
|
loadSendPaidReactionsAs({ chatId });
|
|
}
|
|
}, [chatId, sendPaidReactionsAsPeerIds]);
|
|
|
|
const filteredMyReactorIds = useMemo(() => {
|
|
const result = sendPaidReactionsAsPeerIds?.map((peer) => peer.id)
|
|
.filter((id) => id !== chatId);
|
|
result?.unshift(currentUserId);
|
|
return result;
|
|
}, [sendPaidReactionsAsPeerIds, chatId, currentUserId]);
|
|
|
|
const canChangeSendAsPeer = filteredMyReactorIds && filteredMyReactorIds.length > 1;
|
|
|
|
useEffect(() => {
|
|
if (!modal) {
|
|
unmarkTouched();
|
|
}
|
|
}, [modal]);
|
|
|
|
useEffect(() => {
|
|
const currentReactor = message?.reactions?.topReactors?.find((reactor) => reactor.isMy);
|
|
if (currentReactor) {
|
|
setShouldSendAsAnonymous(Boolean(currentReactor.isAnonymous));
|
|
if (currentReactor.peerId) {
|
|
setSendAsPeerId(currentReactor.peerId);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setShouldSendAsAnonymous(defaultPrivacy?.type === 'anonymous' || false);
|
|
if (defaultPrivacy?.type === 'peer' && filteredMyReactorIds?.includes(defaultPrivacy.peerId)) {
|
|
setSendAsPeerId(defaultPrivacy.peerId);
|
|
return;
|
|
}
|
|
|
|
setSendAsPeerId(currentUserId);
|
|
}, [defaultPrivacy, message?.reactions?.topReactors, filteredMyReactorIds, currentUserId]);
|
|
|
|
const handleSend = useLastCallback(() => {
|
|
if (!modal) return;
|
|
|
|
addLocalPaidReaction({
|
|
chatId: modal.chatId,
|
|
messageId: modal.messageId,
|
|
count: starsAmount,
|
|
isPrivate: shouldSendAsAnonymous,
|
|
peerId: shouldSendAsAnonymous || sendAsPeerId === currentUserId ? undefined : sendAsPeerId,
|
|
shouldIgnoreDefaultPrivacy: true,
|
|
});
|
|
closePaidReactionModal();
|
|
});
|
|
|
|
const handleSendAsPeerChange = useLastCallback((peerId: string) => {
|
|
setShouldSendAsAnonymous(false);
|
|
setSendAsPeerId(peerId);
|
|
});
|
|
|
|
const renderMenuItem = useLastCallback((peerId: string) => {
|
|
const peer = selectPeer(getGlobal(), peerId);
|
|
const isSelected = sendAsPeerId === peerId && !shouldSendAsAnonymous;
|
|
if (!peer) return undefined;
|
|
|
|
return (
|
|
<MenuItem
|
|
// eslint-disable-next-line react/jsx-no-bind
|
|
onClick={() => handleSendAsPeerChange(peerId)}
|
|
>
|
|
<Avatar
|
|
size="small"
|
|
peer={peer}
|
|
/>
|
|
<div className={buildClassName(styles.itemInfo)}>
|
|
<FullNameTitle className={styles.itemTitle} peer={peer} noFake noVerified />
|
|
<span className={styles.itemSubtitle}>
|
|
{isApiPeerUser(peer) ? lang('PeerPersonalAccount') : lang('PeerChannel')}
|
|
</span>
|
|
</div>
|
|
<Icon
|
|
className={styles.itemIcon}
|
|
name={isSelected ? 'check' : 'placeholder'}
|
|
/>
|
|
</MenuItem>
|
|
);
|
|
});
|
|
|
|
const SendAsPeerMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
|
return ({ onTrigger, isOpen }) => (
|
|
<Button
|
|
ripple={!isMobile}
|
|
size="smaller"
|
|
color="translucent"
|
|
className={buildClassName(styles.sendAsPeerMenuButton, isOpen ? 'active' : '')}
|
|
onClick={onTrigger}
|
|
ariaLabel={lang('AccDescrOpenMenu2')}
|
|
>
|
|
<Avatar
|
|
size="mini"
|
|
peer={shouldSendAsAnonymous ? ANONYMOUS_PEER : senderPeer}
|
|
/>
|
|
<Icon
|
|
name="down"
|
|
className={styles.buttonDownIcon}
|
|
/>
|
|
</Button>
|
|
);
|
|
}, [isMobile, lang, senderPeer, shouldSendAsAnonymous]);
|
|
|
|
const sendAsPeersMenu = useMemo(() => {
|
|
if (!canChangeSendAsPeer) return undefined;
|
|
return (
|
|
<DropdownMenu
|
|
className={styles.sendAsPeerMenu}
|
|
bubbleClassName={styles.sendAsPeerMenuBubble}
|
|
trigger={SendAsPeerMenuButton}
|
|
positionX="right"
|
|
autoClose
|
|
>
|
|
{filteredMyReactorIds.map((id) => (
|
|
renderMenuItem(id)
|
|
))}
|
|
</DropdownMenu>
|
|
);
|
|
}, [SendAsPeerMenuButton, filteredMyReactorIds, canChangeSendAsPeer]);
|
|
|
|
const topReactors = useMemo(() => {
|
|
const global = getGlobal();
|
|
const all = message?.reactions?.topReactors;
|
|
if (!all) {
|
|
return undefined;
|
|
}
|
|
|
|
const result: ReactorData[] = [];
|
|
let hasCurrentSender = false;
|
|
let myReactorAmount = 0;
|
|
|
|
all.forEach((reactor) => {
|
|
const peer = reactor.peerId ? selectPeer(global, reactor.peerId) : undefined;
|
|
if (!peer && !reactor.isAnonymous && !reactor.isMy) return;
|
|
if (reactor.isMy) {
|
|
myReactorAmount = reactor.count;
|
|
}
|
|
|
|
if (reactor.isMy && (reactor.peerId !== sendAsPeerId || (reactor.isAnonymous && !shouldSendAsAnonymous))) return;
|
|
|
|
const isCurrentReactor = sendAsPeerId === reactor.peerId || (shouldSendAsAnonymous && reactor.isAnonymous);
|
|
|
|
if (isCurrentReactor) {
|
|
hasCurrentSender = true;
|
|
}
|
|
|
|
result.push({
|
|
amount: reactor.count,
|
|
localAmount: isCurrentReactor && isTouched ? starsAmount : 0,
|
|
isMy: reactor.isMy,
|
|
isAnonymous: reactor.isAnonymous,
|
|
user: peer,
|
|
});
|
|
});
|
|
|
|
if (!hasCurrentSender) {
|
|
const sender = selectPeer(global, sendAsPeerId);
|
|
result.push({
|
|
amount: myReactorAmount,
|
|
localAmount: isTouched ? starsAmount : 0,
|
|
isMy: true,
|
|
user: sender,
|
|
});
|
|
}
|
|
|
|
result.sort((a, b) => (b.amount + b.localAmount) - (a.amount + a.localAmount));
|
|
|
|
return result.slice(0, MAX_TOP_REACTORS);
|
|
}, [isTouched, message?.reactions?.topReactors, starsAmount, sendAsPeerId, shouldSendAsAnonymous]);
|
|
|
|
const chatTitle = chat && getPeerTitle(oldLang, chat);
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={Boolean(modal)}
|
|
onClose={closePaidReactionModal}
|
|
isSlim
|
|
hasAbsoluteCloseButton
|
|
contentClassName={styles.content}
|
|
isLowStackPriority
|
|
>
|
|
<div className={styles.sendAsPeersMenuContainer}>
|
|
{sendAsPeersMenu}
|
|
</div>
|
|
<div className={styles.headerControlPanel}>
|
|
<BalanceBlock balance={starBalance} className={styles.modalBalance} withAddButton />
|
|
</div>
|
|
|
|
<StarSlider
|
|
className={styles.slider}
|
|
defaultValue={DEFAULT_STARS_AMOUNT}
|
|
maxValue={maxAmount}
|
|
onChange={handleAmountChange}
|
|
/>
|
|
<h3 className={styles.title}>{oldLang('StarsReactionTitle')}</h3>
|
|
<div className={styles.description}>
|
|
{renderText(oldLang('StarsReactionText', chatTitle), ['simple_markdown', 'emoji'])}
|
|
</div>
|
|
<Separator>
|
|
{topReactors && <div className={styles.topLabel}>{oldLang('StarsReactionTopSenders')}</div>}
|
|
</Separator>
|
|
{topReactors && (
|
|
<div className={styles.top}>
|
|
{topReactors.map((reactor) => {
|
|
const countText = formatInteger(reactor.amount + reactor.localAmount);
|
|
const peer = (reactor.isAnonymous || !reactor.user || (reactor.isMy && shouldSendAsAnonymous))
|
|
? ANONYMOUS_PEER : reactor.user;
|
|
const text = 'isCustomPeer' in peer ? oldLang(peer.titleKey)
|
|
: peer && getPeerTitle(oldLang, peer);
|
|
return (
|
|
<PeerBadge
|
|
className={styles.topPeer}
|
|
key={`${reactor.user?.id || 'anonymous'}-${countText}`}
|
|
peer={peer}
|
|
badgeText={countText}
|
|
badgeIcon="star"
|
|
badgeClassName={styles.topBadge}
|
|
text={text}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
{topReactors && (<Separator className={styles.separator} />) }
|
|
<Checkbox
|
|
className={buildClassName(styles.checkBox, 'dialog-checkbox')}
|
|
checked={!shouldSendAsAnonymous}
|
|
onChange={handleShowInTopSendersChange}
|
|
label={oldLang('StarsReactionShowMeInTopSenders')}
|
|
/>
|
|
<Button
|
|
size="smaller"
|
|
onClick={handleSend}
|
|
>
|
|
{lang('SendPaidReaction', { amount: starsAmount }, {
|
|
withNodes: true,
|
|
specialReplacement: {
|
|
[STARS_ICON_PLACEHOLDER]: <Icon className={styles.buttonStar} name="star" />,
|
|
},
|
|
})}
|
|
</Button>
|
|
<p className={styles.disclaimer}>
|
|
{lang('StarsReactionTerms', {
|
|
link: <SafeLink text={lang('StarsReactionLinkText')} url={lang('StarsReactionLink')} />,
|
|
}, {
|
|
withNodes: true,
|
|
})}
|
|
</p>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { modal }): StateProps => {
|
|
const chat = modal && selectChat(global, modal.chatId);
|
|
const message = modal && selectChatMessage(global, modal.chatId, modal.messageId);
|
|
const starBalance = global.stars?.balance;
|
|
const maxAmount = global.appConfig?.paidReactionMaxAmount || MAX_REACTION_AMOUNT;
|
|
const defaultPrivacy = global.settings.paidReactionPrivacy;
|
|
const sendPaidReactionsAsPeerIds = chat?.sendPaidReactionsAsPeerIds;
|
|
const currentUserId = global.currentUserId!;
|
|
const currentUser = selectUser(global, currentUserId)!;
|
|
|
|
return {
|
|
chat,
|
|
message,
|
|
starBalance,
|
|
maxAmount,
|
|
defaultPrivacy,
|
|
sendPaidReactionsAsPeerIds,
|
|
currentUserId,
|
|
currentUser,
|
|
};
|
|
},
|
|
)(PaidReactionModal));
|