diff --git a/src/api/gramjs/apiBuilders/peers.ts b/src/api/gramjs/apiBuilders/peers.ts index 6dc3da250..75758363c 100644 --- a/src/api/gramjs/apiBuilders/peers.ts +++ b/src/api/gramjs/apiBuilders/peers.ts @@ -1,9 +1,10 @@ import type BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; -import type { ApiEmojiStatus, ApiPeerColor } from '../../types'; +import type { ApiEmojiStatusType, ApiPeerColor } from '../../types'; import { CHANNEL_ID_LENGTH } from '../../../config'; +import { numberToHexColor } from '../../../util/colors'; export function isPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser { return peer.hasOwnProperty('userId'); @@ -50,14 +51,30 @@ export function buildApiPeerColor(peerColor: GramJs.TypePeerColor): ApiPeerColor }; } -export function buildApiEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus): ApiEmojiStatus | undefined { +export function buildApiEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus): +ApiEmojiStatusType | undefined { if (mtpEmojiStatus instanceof GramJs.EmojiStatus) { - return { documentId: mtpEmojiStatus.documentId.toString(), until: mtpEmojiStatus.until }; + return { + type: 'regular', + documentId: mtpEmojiStatus.documentId.toString(), + until: mtpEmojiStatus.until, + }; } - // TODO: Support other parameters if (mtpEmojiStatus instanceof GramJs.EmojiStatusCollectible) { - return { documentId: mtpEmojiStatus.documentId.toString(), until: mtpEmojiStatus.until }; + return { + type: 'collectible', + collectibleId: mtpEmojiStatus.collectibleId.toString(), + documentId: mtpEmojiStatus.documentId.toString(), + title: mtpEmojiStatus.title, + slug: mtpEmojiStatus.slug, + patternDocumentId: mtpEmojiStatus.patternDocumentId.toString(), + centerColor: numberToHexColor(mtpEmojiStatus.centerColor), + edgeColor: numberToHexColor(mtpEmojiStatus.edgeColor), + patternColor: numberToHexColor(mtpEmojiStatus.patternColor), + textColor: numberToHexColor(mtpEmojiStatus.textColor), + until: mtpEmojiStatus.until, + }; } return undefined; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index a64d4c23d..3c14cfcea 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -8,6 +8,7 @@ import type { ApiChatBannedRights, ApiChatFolder, ApiChatReactions, + ApiEmojiStatusType, ApiFormattedText, ApiGroupCall, ApiInputPrivacyRules, @@ -735,14 +736,21 @@ export function buildInputChatReactions(chatReactions?: ApiChatReactions) { return new GramJs.ChatReactionsNone(); } -export function buildInputEmojiStatus(emojiStatusId: string, expires?: number) { - if (emojiStatusId === DEFAULT_STATUS_ICON_ID) { +export function buildInputEmojiStatus(emojiStatus: ApiEmojiStatusType) { + if (emojiStatus.type === 'collectible') { + return new GramJs.InputEmojiStatusCollectible({ + collectibleId: BigInt(emojiStatus.collectibleId), + until: emojiStatus.until, + }); + } + + if (emojiStatus.documentId === DEFAULT_STATUS_ICON_ID) { return new GramJs.EmojiStatusEmpty(); } return new GramJs.EmojiStatus({ - documentId: BigInt(emojiStatusId), - until: expires, + documentId: BigInt(emojiStatus.documentId), + until: emojiStatus.until, }); } diff --git a/src/api/gramjs/methods/symbols.ts b/src/api/gramjs/methods/symbols.ts index b1adc71d0..823d2ac2d 100644 --- a/src/api/gramjs/methods/symbols.ts +++ b/src/api/gramjs/methods/symbols.ts @@ -276,6 +276,23 @@ export async function fetchDefaultStatusEmojis() { }; } +export async function fetchCollectibleEmojiStatuses({ hash = '0' }: { hash?: string }) { + const result = await invokeRequest(new GramJs.account.GetCollectibleEmojiStatuses( + { hash: BigInt(hash) }, + )); + + if (!(result instanceof GramJs.account.EmojiStatuses)) { + return undefined; + } + + const statuses = result.statuses.map(buildApiEmojiStatus).filter(Boolean); + + return { + statuses, + hash: String(result.hash), + }; +} + export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) { const result = await invokeRequest(new GramJs.messages.SearchStickerSets({ q: query, diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 2c70e3775..8cb868c43 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -2,7 +2,7 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiChat, ApiPeer, ApiUser, + ApiChat, ApiEmojiStatusType, ApiPeer, ApiUser, } from '../../types'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; @@ -304,9 +304,9 @@ export function reportSpam(userOrChat: ApiPeer) { }); } -export function updateEmojiStatus(emojiStatusId: string, expires?: number) { +export function updateEmojiStatus(emojiStatus: ApiEmojiStatusType) { return invokeRequest(new GramJs.account.UpdateEmojiStatus({ - emojiStatus: buildInputEmojiStatus(emojiStatusId, expires), + emojiStatus: buildInputEmojiStatus(emojiStatus), }), { shouldReturnTrue: true, }); diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index dc9df6b5c..2139e3392 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -4,7 +4,7 @@ import type { } from './messages'; import type { ApiBotVerification, ApiChatInviteImporter } from './misc'; import type { - ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, + ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername, } from './users'; type ApiChatType = ( @@ -44,7 +44,7 @@ export interface ApiChat { isProtected?: boolean; fakeType?: ApiFakeType; color?: ApiPeerColor; - emojiStatus?: ApiEmojiStatus; + emojiStatus?: ApiEmojiStatusType; isForum?: boolean; isForumAsMessages?: true; boostLevel?: number; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 2650830c4..982d04f3a 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -40,7 +40,7 @@ import type { ApiStarsAmount } from './payments'; import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { - ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus, + ApiEmojiStatusType, ApiUser, ApiUserFullInfo, ApiUserStatus, } from './users'; export type ApiUpdateReady = { @@ -419,7 +419,7 @@ export type ApiUpdateUserStatus = { export type ApiUpdateUserEmojiStatus = { '@type': 'updateUserEmojiStatus'; userId: string; - emojiStatus?: ApiEmojiStatus; + emojiStatus?: ApiEmojiStatusType; }; export type ApiUpdateRecentEmojiStatuses = { diff --git a/src/api/types/users.ts b/src/api/types/users.ts index c8bfb32db..8831dd2d1 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -28,7 +28,7 @@ export interface ApiUser { canBeInvitedToGroup?: boolean; fakeType?: ApiFakeType; isAttachBot?: boolean; - emojiStatus?: ApiEmojiStatus; + emojiStatus?: ApiEmojiStatusType; areStoriesHidden?: boolean; hasStories?: boolean; hasUnreadStories?: boolean; @@ -132,11 +132,28 @@ export interface ApiPremiumGiftOption { botUrl: string; } +export type ApiEmojiStatusType = ApiEmojiStatus | ApiEmojiStatusCollectible; + export interface ApiEmojiStatus { + type: 'regular'; documentId: string; until?: number; } +export interface ApiEmojiStatusCollectible { + type: 'collectible'; + collectibleId: string; + documentId: string; + title: string; + slug: string; + patternDocumentId: string; + centerColor: string; + edgeColor: string; + patternColor: string; + textColor: string; + until?: number; +} + export interface ApiBirthday { day: number; month: number; diff --git a/src/assets/font-icons/crown-take-off.svg b/src/assets/font-icons/crown-take-off.svg new file mode 100644 index 000000000..67659966f --- /dev/null +++ b/src/assets/font-icons/crown-take-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/crown-wear.svg b/src/assets/font-icons/crown-wear.svg new file mode 100644 index 000000000..0c43c9040 --- /dev/null +++ b/src/assets/font-icons/crown-wear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/proof-of-ownership.svg b/src/assets/font-icons/proof-of-ownership.svg new file mode 100644 index 000000000..e0d28c069 --- /dev/null +++ b/src/assets/font-icons/proof-of-ownership.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/radial-badge.svg b/src/assets/font-icons/radial-badge.svg new file mode 100644 index 000000000..99e1fe77e --- /dev/null +++ b/src/assets/font-icons/radial-badge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/font-icons/unique-profile.svg b/src/assets/font-icons/unique-profile.svg new file mode 100644 index 000000000..41bf39331 --- /dev/null +++ b/src/assets/font-icons/unique-profile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 2103b28d2..be9278104 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1437,6 +1437,9 @@ "GiftInfoViewUpgraded" = "View Upgraded Gift"; "GiftInfoUpgradeBadge" = "upgrade"; "GiftInfoUpgradeForFree" = "Upgrade For Free"; +"GiftInfoWithdraw" = "Withdraw"; +"GiftInfoWear" = "Wear"; +"GiftInfoTakeOff" = "Take Off"; "GiftInfoTransfer" = "Transfer"; "GiftTransferTitle" = "Transfer"; "GiftTransferTON" = "Send via Blockchain"; @@ -1620,3 +1623,13 @@ "CheckPasswordTitle" = "Enter Password"; "CheckPasswordPlaceholder" = "Password"; "CheckPasswordDescription" = "Please enter your password to continue."; +"UniqueStatusWearTitle" = "Wear {gift}"; +"UniqueStatusBenefitsDescription" = "and get these benefits:"; +"UniqueStatusBadgeBenefitTitle" = "Radiant Badge"; +"UniqueStatusBadgeDescription" = "The glittering icon of this item will be displayed next to your name."; +"UniqueStatusProfileDesignBenefitTitle" = "Unique Profile Design"; +"UniqueStatusProfileDesignDescription" = "Your profile page will get the color and the symbol of this item."; +"UniqueStatusProofOfOwnershipBenefitTitle" = "Proof of Ownership"; +"UniqueStatusProofOfOwnershipDescription" = "Tapping the icon of this item next to your name will show its info and owner."; +"UniqueStatusWearButton" = "Start Wearing"; +"CollectibleStatusesCategory" = "Collectibles"; \ No newline at end of file diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index fdef4b332..c0f4ef467 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -9,5 +9,6 @@ export { default as GiftModal } from '../components/modals/gift/GiftModal'; export { default as GiftRecipientPicker } from '../components/modals/gift/recipient/GiftRecipientPicker'; export { default as GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal'; export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal'; +export { default as GiftStatusInfoModal } from '../components/modals/gift/status/GiftStatusInfoModal'; export { default as GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal'; export { default as GiftTransferModal } from '../components/modals/gift/transfer/GiftTransferModal'; diff --git a/src/components/common/CustomEmoji.module.scss b/src/components/common/CustomEmoji.module.scss index e12a7f818..5c6c7c6de 100644 --- a/src/components/common/CustomEmoji.module.scss +++ b/src/components/common/CustomEmoji.module.scss @@ -13,6 +13,15 @@ } } +.withSparkles { + position: relative; +} + +.sparkles { + position: absolute; + inset: -0.25rem; +} + .placeholder { width: 85%; height: 85%; diff --git a/src/components/common/CustomEmoji.tsx b/src/components/common/CustomEmoji.tsx index 0a1c08338..678983a29 100644 --- a/src/components/common/CustomEmoji.tsx +++ b/src/components/common/CustomEmoji.tsx @@ -13,6 +13,7 @@ import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListene import useLastCallback from '../../hooks/useLastCallback'; import useCustomEmoji from './hooks/useCustomEmoji'; +import Sparkles from './Sparkles'; import StickerView from './StickerView'; import styles from './CustomEmoji.module.scss'; @@ -41,6 +42,9 @@ type OwnProps = { observeIntersectionForPlaying?: ObserveFn; onClick?: NoneToVoidFunction; onAnimationEnd?: NoneToVoidFunction; + withSparkles?: boolean; + sparklesClassName?: string; + sparklesStyle?: string; }; const STICKER_SIZE = 20; @@ -67,6 +71,9 @@ const CustomEmoji: FC = ({ observeIntersectionForPlaying, onClick, onAnimationEnd, + withSparkles, + sparklesStyle, + sparklesClassName, }) => { // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); @@ -114,6 +121,7 @@ const CustomEmoji: FC = ({ ref={containerRef} className={buildClassName( styles.root, + withSparkles && styles.withSparkles, className, 'custom-emoji', 'emoji', @@ -125,6 +133,16 @@ const CustomEmoji: FC = ({ data-alt={customEmoji?.emoji} style={style} > + {withSparkles && ( + + )} {isSelectable && ( ; recentCustomEmojiIds?: string[]; recentStatusEmojis?: ApiSticker[]; + collectibleStatuses?: ApiEmojiStatusType[]; chatEmojiSetId?: string; topReactions?: ApiReaction[]; recentReactions?: ApiReaction[]; @@ -114,6 +119,7 @@ const CustomEmojiPicker: FC = ({ recentCustomEmojiIds, selectedReactionIds, recentStatusEmojis, + collectibleStatuses, stickerSetsById, chatEmojiSetId, topReactions, @@ -160,6 +166,11 @@ const CustomEmojiPicker: FC = ({ : Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!)); }, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]); + const collectibleStatusEmojis = useMemo(() => { + const collectibleStatusEmojiIds = collectibleStatuses?.map((status) => status.documentId); + return customEmojisById && collectibleStatusEmojiIds?.map((id) => customEmojisById[id]).filter(Boolean); + }, [customEmojisById, collectibleStatuses]); + const prefix = `${idPrefix}-custom-emoji`; const { activeSetIndex, @@ -172,7 +183,8 @@ const CustomEmojiPicker: FC = ({ const canLoadAndPlay = usePrevDuringAnimation(loadAndPlay || undefined, SLIDE_TRANSITION_DURATION); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const areAddedLoaded = Boolean(addedCustomEmojiIds); @@ -184,7 +196,7 @@ const CustomEmojiPicker: FC = ({ defaultSets.push({ id: TOP_SYMBOL_SET_ID, accessHash: '', - title: lang('PremiumPreviewTags'), + title: oldLang('PremiumPreviewTags'), reactions: defaultTagReactions, count: defaultTagReactions.length, isEmoji: true, @@ -201,7 +213,7 @@ const CustomEmojiPicker: FC = ({ defaultSets.push({ id: TOP_SYMBOL_SET_ID, accessHash: '', - title: lang('Reactions'), + title: oldLang('Reactions'), reactions: topReactionsSlice, count: topReactionsSlice.length, isEmoji: true, @@ -224,7 +236,7 @@ const CustomEmojiPicker: FC = ({ defaultSets.push({ id: isPopular ? POPULAR_SYMBOL_SET_ID : RECENT_SYMBOL_SET_ID, accessHash: '', - title: lang(isPopular ? 'PopularReactions' : 'RecentStickers'), + title: oldLang(isPopular ? 'PopularReactions' : 'RecentStickers'), reactions: allRecentReactions, count: allRecentReactions.length, isEmoji: true, @@ -233,15 +245,26 @@ const CustomEmojiPicker: FC = ({ } else if (isStatusPicker) { const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!]; if (defaultStatusIconsPack?.stickers?.length) { - const stickers = defaultStatusIconsPack.stickers + const stickers = uniqueByField(defaultStatusIconsPack.stickers .slice(0, RECENT_DEFAULT_STATUS_COUNT) - .concat(recentCustomEmojis || []); + .concat(recentCustomEmojis || []), 'id'); defaultSets.push({ ...defaultStatusIconsPack, stickers, count: stickers.length, id: RECENT_SYMBOL_SET_ID, - title: lang('RecentStickers'), + title: oldLang('RecentStickers'), + isEmoji: true, + }); + } + if (collectibleStatusEmojis?.length) { + defaultSets.push({ + id: COLLECTIBLE_STATUS_SET_ID, + accessHash: '', + count: collectibleStatusEmojis.length, + stickers: collectibleStatusEmojis, + title: lang('CollectibleStatusesCategory'), + isEmoji: true, }); } } else if (withDefaultTopicIcons) { @@ -250,14 +273,14 @@ const CustomEmojiPicker: FC = ({ defaultSets.push({ ...defaultTopicIconsPack, id: RECENT_SYMBOL_SET_ID, - title: lang('RecentStickers'), + title: oldLang('RecentStickers'), }); } } else if (recentCustomEmojis?.length) { defaultSets.push({ id: RECENT_SYMBOL_SET_ID, accessHash: '0', - title: lang('RecentStickers'), + title: oldLang('RecentStickers'), stickers: recentCustomEmojis, count: recentCustomEmojis.length, isEmoji: true, @@ -279,9 +302,9 @@ const CustomEmojiPicker: FC = ({ ]; }, [ addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis, - customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions, + customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, oldLang, recentReactions, defaultStatusIconsId, defaultTopicIconsId, isSavedMessages, defaultTagReactions, chatEmojiSetId, - isWithPaidReaction, + isWithPaidReaction, collectibleStatusEmojis, lang, ]); const noPopulatedSets = useMemo(() => ( @@ -383,7 +406,7 @@ const CustomEmojiPicker: FC = ({ return ( {noPopulatedSets ? ( - {lang('NoStickers')} + {oldLang('NoStickers')} ) : ( )} @@ -487,11 +510,13 @@ export default memo(withGlobal( const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId)); const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; + const collectibleStatuses = global.collectibleEmojiStatuses?.statuses; return { - customEmojisById: !isStatusPicker ? customEmojisById : undefined, + customEmojisById, recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined, recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined, + collectibleStatuses: isStatusPicker ? collectibleStatuses : undefined, stickerSetsById, addedCustomEmojiIds: global.customEmojis.added.setIds, canAnimate: selectCanPlayAnimatedEmojis(global), diff --git a/src/components/common/FullNameTitle.module.scss b/src/components/common/FullNameTitle.module.scss index c1b19747a..6bd8dafe5 100644 --- a/src/components/common/FullNameTitle.module.scss +++ b/src/components/common/FullNameTitle.module.scss @@ -6,12 +6,23 @@ :global(.custom-emoji) { color: var(--color-primary); } + + :global(.statusSparkles) { + color: var(--accent-color); + :global(.selected) & { + color: white; + } + } } .fullName { font-size: 1em; margin-bottom: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + &.canCopy { pointer-events: all; } diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index 28ddc00ad..64228e484 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -18,6 +18,7 @@ import { isPeerUser, } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; +import buildStyle from '../../util/buildStyle'; import { copyTextToClipboard } from '../../util/clipboard'; import stopEvent from '../../util/stopEvent'; import renderText from './helpers/renderText'; @@ -47,6 +48,7 @@ type OwnProps = { iconElement?: React.ReactNode; onEmojiStatusClick?: NoneToVoidFunction; observeIntersection?: ObserveFn; + statusSparklesColor?: string; }; const FullNameTitle: FC = ({ @@ -63,6 +65,7 @@ const FullNameTitle: FC = ({ iconElement, onEmojiStatusClick, observeIntersection, + statusSparklesColor, }) => { const lang = useOldLang(); const { showNotification } = getActions(); @@ -72,6 +75,7 @@ const FullNameTitle: FC = ({ const title = realPeer && (isUser ? getUserFullName(realPeer) : getChatTitle(lang, realPeer)); const isPremium = isUser && realPeer.isPremium; const canShowEmojiStatus = withEmojiStatus && !isSavedMessages && realPeer; + const emojiStatus = realPeer?.emojiStatus; const handleTitleClick = useLastCallback((e) => { if (!title || !canCopyTitle) { @@ -134,17 +138,20 @@ const FullNameTitle: FC = ({ <> {!noVerified && peer?.isVerified && } {!noFake && peer?.fakeType && } - {canShowEmojiStatus && realPeer.emojiStatus && ( + {canShowEmojiStatus && emojiStatus && ( = ({ /> )} - {canShowEmojiStatus && !realPeer.emojiStatus && isPremium && } + {canShowEmojiStatus && !emojiStatus && isPremium && } > )} {iconElement} diff --git a/src/components/common/PeerChip.module.scss b/src/components/common/PeerChip.module.scss index e6e2e6604..5b028dba1 100644 --- a/src/components/common/PeerChip.module.scss +++ b/src/components/common/PeerChip.module.scss @@ -3,13 +3,12 @@ align-items: center; background: var(--color-chat-hover); height: 2rem; - min-width: 2rem; margin-inline: 0.25rem; padding-right: 0.75rem; border-radius: 1rem; cursor: var(--custom-cursor, pointer); position: relative; - overflow: hidden; + min-width: 0; flex-shrink: 1; transition: background-color 0.15s ease; @@ -71,7 +70,7 @@ .name { margin-left: 0.5rem; white-space: nowrap; - overflow: hidden; + min-width: 0; text-overflow: ellipsis; :global(.emoji.emoji-small) { diff --git a/src/components/common/ProfileInfo.module.scss b/src/components/common/ProfileInfo.module.scss index 57f3e0ff9..b09247a23 100644 --- a/src/components/common/ProfileInfo.module.scss +++ b/src/components/common/ProfileInfo.module.scss @@ -144,6 +144,10 @@ justify-content: flex-end; pointer-events: none; + :global(.statusSparkles) { + color: var(--color-white) !important; + } + &:dir(rtl) { .status { text-align: right; diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx index 927b3db6d..ef9ad8712 100644 --- a/src/components/common/ProfileInfo.tsx +++ b/src/components/common/ProfileInfo.tsx @@ -57,6 +57,7 @@ type StateProps = topic?: ApiTopic; messagesCount?: number; emojiStatusSticker?: ApiSticker; + emojiStatusSlug?: string; profilePhotos?: ApiPeerPhotos; }; @@ -77,6 +78,7 @@ const ProfileInfo: FC = ({ topic, messagesCount, emojiStatusSticker, + emojiStatusSlug, profilePhotos, peerId, }) => { @@ -86,6 +88,7 @@ const ProfileInfo: FC = ({ openStickerSet, openPrivacySettingsNoticeModal, loadMoreProfilePhotos, + openUniqueGiftBySlug, } = getActions(); const lang = useOldLang(); @@ -137,6 +140,10 @@ const ProfileInfo: FC = ({ }); const handleStatusClick = useLastCallback(() => { + if (emojiStatusSlug) { + openUniqueGiftBySlug({ slug: emojiStatusSlug }); + return; + } if (!peerId) { openStickerSet({ stickerSetInfo: emojiStatusSticker!.stickerSetInfo, @@ -383,6 +390,7 @@ export default memo(withGlobal( const emojiStatus = (user || chat)?.emojiStatus; const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; + const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; return { user, @@ -391,6 +399,7 @@ export default memo(withGlobal( mediaIndex, avatarOwnerId, emojiStatusSticker, + emojiStatusSlug, profilePhotos, ...(topic && { topic, diff --git a/src/components/common/Sparkles.tsx b/src/components/common/Sparkles.tsx index 495b7d0d2..ec1427f2b 100644 --- a/src/components/common/Sparkles.tsx +++ b/src/components/common/Sparkles.tsx @@ -17,6 +17,7 @@ type PresetParameters = ButtonParameters | ProgressParameters; type OwnProps = { className?: string; + style?: string; } & PresetParameters; const SYMBOL = '✦'; @@ -83,11 +84,12 @@ const PROGRESS_POSITIONS = generateRandomProgressPositions(100); const Sparkles = ({ className, + style, ...presetSettings }: OwnProps) => { if (presetSettings.preset === 'button') { return ( - + {BUTTON_POSITIONS.map((position) => { const shiftX = Math.cos(Math.atan2(-50 + position.y, -50 + position.x)) * 100; const shiftY = Math.sin(Math.atan2(-50 + position.y, -50 + position.x)) * 100; @@ -113,7 +115,7 @@ const Sparkles = ({ if (presetSettings.preset === 'progress') { return ( - + {PROGRESS_POSITIONS.map((position) => { return ( = { onContextMenuClose?: NoneToVoidFunction; onContextMenuClick?: NoneToVoidFunction; isEffectEmoji?: boolean; + withSparkles?: boolean; }; const contentForStatusMenuContext = [ @@ -92,6 +94,7 @@ const StickerButton = ) => { const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions(); // eslint-disable-next-line no-null/no-null @@ -198,8 +201,7 @@ const StickerButton = + {withSparkles && } {isIntesectingForShowing && ( = ({ +const StickerSet: FC = ({ stickerSet, loadAndPlay, index, @@ -114,6 +121,7 @@ const StickerSet: FC = ({ onContextMenuOpen, onContextMenuClose, onContextMenuClick, + collectibleStatuses, }) => { const { clearRecentStickers, @@ -149,6 +157,7 @@ const StickerSet: FC = ({ const emojiMarginPx = isMobile ? 8 : 10; const emojiVerticalMarginPx = isMobile ? 8 : 4; const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID; + const isStatusCollectible = stickerSet.id === COLLECTIBLE_STATUS_SET_ID; const isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID; const isPopular = stickerSet.id === POPULAR_SYMBOL_SET_ID; const isEmoji = stickerSet.isEmoji; @@ -255,7 +264,11 @@ const StickerSet: FC = ({ const favoriteStickerIdsSet = useMemo(() => ( favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined ), [favoriteStickers]); - const withAddSetButton = !shouldHideHeader && !isRecent && isEmoji && !isPopular && !isChatEmojiSet + const collectibleEmojiIdsSet = useMemo(() => ( + collectibleStatuses ? new Set(collectibleStatuses.map(({ documentId }) => documentId)) : undefined + ), [collectibleStatuses]); + const withAddSetButton = !shouldHideHeader && !isRecent && !isStatusCollectible + && isEmoji && !isPopular && !isChatEmojiSet && (!isInstalled || (!isCurrentUserPremium && !isSavedMessages)); const addSetButtonText = useMemo(() => { if (isLocked) { @@ -370,6 +383,9 @@ const StickerSet: FC = ({ const reactionId = sticker.isCustomEmoji ? sticker.id : sticker.emoji; const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined; + const withSparkles = sticker.id === COLLECTIBLE_STATUS_SET_ID + || collectibleEmojiIdsSet?.has(sticker.id); + return ( = ({ isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID} noShowPremium={isCurrentUserPremium && (stickerSet.id === EFFECT_STICKERS_SET_ID || stickerSet.id === EFFECT_EMOJIS_SET_ID)} + withSparkles={withSparkles} /> ); })} @@ -428,7 +445,13 @@ const StickerSet: FC = ({ ); }; -export default memo(StickerSet); +export default memo(withGlobal( + (global): StateProps => { + const collectibleStatuses = global.collectibleEmojiStatuses?.statuses; + + return { collectibleStatuses }; + }, +)(StickerSet)); function getItemsPerRowFallback(windowWidth: number): number { return windowWidth > MOBILE_WIDTH_THRESHOLD_PX diff --git a/src/components/common/reactions/CustomEmojiEffect.tsx b/src/components/common/reactions/CustomEmojiEffect.tsx index 8298a93fb..6e9da1d85 100644 --- a/src/components/common/reactions/CustomEmojiEffect.tsx +++ b/src/components/common/reactions/CustomEmojiEffect.tsx @@ -1,7 +1,7 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo, useMemo } from '../../../lib/teact/teact'; -import type { ApiEmojiStatus, ApiReactionCustomEmoji } from '../../../api/types'; +import type { ApiEmojiStatusType, ApiReactionCustomEmoji } from '../../../api/types'; import { getStickerHashById } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; @@ -15,7 +15,7 @@ import CustomEmoji from '../CustomEmoji'; import styles from './CustomEmojiEffect.module.scss'; type OwnProps = { - reaction: ApiReactionCustomEmoji | ApiEmojiStatus; + reaction: ApiReactionCustomEmoji | ApiEmojiStatusType; className?: string; isLottie?: boolean; particleSize?: number; diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index c9230864f..422fd5362 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -117,6 +117,7 @@ } .emoji-status { + overflow: visible; --custom-emoji-size: 1.5rem; color: var(--color-primary); } diff --git a/src/components/left/main/StatusButton.tsx b/src/components/left/main/StatusButton.tsx index ab4967c6b..3fb458d83 100644 --- a/src/components/left/main/StatusButton.tsx +++ b/src/components/left/main/StatusButton.tsx @@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo, useCallback, useRef } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; -import type { ApiEmojiStatus, ApiSticker } from '../../../api/types'; +import type { ApiEmojiStatusCollectible, ApiEmojiStatusType, ApiSticker } from '../../../api/types'; import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config'; import { selectUser } from '../../../global/selectors'; @@ -20,13 +20,14 @@ import Button from '../../ui/Button'; import StatusPickerMenu from './StatusPickerMenu.async'; interface StateProps { - emojiStatus?: ApiEmojiStatus; + emojiStatus?: ApiEmojiStatusType; + collectibleStatuses?: ApiEmojiStatusType[]; } const EFFECT_DURATION_MS = 1500; const EMOJI_STATUS_SIZE = 24; -const StatusButton: FC = ({ emojiStatus }) => { +const StatusButton: FC = ({ emojiStatus, collectibleStatuses }) => { const { setEmojiStatus, loadCurrentUser } = getActions(); // eslint-disable-next-line no-null/no-null @@ -47,9 +48,14 @@ const StatusButton: FC = ({ emojiStatus }) => { }, [emojiStatus, shouldShowEffect, showEffect, unmarkShouldShowEffect]); const handleEmojiStatusSet = useCallback((sticker: ApiSticker) => { + const collectibleStatus = collectibleStatuses?.find( + ((status) => 'collectibleId' in status && status.documentId === sticker.id), + ) as ApiEmojiStatusCollectible | undefined; markShouldShowEffect(); - setEmojiStatus({ emojiStatusId: sticker.id }); - }, [markShouldShowEffect, setEmojiStatus]); + setEmojiStatus({ + emojiStatus: collectibleStatus || { type: 'regular', documentId: sticker.id }, + }); + }, [markShouldShowEffect, setEmojiStatus, collectibleStatuses]); useTimeout(hideEffect, isEffectShown ? EFFECT_DURATION_MS : undefined); @@ -81,6 +87,7 @@ const StatusButton: FC = ({ emojiStatus }) => { documentId={emojiStatus.documentId} size={EMOJI_STATUS_SIZE} loopLimit={EMOJI_STATUS_LOOP_LIMIT} + withSparkles={emojiStatus?.type === 'collectible'} /> ) : } @@ -97,8 +104,10 @@ const StatusButton: FC = ({ emojiStatus }) => { export default memo(withGlobal((global): StateProps => { const { currentUserId } = global; const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined; + const collectibleStatuses = global.collectibleEmojiStatuses?.statuses; return { emojiStatus: currentUser?.emojiStatus, + collectibleStatuses, }; })(StatusButton)); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index b501d6852..c5c6f86b9 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -230,6 +230,7 @@ const Main = ({ openThread, toggleLeftColumn, loadRecentEmojiStatuses, + loadUserCollectibleStatuses, updatePageTitle, loadTopReactions, loadRecentReactions, @@ -335,6 +336,7 @@ const Main = ({ loadTopBotApps(); loadPaidReactionPrivacy(); loadPasswordInfo(); + loadUserCollectibleStatuses(); } }, [isMasterTab, isSynced]); diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index 1ff53f4c1..43c19f4a2 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -69,7 +69,7 @@ .chat-info-wrapper { flex-grow: 1; - overflow: hidden; + min-width: 0; } .header-tools { @@ -154,7 +154,7 @@ flex-direction: column; justify-content: center; flex-grow: 1; - overflow: hidden; + min-width: 0; } .title { diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index af4d3ca56..10f89af76 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -85,6 +85,7 @@ type StateProps = { isSyncing?: boolean; isFetchingDifference?: boolean; emojiStatusSticker?: ApiSticker; + emojiStatusSlug?: string; }; const MiddleHeader: FC = ({ @@ -108,6 +109,7 @@ const MiddleHeader: FC = ({ getCurrentPinnedIndex, getLoadingPinnedId, emojiStatusSticker, + emojiStatusSlug, isSavedDialog, onFocusPinnedMessage, }) => { @@ -120,6 +122,7 @@ const MiddleHeader: FC = ({ openPremiumModal, openStickerSet, updateMiddleSearch, + openUniqueGiftBySlug, } = getActions(); const lang = useOldLang(); @@ -165,10 +168,18 @@ const MiddleHeader: FC = ({ }); const handleUserStatusClick = useLastCallback(() => { + if (emojiStatusSlug) { + openUniqueGiftBySlug({ slug: emojiStatusSlug }); + return; + } openPremiumModal({ fromUserId: chatId }); }); const handleChannelStatusClick = useLastCallback(() => { + if (emojiStatusSlug) { + openUniqueGiftBySlug({ slug: emojiStatusSlug }); + return; + } openStickerSet({ stickerSetInfo: emojiStatusSticker!.stickerSetInfo, }); @@ -388,6 +399,7 @@ export default memo(withGlobal( const emojiStatus = chat?.emojiStatus; const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId]; + const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); @@ -406,6 +418,7 @@ export default memo(withGlobal( isSyncing: global.isSyncing, isFetchingDifference: global.isFetchingDifference, emojiStatusSticker, + emojiStatusSlug, isSavedDialog, }; }, diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index d5a3deafe..a834821ee 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -18,6 +18,7 @@ import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.a import PremiumGiftModal from './gift/GiftModal.async'; import GiftInfoModal from './gift/info/GiftInfoModal.async'; import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async'; +import GiftStatusInfoModal from './gift/status/GiftStatusInfoModal.async'; import GiftTransferModal from './gift/transfer/GiftTransferModal.async'; import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async'; import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async'; @@ -71,6 +72,7 @@ type ModalKey = keyof Pick; @@ -117,6 +119,7 @@ const MODALS: ModalRegistry = { giftUpgradeModal: GiftUpgradeModal, monetizationVerificationModal: VerificationMonetizationModal, giftWithdrawModal: GiftWithdrawModal, + giftStatusInfoModal: GiftStatusInfoModal, giftTransferModal: GiftTransferModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; diff --git a/src/components/modals/common/TableAboutModal.module.scss b/src/components/modals/common/TableAboutModal.module.scss index 7d9b0b148..517c9eeea 100644 --- a/src/components/modals/common/TableAboutModal.module.scss +++ b/src/components/modals/common/TableAboutModal.module.scss @@ -28,6 +28,10 @@ margin-bottom: 1rem; } +.listItemIcon { + color: var(--accent-color) !important; +} + .content { display: flex; flex-direction: column; diff --git a/src/components/modals/common/TableAboutModal.tsx b/src/components/modals/common/TableAboutModal.tsx index 283d7e109..d7db90de0 100644 --- a/src/components/modals/common/TableAboutModal.tsx +++ b/src/components/modals/common/TableAboutModal.tsx @@ -2,6 +2,8 @@ import React, { memo, type TeactNode } from '../../../lib/teact/teact'; import type { IconName } from '../../../types/icons'; +import buildClassName from '../../../util/buildClassName'; + import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import ListItem from '../../ui/ListItem'; @@ -13,6 +15,7 @@ import styles from './TableAboutModal.module.scss'; export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][]; type OwnProps = { + contentClassName?: string; isOpen?: boolean; listItemData?: TableAboutData; headerIconName?: IconName; @@ -36,11 +39,12 @@ const TableAboutModal = ({ withSeparator, onClose, onButtonClick, + contentClassName, }: OwnProps) => { return ( {title} {subtitle} diff --git a/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx index 799ee7d82..af3ea49e9 100644 --- a/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx +++ b/src/components/modals/emojiStatusAccess/EmojiStatusAccessModal.tsx @@ -70,6 +70,7 @@ const EmojiStatusAccessModal: FC = ({ return { ...currentUser, emojiStatus: { + type: 'regular', documentId: stickerSet.stickers[currentStatusIndex].id, }, } satisfies ApiUser; diff --git a/src/components/modals/gift/info/GiftInfoModal.module.scss b/src/components/modals/gift/info/GiftInfoModal.module.scss index 9fb17bf4a..dc3dc4475 100644 --- a/src/components/modals/gift/info/GiftInfoModal.module.scss +++ b/src/components/modals/gift/info/GiftInfoModal.module.scss @@ -62,7 +62,7 @@ } .modalHeader { - z-index: 1; + z-index: 2; width: 100%; padding: 0.375rem; position: absolute; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 3e6a1b5ae..27471b0d4 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -3,14 +3,16 @@ import React, { memo, useMemo } from '../../../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../../../global'; import type { + ApiEmojiStatusCollectible, + ApiEmojiStatusType, ApiPeer, } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; -import { TME_LINK_PREFIX } from '../../../../config'; +import { DEFAULT_STATUS_ICON_ID, TME_LINK_PREFIX } from '../../../../config'; import { getHasAdminRight, getPeerTitle } from '../../../../global/helpers'; import { isApiPeerChat } from '../../../../global/helpers/peers'; -import { selectPeer } from '../../../../global/selectors'; +import { selectPeer, selectUser } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { copyTextToClipboard } from '../../../../util/clipboard'; import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; @@ -51,6 +53,8 @@ type StateProps = { currentUserId?: string; starGiftMaxConvertPeriod?: number; hasAdminRights?: boolean; + currentUserEmojiStatus?: ApiEmojiStatusType; + collectibleEmojiStatuses?: ApiEmojiStatusType[]; }; const STICKER_SIZE = 120; @@ -62,6 +66,8 @@ const GiftInfoModal = ({ currentUserId, starGiftMaxConvertPeriod, hasAdminRights, + currentUserEmojiStatus, + collectibleEmojiStatuses, }: OwnProps & StateProps) => { const { closeGiftInfoModal, @@ -72,6 +78,8 @@ const GiftInfoModal = ({ openGiftUpgradeModal, showNotification, openChatWithDraft, + openGiftStatusInfoModal, + setEmojiStatus, openGiftTransferModal, } = getActions(); @@ -99,6 +107,25 @@ const GiftInfoModal = ({ const gift = isSavedGift ? typeGift.gift : typeGift; const giftSticker = gift && getStickerFromGift(gift); + const currenUniqueEmojiStatusSlug = currentUserEmojiStatus?.type === 'collectible' + ? currentUserEmojiStatus.slug : undefined; + + const starGiftUniqueSlug = gift?.type === 'starGiftUnique' ? gift.slug : undefined; + const starGiftUniqueLink = useMemo(() => { + if (!starGiftUniqueSlug) return undefined; + return `${TME_LINK_PREFIX}nft/${starGiftUniqueSlug}`; + }, [starGiftUniqueSlug]); + const userCollectibleStatus = useMemo(() => { + if (!starGiftUniqueSlug) return undefined; + return collectibleEmojiStatuses?.find(( + status, + ) => status.type === 'collectible' && status.slug === starGiftUniqueSlug) as ApiEmojiStatusCollectible | undefined; + }, [starGiftUniqueSlug, collectibleEmojiStatuses]); + + const isGiftUnique = gift && gift.type === 'starGiftUnique'; + const canTakeOff = isGiftUnique && currenUniqueEmojiStatusSlug === gift.slug; + const canWear = userCollectibleStatus && !canTakeOff; + const canFocusUpgrade = Boolean(savedGift?.upgradeMsgId); const canUpdate = !canFocusUpgrade && savedGift?.inputGift && ( isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId @@ -108,12 +135,6 @@ const GiftInfoModal = ({ closeGiftInfoModal(); }); - const starGiftUniqueLink = useMemo(() => { - const slug = gift?.type === 'starGiftUnique' ? gift.slug : undefined; - if (!slug) return undefined; - return `${TME_LINK_PREFIX}nft/${slug}`; - }, [gift]); - const handleCopyLink = useLastCallback(() => { if (!starGiftUniqueLink) return; copyTextToClipboard(starGiftUniqueLink); @@ -133,6 +154,19 @@ const GiftInfoModal = ({ openGiftTransferModal({ gift: savedGift }); }); + const handleWear = useLastCallback(() => { + if (gift?.type !== 'starGiftUnique' || !userCollectibleStatus) return; + openGiftStatusInfoModal({ emojiStatus: userCollectibleStatus }); + }); + + const handleTakeOff = useLastCallback(() => { + if (canTakeOff) { + setEmojiStatus({ + emojiStatus: { type: 'regular', documentId: DEFAULT_STATUS_ICON_ID }, + }); + } + }); + const handleFocusUpgraded = useLastCallback(() => { if (!savedGift?.upgradeMsgId || !renderingTargetPeer) return; const { upgradeMsgId } = savedGift; @@ -271,37 +305,39 @@ const GiftInfoModal = ({ })(); function getTitle() { - if (gift?.type === 'starGiftUnique') return gift.title; + if (isGiftUnique) return gift.title; if (!savedGift) return lang('GiftInfoSoldOutTitle'); return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle'); } - const isUniqueGift = gift.type === 'starGiftUnique'; - - const contextMenu = ( + const uniqueGiftContextMenu = ( - + {lang('CopyLink')} - + {lang('Share')} - {canUpdate && isUniqueGift && ( + {canUpdate && isGiftUnique && ( {lang('GiftInfoTransfer')} )} + {canWear && ( + + {lang('GiftInfoWear')} + + )} + {canTakeOff && ( + + {lang('GiftInfoTakeOff')} + + )} ); @@ -319,11 +355,11 @@ const GiftInfoModal = ({ > - {isOpen && contextMenu} + {isOpen && uniqueGiftContextMenu} ); - const uniqueGiftHeader = isUniqueGift && ( + const uniqueGiftHeader = isGiftUnique && ( ( const targetPeer = modal?.peerId ? selectPeer(global, modal.peerId) : undefined; const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined; const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages'); + const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined; + const currentUserEmojiStatus = currentUser?.emojiStatus; + const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses; return { fromPeer, @@ -663,6 +704,8 @@ export default memo(withGlobal( currentUserId: global.currentUserId, starGiftMaxConvertPeriod: global.appConfig?.starGiftMaxConvertPeriod, hasAdminRights, + currentUserEmojiStatus, + collectibleEmojiStatuses, }; }, )(GiftInfoModal)); diff --git a/src/components/modals/gift/status/GiftStatusInfoModal.async.tsx b/src/components/modals/gift/status/GiftStatusInfoModal.async.tsx new file mode 100644 index 000000000..8c7b803a6 --- /dev/null +++ b/src/components/modals/gift/status/GiftStatusInfoModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../../lib/teact/teact'; +import React from '../../../../lib/teact/teact'; + +import type { OwnProps } from './GiftStatusInfoModal'; + +import { Bundles } from '../../../../util/moduleLoader'; + +import useModuleLoader from '../../../../hooks/useModuleLoader'; + +const GiftStatusInfoModalAsync: FC = (props) => { + const { modal } = props; + const GiftStatusInfoModal = useModuleLoader(Bundles.Stars, 'GiftStatusInfoModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return GiftStatusInfoModal ? : undefined; +}; + +export default GiftStatusInfoModalAsync; diff --git a/src/components/modals/gift/status/GiftStatusInfoModal.module.scss b/src/components/modals/gift/status/GiftStatusInfoModal.module.scss new file mode 100644 index 000000000..98876f8ad --- /dev/null +++ b/src/components/modals/gift/status/GiftStatusInfoModal.module.scss @@ -0,0 +1,72 @@ +.header, +.profileBlock { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; +} + +.titleContainer { + padding-bottom: 0.5rem; +} + +.profileBlock { + position: relative; + margin-bottom: 0.5rem; + padding-bottom: 1rem; +} + +.radialPattern { + position: absolute; + inset-inline-start: -1.5rem; + height: calc(100% + 2rem); + top: -3rem; + width: calc(100% + 3rem); + z-index: -1; +} + +.lockIcon { + font-size: 1rem; + margin-left: 0.25rem; +} + +.avatar { + margin-top: 2rem; +} + +.userTitle { + font-size: 1.25rem; + color: white; + margin-top: auto; + width: 100%; + justify-content: center; + padding-top: 1rem; +} + +.status { + font-size: 0.875rem; + text-align: center; + padding-bottom: 1rem; +} + +.userTitle, .status { + margin-bottom: 0; + z-index: 1; +} + +.giftTitle { + font-weight: 500; + font-size: 1.5rem; + text-align: center; + padding-bottom: 0.5rem; +} + +.infoDescription { + text-align: center; +} + +.footer { + margin-top: 0.5rem; + display: flex; + align-self: stretch; +} diff --git a/src/components/modals/gift/status/GiftStatusInfoModal.tsx b/src/components/modals/gift/status/GiftStatusInfoModal.tsx new file mode 100644 index 000000000..acbce22f2 --- /dev/null +++ b/src/components/modals/gift/status/GiftStatusInfoModal.tsx @@ -0,0 +1,171 @@ +import React, { memo, useMemo } from '../../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../../global'; + +import type { + ApiUser, +} from '../../../../api/types'; +import type { TabState } from '../../../../global/types'; + +import { selectIsCurrentUserPremium, selectUser } from '../../../../global/selectors'; +import buildClassName from '../../../../util/buildClassName'; +import buildStyle from '../../../../util/buildStyle'; + +import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; +import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; +import useCustomEmoji from '../../../common/hooks/useCustomEmoji'; + +import Avatar from '../../../common/Avatar'; +import FullNameTitle from '../../../common/FullNameTitle'; +import Icon from '../../../common/icons/Icon'; +import RadialPatternBackground from '../../../common/profile/RadialPatternBackground'; +import Button from '../../../ui/Button'; +import TableAboutModal, { type TableAboutData } from '../../common/TableAboutModal'; + +import styles from './GiftStatusInfoModal.module.scss'; + +export type OwnProps = { + modal: TabState['giftStatusInfoModal']; +}; + +type StateProps = { + currentUser: ApiUser; + isCurrentUserPremium?: boolean; +}; + +const GiftStatusInfoModal = ({ + modal, + currentUser, + isCurrentUserPremium, +}: OwnProps & StateProps) => { + const { + closeGiftStatusInfoModal, + setEmojiStatus, + } = getActions(); + const lang = useLang(); + const isOpen = Boolean(modal); + const renderingModal = useCurrentOrPrev(modal); + + const { emojiStatus } = renderingModal || {}; + + const subtitleColor = emojiStatus?.textColor; + + const patternIcon = useCustomEmoji(emojiStatus?.patternDocumentId); + + const handleClose = useLastCallback(() => { + closeGiftStatusInfoModal(); + }); + + const onWearClick = useLastCallback(() => { + if (emojiStatus) { + setEmojiStatus({ emojiStatus }); + } + closeGiftStatusInfoModal(); + }); + + const radialPatternBackdrop = useMemo(() => { + if (!emojiStatus || !isOpen) return undefined; + + const backdropColors = [emojiStatus.centerColor, emojiStatus.edgeColor]; + const patternColor = emojiStatus.patternColor; + + return ( + + ); + }, [emojiStatus, isOpen, patternIcon]); + + const mockPeerWithStatus = useMemo(() => { + return { + ...currentUser, + emojiStatus, + } satisfies ApiUser; + }, [currentUser, emojiStatus]); + + const header = useMemo(() => { + return ( + + + + {radialPatternBackdrop} + + + + {lang('Online')} + + + + { + lang('UniqueStatusWearTitle', { + gift: mockPeerWithStatus?.emojiStatus?.title, + }) + } + + { + lang('UniqueStatusBenefitsDescription') + } + + + + ); + }, [subtitleColor, radialPatternBackdrop, mockPeerWithStatus, lang]); + + const listItemData = [ + ['radial-badge', lang('UniqueStatusBadgeBenefitTitle'), lang('UniqueStatusBadgeDescription')], + ['unique-profile', lang('UniqueStatusProfileDesignBenefitTitle'), lang('UniqueStatusProfileDesignDescription')], + ['proof-of-ownership', lang('UniqueStatusProofOfOwnershipBenefitTitle'), + lang('UniqueStatusProofOfOwnershipDescription')], + ] satisfies TableAboutData; + + const footer = useMemo(() => { + if (!isOpen) return undefined; + return ( + + + {lang('UniqueStatusWearButton')} + {!isCurrentUserPremium && } + + + ); + }, [lang, isCurrentUserPremium, isOpen]); + + return ( + + ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const currentUser = selectUser(global, global.currentUserId!)!; + const isCurrentUserPremium = selectIsCurrentUserPremium(global); + + return { + currentUser, + isCurrentUserPremium, + }; + }, +)(GiftStatusInfoModal)); diff --git a/src/components/modals/suggestedStatus/SuggestedStatusModal.tsx b/src/components/modals/suggestedStatus/SuggestedStatusModal.tsx index 0cc86ed63..6d078d640 100644 --- a/src/components/modals/suggestedStatus/SuggestedStatusModal.tsx +++ b/src/components/modals/suggestedStatus/SuggestedStatusModal.tsx @@ -45,6 +45,7 @@ const SuggestedStatusModal = ({ modal, currentUser, bot }: OwnProps & StateProps return { ...currentUser, emojiStatus: { + type: 'regular', documentId: renderingModal.customEmojiId, }, } satisfies ApiUser; @@ -93,8 +94,7 @@ const SuggestedStatusModal = ({ modal, currentUser, bot }: OwnProps & StateProps setEmojiStatus({ referrerWebAppKey: renderingModal.webAppKey, - emojiStatusId: renderingModal.customEmojiId, - expires, + emojiStatus: { type: 'regular', documentId: renderingModal.customEmojiId, until: expires }, }); closeSuggestedStatusModal(); }); diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 6f00bf5fc..182db03b4 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -86,6 +86,10 @@ z-index: 1; } + .info .Transition { + flex-grow: 0; + } + .Transition { flex: 1; } diff --git a/src/components/ui/ListItem.scss b/src/components/ui/ListItem.scss index 40d9a844c..98cb0cb6e 100644 --- a/src/components/ui/ListItem.scss +++ b/src/components/ui/ListItem.scss @@ -104,7 +104,6 @@ text-align: initial; unicode-bidi: plaintext; text-overflow: ellipsis; - overflow: hidden; } .other-usernames { @@ -225,7 +224,7 @@ .info { flex: 1; - overflow: hidden; + min-width: 0; } .info-name-title { @@ -236,10 +235,10 @@ .info-row, .title, .subtitle { - overflow: hidden; display: flex; justify-content: flex-start; align-items: center; + min-width: 0; } .separator { @@ -287,7 +286,7 @@ display: flex; align-items: center; flex: 1; - overflow: hidden; + min-width: 0; .info { display: flex; diff --git a/src/config.ts b/src/config.ts index f780ece7b..aaf88c8ea 100644 --- a/src/config.ts +++ b/src/config.ts @@ -219,6 +219,7 @@ export const EMOJI_SIZES = 7; export const TOP_SYMBOL_SET_ID = 'top'; export const POPULAR_SYMBOL_SET_ID = 'popular'; export const RECENT_SYMBOL_SET_ID = 'recent'; +export const COLLECTIBLE_STATUS_SET_ID = 'collectibleStatus'; export const FAVORITE_SYMBOL_SET_ID = 'favorite'; export const EFFECT_STICKERS_SET_ID = 'effectStickers'; export const EFFECT_EMOJIS_SET_ID = 'effectEmojis'; diff --git a/src/global/actions/api/symbols.ts b/src/global/actions/api/symbols.ts index cf9eb22b1..b58b2830d 100644 --- a/src/global/actions/api/symbols.ts +++ b/src/global/actions/api/symbols.ts @@ -239,6 +239,31 @@ addActionHandler('loadDefaultStatusIcons', async (global): Promise => { setGlobal(global); }); +addActionHandler('loadUserCollectibleStatuses', async (global, actions): Promise => { + setGlobal(global); + + const { hash } = global.collectibleEmojiStatuses || {}; + + const result = await callApi('fetchCollectibleEmojiStatuses', { hash }); + if (!result) { + return; + } + + global = getGlobal(); + + global = { + ...global, + collectibleEmojiStatuses: { + hash: result.hash, + statuses: result.statuses, + }, + }; + setGlobal(global); + const documentIds = result.statuses.map(({ documentId }) => documentId); + + actions.loadCustomEmojis({ ids: documentIds }); +}); + addActionHandler('loadStickers', (global, actions, payload): ActionReturnType => { const { stickerSetInfo } = payload; const cachedSet = selectStickerSet(global, stickerSetInfo); diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 9408b35d2..5d9a8a668 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -389,7 +389,7 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => { addActionHandler('setEmojiStatus', async (global, actions, payload): Promise => { const { - emojiStatusId, referrerWebAppKey, expires, tabId = getCurrentTabId(), + emojiStatus, referrerWebAppKey, tabId = getCurrentTabId(), } = payload; const isCurrentUserPremium = selectIsCurrentUserPremium(global); @@ -411,7 +411,7 @@ addActionHandler('setEmojiStatus', async (global, actions, payload): Promise { + const { emojiStatus, tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + giftStatusInfoModal: { + emojiStatus, + }, + }, tabId); +}); + +addActionHandler('closeGiftStatusInfoModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + giftStatusInfoModal: undefined, + }, tabId); +}); + addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionReturnType => { const { tabId = getCurrentTabId() } = payload || {}; const tabState = selectTabState(global, tabId); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index ae56d91c0..b152c08ae 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -50,6 +50,7 @@ import type { BotsPrivacyType, PrivacyVisibility, } from '../../api/types'; +import type { ApiEmojiStatusCollectible, ApiEmojiStatusType } from '../../api/types/users'; import type { ApiCredentials } from '../../components/payment/PaymentModal'; import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer'; import type { ReducerAction } from '../../hooks/useReducer'; @@ -1831,6 +1832,7 @@ export interface ActionPayloads { clearRecentCustomEmoji: undefined; loadFeaturedEmojiStickers: undefined; loadDefaultStatusIcons: undefined; + loadUserCollectibleStatuses: undefined; loadRecentEmojiStatuses: undefined; // Bots @@ -2345,6 +2347,10 @@ export interface ActionPayloads { } & WithTabId; clearGiftWithdrawError: WithTabId | undefined; closeGiftWithdrawModal: WithTabId | undefined; + openGiftStatusInfoModal: { + emojiStatus: ApiEmojiStatusCollectible; + } & WithTabId; + closeGiftStatusInfoModal: WithTabId | undefined; processStarGiftWithdrawal: { gift: ApiInputSavedStarGift; password: string; @@ -2379,8 +2385,7 @@ export interface ActionPayloads { closeStarsGiftModal: WithTabId | undefined; setEmojiStatus: { - emojiStatusId: string; - expires?: number; + emojiStatus: ApiEmojiStatusType; referrerWebAppKey?: string; } & WithTabId; openSuggestedStatusModal: { diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index 79d78d8a8..374db0da6 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -11,6 +11,7 @@ import type { ApiConfig, ApiCountry, ApiCountryCode, + ApiEmojiStatusType, ApiGroupCall, ApiLanguage, ApiMessage, @@ -361,6 +362,11 @@ export type GlobalState = { premiumGifts?: ApiStickerSet; emojiKeywords: Record; + collectibleEmojiStatuses?: { + statuses: ApiEmojiStatusType[]; + hash?: string; + }; + gifs: { saved: { hash?: string; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 9a8ce804c..4dce93fd5 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -47,6 +47,7 @@ import type { ApiVideo, ApiWebPage, } from '../../api/types'; +import type { ApiEmojiStatusCollectible } from '../../api/types/users'; import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer'; import type { ReducerAction } from '../../hooks/useReducer'; import type { @@ -737,6 +738,10 @@ export type TabState = { errorKey?: RegularLangFnParameters; }; + giftStatusInfoModal?: { + emojiStatus: ApiEmojiStatusCollectible; + }; + suggestedStatusModal?: { botId: string; webAppKey?: string; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index a129b1b2f..c537264ca 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1462,6 +1462,7 @@ account.reorderUsernames#ef500eab order:Vector = Bool; account.toggleUsername#58d6b376 username:string active:Bool = Bool; account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks; account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; +account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 01759d0c1..6fa2c5dea 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -60,6 +60,7 @@ "account.toggleUsername", "account.resolveBusinessChatLink", "account.toggleSponsoredMessages", + "account.getCollectibleEmojiStatuses", "users.getUsers", "users.getFullUser", "contacts.getContacts", diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 7376f860e..fd408070b 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -94,196 +94,201 @@ $icons-map: ( "comments": "\f139", "copy-media": "\f13a", "copy": "\f13b", - "darkmode": "\f13c", - "data": "\f13d", - "delete-filled": "\f13e", - "delete-left": "\f13f", - "delete-user": "\f140", - "delete": "\f141", - "diamond": "\f142", - "document": "\f143", - "double-badge": "\f144", - "down": "\f145", - "download": "\f146", - "eats": "\f147", - "edit": "\f148", - "email": "\f149", - "enter": "\f14a", - "expand-modal": "\f14b", - "expand": "\f14c", - "eye-closed-outline": "\f14d", - "eye-closed": "\f14e", - "eye-outline": "\f14f", - "eye": "\f150", - "favorite-filled": "\f151", - "favorite": "\f152", - "file-badge": "\f153", - "flag": "\f154", - "folder-badge": "\f155", - "folder": "\f156", - "fontsize": "\f157", - "forums": "\f158", - "forward": "\f159", - "fragment": "\f15a", - "fullscreen": "\f15b", - "gifs": "\f15c", - "gift": "\f15d", - "group-filled": "\f15e", - "group": "\f15f", - "grouped-disable": "\f160", - "grouped": "\f161", - "hand-stop": "\f162", - "hashtag": "\f163", - "heart-outline": "\f164", - "heart": "\f165", - "help": "\f166", - "info-filled": "\f167", - "info": "\f168", - "install": "\f169", - "italic": "\f16a", - "key": "\f16b", - "keyboard": "\f16c", - "lamp": "\f16d", - "language": "\f16e", - "large-pause": "\f16f", - "large-play": "\f170", - "link-badge": "\f171", - "link-broken": "\f172", - "link": "\f173", - "location": "\f174", - "lock-badge": "\f175", - "lock": "\f176", - "logout": "\f177", - "loop": "\f178", - "mention": "\f179", - "message-failed": "\f17a", - "message-pending": "\f17b", - "message-read": "\f17c", - "message-succeeded": "\f17d", - "message": "\f17e", - "microphone-alt": "\f17f", - "microphone": "\f180", - "monospace": "\f181", - "more-circle": "\f182", - "more": "\f183", - "move-caption-down": "\f184", - "move-caption-up": "\f185", - "mute": "\f186", - "muted": "\f187", - "my-notes": "\f188", - "new-chat-filled": "\f189", - "next": "\f18a", - "nochannel": "\f18b", - "noise-suppression": "\f18c", - "non-contacts": "\f18d", - "one-filled": "\f18e", - "open-in-new-tab": "\f18f", - "password-off": "\f190", - "pause": "\f191", - "permissions": "\f192", - "phone-discard-outline": "\f193", - "phone-discard": "\f194", - "phone": "\f195", - "photo": "\f196", - "pin-badge": "\f197", - "pin-list": "\f198", - "pin": "\f199", - "pinned-chat": "\f19a", - "pinned-message": "\f19b", - "pip": "\f19c", - "play-story": "\f19d", - "play": "\f19e", - "poll": "\f19f", - "previous": "\f1a0", - "privacy-policy": "\f1a1", - "quote-text": "\f1a2", - "quote": "\f1a3", - "readchats": "\f1a4", - "recent": "\f1a5", - "reload": "\f1a6", - "remove-quote": "\f1a7", - "remove": "\f1a8", - "reopen-topic": "\f1a9", - "replace": "\f1aa", - "replies": "\f1ab", - "reply-filled": "\f1ac", - "reply": "\f1ad", - "revenue-split": "\f1ae", - "revote": "\f1af", - "save-story": "\f1b0", - "saved-messages": "\f1b1", - "schedule": "\f1b2", - "search": "\f1b3", - "select": "\f1b4", - "send-outline": "\f1b5", - "send": "\f1b6", - "settings-filled": "\f1b7", - "settings": "\f1b8", - "share-filled": "\f1b9", - "share-screen-outlined": "\f1ba", - "share-screen-stop": "\f1bb", - "share-screen": "\f1bc", - "show-message": "\f1bd", - "sidebar": "\f1be", - "skip-next": "\f1bf", - "skip-previous": "\f1c0", - "smallscreen": "\f1c1", - "smile": "\f1c2", - "sort": "\f1c3", - "speaker-muted-story": "\f1c4", - "speaker-outline": "\f1c5", - "speaker-story": "\f1c6", - "speaker": "\f1c7", - "spoiler-disable": "\f1c8", - "spoiler": "\f1c9", - "sport": "\f1ca", - "star": "\f1cb", - "stars-lock": "\f1cc", - "stats": "\f1cd", - "stealth-future": "\f1ce", - "stealth-past": "\f1cf", - "stickers": "\f1d0", - "stop-raising-hand": "\f1d1", - "stop": "\f1d2", - "story-caption": "\f1d3", - "story-expired": "\f1d4", - "story-priority": "\f1d5", - "story-reply": "\f1d6", - "strikethrough": "\f1d7", - "tag-add": "\f1d8", - "tag-crossed": "\f1d9", - "tag-filter": "\f1da", - "tag-name": "\f1db", - "tag": "\f1dc", - "timer": "\f1dd", - "toncoin": "\f1de", - "trade": "\f1df", - "transcribe": "\f1e0", - "truck": "\f1e1", - "unarchive": "\f1e2", - "underlined": "\f1e3", - "unlock-badge": "\f1e4", - "unlock": "\f1e5", - "unmute": "\f1e6", - "unpin": "\f1e7", - "unread": "\f1e8", - "up": "\f1e9", - "user-filled": "\f1ea", - "user-online": "\f1eb", - "user": "\f1ec", - "video-outlined": "\f1ed", - "video-stop": "\f1ee", - "video": "\f1ef", - "view-once": "\f1f0", - "voice-chat": "\f1f1", - "volume-1": "\f1f2", - "volume-2": "\f1f3", - "volume-3": "\f1f4", - "web": "\f1f5", - "webapp": "\f1f6", - "word-wrap": "\f1f7", - "zoom-in": "\f1f8", - "zoom-out": "\f1f9", + "crown-take-off": "\f13c", + "crown-wear": "\f13d", + "darkmode": "\f13e", + "data": "\f13f", + "delete-filled": "\f140", + "delete-left": "\f141", + "delete-user": "\f142", + "delete": "\f143", + "diamond": "\f144", + "document": "\f145", + "double-badge": "\f146", + "down": "\f147", + "download": "\f148", + "eats": "\f149", + "edit": "\f14a", + "email": "\f14b", + "enter": "\f14c", + "expand-modal": "\f14d", + "expand": "\f14e", + "eye-closed-outline": "\f14f", + "eye-closed": "\f150", + "eye-outline": "\f151", + "eye": "\f152", + "favorite-filled": "\f153", + "favorite": "\f154", + "file-badge": "\f155", + "flag": "\f156", + "folder-badge": "\f157", + "folder": "\f158", + "fontsize": "\f159", + "forums": "\f15a", + "forward": "\f15b", + "fragment": "\f15c", + "fullscreen": "\f15d", + "gifs": "\f15e", + "gift": "\f15f", + "group-filled": "\f160", + "group": "\f161", + "grouped-disable": "\f162", + "grouped": "\f163", + "hand-stop": "\f164", + "hashtag": "\f165", + "heart-outline": "\f166", + "heart": "\f167", + "help": "\f168", + "info-filled": "\f169", + "info": "\f16a", + "install": "\f16b", + "italic": "\f16c", + "key": "\f16d", + "keyboard": "\f16e", + "lamp": "\f16f", + "language": "\f170", + "large-pause": "\f171", + "large-play": "\f172", + "link-badge": "\f173", + "link-broken": "\f174", + "link": "\f175", + "location": "\f176", + "lock-badge": "\f177", + "lock": "\f178", + "logout": "\f179", + "loop": "\f17a", + "mention": "\f17b", + "message-failed": "\f17c", + "message-pending": "\f17d", + "message-read": "\f17e", + "message-succeeded": "\f17f", + "message": "\f180", + "microphone-alt": "\f181", + "microphone": "\f182", + "monospace": "\f183", + "more-circle": "\f184", + "more": "\f185", + "move-caption-down": "\f186", + "move-caption-up": "\f187", + "mute": "\f188", + "muted": "\f189", + "my-notes": "\f18a", + "new-chat-filled": "\f18b", + "next": "\f18c", + "nochannel": "\f18d", + "noise-suppression": "\f18e", + "non-contacts": "\f18f", + "one-filled": "\f190", + "open-in-new-tab": "\f191", + "password-off": "\f192", + "pause": "\f193", + "permissions": "\f194", + "phone-discard-outline": "\f195", + "phone-discard": "\f196", + "phone": "\f197", + "photo": "\f198", + "pin-badge": "\f199", + "pin-list": "\f19a", + "pin": "\f19b", + "pinned-chat": "\f19c", + "pinned-message": "\f19d", + "pip": "\f19e", + "play-story": "\f19f", + "play": "\f1a0", + "poll": "\f1a1", + "previous": "\f1a2", + "privacy-policy": "\f1a3", + "proof-of-ownership": "\f1a4", + "quote-text": "\f1a5", + "quote": "\f1a6", + "radial-badge": "\f1a7", + "readchats": "\f1a8", + "recent": "\f1a9", + "reload": "\f1aa", + "remove-quote": "\f1ab", + "remove": "\f1ac", + "reopen-topic": "\f1ad", + "replace": "\f1ae", + "replies": "\f1af", + "reply-filled": "\f1b0", + "reply": "\f1b1", + "revenue-split": "\f1b2", + "revote": "\f1b3", + "save-story": "\f1b4", + "saved-messages": "\f1b5", + "schedule": "\f1b6", + "search": "\f1b7", + "select": "\f1b8", + "send-outline": "\f1b9", + "send": "\f1ba", + "settings-filled": "\f1bb", + "settings": "\f1bc", + "share-filled": "\f1bd", + "share-screen-outlined": "\f1be", + "share-screen-stop": "\f1bf", + "share-screen": "\f1c0", + "show-message": "\f1c1", + "sidebar": "\f1c2", + "skip-next": "\f1c3", + "skip-previous": "\f1c4", + "smallscreen": "\f1c5", + "smile": "\f1c6", + "sort": "\f1c7", + "speaker-muted-story": "\f1c8", + "speaker-outline": "\f1c9", + "speaker-story": "\f1ca", + "speaker": "\f1cb", + "spoiler-disable": "\f1cc", + "spoiler": "\f1cd", + "sport": "\f1ce", + "star": "\f1cf", + "stars-lock": "\f1d0", + "stats": "\f1d1", + "stealth-future": "\f1d2", + "stealth-past": "\f1d3", + "stickers": "\f1d4", + "stop-raising-hand": "\f1d5", + "stop": "\f1d6", + "story-caption": "\f1d7", + "story-expired": "\f1d8", + "story-priority": "\f1d9", + "story-reply": "\f1da", + "strikethrough": "\f1db", + "tag-add": "\f1dc", + "tag-crossed": "\f1dd", + "tag-filter": "\f1de", + "tag-name": "\f1df", + "tag": "\f1e0", + "timer": "\f1e1", + "toncoin": "\f1e2", + "trade": "\f1e3", + "transcribe": "\f1e4", + "truck": "\f1e5", + "unarchive": "\f1e6", + "underlined": "\f1e7", + "unique-profile": "\f1e8", + "unlock-badge": "\f1e9", + "unlock": "\f1ea", + "unmute": "\f1eb", + "unpin": "\f1ec", + "unread": "\f1ed", + "up": "\f1ee", + "user-filled": "\f1ef", + "user-online": "\f1f0", + "user": "\f1f1", + "video-outlined": "\f1f2", + "video-stop": "\f1f3", + "video": "\f1f4", + "view-once": "\f1f5", + "voice-chat": "\f1f6", + "volume-1": "\f1f7", + "volume-2": "\f1f8", + "volume-3": "\f1f9", + "web": "\f1fa", + "webapp": "\f1fb", + "word-wrap": "\f1fc", + "zoom-in": "\f1fd", + "zoom-out": "\f1fe", ); .icon-active-sessions::before { @@ -463,6 +468,12 @@ $icons-map: ( .icon-copy::before { content: map.get($icons-map, "copy"); } +.icon-crown-take-off::before { + content: map.get($icons-map, "crown-take-off"); +} +.icon-crown-wear::before { + content: map.get($icons-map, "crown-wear"); +} .icon-darkmode::before { content: map.get($icons-map, "darkmode"); } @@ -769,12 +780,18 @@ $icons-map: ( .icon-privacy-policy::before { content: map.get($icons-map, "privacy-policy"); } +.icon-proof-of-ownership::before { + content: map.get($icons-map, "proof-of-ownership"); +} .icon-quote-text::before { content: map.get($icons-map, "quote-text"); } .icon-quote::before { content: map.get($icons-map, "quote"); } +.icon-radial-badge::before { + content: map.get($icons-map, "radial-badge"); +} .icon-readchats::before { content: map.get($icons-map, "readchats"); } @@ -967,6 +984,9 @@ $icons-map: ( .icon-underlined::before { content: map.get($icons-map, "underlined"); } +.icon-unique-profile::before { + content: map.get($icons-map, "unique-profile"); +} .icon-unlock-badge::before { content: map.get($icons-map, "unlock-badge"); } diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 2a88b40d5..3509af584 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 9efed8b4e..e119d4030 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index e0ccd605d..bb767492c 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -58,6 +58,8 @@ export type FontIconName = | 'comments' | 'copy-media' | 'copy' + | 'crown-take-off' + | 'crown-wear' | 'darkmode' | 'data' | 'delete-filled' @@ -160,8 +162,10 @@ export type FontIconName = | 'poll' | 'previous' | 'privacy-policy' + | 'proof-of-ownership' | 'quote-text' | 'quote' + | 'radial-badge' | 'readchats' | 'recent' | 'reload' @@ -226,6 +230,7 @@ export type FontIconName = | 'truck' | 'unarchive' | 'underlined' + | 'unique-profile' | 'unlock-badge' | 'unlock' | 'unmute' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index f5aafd356..08f9bf8c0 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1197,6 +1197,9 @@ export interface LangPair { 'GiftInfoViewUpgraded': undefined; 'GiftInfoUpgradeBadge': undefined; 'GiftInfoUpgradeForFree': undefined; + 'GiftInfoWithdraw': undefined; + 'GiftInfoWear': undefined; + 'GiftInfoTakeOff': undefined; 'GiftInfoTransfer': undefined; 'GiftTransferTitle': undefined; 'GiftTransferTON': undefined; @@ -1321,6 +1324,15 @@ export interface LangPair { 'CheckPasswordTitle': undefined; 'CheckPasswordPlaceholder': undefined; 'CheckPasswordDescription': undefined; + 'UniqueStatusBenefitsDescription': undefined; + 'UniqueStatusBadgeBenefitTitle': undefined; + 'UniqueStatusBadgeDescription': undefined; + 'UniqueStatusProfileDesignBenefitTitle': undefined; + 'UniqueStatusProfileDesignDescription': undefined; + 'UniqueStatusProofOfOwnershipBenefitTitle': undefined; + 'UniqueStatusProofOfOwnershipDescription': undefined; + 'UniqueStatusWearButton': undefined; + 'CollectibleStatusesCategory': undefined; } export interface LangPairWithVariables { @@ -1835,6 +1847,9 @@ export interface LangPairWithVariables { 'MoreSimilarBotsText': { 'count': V; }; + 'UniqueStatusWearTitle': { + 'gift': V; + }; } export interface LangPairPlural {
+ {lang('Online')} +