Support birthdays (#4448)

Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
Alexander Zinchuk 2024-04-19 13:38:43 +04:00
parent bb2694dd8c
commit 956a59b457
29 changed files with 599 additions and 110 deletions

View File

@ -83,6 +83,10 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und
return 'voiceMessages'; return 'voiceMessages';
case 'PrivacyKeyChatInvite': case 'PrivacyKeyChatInvite':
return 'chatInvite'; return 'chatInvite';
case 'PrivacyKeyAbout':
return 'bio';
case 'PrivacyKeyBirthday':
return 'birthday';
} }
return undefined; return undefined;

View File

@ -1,6 +1,7 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBirthday,
ApiPremiumGiftOption, ApiPremiumGiftOption,
ApiUser, ApiUser,
ApiUserFullInfo, ApiUserFullInfo,
@ -8,10 +9,10 @@ import type {
ApiUserType, ApiUserType,
} from '../../types'; } from '../../types';
import { omitUndefined } from '../../../util/iteratees';
import { buildApiBotInfo } from './bots'; import { buildApiBotInfo } from './bots';
import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business'; import { buildApiBusinessIntro, buildApiBusinessLocation, buildApiBusinessWorkHours } from './business';
import { buildApiPhoto, buildApiUsernames } from './common'; import { buildApiPhoto, buildApiUsernames } from './common';
import { omitVirtualClassFields } from './helpers';
import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers';
export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo { export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo {
@ -21,14 +22,14 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
profilePhoto, voiceMessagesForbidden, premiumGifts, profilePhoto, voiceMessagesForbidden, premiumGifts,
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
contactRequirePremium, businessWorkHours, businessLocation, businessIntro, contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
personalChannelId, personalChannelMessage, birthday, personalChannelId, personalChannelMessage,
}, },
users, users,
} = mtpUserFull; } = mtpUserFull;
const userId = buildApiPeerId(users[0].id, 'user'); const userId = buildApiPeerId(users[0].id, 'user');
return omitUndefined<ApiUserFullInfo>({ return {
bio: about, bio: about,
commonChatsCount, commonChatsCount,
pinnedMessageId: pinnedMsgId, pinnedMessageId: pinnedMsgId,
@ -42,12 +43,13 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
premiumGifts: premiumGifts?.map((gift) => buildApiPremiumGiftOption(gift)), premiumGifts: premiumGifts?.map((gift) => buildApiPremiumGiftOption(gift)),
botInfo: botInfo && buildApiBotInfo(botInfo, userId), botInfo: botInfo && buildApiBotInfo(botInfo, userId),
isContactRequirePremium: contactRequirePremium, isContactRequirePremium: contactRequirePremium,
birthday: birthday && buildApiBirthday(birthday),
businessLocation: businessLocation && buildApiBusinessLocation(businessLocation), businessLocation: businessLocation && buildApiBusinessLocation(businessLocation),
businessWorkHours: businessWorkHours && buildApiBusinessWorkHours(businessWorkHours), businessWorkHours: businessWorkHours && buildApiBusinessWorkHours(businessWorkHours),
businessIntro: businessIntro && buildApiBusinessIntro(businessIntro), businessIntro: businessIntro && buildApiBusinessIntro(businessIntro),
personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'), personalChannelId: personalChannelId && buildApiPeerId(personalChannelId, 'channel'),
personalChannelMessageId: personalChannelMessage, personalChannelMessageId: personalChannelMessage,
}); };
} }
export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
@ -161,3 +163,7 @@ export function buildApiPremiumGiftOption(option: GramJs.TypePremiumGiftOption):
botUrl, botUrl,
}; };
} }
export function buildApiBirthday(birthday: GramJs.TypeBirthday): ApiBirthday {
return omitVirtualClassFields(birthday);
}

View File

@ -482,6 +482,9 @@ export function buildInputPrivacyKey(privacyKey: ApiPrivacyKey) {
case 'bio': case 'bio':
return new GramJs.InputPrivacyKeyAbout(); return new GramJs.InputPrivacyKeyAbout();
case 'birthday':
return new GramJs.InputPrivacyKeyBirthday();
} }
return undefined; return undefined;

View File

@ -55,6 +55,7 @@ export interface ApiUserFullInfo {
isTranslationDisabled?: true; isTranslationDisabled?: true;
hasPinnedStories?: boolean; hasPinnedStories?: boolean;
isContactRequirePremium?: boolean; isContactRequirePremium?: boolean;
birthday?: ApiBirthday;
personalChannelId?: string; personalChannelId?: string;
personalChannelMessageId?: number; personalChannelMessageId?: number;
businessLocation?: ApiBusinessLocation; businessLocation?: ApiBusinessLocation;
@ -119,3 +120,9 @@ export interface ApiEmojiStatus {
documentId: string; documentId: string;
until?: number; until?: number;
} }
export interface ApiBirthday {
day: number;
month: number;
year?: number;
}

View File

@ -27,6 +27,8 @@
} }
.arrow { .arrow {
margin-inline-end: 0.375rem;
font-size: 1.25rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
} }

View File

