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';
case 'PrivacyKeyChatInvite':
return 'chatInvite';
case 'PrivacyKeyAbout':
return 'bio';
case 'PrivacyKeyBirthday':
return 'birthday';
}
return undefined;

View File

@ -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);
}

View File

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

View File

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

View File

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

View File

@ -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}
>

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, {
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}
>

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.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);

View File

@ -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:

View File

@ -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:

View File

@ -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';

View File

@ -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),
};

View File

@ -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;

View File

@ -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;

View File

@ -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:

View File

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

View File

@ -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();

View File

@ -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';

View File

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

View File

@ -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}

View File

@ -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';

View File

@ -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,
},
},
};

View File

@ -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) {

View File

@ -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);
}

View File

@ -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: {

View File

@ -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 {