diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index d11f77202..1ab04a708 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -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; diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 717dcea8b..72dfd4823 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -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({ + 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); +} diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index e18dfd62f..1f2b85c98 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -482,6 +482,9 @@ export function buildInputPrivacyKey(privacyKey: ApiPrivacyKey) { case 'bio': return new GramJs.InputPrivacyKeyAbout(); + + case 'birthday': + return new GramJs.InputPrivacyKeyBirthday(); } return undefined; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 99cf563dd..301e3dcc9 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -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; +} diff --git a/src/components/common/BusinessHours.module.scss b/src/components/common/profile/BusinessHours.module.scss similarity index 95% rename from src/components/common/BusinessHours.module.scss rename to src/components/common/profile/BusinessHours.module.scss index 798845044..0ddcbca28 100644 --- a/src/components/common/BusinessHours.module.scss +++ b/src/components/common/profile/BusinessHours.module.scss @@ -27,6 +27,8 @@ } .arrow { + margin-inline-end: 0.375rem; + font-size: 1.25rem; color: var(--color-text-secondary); } diff --git a/src/components/common/BusinessHours.tsx b/src/components/common/profile/BusinessHours.tsx similarity index 86% rename from src/components/common/BusinessHours.tsx rename to src/components/common/profile/BusinessHours.tsx index 93c8ab459..ce63c8c13 100644 --- a/src/components/common/BusinessHours.tsx +++ b/src/components/common/profile/BusinessHours.tsx @@ -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} > diff --git a/src/components/common/profile/ChatExtra.module.scss b/src/components/common/profile/ChatExtra.module.scss new file mode 100644 index 000000000..fbb856757 --- /dev/null +++ b/src/components/common/profile/ChatExtra.module.scss @@ -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; +} diff --git a/src/components/common/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx similarity index 88% rename from src/components/common/ChatExtra.tsx rename to src/components/common/profile/ChatExtra.tsx index 91d8c44e7..d12f5eb50 100644 --- a/src/components/common/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -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 = ({ businessLocation, businessWorkHours, personalChannelMessageId, + birthday, } = userFullInfo || {}; const lang = useLang(); @@ -140,10 +142,10 @@ const ChatExtra: FC = ({ const locationRightComponent = useMemo(() => { if (!businessLocation?.geo) return undefined; if (locationBlobUrl) { - return ; + return ; } - return ; + return ; }, [businessLocation, locationBlobUrl]); const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); @@ -329,6 +331,9 @@ const ChatExtra: FC = ({ {lang('SetUrlPlaceholder')} )} + {birthday && ( + + )} {!isInSettings && ( {lang('Notifications')} @@ -348,6 +353,7 @@ const ChatExtra: FC = ({ icon="location" ripple multiline + narrow rightElement={locationRightComponent} onClick={handleClickLocation} > diff --git a/src/components/common/profile/UserBirthday.module.scss b/src/components/common/profile/UserBirthday.module.scss new file mode 100644 index 000000000..0fbab2eb0 --- /dev/null +++ b/src/components/common/profile/UserBirthday.module.scss @@ -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%; + } +} diff --git a/src/components/common/profile/UserBirthday.tsx b/src/components/common/profile/UserBirthday.tsx new file mode 100644 index 000000000..fbca2bc8f --- /dev/null +++ b/src/components/common/profile/UserBirthday.tsx @@ -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(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 ( +
+ +
{renderText(lang(valueKey, [formattedDate, age], undefined, age))}
+ {lang(isToday ? 'ProfileBirthdayToday' : 'ProfileBirthday')} +
+ {isPlayingAnimation && IS_OFFSET_PATH_SUPPORTED && numbersForAge?.map((sticker, index) => ( +
0 && styles.shiftOrigin)} + style={`--digit-offset: ${index}`} + > + +
+ ))} + {isPlayingAnimation && effectSticker && ( +
+ +
+ )} +
+ ); +}; + +export default memo(withGlobal( + (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; +} diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index d64426b40..1344855b8 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -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); diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index c5e4a2b2a..02afcb7ee 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -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 = ({ [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 = ({ 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 = ({ 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 = ({ case SettingsScreens.PrivacyLastSeenDeniedContacts: case SettingsScreens.PrivacyProfilePhotoDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts: + case SettingsScreens.PrivacyBirthdayDeniedContacts: case SettingsScreens.PrivacyPhoneCallDeniedContacts: case SettingsScreens.PrivacyPhoneP2PDeniedContacts: case SettingsScreens.PrivacyForwardingDeniedContacts: diff --git a/src/components/left/settings/SettingsHeader.tsx b/src/components/left/settings/SettingsHeader.tsx index 4425e4937..62a4451e1 100644 --- a/src/components/left/settings/SettingsHeader.tsx +++ b/src/components/left/settings/SettingsHeader.tsx @@ -115,6 +115,8 @@ const SettingsHeader: FC = ({ return

{lang('Privacy.ProfilePhoto')}

; case SettingsScreens.PrivacyBio: return

{lang('PrivacyBio')}

; + case SettingsScreens.PrivacyBirthday: + return

{lang('PrivacyBirthday')}

; case SettingsScreens.PrivacyForwarding: return

{lang('PrivacyForwards')}

; case SettingsScreens.PrivacyVoiceMessages: @@ -130,6 +132,7 @@ const SettingsHeader: FC = ({ 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 = ({ case SettingsScreens.PrivacyLastSeenDeniedContacts: case SettingsScreens.PrivacyProfilePhotoDeniedContacts: case SettingsScreens.PrivacyBioDeniedContacts: + case SettingsScreens.PrivacyBirthdayDeniedContacts: case SettingsScreens.PrivacyForwardingDeniedContacts: case SettingsScreens.PrivacyVoiceMessagesDeniedContacts: case SettingsScreens.PrivacyGroupChatsDeniedContacts: diff --git a/src/components/left/settings/SettingsMain.tsx b/src/components/left/settings/SettingsMain.tsx index 30d0f0cfe..a92bece9b 100644 --- a/src/components/left/settings/SettingsMain.tsx +++ b/src/components/left/settings/SettingsMain.tsx @@ -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'; diff --git a/src/components/left/settings/SettingsPrivacy.tsx b/src/components/left/settings/SettingsPrivacy.tsx index 47628fa35..d6a9a6ca2 100644 --- a/src/components/left/settings/SettingsPrivacy.tsx +++ b/src/components/left/settings/SettingsPrivacy.tsx @@ -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 = ({ @@ -57,14 +51,7 @@ const SettingsPrivacy: FC = ({ shouldNewNonContactPeersRequirePremium, canDisplayChatInTitle, canSetPasscode, - privacyPhoneNumber, - privacyLastSeen, - privacyProfilePhoto, - privacyForwarding, - privacyVoiceMessages, - privacyGroupChats, - privacyPhoneCall, - privacyBio, + privacy, onScreenSelect, onReset, }) => { @@ -206,7 +193,7 @@ const SettingsPrivacy: FC = ({
{lang('PrivacyPhoneTitle')} - {getVisibilityValue(privacyPhoneNumber)} + {getVisibilityValue(privacy.phoneNumber)}
@@ -219,7 +206,7 @@ const SettingsPrivacy: FC = ({
{lang('LastSeenTitle')} - {getVisibilityValue(privacyLastSeen)} + {getVisibilityValue(privacy.lastSeen)}
@@ -232,7 +219,7 @@ const SettingsPrivacy: FC = ({
{lang('PrivacyProfilePhotoTitle')} - {getVisibilityValue(privacyProfilePhoto)} + {getVisibilityValue(privacy.profilePhoto)}
@@ -245,7 +232,20 @@ const SettingsPrivacy: FC = ({
{lang('PrivacyBio')} - {getVisibilityValue(privacyBio)} + {getVisibilityValue(privacy.bio)} + +
+ + onScreenSelect(SettingsScreens.PrivacyBirthday)} + > +
+ {lang('PrivacyBirthday')} + + {getVisibilityValue(privacy.birthday)}
@@ -258,7 +258,7 @@ const SettingsPrivacy: FC = ({
{lang('PrivacyForwardsTitle')} - {getVisibilityValue(privacyForwarding)} + {getVisibilityValue(privacy.forwards)}
@@ -271,7 +271,7 @@ const SettingsPrivacy: FC = ({
{lang('WhoCanCallMe')} - {getVisibilityValue(privacyPhoneCall)} + {getVisibilityValue(privacy.phoneCall)}
@@ -286,7 +286,7 @@ const SettingsPrivacy: FC = ({
{lang('PrivacyVoiceMessagesTitle')} - {getVisibilityValue(privacyVoiceMessages)} + {getVisibilityValue(privacy.voiceMessages)}
@@ -315,7 +315,7 @@ const SettingsPrivacy: FC = ({
{lang('WhoCanAddMe')} - {getVisibilityValue(privacyGroupChats)} + {getVisibilityValue(privacy.chatInvite)}
@@ -392,14 +392,7 @@ export default memo(withGlobal( 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), }; diff --git a/src/components/left/settings/SettingsPrivacyVisibility.tsx b/src/components/left/settings/SettingsPrivacyVisibility.tsx index e3042ecca..cc0569662 100644 --- a/src/components/left/settings/SettingsPrivacyVisibility.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibility.tsx @@ -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( primaryPrivacy = privacy.bio; break; + case SettingsScreens.PrivacyBirthday: + primaryPrivacy = privacy.birthday; + break; + case SettingsScreens.PrivacyPhoneP2P: case SettingsScreens.PrivacyPhoneCall: primaryPrivacy = privacy.phoneCall; diff --git a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx index 1ba8d450c..b63d4679f 100644 --- a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx @@ -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; diff --git a/src/components/left/settings/helpers/privacy.ts b/src/components/left/settings/helpers/privacy.ts index 6d455b327..9249d4c73 100644 --- a/src/components/left/settings/helpers/privacy.ts +++ b/src/components/left/settings/helpers/privacy.ts @@ -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: diff --git a/src/components/main/ConfettiContainer.tsx b/src/components/main/ConfettiContainer.tsx index 1d39ac11c..a7c3e00f7 100644 --- a/src/components/main/ConfettiContainer.tsx +++ b/src/components/main/ConfettiContainer.tsx @@ -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 = ({ 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 = ({ confetti }) => { frameCount: 0, }); } - }, [defaultConfettiAmount]); + }); - const updateCanvas = useCallback(() => { + const updateCanvas = useLastCallback(() => { if (!canvasRef.current || !isRafStartedRef.current) { return; } @@ -121,7 +115,7 @@ const ConfettiContainer: FC = ({ 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 = ({ confetti }) => { } else { isRafStartedRef.current = false; } - }, []); + }); useSyncEffect(([prevConfettiTime]) => { let hideTimeout: ReturnType; @@ -189,7 +183,7 @@ const ConfettiContainer: FC = ({ 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 = ({ confetti }) => { ); return ( -
+
); @@ -206,3 +200,46 @@ const ConfettiContainer: FC = ({ 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; +} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 3fd1557dd..8da311c01 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -236,6 +236,7 @@ const Main: FC = ({ const { initMain, loadAnimatedEmojis, + loadBirthdayNumbersStickers, loadNotificationSettings, loadNotificationExceptions, updateIsOnline, @@ -338,6 +339,7 @@ const Main: FC = ({ initMain(); loadAvailableReactions(); loadAnimatedEmojis(); + loadBirthdayNumbersStickers(); loadGenericEmojiEffects(); loadNotificationSettings(); loadNotificationExceptions(); diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 487b46018..390de827f 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -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'; diff --git a/src/components/ui/ListItem.scss b/src/components/ui/ListItem.scss index 3ec8f08d9..9a00936bd 100644 --- a/src/components/ui/ListItem.scss +++ b/src/components/ui/ListItem.scss @@ -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; } } diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index 6d0be7603..e1202c283 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -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 = ({ buttonClassName, menuBubbleClassName, secondaryIcon, + secondaryIconClassName, rightElement, className, style, @@ -246,20 +249,20 @@ const ListItem: FC = ({ )} {leftElement} {icon && ( - + )} {multiline && (
{children}
)} {!multiline && children} {secondaryIcon && ( )} {rightElement} diff --git a/src/config.ts b/src/config.ts index 363313755..2ff1a7b58 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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'; diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index f3a3515f4..06b472bae 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -421,6 +421,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise => { 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 => { phoneP2PSettings, voiceMessagesSettings, bioSettings, + birthdaySettings, ] = result as { users: ApiUser[]; rules: ApiPrivacySettings; @@ -463,6 +465,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise => { phoneP2P: phoneP2PSettings.rules, voiceMessages: voiceMessagesSettings.rules, bio: bioSettings.rules, + birthday: birthdaySettings.rules, }, }, }; diff --git a/src/global/actions/api/symbols.ts b/src/global/actions/api/symbols.ts index d36d91dbb..1ebdf277d 100644 --- a/src/global/actions/api/symbols.ts +++ b/src/global/actions/api/symbols.ts @@ -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 => { setGlobal(global); }); +addActionHandler('loadBirthdayNumbersStickers', async (global): Promise => { + 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 => { const stickerSet = await callApi('fetchGenericEmojiEffects'); if (!stickerSet) { diff --git a/src/global/helpers/symbols.ts b/src/global/helpers/symbols.ts index aeef7e964..b4d9df016 100644 --- a/src/global/helpers/symbols.ts +++ b/src/global/helpers/symbols.ts @@ -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); } diff --git a/src/global/types.ts b/src/global/types.ts index 893dcf260..38db7859d 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -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: { diff --git a/src/types/index.ts b/src/types/index.ts index 704f74c54..dab3c1c57 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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 {