@ -1,28 +1,28 @@
import React, { import React, {
memo, useEffect, useMemo, useRef, 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 { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatTime, formatWeekday } from '../../util/date/dateFormat'; import { formatTime, formatWeekday } from '../../../util/date/dateFormat';
import { import {
getUtcOffset, getWeekStart, shiftTimeRanges, splitDays, getUtcOffset, getWeekStart, shiftTimeRanges, splitDays,
} from '../../util/date/workHours'; } from '../../../util/date/workHours';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import useInterval from '../../hooks/schedulers/useInterval'; import useInterval from '../../../hooks/schedulers/useInterval';
import useDerivedState from '../../hooks/useDerivedState'; import useDerivedState from '../../../hooks/useDerivedState';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../../hooks/useForceUpdate';
import useLang from '../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useSelectorSignal from '../../hooks/useSelectorSignal'; import useSelectorSignal from '../../../hooks/useSelectorSignal';
import ListItem from '../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../ui/Transition'; import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ui/Transition';
import Icon from './Icon'; import Icon from '../Icon';
import styles from './BusinessHours.module.scss'; import styles from './BusinessHours.module.scss';
@ -142,6 +142,7 @@ const BusinessHours = ({
className={styles.root} className={styles.root}
isStatic={isExpanded} isStatic={isExpanded}
ripple ripple
narrow
withColorTransition withColorTransition
onClick={handleClick} onClick={handleClick}
> >

View 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;
}

View File

@ -1,15 +1,15 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useMemo, useState, memo, useEffect, useMemo, useState,
} from '../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername, ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername,
} from '../../api/types'; } from '../../../api/types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { TME_LINK_PREFIX } from '../../config'; import { TME_LINK_PREFIX } from '../../../config';
import { import {
buildStaticMapHash, buildStaticMapHash,
getChatLink, getChatLink,
@ -17,7 +17,7 @@ import {
isChatChannel, isChatChannel,
isUserRightBanned, isUserRightBanned,
selectIsChatMuted, selectIsChatMuted,
} from '../../global/helpers'; } from '../../../global/helpers';
import { import {
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
@ -27,25 +27,26 @@ import {
selectTopicLink, selectTopicLink,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
} from '../../global/selectors'; } from '../../../global/selectors';
import { copyTextToClipboard } from '../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { formatPhoneNumberWithCode } from '../../util/phoneNumber'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber';
import { debounce } from '../../util/schedulers'; import { debounce } from '../../../util/schedulers';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../../util/stopEvent';
import { ChatAnimationTypes } from '../left/main/hooks'; import { ChatAnimationTypes } from '../../left/main/hooks';
import renderText from './helpers/renderText'; import renderText from '../helpers/renderText';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
import useLang from '../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio'; import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio';
import Chat from '../left/main/Chat'; import Chat from '../../left/main/Chat';
import ListItem from '../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Skeleton from '../ui/placeholder/Skeleton'; import Skeleton from '../../ui/placeholder/Skeleton';
import Switcher from '../ui/Switcher'; import Switcher from '../../ui/Switcher';
import BusinessHours from './BusinessHours'; import BusinessHours from './BusinessHours';
import UserBirthday from './UserBirthday';
import styles from './ChatExtra.module.scss'; import styles from './ChatExtra.module.scss';
@ -115,6 +116,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
businessLocation, businessLocation,
businessWorkHours, businessWorkHours,
personalChannelMessageId, personalChannelMessageId,
birthday,
} = userFullInfo || {}; } = userFullInfo || {};
const lang = useLang(); const lang = useLang();
@ -140,10 +142,10 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
const locationRightComponent = useMemo(() => { const locationRightComponent = useMemo(() => {
if (!businessLocation?.geo) return undefined; if (!businessLocation?.geo) return undefined;
if (locationBlobUrl) { 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]); }, [businessLocation, locationBlobUrl]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
@ -329,6 +331,9 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
<span className="subtitle">{lang('SetUrlPlaceholder')}</span> <span className="subtitle">{lang('SetUrlPlaceholder')}</span>
</ListItem> </ListItem>
)} )}
{birthday && (
<UserBirthday key={peerId} birthday={birthday} user={user!} isInSettings={isInSettings} />
)}
{!isInSettings && ( {!isInSettings && (
<ListItem icon="unmute" ripple onClick={handleNotificationChange}> <ListItem icon="unmute" ripple onClick={handleNotificationChange}>
<span>{lang('Notifications')}</span> <span>{lang('Notifications')}</span>
@ -348,6 +353,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
icon="location" icon="location"
ripple ripple
multiline multiline
narrow
rightElement={locationRightComponent} rightElement={locationRightComponent}
onClick={handleClickLocation} onClick={handleClickLocation}
> >

View 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%;
}
}

View 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;
}

View File

@ -189,6 +189,7 @@ function LeftColumn({
case SettingsScreens.PrivacyLastSeen: case SettingsScreens.PrivacyLastSeen:
case SettingsScreens.PrivacyProfilePhoto: case SettingsScreens.PrivacyProfilePhoto:
case SettingsScreens.PrivacyBio: case SettingsScreens.PrivacyBio:
case SettingsScreens.PrivacyBirthday:
case SettingsScreens.PrivacyPhoneCall: case SettingsScreens.PrivacyPhoneCall:
case SettingsScreens.PrivacyPhoneP2P: case SettingsScreens.PrivacyPhoneP2P:
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
@ -243,6 +244,10 @@ function LeftColumn({
case SettingsScreens.PrivacyBioDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts:
setSettingsScreen(SettingsScreens.PrivacyBio); setSettingsScreen(SettingsScreens.PrivacyBio);
return; return;
case SettingsScreens.PrivacyBirthdayAllowedContacts:
case SettingsScreens.PrivacyBirthdayDeniedContacts:
setSettingsScreen(SettingsScreens.PrivacyBirthday);
return;
case SettingsScreens.PrivacyPhoneCallAllowedContacts: case SettingsScreens.PrivacyPhoneCallAllowedContacts:
case SettingsScreens.PrivacyPhoneCallDeniedContacts: case SettingsScreens.PrivacyPhoneCallDeniedContacts:
setSettingsScreen(SettingsScreens.PrivacyPhoneCall); setSettingsScreen(SettingsScreens.PrivacyPhoneCall);

View File

@ -105,6 +105,11 @@ const PRIVACY_BIO_SCREENS = [
SettingsScreens.PrivacyBioDeniedContacts, SettingsScreens.PrivacyBioDeniedContacts,
]; ];
const PRIVACY_BIRTHDAY_SCREENS = [
SettingsScreens.PrivacyBirthdayAllowedContacts,
SettingsScreens.PrivacyBirthdayDeniedContacts,
];
const PRIVACY_PHONE_CALL_SCREENS = [ const PRIVACY_PHONE_CALL_SCREENS = [
SettingsScreens.PrivacyPhoneCallAllowedContacts, SettingsScreens.PrivacyPhoneCallAllowedContacts,
SettingsScreens.PrivacyPhoneCallDeniedContacts, SettingsScreens.PrivacyPhoneCallDeniedContacts,
@ -198,6 +203,7 @@ const Settings: FC<OwnProps> = ({
[SettingsScreens.PrivacyLastSeen]: PRIVACY_LAST_SEEN_PHONE_SCREENS.includes(activeScreen), [SettingsScreens.PrivacyLastSeen]: PRIVACY_LAST_SEEN_PHONE_SCREENS.includes(activeScreen),
[SettingsScreens.PrivacyProfilePhoto]: PRIVACY_PROFILE_PHOTO_SCREENS.includes(activeScreen), [SettingsScreens.PrivacyProfilePhoto]: PRIVACY_PROFILE_PHOTO_SCREENS.includes(activeScreen),
[SettingsScreens.PrivacyBio]: PRIVACY_BIO_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.PrivacyPhoneCall]: PRIVACY_PHONE_CALL_SCREENS.includes(activeScreen),
[SettingsScreens.PrivacyPhoneP2P]: PRIVACY_PHONE_P2P_SCREENS.includes(activeScreen), [SettingsScreens.PrivacyPhoneP2P]: PRIVACY_PHONE_P2P_SCREENS.includes(activeScreen),
[SettingsScreens.PrivacyForwarding]: PRIVACY_FORWARDING_SCREENS.includes(activeScreen), [SettingsScreens.PrivacyForwarding]: PRIVACY_FORWARDING_SCREENS.includes(activeScreen),
@ -323,6 +329,7 @@ const Settings: FC<OwnProps> = ({
case SettingsScreens.PrivacyLastSeen: case SettingsScreens.PrivacyLastSeen:
case SettingsScreens.PrivacyProfilePhoto: case SettingsScreens.PrivacyProfilePhoto:
case SettingsScreens.PrivacyBio: case SettingsScreens.PrivacyBio:
case SettingsScreens.PrivacyBirthday:
case SettingsScreens.PrivacyPhoneCall: case SettingsScreens.PrivacyPhoneCall:
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
case SettingsScreens.PrivacyVoiceMessages: case SettingsScreens.PrivacyVoiceMessages:
@ -340,6 +347,7 @@ const Settings: FC<OwnProps> = ({
case SettingsScreens.PrivacyLastSeenAllowedContacts: case SettingsScreens.PrivacyLastSeenAllowedContacts:
case SettingsScreens.PrivacyProfilePhotoAllowedContacts: case SettingsScreens.PrivacyProfilePhotoAllowedContacts:
case SettingsScreens.PrivacyBioAllowedContacts: case SettingsScreens.PrivacyBioAllowedContacts:
case SettingsScreens.PrivacyBirthdayAllowedContacts:
case SettingsScreens.PrivacyPhoneCallAllowedContacts: case SettingsScreens.PrivacyPhoneCallAllowedContacts:
case SettingsScreens.PrivacyPhoneP2PAllowedContacts: case SettingsScreens.PrivacyPhoneP2PAllowedContacts:
case SettingsScreens.PrivacyForwardingAllowedContacts: case SettingsScreens.PrivacyForwardingAllowedContacts:
@ -359,6 +367,7 @@ const Settings: FC<OwnProps> = ({
case SettingsScreens.PrivacyLastSeenDeniedContacts: case SettingsScreens.PrivacyLastSeenDeniedContacts:
case SettingsScreens.PrivacyProfilePhotoDeniedContacts: case SettingsScreens.PrivacyProfilePhotoDeniedContacts:
case SettingsScreens.PrivacyBioDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts:
case SettingsScreens.PrivacyBirthdayDeniedContacts:
case SettingsScreens.PrivacyPhoneCallDeniedContacts: case SettingsScreens.PrivacyPhoneCallDeniedContacts:
case SettingsScreens.PrivacyPhoneP2PDeniedContacts: case SettingsScreens.PrivacyPhoneP2PDeniedContacts:
case SettingsScreens.PrivacyForwardingDeniedContacts: case SettingsScreens.PrivacyForwardingDeniedContacts:

View File

@ -115,6 +115,8 @@ const SettingsHeader: FC<OwnProps> = ({
return <h3>{lang('Privacy.ProfilePhoto')}</h3>; return <h3>{lang('Privacy.ProfilePhoto')}</h3>;
case SettingsScreens.PrivacyBio: case SettingsScreens.PrivacyBio:
return <h3>{lang('PrivacyBio')}</h3>; return <h3>{lang('PrivacyBio')}</h3>;
case SettingsScreens.PrivacyBirthday:
return <h3>{lang('PrivacyBirthday')}</h3>;
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
return <h3>{lang('PrivacyForwards')}</h3>; return <h3>{lang('PrivacyForwards')}</h3>;
case SettingsScreens.PrivacyVoiceMessages: case SettingsScreens.PrivacyVoiceMessages:
@ -130,6 +132,7 @@ const SettingsHeader: FC<OwnProps> = ({
case SettingsScreens.PrivacyLastSeenAllowedContacts: case SettingsScreens.PrivacyLastSeenAllowedContacts:
case SettingsScreens.PrivacyProfilePhotoAllowedContacts: case SettingsScreens.PrivacyProfilePhotoAllowedContacts:
case SettingsScreens.PrivacyBioAllowedContacts: case SettingsScreens.PrivacyBioAllowedContacts:
case SettingsScreens.PrivacyBirthdayAllowedContacts:
case SettingsScreens.PrivacyForwardingAllowedContacts: case SettingsScreens.PrivacyForwardingAllowedContacts:
case SettingsScreens.PrivacyVoiceMessagesAllowedContacts: case SettingsScreens.PrivacyVoiceMessagesAllowedContacts:
case SettingsScreens.PrivacyGroupChatsAllowedContacts: case SettingsScreens.PrivacyGroupChatsAllowedContacts:
@ -140,6 +143,7 @@ const SettingsHeader: FC<OwnProps> = ({
case SettingsScreens.PrivacyLastSeenDeniedContacts: case SettingsScreens.PrivacyLastSeenDeniedContacts:
case SettingsScreens.PrivacyProfilePhotoDeniedContacts: case SettingsScreens.PrivacyProfilePhotoDeniedContacts:
case SettingsScreens.PrivacyBioDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts:
case SettingsScreens.PrivacyBirthdayDeniedContacts:
case SettingsScreens.PrivacyForwardingDeniedContacts: case SettingsScreens.PrivacyForwardingDeniedContacts:
case SettingsScreens.PrivacyVoiceMessagesDeniedContacts: case SettingsScreens.PrivacyVoiceMessagesDeniedContacts:
case SettingsScreens.PrivacyGroupChatsDeniedContacts: case SettingsScreens.PrivacyGroupChatsDeniedContacts:

View File

@ -12,8 +12,8 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import ChatExtra from '../../common/ChatExtra';
import PremiumIcon from '../../common/PremiumIcon'; import PremiumIcon from '../../common/PremiumIcon';
import ChatExtra from '../../common/profile/ChatExtra';
import ProfileInfo from '../../common/ProfileInfo'; import ProfileInfo from '../../common/ProfileInfo';
import ConfirmDialog from '../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';

View File

@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { GlobalState } from '../../../global/types';
import type { ApiPrivacySettings } from '../../../types'; import type { ApiPrivacySettings } from '../../../types';
import { SettingsScreens } from '../../../types'; import { SettingsScreens } from '../../../types';
@ -33,14 +34,7 @@ type StateProps = {
shouldArchiveAndMuteNewNonContact?: boolean; shouldArchiveAndMuteNewNonContact?: boolean;
shouldNewNonContactPeersRequirePremium?: boolean; shouldNewNonContactPeersRequirePremium?: boolean;
canDisplayChatInTitle?: boolean; canDisplayChatInTitle?: boolean;
privacyPhoneNumber?: ApiPrivacySettings; privacy: GlobalState['settings']['privacy'];
privacyLastSeen?: ApiPrivacySettings;
privacyProfilePhoto?: ApiPrivacySettings;
privacyForwarding?: ApiPrivacySettings;
privacyVoiceMessages?: ApiPrivacySettings;
privacyGroupChats?: ApiPrivacySettings;
privacyPhoneCall?: ApiPrivacySettings;
privacyBio?: ApiPrivacySettings;
}; };
const SettingsPrivacy: FC<OwnProps & StateProps> = ({ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
@ -57,14 +51,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
shouldNewNonContactPeersRequirePremium, shouldNewNonContactPeersRequirePremium,
canDisplayChatInTitle, canDisplayChatInTitle,
canSetPasscode, canSetPasscode,
privacyPhoneNumber, privacy,
privacyLastSeen,
privacyProfilePhoto,
privacyForwarding,
privacyVoiceMessages,
privacyGroupChats,
privacyPhoneCall,
privacyBio,
onScreenSelect, onScreenSelect,
onReset, onReset,
}) => { }) => {
@ -206,7 +193,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('PrivacyPhoneTitle')}</span> <span className="title">{lang('PrivacyPhoneTitle')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyPhoneNumber)} {getVisibilityValue(privacy.phoneNumber)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -219,7 +206,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('LastSeenTitle')}</span> <span className="title">{lang('LastSeenTitle')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyLastSeen)} {getVisibilityValue(privacy.lastSeen)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -232,7 +219,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('PrivacyProfilePhotoTitle')}</span> <span className="title">{lang('PrivacyProfilePhotoTitle')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyProfilePhoto)} {getVisibilityValue(privacy.profilePhoto)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -245,7 +232,20 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('PrivacyBio')}</span> <span className="title">{lang('PrivacyBio')}</span>
<span className="subtitle" dir="auto"> <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> </span>
</div> </div>
</ListItem> </ListItem>
@ -258,7 +258,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('PrivacyForwardsTitle')}</span> <span className="title">{lang('PrivacyForwardsTitle')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyForwarding)} {getVisibilityValue(privacy.forwards)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -271,7 +271,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('WhoCanCallMe')}</span> <span className="title">{lang('WhoCanCallMe')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyPhoneCall)} {getVisibilityValue(privacy.phoneCall)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -286,7 +286,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('PrivacyVoiceMessagesTitle')}</span> <span className="title">{lang('PrivacyVoiceMessagesTitle')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyVoiceMessages)} {getVisibilityValue(privacy.voiceMessages)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -315,7 +315,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
<div className="multiline-menu-item"> <div className="multiline-menu-item">
<span className="title">{lang('WhoCanAddMe')}</span> <span className="title">{lang('WhoCanAddMe')}</span>
<span className="subtitle" dir="auto"> <span className="subtitle" dir="auto">
{getVisibilityValue(privacyGroupChats)} {getVisibilityValue(privacy.chatInvite)}
</span> </span>
</div> </div>
</ListItem> </ListItem>
@ -392,14 +392,7 @@ export default memo(withGlobal<OwnProps>(
shouldArchiveAndMuteNewNonContact, shouldArchiveAndMuteNewNonContact,
canChangeSensitive, canChangeSensitive,
shouldNewNonContactPeersRequirePremium, shouldNewNonContactPeersRequirePremium,
privacyPhoneNumber: privacy.phoneNumber, privacy,
privacyLastSeen: privacy.lastSeen,
privacyProfilePhoto: privacy.profilePhoto,
privacyForwarding: privacy.forwards,
privacyVoiceMessages: privacy.voiceMessages,
privacyGroupChats: privacy.chatInvite,
privacyPhoneCall: privacy.phoneCall,
privacyBio: privacy.bio,
canDisplayChatInTitle, canDisplayChatInTitle,
canSetPasscode: selectCanSetPasscode(global), canSetPasscode: selectCanSetPasscode(global),
}; };

View File

@ -179,6 +179,8 @@ function PrivacySubsection({
return lang('PrivacyProfilePhotoTitle'); return lang('PrivacyProfilePhotoTitle');
case SettingsScreens.PrivacyBio: case SettingsScreens.PrivacyBio:
return lang('PrivacyBioTitle'); return lang('PrivacyBioTitle');
case SettingsScreens.PrivacyBirthday:
return lang('PrivacyBirthdayTitle');
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
return lang('PrivacyForwardsTitle'); return lang('PrivacyForwardsTitle');
case SettingsScreens.PrivacyVoiceMessages: case SettingsScreens.PrivacyVoiceMessages:
@ -233,6 +235,8 @@ function PrivacySubsection({
return SettingsScreens.PrivacyProfilePhotoAllowedContacts; return SettingsScreens.PrivacyProfilePhotoAllowedContacts;
case SettingsScreens.PrivacyBio: case SettingsScreens.PrivacyBio:
return SettingsScreens.PrivacyBioAllowedContacts; return SettingsScreens.PrivacyBioAllowedContacts;
case SettingsScreens.PrivacyBirthday:
return SettingsScreens.PrivacyBirthdayAllowedContacts;
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
return SettingsScreens.PrivacyForwardingAllowedContacts; return SettingsScreens.PrivacyForwardingAllowedContacts;
case SettingsScreens.PrivacyPhoneCall: case SettingsScreens.PrivacyPhoneCall:
@ -256,6 +260,8 @@ function PrivacySubsection({
return SettingsScreens.PrivacyProfilePhotoDeniedContacts; return SettingsScreens.PrivacyProfilePhotoDeniedContacts;
case SettingsScreens.PrivacyBio: case SettingsScreens.PrivacyBio:
return SettingsScreens.PrivacyBioDeniedContacts; return SettingsScreens.PrivacyBioDeniedContacts;
case SettingsScreens.PrivacyBirthday:
return SettingsScreens.PrivacyBirthdayDeniedContacts;
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
return SettingsScreens.PrivacyForwardingDeniedContacts; return SettingsScreens.PrivacyForwardingDeniedContacts;
case SettingsScreens.PrivacyPhoneCall: case SettingsScreens.PrivacyPhoneCall:
@ -355,6 +361,10 @@ export default memo(withGlobal<OwnProps>(
primaryPrivacy = privacy.bio; primaryPrivacy = privacy.bio;
break; break;
case SettingsScreens.PrivacyBirthday:
primaryPrivacy = privacy.birthday;
break;
case SettingsScreens.PrivacyPhoneP2P: case SettingsScreens.PrivacyPhoneP2P:
case SettingsScreens.PrivacyPhoneCall: case SettingsScreens.PrivacyPhoneCall:
primaryPrivacy = privacy.phoneCall; primaryPrivacy = privacy.phoneCall;

View File

@ -140,6 +140,9 @@ function getCurrentPrivacySettings(global: GlobalState, screen: SettingsScreens)
case SettingsScreens.PrivacyBioAllowedContacts: case SettingsScreens.PrivacyBioAllowedContacts:
case SettingsScreens.PrivacyBioDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts:
return privacy.bio; return privacy.bio;
case SettingsScreens.PrivacyBirthdayAllowedContacts:
case SettingsScreens.PrivacyBirthdayDeniedContacts:
return privacy.birthday;
case SettingsScreens.PrivacyPhoneCallAllowedContacts: case SettingsScreens.PrivacyPhoneCallAllowedContacts:
case SettingsScreens.PrivacyPhoneCallDeniedContacts: case SettingsScreens.PrivacyPhoneCallDeniedContacts:
return privacy.phoneCall; return privacy.phoneCall;

View File

@ -19,6 +19,10 @@ export function getPrivacyKey(screen: SettingsScreens): ApiPrivacyKey | undefine
case SettingsScreens.PrivacyBioAllowedContacts: case SettingsScreens.PrivacyBioAllowedContacts:
case SettingsScreens.PrivacyBioDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts:
return 'bio'; return 'bio';
case SettingsScreens.PrivacyBirthday:
case SettingsScreens.PrivacyBirthdayAllowedContacts:
case SettingsScreens.PrivacyBirthdayDeniedContacts:
return 'birthday';
case SettingsScreens.PrivacyForwarding: case SettingsScreens.PrivacyForwarding:
case SettingsScreens.PrivacyForwardingAllowedContacts: case SettingsScreens.PrivacyForwardingAllowedContacts:
case SettingsScreens.PrivacyForwardingDeniedContacts: case SettingsScreens.PrivacyForwardingDeniedContacts:

View File

@ -1,8 +1,8 @@
import type { FC } from '../../lib/teact/teact'; 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 { withGlobal } from '../../global';
import type { TabState } from '../../global/types'; import type { ConfettiStyle, TabState } from '../../global/types';
import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { selectTabState } from '../../global/selectors'; import { selectTabState } from '../../global/selectors';
@ -11,6 +11,7 @@ import { pick } from '../../util/iteratees';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import useLastCallback from '../../hooks/useLastCallback';
import useSyncEffect from '../../hooks/useSyncEffect'; import useSyncEffect from '../../hooks/useSyncEffect';
import useWindowSize from '../../hooks/window/useWindowSize'; import useWindowSize from '../../hooks/window/useWindowSize';
@ -53,27 +54,20 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
const defaultConfettiAmount = isMobile ? 50 : 100; const defaultConfettiAmount = isMobile ? 50 : 100;
const { const {
lastConfettiTime, top, width, left, height, lastConfettiTime, top, width, left, height, style = 'poppers',
} = confetti || {}; } = 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++) { for (let i = 0; i < amount; i++) {
const leftSide = i % 2; const {
const pos = { position, velocity,
x: w * (leftSide ? -0.1 : 1.1), } = generateRandomPositionData(style, w, h, i);
y: h * 0.75,
}; const size = DEFAULT_CONFETTI_SIZE + randomNumberAroundZero(DEFAULT_CONFETTI_SIZE / 2);
const randomX = Math.random() * w * 1.5;
const randomY = -h / 2 - Math.random() * h;
const velocity = {
x: leftSide ? randomX : randomX * -1,
y: randomY,
};
const randomColor = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)]; const randomColor = CONFETTI_COLORS[Math.floor(Math.random() * CONFETTI_COLORS.length)];
const size = DEFAULT_CONFETTI_SIZE;
confettiRef.current.push({ confettiRef.current.push({
pos, pos: position,
size, size,
color: randomColor, color: randomColor,
velocity, velocity,
@ -84,9 +78,9 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
frameCount: 0, frameCount: 0,
}); });
} }
}, [defaultConfettiAmount]); });
const updateCanvas = useCallback(() => { const updateCanvas = useLastCallback(() => {
if (!canvasRef.current || !isRafStartedRef.current) { if (!canvasRef.current || !isRafStartedRef.current) {
return; return;
} }
@ -121,7 +115,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
}; };
const newVelocity = { const newVelocity = {
x: velocity.x * 0.98, // Air Resistance x: velocity.x * 0.5 ** (diff / 1), // Air Resistance
y: velocity.y += diff * 1000, // Gravity y: velocity.y += diff * 1000, // Gravity
}; };
@ -167,7 +161,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
} else { } else {
isRafStartedRef.current = false; isRafStartedRef.current = false;
} }
}, []); });
useSyncEffect(([prevConfettiTime]) => { useSyncEffect(([prevConfettiTime]) => {
let hideTimeout: ReturnType<typeof setTimeout>; let hideTimeout: ReturnType<typeof setTimeout>;
@ -189,7 +183,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
return undefined; return undefined;
} }
const style = buildStyle( const containerStyle = buildStyle(
Boolean(top) && `top: ${top}px`, Boolean(top) && `top: ${top}px`,
Boolean(left) && `left: ${left}px`, Boolean(left) && `left: ${left}px`,
Boolean(width) && `width: ${width}px`, Boolean(width) && `width: ${width}px`,
@ -197,7 +191,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
); );
return ( 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} /> <canvas ref={canvasRef} className={styles.canvas} width={windowSize.width} height={windowSize.height} />
</div> </div>
); );
@ -206,3 +200,46 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
export default memo(withGlobal( export default memo(withGlobal(
(global): StateProps => pick(selectTabState(global), ['confetti']), (global): StateProps => pick(selectTabState(global), ['confetti']),
)(ConfettiContainer)); )(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;
}

View File

@ -236,6 +236,7 @@ const Main: FC<OwnProps & StateProps> = ({
const { const {
initMain, initMain,
loadAnimatedEmojis, loadAnimatedEmojis,
loadBirthdayNumbersStickers,
loadNotificationSettings, loadNotificationSettings,
loadNotificationExceptions, loadNotificationExceptions,
updateIsOnline, updateIsOnline,
@ -338,6 +339,7 @@ const Main: FC<OwnProps & StateProps> = ({
initMain(); initMain();
loadAvailableReactions(); loadAvailableReactions();
loadAnimatedEmojis(); loadAnimatedEmojis();
loadBirthdayNumbersStickers();
loadGenericEmojiEffects(); loadGenericEmojiEffects();
loadNotificationSettings(); loadNotificationSettings();
loadNotificationExceptions(); loadNotificationExceptions();

View File

@ -62,12 +62,12 @@ import useProfileViewportIds from './hooks/useProfileViewportIds';
import useTransitionFixes from './hooks/useTransitionFixes'; import useTransitionFixes from './hooks/useTransitionFixes';
import Audio from '../common/Audio'; import Audio from '../common/Audio';
import ChatExtra from '../common/ChatExtra';
import Document from '../common/Document'; import Document from '../common/Document';
import GroupChatInfo from '../common/GroupChatInfo'; import GroupChatInfo from '../common/GroupChatInfo';
import Media from '../common/Media'; import Media from '../common/Media';
import NothingFound from '../common/NothingFound'; import NothingFound from '../common/NothingFound';
import PrivateChatInfo from '../common/PrivateChatInfo'; import PrivateChatInfo from '../common/PrivateChatInfo';
import ChatExtra from '../common/profile/ChatExtra';
import ProfileInfo from '../common/ProfileInfo'; import ProfileInfo from '../common/ProfileInfo';
import WebLink from '../common/WebLink'; import WebLink from '../common/WebLink';
import ChatList from '../left/main/ChatList'; import ChatList from '../left/main/ChatList';

View File

@ -80,7 +80,7 @@
border-radius: 0 ; border-radius: 0 ;
} }
> .icon { > .ListItem-main-icon {
font-size: 1.5rem; font-size: 1.5rem;
margin-inline-start: 0.125rem; margin-inline-start: 0.125rem;
margin-inline-end: 1.25rem; margin-inline-end: 1.25rem;
@ -125,7 +125,7 @@
} }
&.multiline { &.multiline {
.ListItem-button > .icon { .ListItem-button > .ListItem-main-icon {
position: relative; position: relative;
} }
} }
@ -194,7 +194,7 @@
.ListItem-button { .ListItem-button {
color: var(--color-error); color: var(--color-error);
.icon { .ListItem-main-icon {
color: inherit; color: inherit;
} }
} }

View File

@ -16,6 +16,7 @@ import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useMenuPosition from '../../hooks/useMenuPosition'; import useMenuPosition from '../../hooks/useMenuPosition';
import Icon from '../common/Icon';
import Button from './Button'; import Button from './Button';
import Menu from './Menu'; import Menu from './Menu';
import MenuItem from './MenuItem'; import MenuItem from './MenuItem';
@ -47,6 +48,7 @@ interface OwnProps {
iconClassName?: string; iconClassName?: string;
leftElement?: TeactNode; leftElement?: TeactNode;
secondaryIcon?: IconName; secondaryIcon?: IconName;
secondaryIconClassName?: string;
rightElement?: TeactNode; rightElement?: TeactNode;
buttonClassName?: string; buttonClassName?: string;
className?: string; className?: string;
@ -84,6 +86,7 @@ const ListItem: FC<OwnProps> = ({
buttonClassName, buttonClassName,
menuBubbleClassName, menuBubbleClassName,
secondaryIcon, secondaryIcon,
secondaryIconClassName,
rightElement, rightElement,
className, className,
style, style,
@ -246,20 +249,20 @@ const ListItem: FC<OwnProps> = ({
)} )}
{leftElement} {leftElement}
{icon && ( {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 && (<div className="multiline-item">{children}</div>)}
{!multiline && children} {!multiline && children}
{secondaryIcon && ( {secondaryIcon && (
<Button <Button
className="secondary-icon" className={buildClassName('secondary-icon', secondaryIconClassName)}
round round
color="translucent" color="translucent"
size="smaller" size="smaller"
onClick={handleSecondaryIconClick} onClick={handleSecondaryIconClick}
onMouseDown={handleSecondaryIconMouseDown} onMouseDown={handleSecondaryIconMouseDown}
> >
<i className={`icon icon-${secondaryIcon}`} /> <Icon name={secondaryIcon} />
</Button> </Button>
)} )}
{rightElement} {rightElement}

View File

@ -213,6 +213,8 @@ export const BASE_EMOJI_KEYWORD_LANG = 'en';
export const MENU_TRANSITION_DURATION = 200; export const MENU_TRANSITION_DURATION = 200;
export const SLIDE_TRANSITION_DURATION = 450; export const SLIDE_TRANSITION_DURATION = 450;
export const BIRTHDAY_NUMBERS_SET = 'FestiveFontEmoji';
export const VIDEO_WEBM_TYPE = 'video/webm'; export const VIDEO_WEBM_TYPE = 'video/webm';
export const GIF_MIME_TYPE = 'image/gif'; export const GIF_MIME_TYPE = 'image/gif';

View File

@ -421,6 +421,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
callApi('fetchPrivacySettings', 'phoneP2P'), callApi('fetchPrivacySettings', 'phoneP2P'),
callApi('fetchPrivacySettings', 'voiceMessages'), callApi('fetchPrivacySettings', 'voiceMessages'),
callApi('fetchPrivacySettings', 'bio'), callApi('fetchPrivacySettings', 'bio'),
callApi('fetchPrivacySettings', 'birthday'),
]); ]);
if (result.some((e) => e === undefined)) { if (result.some((e) => e === undefined)) {
@ -438,6 +439,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
phoneP2PSettings, phoneP2PSettings,
voiceMessagesSettings, voiceMessagesSettings,
bioSettings, bioSettings,
birthdaySettings,
] = result as { ] = result as {
users: ApiUser[]; users: ApiUser[];
rules: ApiPrivacySettings; rules: ApiPrivacySettings;
@ -463,6 +465,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
phoneP2P: phoneP2PSettings.rules, phoneP2P: phoneP2PSettings.rules,
voiceMessages: voiceMessagesSettings.rules, voiceMessages: voiceMessagesSettings.rules,
bio: bioSettings.rules, bio: bioSettings.rules,
birthday: birthdaySettings.rules,
}, },
}, },
}; };

View File

@ -4,6 +4,7 @@ import type {
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { BIRTHDAY_NUMBERS_SET } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { translate } from '../../../util/langProvider'; import { translate } from '../../../util/langProvider';
@ -267,6 +268,26 @@ addActionHandler('loadAnimatedEmojis', async (global): Promise<void> => {
setGlobal(global); 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> => { addActionHandler('loadGenericEmojiEffects', async (global): Promise<void> => {
const stickerSet = await callApi('fetchGenericEmojiEffects'); const stickerSet = await callApi('fetchGenericEmojiEffects');
if (!stickerSet) { if (!stickerSet) {

View File

@ -5,6 +5,10 @@ export function getStickerPreviewHash(stickerId: string) {
return `sticker${stickerId}?size=m`; return `sticker${stickerId}?size=m`;
} }
export function getStickerMediaHash(stickerId: string) {
return `document${stickerId}`;
}
export function containsCustomEmoji(formattedText: ApiFormattedText) { export function containsCustomEmoji(formattedText: ApiFormattedText) {
return formattedText.entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji); return formattedText.entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
} }

View File

@ -148,6 +148,8 @@ export type IDimensions = {
export type ApiPaymentStatus = 'paid' | 'failed' | 'pending' | 'cancelled'; export type ApiPaymentStatus = 'paid' | 'failed' | 'pending' | 'cancelled';
export type ConfettiStyle = 'poppers' | 'top-down';
export interface TabThread { export interface TabThread {
scrollOffset?: number; scrollOffset?: number;
replyStack?: number[]; replyStack?: number[];
@ -602,6 +604,7 @@ export type TabState = {
left?: number; left?: number;
width?: number; width?: number;
height?: number; height?: number;
style?: ConfettiStyle;
}; };
urlAuth?: { urlAuth?: {
@ -970,6 +973,7 @@ export type GlobalState = {
animatedEmojis?: ApiStickerSet; animatedEmojis?: ApiStickerSet;
animatedEmojiEffects?: ApiStickerSet; animatedEmojiEffects?: ApiStickerSet;
genericEmojiEffects?: ApiStickerSet; genericEmojiEffects?: ApiStickerSet;
birthdayNumbers?: ApiStickerSet;
defaultTopicIconsId?: string; defaultTopicIconsId?: string;
defaultStatusIconsId?: string; defaultStatusIconsId?: string;
premiumGifts?: ApiStickerSet; premiumGifts?: ApiStickerSet;
@ -2518,6 +2522,7 @@ export interface ActionPayloads {
loadAnimatedEmojis: undefined; loadAnimatedEmojis: undefined;
loadGreetingStickers: undefined; loadGreetingStickers: undefined;
loadGenericEmojiEffects: undefined; loadGenericEmojiEffects: undefined;
loadBirthdayNumbersStickers: undefined;
addRecentSticker: { addRecentSticker: {
sticker: ApiSticker; sticker: ApiSticker;
@ -2783,6 +2788,7 @@ export interface ActionPayloads {
left: number; left: number;
width: number; width: number;
height: number; height: number;
style?: ConfettiStyle;
} & WithTabId) | WithTabId; } & WithTabId) | WithTabId;
updateAttachmentSettings: { updateAttachmentSettings: {

View File

@ -185,6 +185,7 @@ export enum SettingsScreens {
PrivacyLastSeen, PrivacyLastSeen,
PrivacyProfilePhoto, PrivacyProfilePhoto,
PrivacyBio, PrivacyBio,
PrivacyBirthday,
PrivacyPhoneCall, PrivacyPhoneCall,
PrivacyPhoneP2P, PrivacyPhoneP2P,
PrivacyForwarding, PrivacyForwarding,
@ -199,6 +200,8 @@ export enum SettingsScreens {
PrivacyProfilePhotoDeniedContacts, PrivacyProfilePhotoDeniedContacts,
PrivacyBioAllowedContacts, PrivacyBioAllowedContacts,
PrivacyBioDeniedContacts, PrivacyBioDeniedContacts,
PrivacyBirthdayAllowedContacts,
PrivacyBirthdayDeniedContacts,
PrivacyPhoneCallAllowedContacts, PrivacyPhoneCallAllowedContacts,
PrivacyPhoneCallDeniedContacts, PrivacyPhoneCallDeniedContacts,
PrivacyPhoneP2PAllowedContacts, PrivacyPhoneP2PAllowedContacts,
@ -380,7 +383,7 @@ export type ProfileTabType =
| 'dialogs'; | 'dialogs';
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice'; export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profilePhoto' | 'voiceMessages' | 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 type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';
export enum ProfileState { export enum ProfileState {