Emoji Status: Support EmojiStatusCollectible (#5547)

Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
Alexander Zinchuk 2025-02-13 14:28:01 +01:00
parent 79c904fe17
commit 3491a3b63b
57 changed files with 925 additions and 290 deletions

View File

@ -1,9 +1,10 @@
import type BigInt from 'big-integer'; import type BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; 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 { CHANNEL_ID_LENGTH } from '../../../config';
import { numberToHexColor } from '../../../util/colors';
export function isPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser { export function isPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser {
return peer.hasOwnProperty('userId'); 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) { 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) { 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; return undefined;

View File

@ -8,6 +8,7 @@ import type {
ApiChatBannedRights, ApiChatBannedRights,
ApiChatFolder, ApiChatFolder,
ApiChatReactions, ApiChatReactions,
ApiEmojiStatusType,
ApiFormattedText, ApiFormattedText,
ApiGroupCall, ApiGroupCall,
ApiInputPrivacyRules, ApiInputPrivacyRules,
@ -735,14 +736,21 @@ export function buildInputChatReactions(chatReactions?: ApiChatReactions) {
return new GramJs.ChatReactionsNone(); return new GramJs.ChatReactionsNone();
} }
export function buildInputEmojiStatus(emojiStatusId: string, expires?: number) { export function buildInputEmojiStatus(emojiStatus: ApiEmojiStatusType) {
if (emojiStatusId === DEFAULT_STATUS_ICON_ID) { 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.EmojiStatusEmpty();
} }
return new GramJs.EmojiStatus({ return new GramJs.EmojiStatus({
documentId: BigInt(emojiStatusId), documentId: BigInt(emojiStatus.documentId),
until: expires, until: emojiStatus.until,
}); });
} }

View File

@ -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 }) { export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({ const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
q: query, q: query,

View File

@ -2,7 +2,7 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiChat, ApiPeer, ApiUser, ApiChat, ApiEmojiStatusType, ApiPeer, ApiUser,
} from '../../types'; } from '../../types';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; 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({ return invokeRequest(new GramJs.account.UpdateEmojiStatus({
emojiStatus: buildInputEmojiStatus(emojiStatusId, expires), emojiStatus: buildInputEmojiStatus(emojiStatus),
}), { }), {
shouldReturnTrue: true, shouldReturnTrue: true,
}); });

View File

