TelegramPWA/src/components/modals/profileRating/ProfileRatingModal.tsx
2026-03-31 11:31:16 +02:00

272 lines
9.1 KiB
TypeScript

import { memo, useEffect, useMemo, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiStarsRating, ApiUser } from '../../../api/types';
import type { TabState } from '../../../global/types';
import { getPeerTitle } from '../../../global/helpers/peers';
import { selectUser, selectUserFullInfo } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatShortDuration } from '../../../util/dates/oldDateFormat';
import { getNextArrowReplacement } from '../../../util/localization/format';
import { getServerTime } from '../../../util/serverTime';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PremiumProgress, { type AnimationDirection } from '../../common/PremiumProgress';
import Button from '../../ui/Button';
import Transition from '../../ui/Transition';
import TableAboutModal, { type TableAboutData } from '../common/TableAboutModal';
import styles from './ProfileRatingModal.module.scss';
export type OwnProps = {
modal: TabState['profileRatingModal'];
};
type StateProps = {
user?: ApiUser;
currentUserId?: string;
starsRating?: ApiStarsRating;
pendingRating?: ApiStarsRating;
pendingRatingDate?: number;
};
const ProfileRatingModal = ({
modal,
user,
currentUserId,
starsRating,
pendingRating,
pendingRatingDate,
}: OwnProps & StateProps) => {
const {
closeProfileRatingModal,
} = getActions();
const lang = useLang();
const isOpen = Boolean(modal);
const renderingUser = useCurrentOrPrev(user);
const renderingStarsRating = useCurrentOrPrev(starsRating);
const renderingPendingRating = useCurrentOrPrev(pendingRating);
const renderingPendingRatingDate = useCurrentOrPrev(pendingRatingDate);
const [showFutureRating, setShowFutureRating] = useState(false);
const handleClose = useLastCallback(() => {
closeProfileRatingModal();
});
useEffect(() => {
if (!isOpen) {
setShowFutureRating(false);
}
}, [isOpen]);
const handleShowFuture = useLastCallback(() => {
setShowFutureRating(true);
});
const handleShowCurrent = useLastCallback(() => {
setShowFutureRating(false);
});
const renderBadge = (type: 'added' | 'deducted') => {
const isAdded = type === 'added';
const badgeText = isAdded ? lang('RatingBadgeAdded') : lang('RatingBadgeDeducted');
const badgeClass = isAdded ? styles.badgeAdded : styles.badgeDeducted;
return (
<span className={buildClassName(styles.badge, badgeClass)}>
{badgeText}
</span>
);
};
const header = useMemo(() => {
if (!renderingUser || !renderingStarsRating) return undefined;
const rating = showFutureRating && renderingPendingRating
? renderingPendingRating : renderingStarsRating;
const currentStars = rating.stars;
const currentLevelStars = rating.currentLevelStars;
const nextLevelStars = rating.nextLevelStars;
const currentLevel = rating.level;
const nextLevel = currentLevel + 1;
const isNegative = currentLevel < 0;
const pendingLevel = !showFutureRating && renderingPendingRating
? renderingPendingRating.level : renderingStarsRating.level;
let levelProgress = 0;
if (!nextLevelStars) {
levelProgress = 1;
} else if (nextLevelStars > currentLevelStars) {
levelProgress = Math.max(0.03, (currentStars - currentLevelStars) / (nextLevelStars - currentLevelStars));
} else {
levelProgress = 1;
}
const progress = isNegative ? 0.5 : Math.max(0, Math.min(1, levelProgress));
const waitTime = renderingPendingRatingDate ? renderingPendingRatingDate - getServerTime() : 0;
const pendingPoints = renderingPendingRating ? renderingPendingRating.stars - renderingStarsRating.stars : 0;
const shouldShowPreview = renderingPendingRating && renderingPendingRatingDate;
const renderPreviewDescription = () => {
if (!shouldShowPreview) return undefined;
return (
<Transition
name="fade"
className={buildClassName(styles.descriptionPreview, isNegative && styles.negative)}
activeKey={showFutureRating ? 1 : 0}
shouldCleanup
shouldRestoreHeight
>
{showFutureRating ? (
<p>
{lang('DescriptionFutureRating', {
time: formatShortDuration(lang, waitTime),
points: Math.abs(pendingPoints),
link: (
<span className={styles.backLink} onClick={handleShowCurrent}>
{lang('LinkDescriptionRatingBack',
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
</span>
),
}, {
pluralValue: Math.abs(pendingPoints),
withNodes: true,
})}
</p>
) : (
<p>
{lang('DescriptionPendingRating', {
time: formatShortDuration(lang, waitTime),
points: Math.abs(pendingPoints),
link: (
<span className={styles.previewLink} onClick={handleShowFuture}>
{lang('LinkDescriptionRatingPreview',
undefined, { withNodes: true, specialReplacement: getNextArrowReplacement() })}
</span>
),
}, {
pluralValue: Math.abs(pendingPoints),
withNodes: true,
})}
</p>
)}
</Transition>
);
};
let animationDirection: AnimationDirection = 'none';
if (currentLevel >= 0 && pendingLevel >= 0 && currentLevel !== pendingLevel) {
animationDirection = currentLevel > pendingLevel ? 'forward' : 'backward';
}
if (currentLevel < 0 && pendingLevel < 0 && currentLevel !== pendingLevel) {
animationDirection = currentLevel < pendingLevel ? 'backward' : 'forward';
}
const userFallbackText = lang('ActionFallbackUser');
return (
<div className={styles.header}>
<PremiumProgress
leftText={isNegative ? undefined : lang('RatingLevel', { level: currentLevel })}
rightText={isNegative ? lang('RatingNegativeLevel') : lang('RatingLevel', { level: nextLevel })}
floatingBadgeIcon={isNegative ? 'warning' : 'crown-wear'}
floatingBadgeText={isNegative ? currentLevel.toString()
: `${lang.number(currentStars)} / ${lang.number(nextLevelStars || currentStars)}`}
progress={progress}
isPrimary={currentLevel >= 0}
isNegative={currentLevel < 0}
animationDirection={animationDirection}
className={buildClassName(styles.ratingProgress, shouldShowPreview && styles.withPreview)}
/>
{renderPreviewDescription()}
<div className={styles.title}>
{lang('TitleRating')}
</div>
<p className={styles.description}>
{renderingUser?.id === currentUserId
? lang('RatingYourReflectsActivity')
: lang('RatingReflectsActivity', { name: getPeerTitle(lang, renderingUser) || userFallbackText },
{ withMarkdown: true, withNodes: true })}
</p>
</div>
);
}, [renderingUser, currentUserId, renderingStarsRating,
renderingPendingRating, renderingPendingRatingDate, showFutureRating,
lang, handleShowFuture, handleShowCurrent]);
const listItemData = [
['closed-gift', lang('RatingGiftsFromTelegram'), (
<span>
{renderBadge('added')}
{lang('RatingGiftsFromTelegramDesc')}
</span>
)],
['user-stars', lang('RatingGiftsAndPostsFromUsers'), (
<span>
{renderBadge('added')}
{lang('RatingGiftsAndPostsFromUsersDesc')}
</span>
)],
['stars-refund', lang('RatingRefundsAndConversions'), (
<span>
{renderBadge('deducted')}
{lang('RatingRefundsAndConversionsDesc')}
</span>
)],
] satisfies TableAboutData;
const footer = useMemo(() => {
return (
<div className={styles.footer}>
<Button
onClick={handleClose}
iconName="understood"
iconClassName={styles.understoodIcon}
>
{lang('ButtonUnderstood')}
</Button>
</div>
);
}, [lang, handleClose]);
return (
<TableAboutModal
contentClassName={styles.content}
isOpen={isOpen}
header={header}
listItemData={listItemData}
footer={footer}
onClose={handleClose}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): Complete<StateProps> => {
const currentUserId = global.currentUserId;
const user = modal?.userId ? selectUser(global, modal.userId) : undefined;
const userFullInfo = modal?.userId
? selectUserFullInfo(global, modal.userId) : undefined;
const starsRating = userFullInfo?.starsRating;
const pendingRating = userFullInfo?.starsMyPendingRating;
const pendingRatingDate = userFullInfo?.starsMyPendingRatingDate;
return {
user,
currentUserId,
starsRating,
pendingRating,
pendingRatingDate,
};
},
)(ProfileRatingModal));