Support birthdays (#4448)
Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
parent
bb2694dd8c
commit
956a59b457
@ -83,6 +83,10 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und
|
||||
return 'voiceMessages';
|
||||
case 'PrivacyKeyChatInvite':
|
||||
return 'chatInvite';
|
||||
case 'PrivacyKeyAbout':
|
||||
return 'bio';
|
||||
case 'PrivacyKeyBirthday':
|
||||
return 'birthday';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiBirthday,
|
||||
ApiPremiumGiftOption,
|
||||
ApiUser,
|
||||
ApiUserFullInfo,
|
||||
@ -8,10 +9,10 @@ import type {
|
||||
ApiUserType,
|
||||
} from '../../types';
|
||||
|
||||
import { omitUndefined } from '../../../util/iteratees';
|
||||
import { buildApiBotInfo } from './bots';
|
||||
import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business';
|
||||
import { buildApiPhoto, buildApiUsernames } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers';
|
||||
|
||||
export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo {
|
||||
@ -21,14 +22,14 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
profilePhoto, voiceMessagesForbidden, premiumGifts,
|
||||
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
|
||||
contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
|
||||
personalChannelId, personalChannelMessage,
|
||||
birthday, personalChannelId, personalChannelMessage,
|
||||
},
|
||||
users,
|
||||
} = mtpUserFull;
|
||||
|
||||
const userId = buildApiPeerId(users[0].id, 'user');
|
||||
|
||||
return omitUndefined<ApiUserFullInfo>({
|
||||
return {
|
||||
bio: about,
|
||||
commonChatsCount,
|
||||
pinnedMessageId: pinnedMsgId,
|
||||
@ -42,12 +43,13 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
premiumGifts: premiumGifts?.map((gift) => buildApiPremiumGiftOption(gift)),
|
||||
botInfo: botInfo && buildApiBotInfo(botInfo, userId),
|
||||
isContactRequirePremium: contactRequirePremium,
|
||||
birthday: birthday && buildApiBirthday(birthday),
|
||||
businessLocation: businessLocation && buildApiBusinessLocation(businessLocation),
|
||||
businessWorkHours: businessWorkHours && buildApiBusinessWorkHours(businessWorkHours),
|
||||
businessIntro: businessIntro && buildApiBusinessIntro(businessIntro),
|
||||
personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'),
|
||||
personalChannelMessageId: personalChannelMessage,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
@ -161,3 +163,7 @@ export function buildApiPremiumGiftOption(option: GramJs.TypePremiumGiftOption):
|
||||
botUrl,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiBirthday(birthday: GramJs.TypeBirthday): ApiBirthday {
|
||||
return omitVirtualClassFields(birthday);
|
||||
}
|
||||
|
||||
@ -482,6 +482,9 @@ export function buildInputPrivacyKey(privacyKey: ApiPrivacyKey) {
|
||||
|
||||
case 'bio':
|
||||
return new GramJs.InputPrivacyKeyAbout();
|
||||
|
||||
case 'birthday':
|
||||
return new GramJs.InputPrivacyKeyBirthday();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -55,6 +55,7 @@ export interface ApiUserFullInfo {
|
||||
isTranslationDisabled?: true;
|
||||
hasPinnedStories?: boolean;
|
||||
isContactRequirePremium?: boolean;
|
||||
birthday?: ApiBirthday;
|
||||
personalChannelId?: string;
|
||||
personalChannelMessageId?: number;
|
||||
businessLocation?: ApiBusinessLocation;
|
||||
@ -119,3 +120,9 @@ export interface ApiEmojiStatus {
|
||||
documentId: string;
|
||||
until?: number;
|
||||
}
|
||||
|
||||
export interface ApiBirthday {
|
||||
day: number;
|
||||
month: number;
|
||||
year?: number;
|
||||
}
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-inline-end: 0.375rem;
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@ -1,28 +1,28 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiBusinessWorkHours } from '../../api/types';
|
||||
import type { ApiBusinessWorkHours } from '../../../api/types';
|
||||
|
||||
import { requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatTime, formatWeekday } from '../../util/date/dateFormat';
|
||||
import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatTime, formatWeekday } from '../../../util/date/dateFormat';
|
||||
import {
|
||||
getUtcOffset, getWeekStart, shiftTimeRanges, splitDays,
|
||||
} from '../../util/date/workHours';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
} from '../../../util/date/workHours';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
|
||||
import useInterval from '../../hooks/schedulers/useInterval';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useSelectorSignal from '../../hooks/useSelectorSignal';
|
||||
import useInterval from '../../../hooks/schedulers/useInterval';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useSelectorSignal from '../../../hooks/useSelectorSignal';
|
||||
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../ui/Transition';
|
||||
import Icon from './Icon';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition';
|
||||
import Icon from '../Icon';
|
||||
|
||||
import styles from './BusinessHours.module.scss';
|
||||
|
||||
@ -142,6 +142,7 @@ const BusinessHours = ({
|
||||
className={styles.root}
|
||||
isStatic={isExpanded}
|
||||
ripple
|
||||
narrow
|
||||
withColorTransition
|
||||
onClick={handleClick}
|
||||
>
|
||||
38
src/components/common/profile/ChatExtra.module.scss
Normal file
38
src/components/common/profile/ChatExtra.module.scss
Normal file
@ -0,0 +1,38 @@
|
||||
.businessLocation {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
object-fit: cover;
|
||||
border-radius: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.personalChannel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-rows: auto auto;
|
||||
column-gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.personalChannelTitle {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-inline-start: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.personalChannelSubscribers {
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
}
|
||||
|
||||
.personalChannelItem {
|
||||
grid-column: 1 / span 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
@ -1,15 +1,15 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername,
|
||||
} from '../../api/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
} from '../../../api/types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../config';
|
||||
import { TME_LINK_PREFIX } from '../../../config';
|
||||
import {
|
||||
buildStaticMapHash,
|
||||
getChatLink,
|
||||
@ -17,7 +17,7 @@ import {
|
||||
isChatChannel,
|
||||
isUserRightBanned,
|
||||
selectIsChatMuted,
|
||||
} from '../../global/helpers';
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
@ -27,25 +27,26 @@ import {
|
||||
selectTopicLink,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { copyTextToClipboard } from '../../util/clipboard';
|
||||
import { formatPhoneNumberWithCode } from '../../util/phoneNumber';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import stopEvent from '../../util/stopEvent';
|
||||
import { ChatAnimationTypes } from '../left/main/hooks';
|
||||
import renderText from './helpers/renderText';
|
||||
} from '../../../global/selectors';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
|
||||
import { debounce } from '../../../util/schedulers';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { ChatAnimationTypes } from '../../left/main/hooks';
|
||||
import renderText from '../helpers/renderText';
|
||||
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio';
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
|
||||
|
||||
import Chat from '../left/main/Chat';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import Skeleton from '../ui/placeholder/Skeleton';
|
||||
import Switcher from '../ui/Switcher';
|
||||
import Chat from '../../left/main/Chat';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Skeleton from '../../ui/placeholder/Skeleton';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import BusinessHours from './BusinessHours';
|
||||
import UserBirthday from './UserBirthday';
|
||||
|
||||
import styles from './ChatExtra.module.scss';
|
||||
|
||||
@ -115,6 +116,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
businessLocation,
|
||||
businessWorkHours,
|
||||
personalChannelMessageId,
|
||||
birthday,
|
||||
} = userFullInfo || {};
|
||||
const lang = useLang();
|
||||
|
||||
@ -140,10 +142,10 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
const locationRightComponent = useMemo(() => {
|
||||
if (!businessLocation?.geo) return undefined;
|
||||
if (locationBlobUrl) {
|
||||
return <img src={locationBlobUrl} alt="" className="business-location" />;
|
||||
return <img src={locationBlobUrl} alt="" className={styles.businessLocation} />;
|
||||
}
|
||||
|
||||
return <Skeleton className="business-location" />;
|
||||
return <Skeleton className={styles.businessLocation} />;
|
||||
}, [businessLocation, locationBlobUrl]);
|
||||
|
||||
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
|
||||
@ -329,6 +331,9 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
<span className="subtitle">{lang('SetUrlPlaceholder')}</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{birthday && (
|
||||
<UserBirthday key={peerId} birthday={birthday} user={user!} isInSettings={isInSettings} />
|
||||
)}
|
||||
{!isInSettings && (
|
||||
<ListItem icon="unmute" ripple onClick={handleNotificationChange}>
|
||||
<span>{lang('Notifications')}</span>
|
||||
@ -348,6 +353,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
||||
icon="location"
|
||||
ripple
|
||||
multiline
|
||||
narrow
|
||||
rightElement={locationRightComponent}
|
||||
onClick={handleClickLocation}
|
||||
>
|
||||
83
src/components/common/profile/UserBirthday.module.scss
Normal file
83
src/components/common/profile/UserBirthday.module.scss
Normal file
@ -0,0 +1,83 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.number {
|
||||
--digit-offset: 0;
|
||||
--digit-offset-x: calc(8rem * var(--digit-offset) * 0.75);
|
||||
|
||||
position: absolute;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
z-index: 2;
|
||||
|
||||
transform: scale(0);
|
||||
top: 50%;
|
||||
left: calc(10% + var(--digit-offset-x));
|
||||
offset-path: path('M 0 0 C 128 -46 99 -376 93 -529');
|
||||
offset-anchor: center;
|
||||
offset-rotate: 0deg;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
animation: 2.75s float 0.25s,
|
||||
2s show-up calc(var(--digit-offset) * 0.5s),
|
||||
1s dissapear 2s;
|
||||
animation-timing-function: ease-in;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
.shiftOrigin {
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.effect {
|
||||
position: absolute;
|
||||
top: calc(50% - 1rem);
|
||||
left: 10rem;
|
||||
transform: translate(-50%, -50%) scaleX(-1);
|
||||
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
width: 18rem;
|
||||
height: 18rem;
|
||||
}
|
||||
|
||||
.giftIcon {
|
||||
margin-inline-end: -0.375rem;
|
||||
}
|
||||
|
||||
@keyframes show-up {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: scale(50%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dissapear {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
from {
|
||||
offset-distance: 0%;
|
||||
}
|
||||
|
||||
to {
|
||||
offset-distance: 100%;
|
||||
}
|
||||
}
|
||||
230
src/components/common/profile/UserBirthday.tsx
Normal file
230
src/components/common/profile/UserBirthday.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
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/date/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 useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
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 { openGiftPremiumModal, 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 = useLang();
|
||||
|
||||
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 || true, // TODO REMOVE AFTER TESTING
|
||||
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.id);
|
||||
mediaLoader.fetch(hash, ApiMediaFormat.BlobUrl);
|
||||
});
|
||||
|
||||
if (effectSticker) {
|
||||
const effectHash = getStickerMediaHash(effectSticker.id);
|
||||
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(() => {
|
||||
openGiftPremiumModal({ 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">{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;
|
||||
}
|
||||
@ -189,6 +189,7 @@ function LeftColumn({
|
||||
case SettingsScreens.PrivacyLastSeen:
|
||||
case SettingsScreens.PrivacyProfilePhoto:
|
||||
case SettingsScreens.PrivacyBio:
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
case SettingsScreens.PrivacyPhoneCall:
|
||||
case SettingsScreens.PrivacyPhoneP2P:
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
@ -243,6 +244,10 @@ function LeftColumn({
|
||||
case SettingsScreens.PrivacyBioDeniedContacts:
|
||||
setSettingsScreen(SettingsScreens.PrivacyBio);
|
||||
return;
|
||||
case SettingsScreens.PrivacyBirthdayAllowedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayDeniedContacts:
|
||||
setSettingsScreen(SettingsScreens.PrivacyBirthday);
|
||||
return;
|
||||
case SettingsScreens.PrivacyPhoneCallAllowedContacts:
|
||||
case SettingsScreens.PrivacyPhoneCallDeniedContacts:
|
||||
setSettingsScreen(SettingsScreens.PrivacyPhoneCall);
|
||||
|
||||
@ -105,6 +105,11 @@ const PRIVACY_BIO_SCREENS = [
|
||||
SettingsScreens.PrivacyBioDeniedContacts,
|
||||
];
|
||||
|
||||
const PRIVACY_BIRTHDAY_SCREENS = [
|
||||
SettingsScreens.PrivacyBirthdayAllowedContacts,
|
||||
SettingsScreens.PrivacyBirthdayDeniedContacts,
|
||||
];
|
||||
|
||||
const PRIVACY_PHONE_CALL_SCREENS = [
|
||||
SettingsScreens.PrivacyPhoneCallAllowedContacts,
|
||||
SettingsScreens.PrivacyPhoneCallDeniedContacts,
|
||||
@ -198,6 +203,7 @@ const Settings: FC<OwnProps> = ({
|
||||
[SettingsScreens.PrivacyLastSeen]: PRIVACY_LAST_SEEN_PHONE_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyProfilePhoto]: PRIVACY_PROFILE_PHOTO_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyBio]: PRIVACY_BIO_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyBirthday]: PRIVACY_BIRTHDAY_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyPhoneCall]: PRIVACY_PHONE_CALL_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyPhoneP2P]: PRIVACY_PHONE_P2P_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyForwarding]: PRIVACY_FORWARDING_SCREENS.includes(activeScreen),
|
||||
@ -323,6 +329,7 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyLastSeen:
|
||||
case SettingsScreens.PrivacyProfilePhoto:
|
||||
case SettingsScreens.PrivacyBio:
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
case SettingsScreens.PrivacyPhoneCall:
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
case SettingsScreens.PrivacyVoiceMessages:
|
||||
@ -340,6 +347,7 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyLastSeenAllowedContacts:
|
||||
case SettingsScreens.PrivacyProfilePhotoAllowedContacts:
|
||||
case SettingsScreens.PrivacyBioAllowedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayAllowedContacts:
|
||||
case SettingsScreens.PrivacyPhoneCallAllowedContacts:
|
||||
case SettingsScreens.PrivacyPhoneP2PAllowedContacts:
|
||||
case SettingsScreens.PrivacyForwardingAllowedContacts:
|
||||
@ -359,6 +367,7 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyLastSeenDeniedContacts:
|
||||
case SettingsScreens.PrivacyProfilePhotoDeniedContacts:
|
||||
case SettingsScreens.PrivacyBioDeniedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayDeniedContacts:
|
||||
case SettingsScreens.PrivacyPhoneCallDeniedContacts:
|
||||
case SettingsScreens.PrivacyPhoneP2PDeniedContacts:
|
||||
case SettingsScreens.PrivacyForwardingDeniedContacts:
|
||||
|
||||
@ -115,6 +115,8 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
return <h3>{lang('Privacy.ProfilePhoto')}</h3>;
|
||||
case SettingsScreens.PrivacyBio:
|
||||
return <h3>{lang('PrivacyBio')}</h3>;
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
return <h3>{lang('PrivacyBirthday')}</h3>;
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
return <h3>{lang('PrivacyForwards')}</h3>;
|
||||
case SettingsScreens.PrivacyVoiceMessages:
|
||||
@ -130,6 +132,7 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyLastSeenAllowedContacts:
|
||||
case SettingsScreens.PrivacyProfilePhotoAllowedContacts:
|
||||
case SettingsScreens.PrivacyBioAllowedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayAllowedContacts:
|
||||
case SettingsScreens.PrivacyForwardingAllowedContacts:
|
||||
case SettingsScreens.PrivacyVoiceMessagesAllowedContacts:
|
||||
case SettingsScreens.PrivacyGroupChatsAllowedContacts:
|
||||
@ -140,6 +143,7 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyLastSeenDeniedContacts:
|
||||
case SettingsScreens.PrivacyProfilePhotoDeniedContacts:
|
||||
case SettingsScreens.PrivacyBioDeniedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayDeniedContacts:
|
||||
case SettingsScreens.PrivacyForwardingDeniedContacts:
|
||||
case SettingsScreens.PrivacyVoiceMessagesDeniedContacts:
|
||||
case SettingsScreens.PrivacyGroupChatsDeniedContacts:
|
||||
|
||||
@ -12,8 +12,8 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import ChatExtra from '../../common/ChatExtra';
|
||||
import PremiumIcon from '../../common/PremiumIcon';
|
||||
import ChatExtra from '../../common/profile/ChatExtra';
|
||||
import ProfileInfo from '../../common/ProfileInfo';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { ApiPrivacySettings } from '../../../types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
@ -33,14 +34,7 @@ type StateProps = {
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
shouldNewNonContactPeersRequirePremium?: boolean;
|
||||
canDisplayChatInTitle?: boolean;
|
||||
privacyPhoneNumber?: ApiPrivacySettings;
|
||||
privacyLastSeen?: ApiPrivacySettings;
|
||||
privacyProfilePhoto?: ApiPrivacySettings;
|
||||
privacyForwarding?: ApiPrivacySettings;
|
||||
privacyVoiceMessages?: ApiPrivacySettings;
|
||||
privacyGroupChats?: ApiPrivacySettings;
|
||||
privacyPhoneCall?: ApiPrivacySettings;
|
||||
privacyBio?: ApiPrivacySettings;
|
||||
privacy: GlobalState['settings']['privacy'];
|
||||
};
|
||||
|
||||
const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
@ -57,14 +51,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
canDisplayChatInTitle,
|
||||
canSetPasscode,
|
||||
privacyPhoneNumber,
|
||||
privacyLastSeen,
|
||||
privacyProfilePhoto,
|
||||
privacyForwarding,
|
||||
privacyVoiceMessages,
|
||||
privacyGroupChats,
|
||||
privacyPhoneCall,
|
||||
privacyBio,
|
||||
privacy,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
}) => {
|
||||
@ -206,7 +193,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('PrivacyPhoneTitle')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyPhoneNumber)}
|
||||
{getVisibilityValue(privacy.phoneNumber)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -219,7 +206,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('LastSeenTitle')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyLastSeen)}
|
||||
{getVisibilityValue(privacy.lastSeen)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -232,7 +219,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('PrivacyProfilePhotoTitle')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyProfilePhoto)}
|
||||
{getVisibilityValue(privacy.profilePhoto)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -245,7 +232,20 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('PrivacyBio')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyBio)}
|
||||
{getVisibilityValue(privacy.bio)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
narrow
|
||||
className="no-icon"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onScreenSelect(SettingsScreens.PrivacyBirthday)}
|
||||
>
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('PrivacyBirthday')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacy.birthday)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -258,7 +258,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('PrivacyForwardsTitle')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyForwarding)}
|
||||
{getVisibilityValue(privacy.forwards)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -271,7 +271,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('WhoCanCallMe')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyPhoneCall)}
|
||||
{getVisibilityValue(privacy.phoneCall)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -286,7 +286,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('PrivacyVoiceMessagesTitle')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyVoiceMessages)}
|
||||
{getVisibilityValue(privacy.voiceMessages)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -315,7 +315,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-menu-item">
|
||||
<span className="title">{lang('WhoCanAddMe')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{getVisibilityValue(privacyGroupChats)}
|
||||
{getVisibilityValue(privacy.chatInvite)}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -392,14 +392,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
shouldArchiveAndMuteNewNonContact,
|
||||
canChangeSensitive,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
privacyPhoneNumber: privacy.phoneNumber,
|
||||
privacyLastSeen: privacy.lastSeen,
|
||||
privacyProfilePhoto: privacy.profilePhoto,
|
||||
privacyForwarding: privacy.forwards,
|
||||
privacyVoiceMessages: privacy.voiceMessages,
|
||||
privacyGroupChats: privacy.chatInvite,
|
||||
privacyPhoneCall: privacy.phoneCall,
|
||||
privacyBio: privacy.bio,
|
||||
privacy,
|
||||
canDisplayChatInTitle,
|
||||
canSetPasscode: selectCanSetPasscode(global),
|
||||
};
|
||||
|
||||
@ -179,6 +179,8 @@ function PrivacySubsection({
|
||||
return lang('PrivacyProfilePhotoTitle');
|
||||
case SettingsScreens.PrivacyBio:
|
||||
return lang('PrivacyBioTitle');
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
return lang('PrivacyBirthdayTitle');
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
return lang('PrivacyForwardsTitle');
|
||||
case SettingsScreens.PrivacyVoiceMessages:
|
||||
@ -233,6 +235,8 @@ function PrivacySubsection({
|
||||
return SettingsScreens.PrivacyProfilePhotoAllowedContacts;
|
||||
case SettingsScreens.PrivacyBio:
|
||||
return SettingsScreens.PrivacyBioAllowedContacts;
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
return SettingsScreens.PrivacyBirthdayAllowedContacts;
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
return SettingsScreens.PrivacyForwardingAllowedContacts;
|
||||
case SettingsScreens.PrivacyPhoneCall:
|
||||
@ -256,6 +260,8 @@ function PrivacySubsection({
|
||||
return SettingsScreens.PrivacyProfilePhotoDeniedContacts;
|
||||
case SettingsScreens.PrivacyBio:
|
||||
return SettingsScreens.PrivacyBioDeniedContacts;
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
return SettingsScreens.PrivacyBirthdayDeniedContacts;
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
return SettingsScreens.PrivacyForwardingDeniedContacts;
|
||||
case SettingsScreens.PrivacyPhoneCall:
|
||||
@ -355,6 +361,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
primaryPrivacy = privacy.bio;
|
||||
break;
|
||||
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
primaryPrivacy = privacy.birthday;
|
||||
break;
|
||||
|
||||
case SettingsScreens.PrivacyPhoneP2P:
|
||||
case SettingsScreens.PrivacyPhoneCall:
|
||||
primaryPrivacy = privacy.phoneCall;
|
||||
|
||||
@ -140,6 +140,9 @@ function getCurrentPrivacySettings(global: GlobalState, screen: SettingsScreens)
|
||||
case SettingsScreens.PrivacyBioAllowedContacts:
|
||||
case SettingsScreens.PrivacyBioDeniedContacts:
|
||||
return privacy.bio;
|
||||
case SettingsScreens.PrivacyBirthdayAllowedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayDeniedContacts:
|
||||
return privacy.birthday;
|
||||
case SettingsScreens.PrivacyPhoneCallAllowedContacts:
|
||||
case SettingsScreens.PrivacyPhoneCallDeniedContacts:
|
||||
return privacy.phoneCall;
|
||||
|
||||
@ -19,6 +19,10 @@ export function getPrivacyKey(screen: SettingsScreens): ApiPrivacyKey | undefine
|
||||
case SettingsScreens.PrivacyBioAllowedContacts:
|
||||
case SettingsScreens.PrivacyBioDeniedContacts:
|
||||
return 'bio';
|
||||
case SettingsScreens.PrivacyBirthday:
|
||||
case SettingsScreens.PrivacyBirthdayAllowedContacts:
|
||||
case SettingsScreens.PrivacyBirthdayDeniedContacts:
|
||||
return 'birthday';
|
||||
case SettingsScreens.PrivacyForwarding:
|
||||
case SettingsScreens.PrivacyForwardingAllowedContacts:
|
||||
case SettingsScreens.PrivacyForwardingDeniedContacts:
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useRef } from '../../lib/teact/teact';
|
||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { TabState } from '../../global/types';
|
||||
import type { ConfettiStyle, TabState } from '../../global/types';
|
||||
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
@ -11,6 +11,7 @@ import { pick } from '../../util/iteratees';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
|
||||
@ -53,27 +54,20 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
|
||||
const defaultConfettiAmount = isMobile ? 50 : 100;
|
||||
const {
|
||||
lastConfettiTime, top, width, left, height,
|
||||
lastConfettiTime, top, width, left, height, style = 'poppers',
|
||||
} = confetti || {};
|
||||
|
||||
const generateConfetti = useCallback((w: number, h: number, amount = defaultConfettiAmount) => {
|
||||
const generateConfetti = useLastCallback((w: number, h: number, amount = defaultConfettiAmount) => {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
const leftSide = i % 2;
|
||||
const pos = {
|
||||
x: w * (leftSide ? -0.1 : 1.1),
|
||||
y: h * 0.75,
|
||||
};
|
||||
const randomX = Math.random() * w * 1.5;
|
||||
const randomY = -h / 2 - Math.random() * h;
|
||||
const velocity = {
|
||||
x: leftSide ? randomX : randomX * -1,
|
||||
y: randomY,
|
||||
};
|
||||
const {
|
||||
position, velocity,
|
||||
} = generateRandomPositionData(style, w, h, i);
|
||||
|
||||
const size = DEFAULT_CONFETTI_SIZE + randomNumberAroundZero(DEFAULT_CONFETTI_SIZE / 2);
|
||||
|
||||
const randomColor = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)];
|
||||
const size = DEFAULT_CONFETTI_SIZE;
|
||||
confettiRef.current.push({
|
||||
pos,
|
||||
pos: position,
|
||||
size,
|
||||
color: randomColor,
|
||||
velocity,
|
||||
@ -84,9 +78,9 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
frameCount: 0,
|
||||
});
|
||||
}
|
||||
}, [defaultConfettiAmount]);
|
||||
});
|
||||
|
||||
const updateCanvas = useCallback(() => {
|
||||
const updateCanvas = useLastCallback(() => {
|
||||
if (!canvasRef.current || !isRafStartedRef.current) {
|
||||
return;
|
||||
}
|
||||
@ -121,7 +115,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
};
|
||||
|
||||
const newVelocity = {
|
||||
x: velocity.x * 0.98, // Air Resistance
|
||||
x: velocity.x * 0.5 ** (diff / 1), // Air Resistance
|
||||
y: velocity.y += diff * 1000, // Gravity
|
||||
};
|
||||
|
||||
@ -167,7 +161,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
} else {
|
||||
isRafStartedRef.current = false;
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
useSyncEffect(([prevConfettiTime]) => {
|
||||
let hideTimeout: ReturnType<typeof setTimeout>;
|
||||
@ -189,7 +183,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const style = buildStyle(
|
||||
const containerStyle = buildStyle(
|
||||
Boolean(top) && `top: ${top}px`,
|
||||
Boolean(left) && `left: ${left}px`,
|
||||
Boolean(width) && `width: ${width}px`,
|
||||
@ -197,7 +191,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="Confetti" className={styles.root} style={style}>
|
||||
<div id="Confetti" className={styles.root} style={containerStyle}>
|
||||
<canvas ref={canvasRef} className={styles.canvas} width={windowSize.width} height={windowSize.height} />
|
||||
</div>
|
||||
);
|
||||
@ -206,3 +200,46 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => pick(selectTabState(global), ['confetti']),
|
||||
)(ConfettiContainer));
|
||||
|
||||
function generateRandomPositionData(
|
||||
style: ConfettiStyle, containerWidth: number, containerHeight: number, index: number,
|
||||
) {
|
||||
if (style === 'poppers') {
|
||||
const leftSide = index % 2;
|
||||
const position = {
|
||||
x: containerWidth * (leftSide ? -0.1 : 1.1),
|
||||
y: containerHeight * 0.66,
|
||||
};
|
||||
const randomX = Math.random() * containerWidth;
|
||||
const randomY = -containerHeight - randomNumberAroundZero(containerHeight * 0.75);
|
||||
const velocity = {
|
||||
x: leftSide ? randomX : randomX * -1,
|
||||
y: randomY,
|
||||
};
|
||||
|
||||
return {
|
||||
position,
|
||||
velocity,
|
||||
};
|
||||
} else {
|
||||
const position = {
|
||||
x: Math.random() * containerWidth,
|
||||
y: -DEFAULT_CONFETTI_SIZE * 2,
|
||||
};
|
||||
const randomX = randomNumberAroundZero(containerWidth);
|
||||
const randomY = -containerHeight * Math.random() * 1.25;
|
||||
const velocity = {
|
||||
x: randomX,
|
||||
y: randomY,
|
||||
};
|
||||
|
||||
return {
|
||||
position,
|
||||
velocity,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function randomNumberAroundZero(max: number = 1) {
|
||||
return Math.random() * max - max / 2;
|
||||
}
|
||||
|
||||
@ -236,6 +236,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
const {
|
||||
initMain,
|
||||
loadAnimatedEmojis,
|
||||
loadBirthdayNumbersStickers,
|
||||
loadNotificationSettings,
|
||||
loadNotificationExceptions,
|
||||
updateIsOnline,
|
||||
@ -338,6 +339,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
initMain();
|
||||
loadAvailableReactions();
|
||||
loadAnimatedEmojis();
|
||||
loadBirthdayNumbersStickers();
|
||||
loadGenericEmojiEffects();
|
||||
loadNotificationSettings();
|
||||
loadNotificationExceptions();
|
||||
|
||||
@ -62,12 +62,12 @@ import useProfileViewportIds from './hooks/useProfileViewportIds';
|
||||
import useTransitionFixes from './hooks/useTransitionFixes';
|
||||
|
||||
import Audio from '../common/Audio';
|
||||
import ChatExtra from '../common/ChatExtra';
|
||||
import Document from '../common/Document';
|
||||
import GroupChatInfo from '../common/GroupChatInfo';
|
||||
import Media from '../common/Media';
|
||||
import NothingFound from '../common/NothingFound';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ChatExtra from '../common/profile/ChatExtra';
|
||||
import ProfileInfo from '../common/ProfileInfo';
|
||||
import WebLink from '../common/WebLink';
|
||||
import ChatList from '../left/main/ChatList';
|
||||
|
||||
@ -80,7 +80,7 @@
|
||||
border-radius: 0 ;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
> .ListItem-main-icon {
|
||||
font-size: 1.5rem;
|
||||
margin-inline-start: 0.125rem;
|
||||
margin-inline-end: 1.25rem;
|
||||
@ -125,7 +125,7 @@
|
||||
}
|
||||
|
||||
&.multiline {
|
||||
.ListItem-button > .icon {
|
||||
.ListItem-button > .ListItem-main-icon {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@ -194,7 +194,7 @@
|
||||
.ListItem-button {
|
||||
color: var(--color-error);
|
||||
|
||||
.icon {
|
||||
.ListItem-main-icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
|
||||
import Icon from '../common/Icon';
|
||||
import Button from './Button';
|
||||
import Menu from './Menu';
|
||||
import MenuItem from './MenuItem';
|
||||
@ -47,6 +48,7 @@ interface OwnProps {
|
||||
iconClassName?: string;
|
||||
leftElement?: TeactNode;
|
||||
secondaryIcon?: IconName;
|
||||
secondaryIconClassName?: string;
|
||||
rightElement?: TeactNode;
|
||||
buttonClassName?: string;
|
||||
className?: string;
|
||||
@ -84,6 +86,7 @@ const ListItem: FC<OwnProps> = ({
|
||||
buttonClassName,
|
||||
menuBubbleClassName,
|
||||
secondaryIcon,
|
||||
secondaryIconClassName,
|
||||
rightElement,
|
||||
className,
|
||||
style,
|
||||
@ -246,20 +249,20 @@ const ListItem: FC<OwnProps> = ({
|
||||
)}
|
||||
{leftElement}
|
||||
{icon && (
|
||||
<i className={buildClassName('icon', `icon-${icon}`, iconClassName)} />
|
||||
<Icon name={icon} className={buildClassName('ListItem-main-icon', iconClassName)} />
|
||||
)}
|
||||
{multiline && (<div className="multiline-item">{children}</div>)}
|
||||
{!multiline && children}
|
||||
{secondaryIcon && (
|
||||
<Button
|
||||
className="secondary-icon"
|
||||
className={buildClassName('secondary-icon', secondaryIconClassName)}
|
||||
round
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
onClick={handleSecondaryIconClick}
|
||||
onMouseDown={handleSecondaryIconMouseDown}
|
||||
>
|
||||
<i className={`icon icon-${secondaryIcon}`} />
|
||||
<Icon name={secondaryIcon} />
|
||||
</Button>
|
||||
)}
|
||||
{rightElement}
|
||||
|
||||
@ -213,6 +213,8 @@ export const BASE_EMOJI_KEYWORD_LANG = 'en';
|
||||
export const MENU_TRANSITION_DURATION = 200;
|
||||
export const SLIDE_TRANSITION_DURATION = 450;
|
||||
|
||||
export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji';
|
||||
|
||||
export const VIDEO_WEBM_TYPE = 'video/webm';
|
||||
export const GIF_MIME_TYPE = 'image/gif';
|
||||
|
||||
|
||||
@ -421,6 +421,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
callApi('fetchPrivacySettings', 'phoneP2P'),
|
||||
callApi('fetchPrivacySettings', 'voiceMessages'),
|
||||
callApi('fetchPrivacySettings', 'bio'),
|
||||
callApi('fetchPrivacySettings', 'birthday'),
|
||||
]);
|
||||
|
||||
if (result.some((e) => e === undefined)) {
|
||||
@ -438,6 +439,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
phoneP2PSettings,
|
||||
voiceMessagesSettings,
|
||||
bioSettings,
|
||||
birthdaySettings,
|
||||
] = result as {
|
||||
users: ApiUser[];
|
||||
rules: ApiPrivacySettings;
|
||||
@ -463,6 +465,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
phoneP2P: phoneP2PSettings.rules,
|
||||
voiceMessages: voiceMessagesSettings.rules,
|
||||
bio: bioSettings.rules,
|
||||
birthday: birthdaySettings.rules,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -4,6 +4,7 @@ import type {
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
||||
|
||||
import { BIRTHDAY_NUMBERS_SET } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { translate } from '../../../util/langProvider';
|
||||
@ -267,6 +268,26 @@ addActionHandler('loadAnimatedEmojis', async (global): Promise<void> => {
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadBirthdayNumbersStickers', async (global): Promise<void> => {
|
||||
const emojis = await callApi('fetchStickers', {
|
||||
stickerSetInfo: {
|
||||
shortName: BIRTHDAY_NUMBERS_SET,
|
||||
},
|
||||
});
|
||||
if (!emojis) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
global = {
|
||||
...global,
|
||||
birthdayNumbers: { ...emojis.set, stickers: emojis.stickers },
|
||||
};
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadGenericEmojiEffects', async (global): Promise<void> => {
|
||||
const stickerSet = await callApi('fetchGenericEmojiEffects');
|
||||
if (!stickerSet) {
|
||||
|
||||
@ -5,6 +5,10 @@ export function getStickerPreviewHash(stickerId: string) {
|
||||
return `sticker${stickerId}?size=m`;
|
||||
}
|
||||
|
||||
export function getStickerMediaHash(stickerId: string) {
|
||||
return `document${stickerId}`;
|
||||
}
|
||||
|
||||
export function containsCustomEmoji(formattedText: ApiFormattedText) {
|
||||
return formattedText.entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
||||
}
|
||||
|
||||
@ -148,6 +148,8 @@ export type IDimensions = {
|
||||
|
||||
export type ApiPaymentStatus = 'paid' | 'failed' | 'pending' | 'cancelled';
|
||||
|
||||
export type ConfettiStyle = 'poppers' | 'top-down';
|
||||
|
||||
export interface TabThread {
|
||||
scrollOffset?: number;
|
||||
replyStack?: number[];
|
||||
@ -602,6 +604,7 @@ export type TabState = {
|
||||
left?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
style?: ConfettiStyle;
|
||||
};
|
||||
|
||||
urlAuth?: {
|
||||
@ -970,6 +973,7 @@ export type GlobalState = {
|
||||
animatedEmojis?: ApiStickerSet;
|
||||
animatedEmojiEffects?: ApiStickerSet;
|
||||
genericEmojiEffects?: ApiStickerSet;
|
||||
birthdayNumbers?: ApiStickerSet;
|
||||
defaultTopicIconsId?: string;
|
||||
defaultStatusIconsId?: string;
|
||||
premiumGifts?: ApiStickerSet;
|
||||
@ -2518,6 +2522,7 @@ export interface ActionPayloads {
|
||||
loadAnimatedEmojis: undefined;
|
||||
loadGreetingStickers: undefined;
|
||||
loadGenericEmojiEffects: undefined;
|
||||
loadBirthdayNumbersStickers: undefined;
|
||||
|
||||
addRecentSticker: {
|
||||
sticker: ApiSticker;
|
||||
@ -2783,6 +2788,7 @@ export interface ActionPayloads {
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
style?: ConfettiStyle;
|
||||
} & WithTabId) | WithTabId;
|
||||
|
||||
updateAttachmentSettings: {
|
||||
|
||||
@ -185,6 +185,7 @@ export enum SettingsScreens {
|
||||
PrivacyLastSeen,
|
||||
PrivacyProfilePhoto,
|
||||
PrivacyBio,
|
||||
PrivacyBirthday,
|
||||
PrivacyPhoneCall,
|
||||
PrivacyPhoneP2P,
|
||||
PrivacyForwarding,
|
||||
@ -199,6 +200,8 @@ export enum SettingsScreens {
|
||||
PrivacyProfilePhotoDeniedContacts,
|
||||
PrivacyBioAllowedContacts,
|
||||
PrivacyBioDeniedContacts,
|
||||
PrivacyBirthdayAllowedContacts,
|
||||
PrivacyBirthdayDeniedContacts,
|
||||
PrivacyPhoneCallAllowedContacts,
|
||||
PrivacyPhoneCallDeniedContacts,
|
||||
PrivacyPhoneP2PAllowedContacts,
|
||||
@ -380,7 +383,7 @@ export type ProfileTabType =
|
||||
| 'dialogs';
|
||||
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
|
||||
export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profilePhoto' | 'voiceMessages' |
|
||||
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio';
|
||||
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday';
|
||||
export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';
|
||||
|
||||
export enum ProfileState {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user