@ -4,7 +4,7 @@ import type {
} from './messages'; } from './messages';
import type { ApiBotVerification, ApiChatInviteImporter } from './misc'; import type { ApiBotVerification, ApiChatInviteImporter } from './misc';
import type { import type {
ApiEmojiStatus, ApiFakeType, ApiUser, ApiUsername, ApiEmojiStatusType, ApiFakeType, ApiUser, ApiUsername,
} from './users'; } from './users';
type ApiChatType = ( type ApiChatType = (
@ -44,7 +44,7 @@ export interface ApiChat {
isProtected?: boolean; isProtected?: boolean;
fakeType?: ApiFakeType; fakeType?: ApiFakeType;
color?: ApiPeerColor; color?: ApiPeerColor;
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatusType;
isForum?: boolean; isForum?: boolean;
isForumAsMessages?: true; isForumAsMessages?: true;
boostLevel?: number; boostLevel?: number;

View File

@ -40,7 +40,7 @@ import type { ApiStarsAmount } from './payments';
import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings'; import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings';
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
import type { import type {
ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus, ApiEmojiStatusType, ApiUser, ApiUserFullInfo, ApiUserStatus,
} from './users'; } from './users';
export type ApiUpdateReady = { export type ApiUpdateReady = {
@ -419,7 +419,7 @@ export type ApiUpdateUserStatus = {
export type ApiUpdateUserEmojiStatus = { export type ApiUpdateUserEmojiStatus = {
'@type': 'updateUserEmojiStatus'; '@type': 'updateUserEmojiStatus';
userId: string; userId: string;
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatusType;
}; };
export type ApiUpdateRecentEmojiStatuses = { export type ApiUpdateRecentEmojiStatuses = {

View File

@ -28,7 +28,7 @@ export interface ApiUser {
canBeInvitedToGroup?: boolean; canBeInvitedToGroup?: boolean;
fakeType?: ApiFakeType; fakeType?: ApiFakeType;
isAttachBot?: boolean; isAttachBot?: boolean;
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatusType;
areStoriesHidden?: boolean; areStoriesHidden?: boolean;
hasStories?: boolean; hasStories?: boolean;
hasUnreadStories?: boolean; hasUnreadStories?: boolean;
@ -132,11 +132,28 @@ export interface ApiPremiumGiftOption {
botUrl: string; botUrl: string;
} }
export type ApiEmojiStatusType = ApiEmojiStatus | ApiEmojiStatusCollectible;
export interface ApiEmojiStatus { export interface ApiEmojiStatus {
type: 'regular';
documentId: string; documentId: string;
until?: number; 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 { export interface ApiBirthday {
day: number; day: number;
month: number; month: number;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" fill-rule="evenodd" d="M2.17 1.348a1.206 1.206 0 0 0-1.694 0 1.18 1.18 0 0 0 0 1.68L29.35 31.652a1.206 1.206 0 0 0 1.694 0 1.18 1.18 0 0 0 0-1.68l-4.523-4.484q.072-.228.12-.466l.23-1.146h-1.976l-2.529-2.508h5.012l2.04-10.11A3.03 3.03 0 0 0 32 8.27c0-1.669-1.364-3.022-3.047-3.022s-3.048 1.353-3.048 3.022c0 .812.324 1.55.85 2.093l-4.66 5.46-4.66-8.664a3.02 3.02 0 0 0 1.613-2.666c0-1.669-1.365-3.022-3.048-3.022s-3.048 1.353-3.048 3.022c0 1.154.654 2.158 1.614 2.666l-2.272 4.224zm7.818 14.321-1.4-1.387L.713 6.329c.07.163-.135-.133 0 0A3 3 0 0 0 0 8.27a3.03 3.03 0 0 0 2.583 2.986l2.04 10.112h11.113zm-4.859 8.207h13.137l4.75 4.71q-.42.079-.859.08H9.843c-2.18 0-4.055-1.526-4.483-3.644z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="M5.452 25.987c.363 2.18 2.362 3.635 4.543 3.635h12.357c2.18 0 3.997-1.636 4.543-3.635l.181-1.09H5.27zM29.075 5.816c-1.635 0-3.089 1.454-3.089 3.09 0 .908.363 1.635.909 2.18l-4.725 5.452-4.543-8.723c.908-.545 1.635-1.454 1.635-2.726C19.262 3.454 17.81 2 16.173 2c-1.817 0-3.09 1.454-3.09 3.09 0 1.09.728 2.18 1.636 2.725l-4.724 8.723-4.725-5.452a3 3 0 0 0 .908-2.18c0-1.636-1.453-3.09-3.089-3.09S0 7.27 0 8.906c0 1.453 1.09 2.725 2.544 3.089l1.999 10.358H27.44l1.999-10.358c1.453-.182 2.544-1.454 2.544-3.09.181-1.635-1.09-3.089-2.908-3.089"/></svg>

After

Width:  |  Height:  |  Size: 644 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" fill-rule="evenodd" d="M1.62 14.005a2.115 2.115 0 0 0 0 2.99l3.007 3.008v4.255c0 1.168.947 2.114 2.115 2.114h4.254l3.009 3.009a2.115 2.115 0 0 0 2.99 0l3.009-3.009h4.254a2.115 2.115 0 0 0 2.114-2.114v-4.254l3.009-3.009a2.114 2.114 0 0 0 0-2.99l-3.009-3.009V6.742a2.115 2.115 0 0 0-2.114-2.114h-4.254l-3.009-3.009a2.115 2.115 0 0 0-2.99 0l-3.008 3.009H6.742a2.115 2.115 0 0 0-2.115 2.114v4.255zm5.122 8.138v-3.016L4.61 16.995 3.115 15.5l1.495-1.495 2.132-2.132V6.742h5.131l2.132-2.132L15.5 3.115l1.495 1.495 2.133 2.132h5.13v5.13l2.132 2.133 1.495 1.495-1.495 1.495-2.132 2.133v5.13h-5.13l-2.133 2.132-1.495 1.495-1.495-1.495-2.133-2.132h-5.13zm14.522-8.682a1.058 1.058 0 0 0-1.496-1.495l-5.94 5.94-2.596-2.596a1.057 1.057 0 1 0-1.495 1.495l3.344 3.344a1.057 1.057 0 0 0 1.495 0z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 900 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="m1.244 5.276 2.571.67a.24.24 0 0 1 .224.224l.67 2.571c.112.335.56.335.56 0l.67-2.571a.24.24 0 0 1 .224-.224l2.794-.67c.336-.112.336-.56 0-.56l-2.57-.67a.24.24 0 0 1-.224-.223L5.38 1.252c-.112-.336-.559-.336-.559 0l-.67 2.57a.24.24 0 0 1-.224.224l-2.683.67c-.335 0-.335.448 0 .56M14.658 25.397l-3.242-.895a.24.24 0 0 1-.223-.223l-.894-3.13c-.112-.335-.671-.335-.783 0l-.894 3.13a.24.24 0 0 1-.224.223l-3.241.895c-.336.111-.336.67 0 .782l3.241.894a.24.24 0 0 1 .224.224l.894 3.13c.112.335.67.335.783 0l.894-3.13a.24.24 0 0 1 .223-.224l3.242-.894c.447-.112.447-.67 0-.782M31.761 11.983l-2.683-.671h-.112c-.111 0-.111-.112-.111-.224l-.671-2.57c-.112-.336-.559-.336-.559 0l-.112.335-.67 2.235a.24.24 0 0 1-.224.224l-2.571.67c-.335.112-.335.56 0 .56l2.571.67c.112 0 .112.112.224.224l.67 2.57c.112.336.56.336.56 0l.558-1.788.224-.782a.24.24 0 0 1 .223-.224l2.571-.67c.447 0 .447-.448.112-.56"/><path fill="#000" d="m25.948 14.33-8.607 11.29 3.577-12.072c0-.112.112-.224.112-.336h2.012c-.224-.223-.335-.559-.335-.894 0-.559.335-1.006.894-1.23h-2.906l-2.124-5.03h3.688c.336 0 .671.224.895.447l2.794 3.913.56-2.124c0-.112.111-.336.223-.447l-1.9-2.683c-.56-.894-1.565-1.341-2.683-1.341H9.74c.335.223.559.67.559 1.117s-.224.783-.447 1.006h1.006l-2.236 5.142H2.92l1.118-1.565c-.112-.111-.224-.335-.335-.447l-.56-2.124-2.57 3.578c-.783 1.117-.783 2.682.111 3.912l7.043 9.166.782-2.57-5.924-7.714h5.812c0 .112 0 .224.112.335l1.789 6.26c.559.112.894.447 1.117 1.006l.783 2.795 1.677.447-3.13-10.843h8.16l-3.353 11.29c.447.224.67.783.67 1.23 0 .67-.447 1.341-1.118 1.453l-2.794.782-.447.895.223.335c1.342 1.677 3.913 1.677 5.142 0l9.502-12.52c-.112-.112-.224-.335-.224-.447zm-14.867-3.242 1.789-4.47q.335-.672 1.006-.672h1.453q.67 0 1.006.671l1.9 4.471z"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" d="m4.925 1.225.6 2.3c0 .1.1.2.2.2l2.3.6c.3.1.3.5 0 .5l-2.3.6c-.1 0-.2.1-.2.2l-.6 2.3c-.1.3-.5.3-.5 0l-.7-2.3c0-.1-.1-.2-.2-.2l-2.3-.6c-.3-.1-.3-.5 0-.5l2.3-.6c.1 0 .2-.1.2-.2l.6-2.3c.2-.3.5-.3.6 0M28.325 8.125l.6 2.3c0 .1.1.2.2.2l2.3.6c.3.1.3.5 0 .5l-2.3.6c-.1 0-.2.1-.2.2l-.6 2.3c-.1.3-.5.3-.5 0l-.6-2.3c0-.1-.1-.2-.2-.2l-2.3-.6c-.3-.1-.3-.5 0-.5l2.3-.6c.1 0 .2-.1.2-.2l.6-2.3c0-.2.4-.2.5 0M15.625 9.425c1.9 0 3.5 1.5 3.5 3.5s-1.5 3.5-3.5 3.5-3.5-1.5-3.5-3.5 1.6-3.5 3.5-3.5m0-2c-3 0-5.5 2.4-5.5 5.5s2.4 5.5 5.5 5.5 5.5-2.4 5.5-5.5-2.5-5.5-5.5-5.5M20.525 23.325h-9.7c-.6 0-1-.4-1-1s.4-1 1-1h9.6c.6 0 1 .4 1 1s-.4 1-.9 1M5.925 21.825l.8 2.8c0 .1.1.2.2.2l2.9.8c.3.1.3.6 0 .7l-2.9.8c-.1 0-.2.1-.2.2l-.8 2.8c-.1.3-.6.3-.7 0l-.8-2.8c0-.1-.1-.2-.2-.2l-2.9-.8c-.3-.1-.3-.6 0-.7l2.9-.8c.1 0 .2-.1.2-.2l.8-2.8c.1-.3.6-.3.7 0M8.625 2.325c.8.3 1.4.9 1.6 1.7h14.6c1.2 0 2.2.9 2.4 2.1.2-.1.5-.2.8-.2q.6 0 1.2.3c-.1-2.4-2-4.2-4.4-4.2h-17.4z"/><path fill="#000" d="M28.025 17.025c-.3 0-.5-.1-.8-.1v8c0 1.3-1.1 2.4-2.4 2.4h-13.1c-.3.5-.8.8-1.3 1l-2 .5-.2.5h16.6c2.4 0 4.4-2 4.4-4.4v-8.3c-.3.3-.8.4-1.2.4M2.825 23.225l.5-1.9c.1-.4.3-.8.6-1.1v-10.2c-.7-.3-1.2-.8-1.4-1.6l-.4-1.3h-.1v16.3z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1437,6 +1437,9 @@
"GiftInfoViewUpgraded" = "View Upgraded Gift"; "GiftInfoViewUpgraded" = "View Upgraded Gift";
"GiftInfoUpgradeBadge" = "upgrade"; "GiftInfoUpgradeBadge" = "upgrade";
"GiftInfoUpgradeForFree" = "Upgrade For Free"; "GiftInfoUpgradeForFree" = "Upgrade For Free";
"GiftInfoWithdraw" = "Withdraw";
"GiftInfoWear" = "Wear";
"GiftInfoTakeOff" = "Take Off";
"GiftInfoTransfer" = "Transfer"; "GiftInfoTransfer" = "Transfer";
"GiftTransferTitle" = "Transfer"; "GiftTransferTitle" = "Transfer";
"GiftTransferTON" = "Send via Blockchain"; "GiftTransferTON" = "Send via Blockchain";
@ -1620,3 +1623,13 @@
"CheckPasswordTitle" = "Enter Password"; "CheckPasswordTitle" = "Enter Password";
"CheckPasswordPlaceholder" = "Password"; "CheckPasswordPlaceholder" = "Password";
"CheckPasswordDescription" = "Please enter your password to continue."; "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";

View File

@ -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 GiftRecipientPicker } from '../components/modals/gift/recipient/GiftRecipientPicker';
export { default as GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal'; export { default as GiftInfoModal } from '../components/modals/gift/info/GiftInfoModal';
export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/GiftUpgradeModal'; 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 GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal';
export { default as GiftTransferModal } from '../components/modals/gift/transfer/GiftTransferModal'; export { default as GiftTransferModal } from '../components/modals/gift/transfer/GiftTransferModal';

View File

@ -13,6 +13,15 @@
} }
} }
.withSparkles {
position: relative;
}
.sparkles {
position: absolute;
inset: -0.25rem;
}
.placeholder { .placeholder {
width: 85%; width: 85%;
height: 85%; height: 85%;

View File

@ -13,6 +13,7 @@ import useDynamicColorListener from '../../hooks/stickers/useDynamicColorListene
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useCustomEmoji from './hooks/useCustomEmoji'; import useCustomEmoji from './hooks/useCustomEmoji';
import Sparkles from './Sparkles';
import StickerView from './StickerView'; import StickerView from './StickerView';
import styles from './CustomEmoji.module.scss'; import styles from './CustomEmoji.module.scss';
@ -41,6 +42,9 @@ type OwnProps = {
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForPlaying?: ObserveFn;
onClick?: NoneToVoidFunction; onClick?: NoneToVoidFunction;
onAnimationEnd?: NoneToVoidFunction; onAnimationEnd?: NoneToVoidFunction;
withSparkles?: boolean;
sparklesClassName?: string;
sparklesStyle?: string;
}; };
const STICKER_SIZE = 20; const STICKER_SIZE = 20;
@ -67,6 +71,9 @@ const CustomEmoji: FC<OwnProps> = ({
observeIntersectionForPlaying, observeIntersectionForPlaying,
onClick, onClick,
onAnimationEnd, onAnimationEnd,
withSparkles,
sparklesStyle,
sparklesClassName,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null); let containerRef = useRef<HTMLDivElement>(null);
@ -114,6 +121,7 @@ const CustomEmoji: FC<OwnProps> = ({
ref={containerRef} ref={containerRef}
className={buildClassName( className={buildClassName(
styles.root, styles.root,
withSparkles && styles.withSparkles,
className, className,
'custom-emoji', 'custom-emoji',
'emoji', 'emoji',
@ -125,6 +133,16 @@ const CustomEmoji: FC<OwnProps> = ({
data-alt={customEmoji?.emoji} data-alt={customEmoji?.emoji}
style={style} style={style}
> >
{withSparkles && (
<Sparkles
className={buildClassName(
styles.sparkles,
sparklesClassName,
)}
style={sparklesStyle}
preset="button"
/>
)}
{isSelectable && ( {isSelectable && (
<img <img
className={styles.highlightCatch} className={styles.highlightCatch}

View File

@ -5,11 +5,14 @@ import React, {
import { getGlobal, withGlobal } from '../../global'; import { getGlobal, withGlobal } from '../../global';
import type { import type {
ApiAvailableReaction, ApiReaction, ApiReactionWithPaid, ApiSticker, ApiStickerSet, ApiAvailableReaction,
ApiEmojiStatusType,
ApiReaction, ApiReactionWithPaid, ApiSticker, ApiStickerSet,
} from '../../api/types'; } from '../../api/types';
import type { StickerSetOrReactionsSetOrRecent } from '../../types'; import type { StickerSetOrReactionsSetOrRecent } from '../../types';
import { import {
COLLECTIBLE_STATUS_SET_ID,
FAVORITE_SYMBOL_SET_ID, FAVORITE_SYMBOL_SET_ID,
POPULAR_SYMBOL_SET_ID, POPULAR_SYMBOL_SET_ID,
RECENT_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID,
@ -28,12 +31,13 @@ import {
} from '../../global/selectors'; } from '../../global/selectors';
import animateHorizontalScroll from '../../util/animateHorizontalScroll'; import animateHorizontalScroll from '../../util/animateHorizontalScroll';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { pickTruthy, unique } from '../../util/iteratees'; import { pickTruthy, unique, uniqueByField } from '../../util/iteratees';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { REM } from './helpers/mediaDimensions'; import { REM } from './helpers/mediaDimensions';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useHorizontalScroll from '../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../hooks/useHorizontalScroll';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
@ -75,6 +79,7 @@ type StateProps = {
customEmojisById?: Record<string, ApiSticker>; customEmojisById?: Record<string, ApiSticker>;
recentCustomEmojiIds?: string[]; recentCustomEmojiIds?: string[];
recentStatusEmojis?: ApiSticker[]; recentStatusEmojis?: ApiSticker[];
collectibleStatuses?: ApiEmojiStatusType[];
chatEmojiSetId?: string; chatEmojiSetId?: string;
topReactions?: ApiReaction[]; topReactions?: ApiReaction[];
recentReactions?: ApiReaction[]; recentReactions?: ApiReaction[];
@ -114,6 +119,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
recentCustomEmojiIds, recentCustomEmojiIds,
selectedReactionIds, selectedReactionIds,
recentStatusEmojis, recentStatusEmojis,
collectibleStatuses,
stickerSetsById, stickerSetsById,
chatEmojiSetId, chatEmojiSetId,
topReactions, topReactions,
@ -160,6 +166,11 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
: Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!)); : Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!));
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]); }, [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 prefix = `${idPrefix}-custom-emoji`;
const { const {
activeSetIndex, activeSetIndex,
@ -172,7 +183,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
const canLoadAndPlay = usePrevDuringAnimation(loadAndPlay || undefined, SLIDE_TRANSITION_DURATION); const canLoadAndPlay = usePrevDuringAnimation(loadAndPlay || undefined, SLIDE_TRANSITION_DURATION);
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
const areAddedLoaded = Boolean(addedCustomEmojiIds); const areAddedLoaded = Boolean(addedCustomEmojiIds);
@ -184,7 +196,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
defaultSets.push({ defaultSets.push({
id: TOP_SYMBOL_SET_ID, id: TOP_SYMBOL_SET_ID,
accessHash: '', accessHash: '',
title: lang('PremiumPreviewTags'), title: oldLang('PremiumPreviewTags'),
reactions: defaultTagReactions, reactions: defaultTagReactions,
count: defaultTagReactions.length, count: defaultTagReactions.length,
isEmoji: true, isEmoji: true,
@ -201,7 +213,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
defaultSets.push({ defaultSets.push({
id: TOP_SYMBOL_SET_ID, id: TOP_SYMBOL_SET_ID,
accessHash: '', accessHash: '',
title: lang('Reactions'), title: oldLang('Reactions'),
reactions: topReactionsSlice, reactions: topReactionsSlice,
count: topReactionsSlice.length, count: topReactionsSlice.length,
isEmoji: true, isEmoji: true,
@ -224,7 +236,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
defaultSets.push({ defaultSets.push({
id: isPopular ? POPULAR_SYMBOL_SET_ID : RECENT_SYMBOL_SET_ID, id: isPopular ? POPULAR_SYMBOL_SET_ID : RECENT_SYMBOL_SET_ID,
accessHash: '', accessHash: '',
title: lang(isPopular ? 'PopularReactions' : 'RecentStickers'), title: oldLang(isPopular ? 'PopularReactions' : 'RecentStickers'),
reactions: allRecentReactions, reactions: allRecentReactions,
count: allRecentReactions.length, count: allRecentReactions.length,
isEmoji: true, isEmoji: true,
@ -233,15 +245,26 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
} else if (isStatusPicker) { } else if (isStatusPicker) {
const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!]; const defaultStatusIconsPack = stickerSetsById[defaultStatusIconsId!];
if (defaultStatusIconsPack?.stickers?.length) { if (defaultStatusIconsPack?.stickers?.length) {
const stickers = defaultStatusIconsPack.stickers const stickers = uniqueByField(defaultStatusIconsPack.stickers
.slice(0, RECENT_DEFAULT_STATUS_COUNT) .slice(0, RECENT_DEFAULT_STATUS_COUNT)
.concat(recentCustomEmojis || []); .concat(recentCustomEmojis || []), 'id');
defaultSets.push({ defaultSets.push({
...defaultStatusIconsPack, ...defaultStatusIconsPack,
stickers, stickers,
count: stickers.length, count: stickers.length,
id: RECENT_SYMBOL_SET_ID, 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) { } else if (withDefaultTopicIcons) {
@ -250,14 +273,14 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
defaultSets.push({ defaultSets.push({
...defaultTopicIconsPack, ...defaultTopicIconsPack,
id: RECENT_SYMBOL_SET_ID, id: RECENT_SYMBOL_SET_ID,
title: lang('RecentStickers'), title: oldLang('RecentStickers'),
}); });
} }
} else if (recentCustomEmojis?.length) { } else if (recentCustomEmojis?.length) {
defaultSets.push({ defaultSets.push({
id: RECENT_SYMBOL_SET_ID, id: RECENT_SYMBOL_SET_ID,
accessHash: '0', accessHash: '0',
title: lang('RecentStickers'), title: oldLang('RecentStickers'),
stickers: recentCustomEmojis, stickers: recentCustomEmojis,
count: recentCustomEmojis.length, count: recentCustomEmojis.length,
isEmoji: true, isEmoji: true,
@ -279,9 +302,9 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
]; ];
}, [ }, [
addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis, addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions, customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, oldLang, recentReactions,
defaultStatusIconsId, defaultTopicIconsId, isSavedMessages, defaultTagReactions, chatEmojiSetId, defaultStatusIconsId, defaultTopicIconsId, isSavedMessages, defaultTagReactions, chatEmojiSetId,
isWithPaidReaction, isWithPaidReaction, collectibleStatusEmojis, lang,
]); ]);
const noPopulatedSets = useMemo(() => ( const noPopulatedSets = useMemo(() => (
@ -383,7 +406,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
return ( return (
<div className={fullClassName}> <div className={fullClassName}>
{noPopulatedSets ? ( {noPopulatedSets ? (
<div className={pickerStyles.pickerDisabled}>{lang('NoStickers')}</div> <div className={pickerStyles.pickerDisabled}>{oldLang('NoStickers')}</div>
) : ( ) : (
<Loading /> <Loading />
)} )}
@ -487,11 +510,13 @@ export default memo(withGlobal<OwnProps>(
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId)); const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
const collectibleStatuses = global.collectibleEmojiStatuses?.statuses;
return { return {
customEmojisById: !isStatusPicker ? customEmojisById : undefined, customEmojisById,
recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined, recentCustomEmojiIds: !isStatusPicker ? recentCustomEmojiIds : undefined,
recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined, recentStatusEmojis: isStatusPicker ? recentStatusEmojis : undefined,
collectibleStatuses: isStatusPicker ? collectibleStatuses : undefined,
stickerSetsById, stickerSetsById,
addedCustomEmojiIds: global.customEmojis.added.setIds, addedCustomEmojiIds: global.customEmojis.added.setIds,
canAnimate: selectCanPlayAnimatedEmojis(global), canAnimate: selectCanPlayAnimatedEmojis(global),

View File

@ -6,12 +6,23 @@
:global(.custom-emoji) { :global(.custom-emoji) {
color: var(--color-primary); color: var(--color-primary);
} }
:global(.statusSparkles) {
color: var(--accent-color);
:global(.selected) & {
color: white;
}
}
} }
.fullName { .fullName {
font-size: 1em; font-size: 1em;
margin-bottom: 0; margin-bottom: 0;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
&.canCopy { &.canCopy {
pointer-events: all; pointer-events: all;
} }

View File

@ -18,6 +18,7 @@ import {
isPeerUser, isPeerUser,
} from '../../global/helpers'; } from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import { copyTextToClipboard } from '../../util/clipboard'; import { copyTextToClipboard } from '../../util/clipboard';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
@ -47,6 +48,7 @@ type OwnProps = {
iconElement?: React.ReactNode; iconElement?: React.ReactNode;
onEmojiStatusClick?: NoneToVoidFunction; onEmojiStatusClick?: NoneToVoidFunction;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
statusSparklesColor?: string;
}; };
const FullNameTitle: FC<OwnProps> = ({ const FullNameTitle: FC<OwnProps> = ({
@ -63,6 +65,7 @@ const FullNameTitle: FC<OwnProps> = ({
iconElement, iconElement,
onEmojiStatusClick, onEmojiStatusClick,
observeIntersection, observeIntersection,
statusSparklesColor,
}) => { }) => {
const lang = useOldLang(); const lang = useOldLang();
const { showNotification } = getActions(); const { showNotification } = getActions();
@ -72,6 +75,7 @@ const FullNameTitle: FC<OwnProps> = ({
const title = realPeer && (isUser ? getUserFullName(realPeer) : getChatTitle(lang, realPeer)); const title = realPeer && (isUser ? getUserFullName(realPeer) : getChatTitle(lang, realPeer));
const isPremium = isUser && realPeer.isPremium; const isPremium = isUser && realPeer.isPremium;
const canShowEmojiStatus = withEmojiStatus && !isSavedMessages && realPeer; const canShowEmojiStatus = withEmojiStatus && !isSavedMessages && realPeer;
const emojiStatus = realPeer?.emojiStatus;
const handleTitleClick = useLastCallback((e) => { const handleTitleClick = useLastCallback((e) => {
if (!title || !canCopyTitle) { if (!title || !canCopyTitle) {
@ -134,17 +138,20 @@ const FullNameTitle: FC<OwnProps> = ({
<> <>
{!noVerified && peer?.isVerified && <VerifiedIcon />} {!noVerified && peer?.isVerified && <VerifiedIcon />}
{!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />} {!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />}
{canShowEmojiStatus && realPeer.emojiStatus && ( {canShowEmojiStatus && emojiStatus && (
<Transition <Transition
className={styles.transition} className={styles.transition}
activeKey={Number(realPeer.emojiStatus.documentId)} activeKey={Number(emojiStatus.documentId)}
name="fade" name="fade"
shouldCleanup shouldCleanup
shouldRestoreHeight shouldRestoreHeight
> >
<CustomEmoji <CustomEmoji
forceAlways forceAlways
documentId={realPeer.emojiStatus.documentId} withSparkles={emojiStatus.type === 'collectible'}
sparklesClassName="statusSparkles"
sparklesStyle={buildStyle(statusSparklesColor && `color: ${statusSparklesColor}`)}
documentId={emojiStatus.documentId}
size={emojiStatusSize} size={emojiStatusSize}
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined} loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
observeIntersectionForLoading={observeIntersection} observeIntersectionForLoading={observeIntersection}
@ -152,7 +159,7 @@ const FullNameTitle: FC<OwnProps> = ({
/> />
</Transition> </Transition>
)} )}
{canShowEmojiStatus && !realPeer.emojiStatus && isPremium && <StarIcon />} {canShowEmojiStatus && !emojiStatus && isPremium && <StarIcon />}
</> </>
)} )}
{iconElement} {iconElement}

View File

@ -3,13 +3,12 @@
align-items: center; align-items: center;
background: var(--color-chat-hover); background: var(--color-chat-hover);
height: 2rem; height: 2rem;
min-width: 2rem;
margin-inline: 0.25rem; margin-inline: 0.25rem;
padding-right: 0.75rem; padding-right: 0.75rem;
border-radius: 1rem; border-radius: 1rem;
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
position: relative; position: relative;
overflow: hidden; min-width: 0;
flex-shrink: 1; flex-shrink: 1;
transition: background-color 0.15s ease; transition: background-color 0.15s ease;
@ -71,7 +70,7 @@
.name { .name {
margin-left: 0.5rem; margin-left: 0.5rem;
white-space: nowrap; white-space: nowrap;
overflow: hidden; min-width: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
:global(.emoji.emoji-small) { :global(.emoji.emoji-small) {

View File

@ -144,6 +144,10 @@
justify-content: flex-end; justify-content: flex-end;
pointer-events: none; pointer-events: none;
:global(.statusSparkles) {
color: var(--color-white) !important;
}
&:dir(rtl) { &:dir(rtl) {
.status { .status {
text-align: right; text-align: right;

View File

@ -57,6 +57,7 @@ type StateProps =
topic?: ApiTopic; topic?: ApiTopic;
messagesCount?: number; messagesCount?: number;
emojiStatusSticker?: ApiSticker; emojiStatusSticker?: ApiSticker;
emojiStatusSlug?: string;
profilePhotos?: ApiPeerPhotos; profilePhotos?: ApiPeerPhotos;
}; };
@ -77,6 +78,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
topic, topic,
messagesCount, messagesCount,
emojiStatusSticker, emojiStatusSticker,
emojiStatusSlug,
profilePhotos, profilePhotos,
peerId, peerId,
}) => { }) => {
@ -86,6 +88,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
openStickerSet, openStickerSet,
openPrivacySettingsNoticeModal, openPrivacySettingsNoticeModal,
loadMoreProfilePhotos, loadMoreProfilePhotos,
openUniqueGiftBySlug,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -137,6 +140,10 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}); });
const handleStatusClick = useLastCallback(() => { const handleStatusClick = useLastCallback(() => {
if (emojiStatusSlug) {
openUniqueGiftBySlug({ slug: emojiStatusSlug });
return;
}
if (!peerId) { if (!peerId) {
openStickerSet({ openStickerSet({
stickerSetInfo: emojiStatusSticker!.stickerSetInfo, stickerSetInfo: emojiStatusSticker!.stickerSetInfo,
@ -383,6 +390,7 @@ export default memo(withGlobal<OwnProps>(
const emojiStatus = (user || chat)?.emojiStatus; const emojiStatus = (user || chat)?.emojiStatus;
const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined; const emojiStatusSticker = emojiStatus ? global.customEmojis.byId[emojiStatus.documentId] : undefined;
const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined;
return { return {
user, user,
@ -391,6 +399,7 @@ export default memo(withGlobal<OwnProps>(
mediaIndex, mediaIndex,
avatarOwnerId, avatarOwnerId,
emojiStatusSticker, emojiStatusSticker,
emojiStatusSlug,
profilePhotos, profilePhotos,
...(topic && { ...(topic && {
topic, topic,

View File

@ -17,6 +17,7 @@ type PresetParameters = ButtonParameters | ProgressParameters;
type OwnProps = { type OwnProps = {
className?: string; className?: string;
style?: string;
} & PresetParameters; } & PresetParameters;
const SYMBOL = '✦'; const SYMBOL = '✦';
@ -83,11 +84,12 @@ const PROGRESS_POSITIONS = generateRandomProgressPositions(100);
const Sparkles = ({ const Sparkles = ({
className, className,
style,
...presetSettings ...presetSettings
}: OwnProps) => { }: OwnProps) => {
if (presetSettings.preset === 'button') { if (presetSettings.preset === 'button') {
return ( return (
<div className={buildClassName(styles.root, styles.button, className)}> <div className={buildClassName(styles.root, styles.button, className)} style={style}>
{BUTTON_POSITIONS.map((position) => { {BUTTON_POSITIONS.map((position) => {
const shiftX = Math.cos(Math.atan2(-50 + position.y, -50 + position.x)) * 100; 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; const shiftY = Math.sin(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
@ -113,7 +115,7 @@ const Sparkles = ({
if (presetSettings.preset === 'progress') { if (presetSettings.preset === 'progress') {
return ( return (
<div className={buildClassName(styles.root, styles.progress, className)}> <div className={buildClassName(styles.root, styles.progress, className)} style={style}>
{PROGRESS_POSITIONS.map((position) => { {PROGRESS_POSITIONS.map((position) => {
return ( return (
<div <div

View File

@ -22,6 +22,7 @@ import Button from '../ui/Button';
import Menu from '../ui/Menu'; import Menu from '../ui/Menu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
import Sparkles from './Sparkles';
import StickerView from './StickerView'; import StickerView from './StickerView';
import './StickerButton.scss'; import './StickerButton.scss';
@ -54,6 +55,7 @@ type OwnProps<T> = {
onContextMenuClose?: NoneToVoidFunction; onContextMenuClose?: NoneToVoidFunction;
onContextMenuClick?: NoneToVoidFunction; onContextMenuClick?: NoneToVoidFunction;
isEffectEmoji?: boolean; isEffectEmoji?: boolean;
withSparkles?: boolean;
}; };
const contentForStatusMenuContext = [ const contentForStatusMenuContext = [
@ -92,6 +94,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
onContextMenuClose, onContextMenuClose,
onContextMenuClick, onContextMenuClick,
isEffectEmoji, isEffectEmoji,
withSparkles,
}: OwnProps<T>) => { }: OwnProps<T>) => {
const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions(); const { openStickerSet, openPremiumModal, setEmojiStatus } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -198,8 +201,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
handleContextMenuClose(); handleContextMenuClose();
onContextMenuClick?.(); onContextMenuClick?.();
setEmojiStatus({ setEmojiStatus({
emojiStatusId: sticker.id, emojiStatus: { type: 'regular', documentId: sticker.id, until: getServerTime() + duration },
expires: getServerTime() + duration,
}); });
}); });
@ -289,6 +291,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
onClick={handleClick} onClick={handleClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
{withSparkles && <Sparkles preset="button" /> }
{isIntesectingForShowing && ( {isIntesectingForShowing && (
<StickerView <StickerView
containerRef={ref} containerRef={ref}

View File

@ -2,13 +2,16 @@ import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useMemo, useRef, useState, memo, useEffect, useMemo, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiAvailableReaction, ApiReactionWithPaid, ApiSticker } from '../../api/types'; import type {
ApiAvailableReaction, ApiEmojiStatusType, ApiReactionWithPaid, ApiSticker,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { StickerSetOrReactionsSetOrRecent } from '../../types'; import type { StickerSetOrReactionsSetOrRecent } from '../../types';
import { import {
COLLECTIBLE_STATUS_SET_ID,
DEFAULT_STATUS_ICON_ID, DEFAULT_STATUS_ICON_ID,
DEFAULT_TOPIC_ICON_STICKER_ID, DEFAULT_TOPIC_ICON_STICKER_ID,
EFFECT_EMOJIS_SET_ID, EFFECT_EMOJIS_SET_ID,
@ -75,13 +78,17 @@ type OwnProps = {
onContextMenuClick?: NoneToVoidFunction; onContextMenuClick?: NoneToVoidFunction;
}; };
type StateProps = {
collectibleStatuses?: ApiEmojiStatusType[];
};
const ITEMS_PER_ROW_FALLBACK = 8; const ITEMS_PER_ROW_FALLBACK = 8;
const ITEMS_MOBILE_PER_ROW_FALLBACK = 7; const ITEMS_MOBILE_PER_ROW_FALLBACK = 7;
const ITEMS_MINI_MOBILE_PER_ROW_FALLBACK = 6; const ITEMS_MINI_MOBILE_PER_ROW_FALLBACK = 6;
const MOBILE_WIDTH_THRESHOLD_PX = 440; const MOBILE_WIDTH_THRESHOLD_PX = 440;
const MINI_MOBILE_WIDTH_THRESHOLD_PX = 362; const MINI_MOBILE_WIDTH_THRESHOLD_PX = 362;
const StickerSet: FC<OwnProps> = ({ const StickerSet: FC<OwnProps & StateProps> = ({
stickerSet, stickerSet,
loadAndPlay, loadAndPlay,
index, index,
@ -114,6 +121,7 @@ const StickerSet: FC<OwnProps> = ({
onContextMenuOpen, onContextMenuOpen,
onContextMenuClose, onContextMenuClose,
onContextMenuClick, onContextMenuClick,
collectibleStatuses,
}) => { }) => {
const { const {
clearRecentStickers, clearRecentStickers,
@ -149,6 +157,7 @@ const StickerSet: FC<OwnProps> = ({
const emojiMarginPx = isMobile ? 8 : 10; const emojiMarginPx = isMobile ? 8 : 10;
const emojiVerticalMarginPx = isMobile ? 8 : 4; const emojiVerticalMarginPx = isMobile ? 8 : 4;
const isRecent = stickerSet.id === RECENT_SYMBOL_SET_ID; 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 isFavorite = stickerSet.id === FAVORITE_SYMBOL_SET_ID;
const isPopular = stickerSet.id === POPULAR_SYMBOL_SET_ID; const isPopular = stickerSet.id === POPULAR_SYMBOL_SET_ID;
const isEmoji = stickerSet.isEmoji; const isEmoji = stickerSet.isEmoji;
@ -255,7 +264,11 @@ const StickerSet: FC<OwnProps> = ({
const favoriteStickerIdsSet = useMemo(() => ( const favoriteStickerIdsSet = useMemo(() => (
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
), [favoriteStickers]); ), [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)); && (!isInstalled || (!isCurrentUserPremium && !isSavedMessages));
const addSetButtonText = useMemo(() => { const addSetButtonText = useMemo(() => {
if (isLocked) { if (isLocked) {
@ -370,6 +383,9 @@ const StickerSet: FC<OwnProps> = ({
const reactionId = sticker.isCustomEmoji ? sticker.id : sticker.emoji; const reactionId = sticker.isCustomEmoji ? sticker.id : sticker.emoji;
const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined; const isSelected = reactionId ? selectedReactionIds?.includes(reactionId) : undefined;
const withSparkles = sticker.id === COLLECTIBLE_STATUS_SET_ID
|| collectibleEmojiIdsSet?.has(sticker.id);
return ( return (
<StickerButton <StickerButton
key={sticker.id} key={sticker.id}
@ -399,6 +415,7 @@ const StickerSet: FC<OwnProps> = ({
isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID} isEffectEmoji={stickerSet.id === EFFECT_EMOJIS_SET_ID}
noShowPremium={isCurrentUserPremium noShowPremium={isCurrentUserPremium
&& (stickerSet.id === EFFECT_STICKERS_SET_ID || stickerSet.id === EFFECT_EMOJIS_SET_ID)} && (stickerSet.id === EFFECT_STICKERS_SET_ID || stickerSet.id === EFFECT_EMOJIS_SET_ID)}
withSparkles={withSparkles}
/> />
); );
})} })}
@ -428,7 +445,13 @@ const StickerSet: FC<OwnProps> = ({
); );
}; };
export default memo(StickerSet); export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const collectibleStatuses = global.collectibleEmojiStatuses?.statuses;
return { collectibleStatuses };
},
)(StickerSet));
function getItemsPerRowFallback(windowWidth: number): number { function getItemsPerRowFallback(windowWidth: number): number {
return windowWidth > MOBILE_WIDTH_THRESHOLD_PX return windowWidth > MOBILE_WIDTH_THRESHOLD_PX

View File

@ -1,7 +1,7 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo } 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 { getStickerHashById } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -15,7 +15,7 @@ import CustomEmoji from '../CustomEmoji';
import styles from './CustomEmojiEffect.module.scss'; import styles from './CustomEmojiEffect.module.scss';
type OwnProps = { type OwnProps = {
reaction: ApiReactionCustomEmoji | ApiEmojiStatus; reaction: ApiReactionCustomEmoji | ApiEmojiStatusType;
className?: string; className?: string;
isLottie?: boolean; isLottie?: boolean;
particleSize?: number; particleSize?: number;

View File

@ -117,6 +117,7 @@
} }
.emoji-status { .emoji-status {
overflow: visible;
--custom-emoji-size: 1.5rem; --custom-emoji-size: 1.5rem;
color: var(--color-primary); color: var(--color-primary);
} }

View File

@ -2,7 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useRef } from '../../../lib/teact/teact'; import React, { memo, useCallback, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; 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 { EMOJI_STATUS_LOOP_LIMIT } from '../../../config';
import { selectUser } from '../../../global/selectors'; import { selectUser } from '../../../global/selectors';
@ -20,13 +20,14 @@ import Button from '../../ui/Button';
import StatusPickerMenu from './StatusPickerMenu.async'; import StatusPickerMenu from './StatusPickerMenu.async';
interface StateProps { interface StateProps {
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatusType;
collectibleStatuses?: ApiEmojiStatusType[];
} }
const EFFECT_DURATION_MS = 1500; const EFFECT_DURATION_MS = 1500;
const EMOJI_STATUS_SIZE = 24; const EMOJI_STATUS_SIZE = 24;
const StatusButton: FC<StateProps> = ({ emojiStatus }) => { const StatusButton: FC<StateProps> = ({ emojiStatus, collectibleStatuses }) => {
const { setEmojiStatus, loadCurrentUser } = getActions(); const { setEmojiStatus, loadCurrentUser } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -47,9 +48,14 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
}, [emojiStatus, shouldShowEffect, showEffect, unmarkShouldShowEffect]); }, [emojiStatus, shouldShowEffect, showEffect, unmarkShouldShowEffect]);
const handleEmojiStatusSet = useCallback((sticker: ApiSticker) => { const handleEmojiStatusSet = useCallback((sticker: ApiSticker) => {
const collectibleStatus = collectibleStatuses?.find(
((status) => 'collectibleId' in status && status.documentId === sticker.id),
) as ApiEmojiStatusCollectible | undefined;
markShouldShowEffect(); markShouldShowEffect();
setEmojiStatus({ emojiStatusId: sticker.id }); setEmojiStatus({
}, [markShouldShowEffect, setEmojiStatus]); emojiStatus: collectibleStatus || { type: 'regular', documentId: sticker.id },
});
}, [markShouldShowEffect, setEmojiStatus, collectibleStatuses]);
useTimeout(hideEffect, isEffectShown ? EFFECT_DURATION_MS : undefined); useTimeout(hideEffect, isEffectShown ? EFFECT_DURATION_MS : undefined);
@ -81,6 +87,7 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
documentId={emojiStatus.documentId} documentId={emojiStatus.documentId}
size={EMOJI_STATUS_SIZE} size={EMOJI_STATUS_SIZE}
loopLimit={EMOJI_STATUS_LOOP_LIMIT} loopLimit={EMOJI_STATUS_LOOP_LIMIT}
withSparkles={emojiStatus?.type === 'collectible'}
/> />
) : <StarIcon />} ) : <StarIcon />}
</Button> </Button>
@ -97,8 +104,10 @@ const StatusButton: FC<StateProps> = ({ emojiStatus }) => {
export default memo(withGlobal((global): StateProps => { export default memo(withGlobal((global): StateProps => {
const { currentUserId } = global; const { currentUserId } = global;
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined; const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
const collectibleStatuses = global.collectibleEmojiStatuses?.statuses;
return { return {
emojiStatus: currentUser?.emojiStatus, emojiStatus: currentUser?.emojiStatus,
collectibleStatuses,
}; };
})(StatusButton)); })(StatusButton));

View File

@ -230,6 +230,7 @@ const Main = ({
openThread, openThread,
toggleLeftColumn, toggleLeftColumn,
loadRecentEmojiStatuses, loadRecentEmojiStatuses,
loadUserCollectibleStatuses,
updatePageTitle, updatePageTitle,
loadTopReactions, loadTopReactions,
loadRecentReactions, loadRecentReactions,
@ -335,6 +336,7 @@ const Main = ({
loadTopBotApps(); loadTopBotApps();
loadPaidReactionPrivacy(); loadPaidReactionPrivacy();
loadPasswordInfo(); loadPasswordInfo();
loadUserCollectibleStatuses();
} }
}, [isMasterTab, isSynced]); }, [isMasterTab, isSynced]);

View File

@ -69,7 +69,7 @@
.chat-info-wrapper { .chat-info-wrapper {
flex-grow: 1; flex-grow: 1;
overflow: hidden; min-width: 0;
} }
.header-tools { .header-tools {
@ -154,7 +154,7 @@
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
flex-grow: 1; flex-grow: 1;
overflow: hidden; min-width: 0;
} }
.title { .title {

View File

@ -85,6 +85,7 @@ type StateProps = {
isSyncing?: boolean; isSyncing?: boolean;
isFetchingDifference?: boolean; isFetchingDifference?: boolean;
emojiStatusSticker?: ApiSticker; emojiStatusSticker?: ApiSticker;
emojiStatusSlug?: string;
}; };
const MiddleHeader: FC<OwnProps & StateProps> = ({ const MiddleHeader: FC<OwnProps & StateProps> = ({
@ -108,6 +109,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
getCurrentPinnedIndex, getCurrentPinnedIndex,
getLoadingPinnedId, getLoadingPinnedId,
emojiStatusSticker, emojiStatusSticker,
emojiStatusSlug,
isSavedDialog, isSavedDialog,
onFocusPinnedMessage, onFocusPinnedMessage,
}) => { }) => {
@ -120,6 +122,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
openPremiumModal, openPremiumModal,
openStickerSet, openStickerSet,
updateMiddleSearch, updateMiddleSearch,
openUniqueGiftBySlug,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
@ -165,10 +168,18 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
}); });
const handleUserStatusClick = useLastCallback(() => { const handleUserStatusClick = useLastCallback(() => {
if (emojiStatusSlug) {
openUniqueGiftBySlug({ slug: emojiStatusSlug });
return;
}
openPremiumModal({ fromUserId: chatId }); openPremiumModal({ fromUserId: chatId });
}); });
const handleChannelStatusClick = useLastCallback(() => { const handleChannelStatusClick = useLastCallback(() => {
if (emojiStatusSlug) {
openUniqueGiftBySlug({ slug: emojiStatusSlug });
return;
}
openStickerSet({ openStickerSet({
stickerSetInfo: emojiStatusSticker!.stickerSetInfo, stickerSetInfo: emojiStatusSticker!.stickerSetInfo,
}); });
@ -388,6 +399,7 @@ export default memo(withGlobal<OwnProps>(
const emojiStatus = chat?.emojiStatus; const emojiStatus = chat?.emojiStatus;
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId]; const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
@ -406,6 +418,7 @@ export default memo(withGlobal<OwnProps>(
isSyncing: global.isSyncing, isSyncing: global.isSyncing,
isFetchingDifference: global.isFetchingDifference, isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker, emojiStatusSticker,
emojiStatusSlug,
isSavedDialog, isSavedDialog,
}; };
}, },

View File

@ -18,6 +18,7 @@ import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.a
import PremiumGiftModal from './gift/GiftModal.async'; import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async'; import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async'; import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
import GiftStatusInfoModal from './gift/status/GiftStatusInfoModal.async';
import GiftTransferModal from './gift/transfer/GiftTransferModal.async'; import GiftTransferModal from './gift/transfer/GiftTransferModal.async';
import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async'; import GiftUpgradeModal from './gift/upgrade/GiftUpgradeModal.async';
import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async'; import GiftWithdrawModal from './gift/withdraw/GiftWithdrawModal.async';
@ -71,6 +72,7 @@ type ModalKey = keyof Pick<TabState,
'giftUpgradeModal' | 'giftUpgradeModal' |
'monetizationVerificationModal' | 'monetizationVerificationModal' |
'giftWithdrawModal' | 'giftWithdrawModal' |
'giftStatusInfoModal' |
'giftTransferModal' 'giftTransferModal'
>; >;
@ -117,6 +119,7 @@ const MODALS: ModalRegistry = {
giftUpgradeModal: GiftUpgradeModal, giftUpgradeModal: GiftUpgradeModal,
monetizationVerificationModal: VerificationMonetizationModal, monetizationVerificationModal: VerificationMonetizationModal,
giftWithdrawModal: GiftWithdrawModal, giftWithdrawModal: GiftWithdrawModal,
giftStatusInfoModal: GiftStatusInfoModal,
giftTransferModal: GiftTransferModal, giftTransferModal: GiftTransferModal,
}; };
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];

View File

@ -28,6 +28,10 @@
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.listItemIcon {
color: var(--accent-color) !important;
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -2,6 +2,8 @@ import React, { memo, type TeactNode } from '../../../lib/teact/teact';
import type { IconName } from '../../../types/icons'; import type { IconName } from '../../../types/icons';
import buildClassName from '../../../util/buildClassName';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
@ -13,6 +15,7 @@ import styles from './TableAboutModal.module.scss';
export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][]; export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][];
type OwnProps = { type OwnProps = {
contentClassName?: string;
isOpen?: boolean; isOpen?: boolean;
listItemData?: TableAboutData; listItemData?: TableAboutData;
headerIconName?: IconName; headerIconName?: IconName;
@ -36,11 +39,12 @@ const TableAboutModal = ({
withSeparator, withSeparator,
onClose, onClose,
onButtonClick, onButtonClick,
contentClassName,
}: OwnProps) => { }: OwnProps) => {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
className={styles.root} className={buildClassName(styles.root, contentClassName)}
contentClassName={styles.content} contentClassName={styles.content}
hasAbsoluteCloseButton hasAbsoluteCloseButton
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined} absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
@ -55,6 +59,7 @@ const TableAboutModal = ({
isStatic isStatic
multiline multiline
icon={icon} icon={icon}
iconClassName={styles.listItemIcon}
> >
<span className="title">{title}</span> <span className="title">{title}</span>
<span className="subtitle">{subtitle}</span> <span className="subtitle">{subtitle}</span>

View File

@ -70,6 +70,7 @@ const EmojiStatusAccessModal: FC<OwnProps & StateProps> = ({
return { return {
...currentUser, ...currentUser,
emojiStatus: { emojiStatus: {
type: 'regular',
documentId: stickerSet.stickers[currentStatusIndex].id, documentId: stickerSet.stickers[currentStatusIndex].id,
}, },
} satisfies ApiUser; } satisfies ApiUser;

View File

@ -62,7 +62,7 @@
} }
.modalHeader { .modalHeader {
z-index: 1; z-index: 2;
width: 100%; width: 100%;
padding: 0.375rem; padding: 0.375rem;
position: absolute; position: absolute;

View File

@ -3,14 +3,16 @@ import React, { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, getGlobal, withGlobal } from '../../../../global';
import type { import type {
ApiEmojiStatusCollectible,
ApiEmojiStatusType,
ApiPeer, ApiPeer,
} from '../../../../api/types'; } from '../../../../api/types';
import type { TabState } from '../../../../global/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 { getHasAdminRight, getPeerTitle } from '../../../../global/helpers';
import { isApiPeerChat } from '../../../../global/helpers/peers'; import { isApiPeerChat } from '../../../../global/helpers/peers';
import { selectPeer } from '../../../../global/selectors'; import { selectPeer, selectUser } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import { copyTextToClipboard } from '../../../../util/clipboard'; import { copyTextToClipboard } from '../../../../util/clipboard';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
@ -51,6 +53,8 @@ type StateProps = {
currentUserId?: string; currentUserId?: string;
starGiftMaxConvertPeriod?: number; starGiftMaxConvertPeriod?: number;
hasAdminRights?: boolean; hasAdminRights?: boolean;
currentUserEmojiStatus?: ApiEmojiStatusType;
collectibleEmojiStatuses?: ApiEmojiStatusType[];
}; };
const STICKER_SIZE = 120; const STICKER_SIZE = 120;
@ -62,6 +66,8 @@ const GiftInfoModal = ({
currentUserId, currentUserId,
starGiftMaxConvertPeriod, starGiftMaxConvertPeriod,
hasAdminRights, hasAdminRights,
currentUserEmojiStatus,
collectibleEmojiStatuses,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { const {
closeGiftInfoModal, closeGiftInfoModal,
@ -72,6 +78,8 @@ const GiftInfoModal = ({
openGiftUpgradeModal, openGiftUpgradeModal,
showNotification, showNotification,
openChatWithDraft, openChatWithDraft,
openGiftStatusInfoModal,
setEmojiStatus,
openGiftTransferModal, openGiftTransferModal,
} = getActions(); } = getActions();
@ -99,6 +107,25 @@ const GiftInfoModal = ({
const gift = isSavedGift ? typeGift.gift : typeGift; const gift = isSavedGift ? typeGift.gift : typeGift;
const giftSticker = gift && getStickerFromGift(gift); 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 canFocusUpgrade = Boolean(savedGift?.upgradeMsgId);
const canUpdate = !canFocusUpgrade && savedGift?.inputGift && ( const canUpdate = !canFocusUpgrade && savedGift?.inputGift && (
isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId
@ -108,12 +135,6 @@ const GiftInfoModal = ({
closeGiftInfoModal(); 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(() => { const handleCopyLink = useLastCallback(() => {
if (!starGiftUniqueLink) return; if (!starGiftUniqueLink) return;
copyTextToClipboard(starGiftUniqueLink); copyTextToClipboard(starGiftUniqueLink);
@ -133,6 +154,19 @@ const GiftInfoModal = ({
openGiftTransferModal({ gift: savedGift }); 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(() => { const handleFocusUpgraded = useLastCallback(() => {
if (!savedGift?.upgradeMsgId || !renderingTargetPeer) return; if (!savedGift?.upgradeMsgId || !renderingTargetPeer) return;
const { upgradeMsgId } = savedGift; const { upgradeMsgId } = savedGift;
@ -271,37 +305,39 @@ const GiftInfoModal = ({
})(); })();
function getTitle() { function getTitle() {
if (gift?.type === 'starGiftUnique') return gift.title; if (isGiftUnique) return gift.title;
if (!savedGift) return lang('GiftInfoSoldOutTitle'); if (!savedGift) return lang('GiftInfoSoldOutTitle');
return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle'); return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
} }
const isUniqueGift = gift.type === 'starGiftUnique'; const uniqueGiftContextMenu = (
const contextMenu = (
<DropdownMenu <DropdownMenu
className="with-menu-transitions" className="with-menu-transitions"
trigger={SettingsMenuButton} trigger={SettingsMenuButton}
positionX="right" positionX="right"
> >
<MenuItem <MenuItem icon="link-badge" onClick={handleCopyLink}>
icon="link-badge"
onClick={handleCopyLink}
>
{lang('CopyLink')} {lang('CopyLink')}
</MenuItem> </MenuItem>
<MenuItem <MenuItem icon="forward" onClick={handleLinkShare}>
icon="forward"
onClick={handleLinkShare}
>
{lang('Share')} {lang('Share')}
</MenuItem> </MenuItem>
{canUpdate && isUniqueGift && ( {canUpdate && isGiftUnique && (
<MenuItem icon="diamond" onClick={handleTransfer}> <MenuItem icon="diamond" onClick={handleTransfer}>
{lang('GiftInfoTransfer')} {lang('GiftInfoTransfer')}
</MenuItem> </MenuItem>
)} )}
{canWear && (
<MenuItem icon="crown-wear" onClick={handleWear}>
{lang('GiftInfoWear')}
</MenuItem>
)}
{canTakeOff && (
<MenuItem icon="crown-take-off" onClick={handleTakeOff}>
{lang('GiftInfoTakeOff')}
</MenuItem>
)}
</DropdownMenu> </DropdownMenu>
); );
@ -319,11 +355,11 @@ const GiftInfoModal = ({
> >
<Icon name="close" /> <Icon name="close" />
</Button> </Button>
{isOpen && contextMenu} {isOpen && uniqueGiftContextMenu}
</div> </div>
); );
const uniqueGiftHeader = isUniqueGift && ( const uniqueGiftHeader = isGiftUnique && (
<div className={buildClassName(styles.header, styles.uniqueGift)}> <div className={buildClassName(styles.header, styles.uniqueGift)}>
<UniqueGiftHeader <UniqueGiftHeader
backdropAttribute={giftAttributes!.backdrop!} backdropAttribute={giftAttributes!.backdrop!}
@ -432,7 +468,7 @@ const GiftInfoModal = ({
} }
} }
if (gift.type === 'starGiftUnique') { if (isGiftUnique) {
const { ownerName, ownerAddress, ownerId } = gift; const { ownerName, ownerAddress, ownerId } = gift;
const { const {
model, backdrop, pattern, originalDetails, model, backdrop, pattern, originalDetails,
@ -458,7 +494,7 @@ const GiftInfoModal = ({
} else { } else {
tableData.push([ tableData.push([
lang('GiftInfoOwner'), lang('GiftInfoOwner'),
ownerId ? { chatId: ownerId } : ownerName || '', ownerId ? { chatId: ownerId, withEmojiStatus: true } : ownerName || '',
]); ]);
} }
@ -590,14 +626,16 @@ const GiftInfoModal = ({
); );
return { return {
modalHeader: isUniqueGift ? uniqueGiftModalHeader : undefined, modalHeader: isGiftUnique ? uniqueGiftModalHeader : undefined,
header: isUniqueGift ? uniqueGiftHeader : regularHeader, header: isGiftUnique ? uniqueGiftHeader : regularHeader,
tableData, tableData,
footer, footer,
}; };
}, [ }, [
typeGift, savedGift, renderingTargetPeer, giftSticker, lang, canUpdate, canConvertDifference, isSender, oldLang, typeGift, savedGift, renderingTargetPeer, giftSticker, lang,
gift, giftAttributes, renderFooterButton, isTargetChat, SettingsMenuButton, isOpen, canUpdate, canConvertDifference, isSender, oldLang,
gift, giftAttributes, renderFooterButton, isTargetChat,
SettingsMenuButton, isOpen, isGiftUnique, canWear, canTakeOff,
]); ]);
return ( return (
@ -606,7 +644,7 @@ const GiftInfoModal = ({
isOpen={isOpen} isOpen={isOpen}
modalHeader={modalData?.modalHeader} modalHeader={modalData?.modalHeader}
header={modalData?.header} header={modalData?.header}
hasBackdrop={gift?.type === 'starGiftUnique'} hasBackdrop={isGiftUnique}
tableData={modalData?.tableData} tableData={modalData?.tableData}
footer={modalData?.footer} footer={modalData?.footer}
className={styles.modal} className={styles.modal}
@ -656,6 +694,9 @@ export default memo(withGlobal<OwnProps>(
const targetPeer = modal?.peerId ? selectPeer(global, modal.peerId) : undefined; const targetPeer = modal?.peerId ? selectPeer(global, modal.peerId) : undefined;
const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined; const chat = targetPeer && isApiPeerChat(targetPeer) ? targetPeer : undefined;
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages'); 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 { return {
fromPeer, fromPeer,
@ -663,6 +704,8 @@ export default memo(withGlobal<OwnProps>(
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
starGiftMaxConvertPeriod: global.appConfig?.starGiftMaxConvertPeriod, starGiftMaxConvertPeriod: global.appConfig?.starGiftMaxConvertPeriod,
hasAdminRights, hasAdminRights,
currentUserEmojiStatus,
collectibleEmojiStatuses,
}; };
}, },
)(GiftInfoModal)); )(GiftInfoModal));

View File

@ -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<OwnProps> = (props) => {
const { modal } = props;
const GiftStatusInfoModal = useModuleLoader(Bundles.Stars, 'GiftStatusInfoModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return GiftStatusInfoModal ? <GiftStatusInfoModal {...props} /> : undefined;
};
export default GiftStatusInfoModalAsync;

View File

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

View File

@ -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 (
<RadialPatternBackground
className={styles.radialPattern}
backgroundColors={backdropColors}
patternColor={patternColor}
patternIcon={patternIcon.customEmoji}
/>
);
}, [emojiStatus, isOpen, patternIcon]);
const mockPeerWithStatus = useMemo(() => {
return {
...currentUser,
emojiStatus,
} satisfies ApiUser;
}, [currentUser, emojiStatus]);
const header = useMemo(() => {
return (
<div className={styles.header}>
<div
className={buildClassName(styles.profileBlock)}
style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}
>
{radialPatternBackdrop}
<Avatar peer={mockPeerWithStatus} size="jumbo" className={styles.avatar} />
<FullNameTitle
peer={mockPeerWithStatus}
className={styles.userTitle}
withEmojiStatus
noFake
noVerified
statusSparklesColor={subtitleColor}
/>
<p className={styles.status} style={buildStyle(subtitleColor && `color: ${subtitleColor}`)}>
{lang('Online')}
</p>
</div>
<div className={styles.titleContainer}>
<div className={styles.giftTitle}>{
lang('UniqueStatusWearTitle', {
gift: mockPeerWithStatus?.emojiStatus?.title,
})
}
</div>
<div className={styles.infoDescription}>{
lang('UniqueStatusBenefitsDescription')
}
</div>
</div>
</div>
);
}, [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 (
<div className={styles.footer}>
<Button
size="smaller"
onClick={onWearClick}
>
{lang('UniqueStatusWearButton')}
{!isCurrentUserPremium && <Icon name="lock-badge" className={styles.lockIcon} />}
</Button>
</div>
);
}, [lang, isCurrentUserPremium, isOpen]);
return (
<TableAboutModal
isOpen={isOpen}
header={header}
listItemData={listItemData}
footer={footer}
hasBackdrop
onClose={handleClose}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const currentUser = selectUser(global, global.currentUserId!)!;
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
return {
currentUser,
isCurrentUserPremium,
};
},
)(GiftStatusInfoModal));

View File

@ -45,6 +45,7 @@ const SuggestedStatusModal = ({ modal, currentUser, bot }: OwnProps & StateProps
return { return {
...currentUser, ...currentUser,
emojiStatus: { emojiStatus: {
type: 'regular',
documentId: renderingModal.customEmojiId, documentId: renderingModal.customEmojiId,
}, },
} satisfies ApiUser; } satisfies ApiUser;
@ -93,8 +94,7 @@ const SuggestedStatusModal = ({ modal, currentUser, bot }: OwnProps & StateProps
setEmojiStatus({ setEmojiStatus({
referrerWebAppKey: renderingModal.webAppKey, referrerWebAppKey: renderingModal.webAppKey,
emojiStatusId: renderingModal.customEmojiId, emojiStatus: { type: 'regular', documentId: renderingModal.customEmojiId, until: expires },
expires,
}); });
closeSuggestedStatusModal(); closeSuggestedStatusModal();
}); });

View File

@ -86,6 +86,10 @@
z-index: 1; z-index: 1;
} }
.info .Transition {
flex-grow: 0;
}
.Transition { .Transition {
flex: 1; flex: 1;
} }

View File

@ -104,7 +104,6 @@
text-align: initial; text-align: initial;
unicode-bidi: plaintext; unicode-bidi: plaintext;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden;
} }
.other-usernames { .other-usernames {
@ -225,7 +224,7 @@
.info { .info {
flex: 1; flex: 1;
overflow: hidden; min-width: 0;
} }
.info-name-title { .info-name-title {
@ -236,10 +235,10 @@
.info-row, .info-row,
.title, .title,
.subtitle { .subtitle {
overflow: hidden;
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
min-width: 0;
} }
.separator { .separator {
@ -287,7 +286,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
overflow: hidden; min-width: 0;
.info { .info {
display: flex; display: flex;

View File

@ -219,6 +219,7 @@ export const EMOJI_SIZES = 7;
export const TOP_SYMBOL_SET_ID = 'top'; export const TOP_SYMBOL_SET_ID = 'top';
export const POPULAR_SYMBOL_SET_ID = 'popular'; export const POPULAR_SYMBOL_SET_ID = 'popular';
export const RECENT_SYMBOL_SET_ID = 'recent'; export const RECENT_SYMBOL_SET_ID = 'recent';
export const COLLECTIBLE_STATUS_SET_ID = 'collectibleStatus';
export const FAVORITE_SYMBOL_SET_ID = 'favorite'; export const FAVORITE_SYMBOL_SET_ID = 'favorite';
export const EFFECT_STICKERS_SET_ID = 'effectStickers'; export const EFFECT_STICKERS_SET_ID = 'effectStickers';
export const EFFECT_EMOJIS_SET_ID = 'effectEmojis'; export const EFFECT_EMOJIS_SET_ID = 'effectEmojis';

View File

@ -239,6 +239,31 @@ addActionHandler('loadDefaultStatusIcons', async (global): Promise<void> => {
setGlobal(global); setGlobal(global);
}); });
addActionHandler('loadUserCollectibleStatuses', async (global, actions): Promise<void> => {
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 => { addActionHandler('loadStickers', (global, actions, payload): ActionReturnType => {
const { stickerSetInfo } = payload; const { stickerSetInfo } = payload;
const cachedSet = selectStickerSet(global, stickerSetInfo); const cachedSet = selectStickerSet(global, stickerSetInfo);

View File

@ -389,7 +389,7 @@ addActionHandler('reportSpam', (global, actions, payload): ActionReturnType => {
addActionHandler('setEmojiStatus', async (global, actions, payload): Promise<void> => { addActionHandler('setEmojiStatus', async (global, actions, payload): Promise<void> => {
const { const {
emojiStatusId, referrerWebAppKey, expires, tabId = getCurrentTabId(), emojiStatus, referrerWebAppKey, tabId = getCurrentTabId(),
} = payload; } = payload;
const isCurrentUserPremium = selectIsCurrentUserPremium(global); const isCurrentUserPremium = selectIsCurrentUserPremium(global);
@ -411,7 +411,7 @@ addActionHandler('setEmojiStatus', async (global, actions, payload): Promise<voi
return; return;
} }
const result = await callApi('updateEmojiStatus', emojiStatusId, expires); const result = await callApi('updateEmojiStatus', emojiStatus);
if (referrerWebAppKey) { if (referrerWebAppKey) {
if (!result) { if (!result) {
@ -439,7 +439,7 @@ addActionHandler('setEmojiStatus', async (global, actions, payload): Promise<voi
message: { message: {
key: 'BotSuggestedStatusUpdated', key: 'BotSuggestedStatusUpdated',
}, },
customEmojiIconId: emojiStatusId, customEmojiIconId: emojiStatus.documentId,
tabId, tabId,
}); });
} }

View File

@ -269,6 +269,24 @@ addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionRetu
addTabStateResetterAction('closeGiftWithdrawModal', 'giftWithdrawModal'); addTabStateResetterAction('closeGiftWithdrawModal', 'giftWithdrawModal');
addActionHandler('openGiftStatusInfoModal', (global, actions, payload): ActionReturnType => {
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 => { addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId); const tabState = selectTabState(global, tabId);

View File

@ -50,6 +50,7 @@ import type {
BotsPrivacyType, BotsPrivacyType,
PrivacyVisibility, PrivacyVisibility,
} from '../../api/types'; } from '../../api/types';
import type { ApiEmojiStatusCollectible, ApiEmojiStatusType } from '../../api/types/users';
import type { ApiCredentials } from '../../components/payment/PaymentModal'; import type { ApiCredentials } from '../../components/payment/PaymentModal';
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer'; import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
import type { ReducerAction } from '../../hooks/useReducer'; import type { ReducerAction } from '../../hooks/useReducer';
@ -1831,6 +1832,7 @@ export interface ActionPayloads {
clearRecentCustomEmoji: undefined; clearRecentCustomEmoji: undefined;
loadFeaturedEmojiStickers: undefined; loadFeaturedEmojiStickers: undefined;
loadDefaultStatusIcons: undefined; loadDefaultStatusIcons: undefined;
loadUserCollectibleStatuses: undefined;
loadRecentEmojiStatuses: undefined; loadRecentEmojiStatuses: undefined;
// Bots // Bots
@ -2345,6 +2347,10 @@ export interface ActionPayloads {
} & WithTabId; } & WithTabId;
clearGiftWithdrawError: WithTabId | undefined; clearGiftWithdrawError: WithTabId | undefined;
closeGiftWithdrawModal: WithTabId | undefined; closeGiftWithdrawModal: WithTabId | undefined;
openGiftStatusInfoModal: {
emojiStatus: ApiEmojiStatusCollectible;
} & WithTabId;
closeGiftStatusInfoModal: WithTabId | undefined;
processStarGiftWithdrawal: { processStarGiftWithdrawal: {
gift: ApiInputSavedStarGift; gift: ApiInputSavedStarGift;
password: string; password: string;
@ -2379,8 +2385,7 @@ export interface ActionPayloads {
closeStarsGiftModal: WithTabId | undefined; closeStarsGiftModal: WithTabId | undefined;
setEmojiStatus: { setEmojiStatus: {
emojiStatusId: string; emojiStatus: ApiEmojiStatusType;
expires?: number;
referrerWebAppKey?: string; referrerWebAppKey?: string;
} & WithTabId; } & WithTabId;
openSuggestedStatusModal: { openSuggestedStatusModal: {

View File

@ -11,6 +11,7 @@ import type {
ApiConfig, ApiConfig,
ApiCountry, ApiCountry,
ApiCountryCode, ApiCountryCode,
ApiEmojiStatusType,
ApiGroupCall, ApiGroupCall,
ApiLanguage, ApiLanguage,
ApiMessage, ApiMessage,
@ -361,6 +362,11 @@ export type GlobalState = {
premiumGifts?: ApiStickerSet; premiumGifts?: ApiStickerSet;
emojiKeywords: Record<string, EmojiKeywords | undefined>; emojiKeywords: Record<string, EmojiKeywords | undefined>;
collectibleEmojiStatuses?: {
statuses: ApiEmojiStatusType[];
hash?: string;
};
gifs: { gifs: {
saved: { saved: {
hash?: string; hash?: string;

View File

@ -47,6 +47,7 @@ import type {
ApiVideo, ApiVideo,
ApiWebPage, ApiWebPage,
} from '../../api/types'; } from '../../api/types';
import type { ApiEmojiStatusCollectible } from '../../api/types/users';
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer'; import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
import type { ReducerAction } from '../../hooks/useReducer'; import type { ReducerAction } from '../../hooks/useReducer';
import type { import type {
@ -737,6 +738,10 @@ export type TabState = {
errorKey?: RegularLangFnParameters; errorKey?: RegularLangFnParameters;
}; };
giftStatusInfoModal?: {
emojiStatus: ApiEmojiStatusCollectible;
};
suggestedStatusModal?: { suggestedStatusModal?: {
botId: string; botId: string;
webAppKey?: string; webAppKey?: string;

View File

@ -1462,6 +1462,7 @@ account.reorderUsernames#ef500eab order:Vector<string> = Bool;
account.toggleUsername#58d6b376 username:string active:Bool = Bool; account.toggleUsername#58d6b376 username:string active:Bool = Bool;
account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks; account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool; account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>; users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.getFullUser#b60f5918 id:InputUser = users.UserFull;
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts; contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;

View File

@ -60,6 +60,7 @@
"account.toggleUsername", "account.toggleUsername",
"account.resolveBusinessChatLink", "account.resolveBusinessChatLink",
"account.toggleSponsoredMessages", "account.toggleSponsoredMessages",
"account.getCollectibleEmojiStatuses",
"users.getUsers", "users.getUsers",
"users.getFullUser", "users.getFullUser",
"contacts.getContacts", "contacts.getContacts",

View File

@ -94,196 +94,201 @@ $icons-map: (
"comments": "\f139", "comments": "\f139",
"copy-media": "\f13a", "copy-media": "\f13a",
"copy": "\f13b", "copy": "\f13b",
"darkmode": "\f13c", "crown-take-off": "\f13c",
"data": "\f13d", "crown-wear": "\f13d",
"delete-filled": "\f13e", "darkmode": "\f13e",
"delete-left": "\f13f", "data": "\f13f",
"delete-user": "\f140", "delete-filled": "\f140",
"delete": "\f141", "delete-left": "\f141",
"diamond": "\f142", "delete-user": "\f142",
"document": "\f143", "delete": "\f143",
"double-badge": "\f144", "diamond": "\f144",
"down": "\f145", "document": "\f145",
"download": "\f146", "double-badge": "\f146",
"eats": "\f147", "down": "\f147",
"edit": "\f148", "download": "\f148",
"email": "\f149", "eats": "\f149",
"enter": "\f14a", "edit": "\f14a",
"expand-modal": "\f14b", "email": "\f14b",
"expand": "\f14c", "enter": "\f14c",
"eye-closed-outline": "\f14d", "expand-modal": "\f14d",
"eye-closed": "\f14e", "expand": "\f14e",
"eye-outline": "\f14f", "eye-closed-outline": "\f14f",
"eye": "\f150", "eye-closed": "\f150",
"favorite-filled": "\f151", "eye-outline": "\f151",
"favorite": "\f152", "eye": "\f152",
"file-badge": "\f153", "favorite-filled": "\f153",
"flag": "\f154", "favorite": "\f154",
"folder-badge": "\f155", "file-badge": "\f155",
"folder": "\f156", "flag": "\f156",
"fontsize": "\f157", "folder-badge": "\f157",
"forums": "\f158", "folder": "\f158",
"forward": "\f159", "fontsize": "\f159",
"fragment": "\f15a", "forums": "\f15a",
"fullscreen": "\f15b", "forward": "\f15b",
"gifs": "\f15c", "fragment": "\f15c",
"gift": "\f15d", "fullscreen": "\f15d",
"group-filled": "\f15e", "gifs": "\f15e",
"group": "\f15f", "gift": "\f15f",
"grouped-disable": "\f160", "group-filled": "\f160",
"grouped": "\f161", "group": "\f161",
"hand-stop": "\f162", "grouped-disable": "\f162",
"hashtag": "\f163", "grouped": "\f163",
"heart-outline": "\f164", "hand-stop": "\f164",
"heart": "\f165", "hashtag": "\f165",
"help": "\f166", "heart-outline": "\f166",
"info-filled": "\f167", "heart": "\f167",
"info": "\f168", "help": "\f168",
"install": "\f169", "info-filled": "\f169",
"italic": "\f16a", "info": "\f16a",
"key": "\f16b", "install": "\f16b",
"keyboard": "\f16c", "italic": "\f16c",
"lamp": "\f16d", "key": "\f16d",
"language": "\f16e", "keyboard": "\f16e",
"large-pause": "\f16f", "lamp": "\f16f",
"large-play": "\f170", "language": "\f170",
"link-badge": "\f171", "large-pause": "\f171",
"link-broken": "\f172", "large-play": "\f172",
"link": "\f173", "link-badge": "\f173",
"location": "\f174", "link-broken": "\f174",
"lock-badge": "\f175", "link": "\f175",
"lock": "\f176", "location": "\f176",
"logout": "\f177", "lock-badge": "\f177",
"loop": "\f178", "lock": "\f178",
"mention": "\f179", "logout": "\f179",
"message-failed": "\f17a", "loop": "\f17a",
"message-pending": "\f17b", "mention": "\f17b",
"message-read": "\f17c", "message-failed": "\f17c",
"message-succeeded": "\f17d", "message-pending": "\f17d",
"message": "\f17e", "message-read": "\f17e",
"microphone-alt": "\f17f", "message-succeeded": "\f17f",
"microphone": "\f180", "message": "\f180",
"monospace": "\f181", "microphone-alt": "\f181",
"more-circle": "\f182", "microphone": "\f182",
"more": "\f183", "monospace": "\f183",
"move-caption-down": "\f184", "more-circle": "\f184",
"move-caption-up": "\f185", "more": "\f185",
"mute": "\f186", "move-caption-down": "\f186",
"muted": "\f187", "move-caption-up": "\f187",
"my-notes": "\f188", "mute": "\f188",
"new-chat-filled": "\f189", "muted": "\f189",
"next": "\f18a", "my-notes": "\f18a",
"nochannel": "\f18b", "new-chat-filled": "\f18b",
"noise-suppression": "\f18c", "next": "\f18c",
"non-contacts": "\f18d", "nochannel": "\f18d",
"one-filled": "\f18e", "noise-suppression": "\f18e",
"open-in-new-tab": "\f18f", "non-contacts": "\f18f",
"password-off": "\f190", "one-filled": "\f190",
"pause": "\f191", "open-in-new-tab": "\f191",
"permissions": "\f192", "password-off": "\f192",
"phone-discard-outline": "\f193", "pause": "\f193",
"phone-discard": "\f194", "permissions": "\f194",
"phone": "\f195", "phone-discard-outline": "\f195",
"photo": "\f196", "phone-discard": "\f196",
"pin-badge": "\f197", "phone": "\f197",
"pin-list": "\f198", "photo": "\f198",
"pin": "\f199", "pin-badge": "\f199",
"pinned-chat": "\f19a", "pin-list": "\f19a",
"pinned-message": "\f19b", "pin": "\f19b",
"pip": "\f19c", "pinned-chat": "\f19c",
"play-story": "\f19d", "pinned-message": "\f19d",
"play": "\f19e", "pip": "\f19e",
"poll": "\f19f", "play-story": "\f19f",
"previous": "\f1a0", "play": "\f1a0",
"privacy-policy": "\f1a1", "poll": "\f1a1",
"quote-text": "\f1a2", "previous": "\f1a2",
"quote": "\f1a3", "privacy-policy": "\f1a3",
"readchats": "\f1a4", "proof-of-ownership": "\f1a4",
"recent": "\f1a5", "quote-text": "\f1a5",
"reload": "\f1a6", "quote": "\f1a6",
"remove-quote": "\f1a7", "radial-badge": "\f1a7",
"remove": "\f1a8", "readchats": "\f1a8",
"reopen-topic": "\f1a9", "recent": "\f1a9",
"replace": "\f1aa", "reload": "\f1aa",
"replies": "\f1ab", "remove-quote": "\f1ab",
"reply-filled": "\f1ac", "remove": "\f1ac",
"reply": "\f1ad", "reopen-topic": "\f1ad",
"revenue-split": "\f1ae", "replace": "\f1ae",
"revote": "\f1af", "replies": "\f1af",
"save-story": "\f1b0", "reply-filled": "\f1b0",
"saved-messages": "\f1b1", "reply": "\f1b1",
"schedule": "\f1b2", "revenue-split": "\f1b2",
"search": "\f1b3", "revote": "\f1b3",
"select": "\f1b4", "save-story": "\f1b4",
"send-outline": "\f1b5", "saved-messages": "\f1b5",
"send": "\f1b6", "schedule": "\f1b6",
"settings-filled": "\f1b7", "search": "\f1b7",
"settings": "\f1b8", "select": "\f1b8",
"share-filled": "\f1b9", "send-outline": "\f1b9",
"share-screen-outlined": "\f1ba", "send": "\f1ba",
"share-screen-stop": "\f1bb", "settings-filled": "\f1bb",
"share-screen": "\f1bc", "settings": "\f1bc",
"show-message": "\f1bd", "share-filled": "\f1bd",
"sidebar": "\f1be", "share-screen-outlined": "\f1be",
"skip-next": "\f1bf", "share-screen-stop": "\f1bf",
"skip-previous": "\f1c0", "share-screen": "\f1c0",
"smallscreen": "\f1c1", "show-message": "\f1c1",
"smile": "\f1c2", "sidebar": "\f1c2",
"sort": "\f1c3", "skip-next": "\f1c3",
"speaker-muted-story": "\f1c4", "skip-previous": "\f1c4",
"speaker-outline": "\f1c5", "smallscreen": "\f1c5",
"speaker-story": "\f1c6", "smile": "\f1c6",
"speaker": "\f1c7", "sort": "\f1c7",
"spoiler-disable": "\f1c8", "speaker-muted-story": "\f1c8",
"spoiler": "\f1c9", "speaker-outline": "\f1c9",
"sport": "\f1ca", "speaker-story": "\f1ca",
"star": "\f1cb", "speaker": "\f1cb",
"stars-lock": "\f1cc", "spoiler-disable": "\f1cc",
"stats": "\f1cd", "spoiler": "\f1cd",
"stealth-future": "\f1ce", "sport": "\f1ce",
"stealth-past": "\f1cf", "star": "\f1cf",
"stickers": "\f1d0", "stars-lock": "\f1d0",
"stop-raising-hand": "\f1d1", "stats": "\f1d1",
"stop": "\f1d2", "stealth-future": "\f1d2",
"story-caption": "\f1d3", "stealth-past": "\f1d3",
"story-expired": "\f1d4", "stickers": "\f1d4",
"story-priority": "\f1d5", "stop-raising-hand": "\f1d5",
"story-reply": "\f1d6", "stop": "\f1d6",
"strikethrough": "\f1d7", "story-caption": "\f1d7",
"tag-add": "\f1d8", "story-expired": "\f1d8",
"tag-crossed": "\f1d9", "story-priority": "\f1d9",
"tag-filter": "\f1da", "story-reply": "\f1da",
"tag-name": "\f1db", "strikethrough": "\f1db",
"tag": "\f1dc", "tag-add": "\f1dc",
"timer": "\f1dd", "tag-crossed": "\f1dd",
"toncoin": "\f1de", "tag-filter": "\f1de",
"trade": "\f1df", "tag-name": "\f1df",
"transcribe": "\f1e0", "tag": "\f1e0",
"truck": "\f1e1", "timer": "\f1e1",
"unarchive": "\f1e2", "toncoin": "\f1e2",
"underlined": "\f1e3", "trade": "\f1e3",
"unlock-badge": "\f1e4", "transcribe": "\f1e4",
"unlock": "\f1e5", "truck": "\f1e5",
"unmute": "\f1e6", "unarchive": "\f1e6",
"unpin": "\f1e7", "underlined": "\f1e7",
"unread": "\f1e8", "unique-profile": "\f1e8",
"up": "\f1e9", "unlock-badge": "\f1e9",
"user-filled": "\f1ea", "unlock": "\f1ea",
"user-online": "\f1eb", "unmute": "\f1eb",
"user": "\f1ec", "unpin": "\f1ec",
"video-outlined": "\f1ed", "unread": "\f1ed",
"video-stop": "\f1ee", "up": "\f1ee",
"video": "\f1ef", "user-filled": "\f1ef",
"view-once": "\f1f0", "user-online": "\f1f0",
"voice-chat": "\f1f1", "user": "\f1f1",
"volume-1": "\f1f2", "video-outlined": "\f1f2",
"volume-2": "\f1f3", "video-stop": "\f1f3",
"volume-3": "\f1f4", "video": "\f1f4",
"web": "\f1f5", "view-once": "\f1f5",
"webapp": "\f1f6", "voice-chat": "\f1f6",
"word-wrap": "\f1f7", "volume-1": "\f1f7",
"zoom-in": "\f1f8", "volume-2": "\f1f8",
"zoom-out": "\f1f9", "volume-3": "\f1f9",
"web": "\f1fa",
"webapp": "\f1fb",
"word-wrap": "\f1fc",
"zoom-in": "\f1fd",
"zoom-out": "\f1fe",
); );
.icon-active-sessions::before { .icon-active-sessions::before {
@ -463,6 +468,12 @@ $icons-map: (
.icon-copy::before { .icon-copy::before {
content: map.get($icons-map, "copy"); 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 { .icon-darkmode::before {
content: map.get($icons-map, "darkmode"); content: map.get($icons-map, "darkmode");
} }
@ -769,12 +780,18 @@ $icons-map: (
.icon-privacy-policy::before { .icon-privacy-policy::before {
content: map.get($icons-map, "privacy-policy"); content: map.get($icons-map, "privacy-policy");
} }
.icon-proof-of-ownership::before {
content: map.get($icons-map, "proof-of-ownership");
}
.icon-quote-text::before { .icon-quote-text::before {
content: map.get($icons-map, "quote-text"); content: map.get($icons-map, "quote-text");
} }
.icon-quote::before { .icon-quote::before {
content: map.get($icons-map, "quote"); content: map.get($icons-map, "quote");
} }
.icon-radial-badge::before {
content: map.get($icons-map, "radial-badge");
}
.icon-readchats::before { .icon-readchats::before {
content: map.get($icons-map, "readchats"); content: map.get($icons-map, "readchats");
} }
@ -967,6 +984,9 @@ $icons-map: (
.icon-underlined::before { .icon-underlined::before {
content: map.get($icons-map, "underlined"); content: map.get($icons-map, "underlined");
} }
.icon-unique-profile::before {
content: map.get($icons-map, "unique-profile");
}
.icon-unlock-badge::before { .icon-unlock-badge::before {
content: map.get($icons-map, "unlock-badge"); content: map.get($icons-map, "unlock-badge");
} }

Binary file not shown.

Binary file not shown.

View File

@ -58,6 +58,8 @@ export type FontIconName =
| 'comments' | 'comments'
| 'copy-media' | 'copy-media'
| 'copy' | 'copy'
| 'crown-take-off'
| 'crown-wear'
| 'darkmode' | 'darkmode'
| 'data' | 'data'
| 'delete-filled' | 'delete-filled'
@ -160,8 +162,10 @@ export type FontIconName =
| 'poll' | 'poll'
| 'previous' | 'previous'
| 'privacy-policy' | 'privacy-policy'
| 'proof-of-ownership'
| 'quote-text' | 'quote-text'
| 'quote' | 'quote'
| 'radial-badge'
| 'readchats' | 'readchats'
| 'recent' | 'recent'
| 'reload' | 'reload'
@ -226,6 +230,7 @@ export type FontIconName =
| 'truck' | 'truck'
| 'unarchive' | 'unarchive'
| 'underlined' | 'underlined'
| 'unique-profile'
| 'unlock-badge' | 'unlock-badge'
| 'unlock' | 'unlock'
| 'unmute' | 'unmute'

View File

@ -1197,6 +1197,9 @@ export interface LangPair {
'GiftInfoViewUpgraded': undefined; 'GiftInfoViewUpgraded': undefined;
'GiftInfoUpgradeBadge': undefined; 'GiftInfoUpgradeBadge': undefined;
'GiftInfoUpgradeForFree': undefined; 'GiftInfoUpgradeForFree': undefined;
'GiftInfoWithdraw': undefined;
'GiftInfoWear': undefined;
'GiftInfoTakeOff': undefined;
'GiftInfoTransfer': undefined; 'GiftInfoTransfer': undefined;
'GiftTransferTitle': undefined; 'GiftTransferTitle': undefined;
'GiftTransferTON': undefined; 'GiftTransferTON': undefined;
@ -1321,6 +1324,15 @@ export interface LangPair {
'CheckPasswordTitle': undefined; 'CheckPasswordTitle': undefined;
'CheckPasswordPlaceholder': undefined; 'CheckPasswordPlaceholder': undefined;
'CheckPasswordDescription': undefined; 'CheckPasswordDescription': undefined;
'UniqueStatusBenefitsDescription': undefined;
'UniqueStatusBadgeBenefitTitle': undefined;
'UniqueStatusBadgeDescription': undefined;
'UniqueStatusProfileDesignBenefitTitle': undefined;
'UniqueStatusProfileDesignDescription': undefined;
'UniqueStatusProofOfOwnershipBenefitTitle': undefined;
'UniqueStatusProofOfOwnershipDescription': undefined;
'UniqueStatusWearButton': undefined;
'CollectibleStatusesCategory': undefined;
} }
export interface LangPairWithVariables<V extends unknown = LangVariable> { export interface LangPairWithVariables<V extends unknown = LangVariable> {
@ -1835,6 +1847,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'MoreSimilarBotsText': { 'MoreSimilarBotsText': {
'count': V; 'count': V;
}; };
'UniqueStatusWearTitle': {
'gift': V;
};
} }
export interface LangPairPlural { export interface LangPairPlural {