Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com> Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
233 lines
7.0 KiB
TypeScript
233 lines
7.0 KiB
TypeScript
import React, {
|
|
memo, useEffect, useMemo, useRef,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import {
|
|
type ApiBirthday, ApiMediaFormat, type ApiStickerSet, type ApiUser,
|
|
} from '../../../api/types';
|
|
|
|
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
|
import { getStickerMediaHash } from '../../../global/helpers';
|
|
import { selectIsPremiumPurchaseBlocked } from '../../../global/selectors';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { formatDateToString } from '../../../util/dates/dateFormat';
|
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
|
import * as mediaLoader from '../../../util/mediaLoader';
|
|
import { IS_OFFSET_PATH_SUPPORTED } from '../../../util/windowEnvironment';
|
|
import renderText from '../helpers/renderText';
|
|
|
|
import useTimeout from '../../../hooks/schedulers/useTimeout';
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
|
|
import ListItem from '../../ui/ListItem';
|
|
import StickerView from '../StickerView';
|
|
|
|
import styles from './UserBirthday.module.scss';
|
|
|
|
const NUMBER_EMOJI_SUFFIX = '\uFE0F\u20E3';
|
|
const NUMBER_STICKER_SIZE = 128;
|
|
const EFFECT_EMOJIS = ['🎉', '🎆', '🎈'];
|
|
const EFFECT_SIZE = 288;
|
|
const ANIMATION_DURATION = 3000;
|
|
|
|
type OwnProps = {
|
|
user: ApiUser;
|
|
birthday: ApiBirthday;
|
|
isInSettings?: boolean;
|
|
};
|
|
|
|
type StateProps = {
|
|
isPremiumPurchaseBlocked?: boolean;
|
|
birthdayNumbers?: ApiStickerSet;
|
|
animatedEmojiEffects?: ApiStickerSet;
|
|
};
|
|
|
|
const UserBirthday = ({
|
|
user,
|
|
birthday,
|
|
isPremiumPurchaseBlocked,
|
|
birthdayNumbers,
|
|
animatedEmojiEffects,
|
|
isInSettings,
|
|
}: OwnProps & StateProps) => {
|
|
const { openGiftModal, requestConfetti } = getActions();
|
|
// eslint-disable-next-line no-null/no-null
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
const animationPlayedRef = useRef(false);
|
|
const [isPlayingAnimation, playAnimation, stopAnimation] = useFlag();
|
|
|
|
const lang = useOldLang();
|
|
|
|
const {
|
|
formattedDate,
|
|
isToday,
|
|
age,
|
|
} = useMemo(() => {
|
|
const today = new Date();
|
|
const date = new Date();
|
|
if (birthday.year) {
|
|
date.setFullYear(birthday.year);
|
|
}
|
|
date.setMonth(birthday.month - 1);
|
|
date.setDate(birthday.day);
|
|
date.setHours(0, 0, 0, 0);
|
|
|
|
const formatted = formatDateToString(date, lang.code, true, 'long');
|
|
const isBirthdayToday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
|
|
return {
|
|
formattedDate: formatted,
|
|
isToday: isBirthdayToday,
|
|
age: birthday.year && getAge(date),
|
|
};
|
|
}, [birthday, lang]);
|
|
|
|
const numbersForAge = useMemo(() => {
|
|
if (!age || !isToday) return undefined;
|
|
const numbers = birthdayNumbers?.stickers?.filter(({ emoji }) => emoji?.endsWith(NUMBER_EMOJI_SUFFIX));
|
|
if (!numbers) return undefined;
|
|
const byEmoji = buildCollectionByKey(numbers, 'emoji');
|
|
|
|
const ageDigits = age.toString().split('');
|
|
return ageDigits.map((digit) => byEmoji[digit + NUMBER_EMOJI_SUFFIX]);
|
|
}, [age, birthdayNumbers?.stickers, isToday]);
|
|
|
|
const effectSticker = useMemo(() => {
|
|
if (!isToday) return undefined;
|
|
const randomEffect = EFFECT_EMOJIS[Math.floor(Math.random() * EFFECT_EMOJIS.length)];
|
|
return animatedEmojiEffects?.stickers?.find(({ emoji }) => emoji === randomEffect);
|
|
}, [animatedEmojiEffects?.stickers, isToday]);
|
|
|
|
// Preload stickers
|
|
useEffect(() => {
|
|
if (!isToday || !numbersForAge) return;
|
|
|
|
numbersForAge.forEach((sticker) => {
|
|
const hash = getStickerMediaHash(sticker, 'preview');
|
|
mediaLoader.fetch(hash, ApiMediaFormat.BlobUrl);
|
|
});
|
|
|
|
if (effectSticker) {
|
|
const effectHash = getStickerMediaHash(effectSticker, 'preview');
|
|
mediaLoader.fetch(effectHash, ApiMediaFormat.BlobUrl);
|
|
}
|
|
}, [effectSticker, isToday, numbersForAge]);
|
|
|
|
useTimeout(stopAnimation, isPlayingAnimation ? ANIMATION_DURATION : undefined);
|
|
|
|
useEffect(() => {
|
|
if (isPlayingAnimation) {
|
|
animationPlayedRef.current = true;
|
|
|
|
const column = document.getElementById(isInSettings ? 'LeftColumn' : 'RightColumn');
|
|
if (!column) return;
|
|
|
|
requestMeasure(() => {
|
|
const {
|
|
top, left, width, height,
|
|
} = column.getBoundingClientRect();
|
|
|
|
requestConfetti({
|
|
top,
|
|
left,
|
|
width,
|
|
height,
|
|
style: 'top-down',
|
|
});
|
|
});
|
|
}
|
|
}, [isInSettings, isPlayingAnimation]);
|
|
|
|
const valueKey = `ProfileBirthday${isToday ? 'Today' : ''}Value${age ? 'Year' : ''}`;
|
|
|
|
const canGiftPremium = isToday && !user.isPremium && !user.isSelf && !isPremiumPurchaseBlocked;
|
|
|
|
const handleOpenGiftModal = useLastCallback(() => {
|
|
openGiftModal({ forUserId: user.id });
|
|
});
|
|
|
|
const handleClick = useLastCallback(() => {
|
|
if (!isToday) return;
|
|
|
|
if (canGiftPremium && animationPlayedRef.current) {
|
|
handleOpenGiftModal();
|
|
return;
|
|
}
|
|
|
|
playAnimation();
|
|
});
|
|
|
|
const isStatic = !isToday && !canGiftPremium;
|
|
|
|
return (
|
|
<div className={styles.root}>
|
|
<ListItem
|
|
icon="calendar"
|
|
secondaryIcon={canGiftPremium ? 'gift' : undefined}
|
|
secondaryIconClassName={styles.giftIcon}
|
|
multiline
|
|
narrow
|
|
ref={ref}
|
|
ripple={!isStatic}
|
|
onClick={handleClick}
|
|
isStatic={isStatic}
|
|
onSecondaryIconClick={handleOpenGiftModal}
|
|
>
|
|
<div className="title" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
{renderText(lang(valueKey, [formattedDate, age], undefined, age))}
|
|
</div>
|
|
<span className="subtitle">{lang(isToday ? 'ProfileBirthdayToday' : 'ProfileBirthday')}</span>
|
|
</ListItem>
|
|
{isPlayingAnimation && IS_OFFSET_PATH_SUPPORTED && numbersForAge?.map((sticker, index) => (
|
|
<div
|
|
className={buildClassName(styles.number, index > 0 && styles.shiftOrigin)}
|
|
style={`--digit-offset: ${index}`}
|
|
>
|
|
<StickerView
|
|
containerRef={ref}
|
|
sticker={sticker}
|
|
size={NUMBER_STICKER_SIZE}
|
|
forceAlways
|
|
/>
|
|
</div>
|
|
))}
|
|
{isPlayingAnimation && effectSticker && (
|
|
<div className={styles.effect}>
|
|
<StickerView
|
|
containerRef={ref}
|
|
sticker={effectSticker}
|
|
size={EFFECT_SIZE}
|
|
shouldLoop
|
|
forceAlways
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global): StateProps => {
|
|
const { birthdayNumbers, animatedEmojiEffects } = global;
|
|
return {
|
|
birthdayNumbers,
|
|
animatedEmojiEffects,
|
|
isPremiumPurchaseBlocked: selectIsPremiumPurchaseBlocked(global),
|
|
};
|
|
},
|
|
)(UserBirthday));
|
|
|
|
// https://stackoverflow.com/a/7091965
|
|
function getAge(birthdate: Date) {
|
|
const today = new Date();
|
|
let age = today.getFullYear() - birthdate.getFullYear();
|
|
const m = today.getMonth() - birthdate.getMonth();
|
|
if (m < 0 || (m === 0 && today.getDate() < birthdate.getDate())) {
|
|
age--;
|
|
}
|
|
|
|
return age;
|
|
}
|