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 { Api as GramJs } from '../../../lib/gramjs';
import type { ApiEmojiStatus, ApiPeerColor } from '../../types';
import type { ApiEmojiStatusType, ApiPeerColor } from '../../types';
import { CHANNEL_ID_LENGTH } from '../../../config';
import { numberToHexColor } from '../../../util/colors';
export function isPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser {
return peer.hasOwnProperty('userId');
@ -50,14 +51,30 @@ export function buildApiPeerColor(peerColor: GramJs.TypePeerColor): ApiPeerColor
};
}
export function buildApiEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus): ApiEmojiStatus | undefined {
export function buildApiEmojiStatus(mtpEmojiStatus: GramJs.TypeEmojiStatus):
ApiEmojiStatusType | undefined {
if (mtpEmojiStatus instanceof GramJs.EmojiStatus) {
return { documentId: mtpEmojiStatus.documentId.toString(), until: mtpEmojiStatus.until };
return {
type: 'regular',
documentId: mtpEmojiStatus.documentId.toString(),
until: mtpEmojiStatus.until,
};
}
// TODO: Support other parameters
if (mtpEmojiStatus instanceof GramJs.EmojiStatusCollectible) {
return { documentId: mtpEmojiStatus.documentId.toString(), until: mtpEmojiStatus.until };
return {
type: 'collectible',
collectibleId: mtpEmojiStatus.collectibleId.toString(),
documentId: mtpEmojiStatus.documentId.toString(),
title: mtpEmojiStatus.title,
slug: mtpEmojiStatus.slug,
patternDocumentId: mtpEmojiStatus.patternDocumentId.toString(),
centerColor: numberToHexColor(mtpEmojiStatus.centerColor),
edgeColor: numberToHexColor(mtpEmojiStatus.edgeColor),
patternColor: numberToHexColor(mtpEmojiStatus.patternColor),
textColor: numberToHexColor(mtpEmojiStatus.textColor),
until: mtpEmojiStatus.until,
};
}
return undefined;

View File

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

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 }) {
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
q: query,

View File

@ -2,7 +2,7 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiChat, ApiPeer, ApiUser,
ApiChat, ApiEmojiStatusType, ApiPeer, ApiUser,
} from '../../types';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
@ -304,9 +304,9 @@ export function reportSpam(userOrChat: ApiPeer) {
});
}
export function updateEmojiStatus(emojiStatusId: string, expires?: number) {
export function updateEmojiStatus(emojiStatus: ApiEmojiStatusType) {
return invokeRequest(new GramJs.account.UpdateEmojiStatus({
emojiStatus: buildInputEmojiStatus(emojiStatusId, expires),
emojiStatus: buildInputEmojiStatus(emojiStatus),
}), {
shouldReturnTrue: true,
});

View File

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

View File

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

View File

@ -28,7 +28,7 @@ export interface ApiUser {
canBeInvitedToGroup?: boolean;
fakeType?: ApiFakeType;
isAttachBot?: boolean;
emojiStatus?: ApiEmojiStatus;
emojiStatus?: ApiEmojiStatusType;
areStoriesHidden?: boolean;
hasStories?: boolean;
hasUnreadStories?: boolean;
@ -132,11 +132,28 @@ export interface ApiPremiumGiftOption {
botUrl: string;
}
export type ApiEmojiStatusType = ApiEmojiStatus | ApiEmojiStatusCollectible;
export interface ApiEmojiStatus {
type: 'regular';
documentId: string;
until?: number;
}
export interface ApiEmojiStatusCollectible {
type: 'collectible';
collectibleId: string;
documentId: string;
title: string;
slug: string;
patternDocumentId: string;
centerColor: string;
edgeColor: string;
patternColor: string;
textColor: string;
until?: number;
}
export interface ApiBirthday {
day: number;
month: number;

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";
"GiftInfoUpgradeBadge" = "upgrade";
"GiftInfoUpgradeForFree" = "Upgrade For Free";
"GiftInfoWithdraw" = "Withdraw";
"GiftInfoWear" = "Wear";
"GiftInfoTakeOff" = "Take Off";
"GiftInfoTransfer" = "Transfer";
"GiftTransferTitle" = "Transfer";
"GiftTransferTON" = "Send via Blockchain";
@ -1620,3 +1623,13 @@
"CheckPasswordTitle" = "Enter Password";
"CheckPasswordPlaceholder" = "Password";
"CheckPasswordDescription" = "Please enter your password to continue.";
"UniqueStatusWearTitle" = "Wear {gift}";
"UniqueStatusBenefitsDescription" = "and get these benefits:";
"UniqueStatusBadgeBenefitTitle" = "Radiant Badge";
"UniqueStatusBadgeDescription" = "The glittering icon of this item will be displayed next to your name.";
"UniqueStatusProfileDesignBenefitTitle" = "Unique Profile Design";
"UniqueStatusProfileDesignDescription" = "Your profile page will get the color and the symbol of this item.";
"UniqueStatusProofOfOwnershipBenefitTitle" = "Proof of Ownership";
"UniqueStatusProofOfOwnershipDescription" = "Tapping the icon of this item next to your name will show its info and owner.";
"UniqueStatusWearButton" = "Start Wearing";
"CollectibleStatusesCategory" = "Collectibles";

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,6 +28,10 @@
margin-bottom: 1rem;
}
.listItemIcon {
color: var(--accent-color) !important;
}
.content {
display: flex;
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 buildClassName from '../../../util/buildClassName';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import ListItem from '../../ui/ListItem';
@ -13,6 +15,7 @@ import styles from './TableAboutModal.module.scss';
export type TableAboutData = [IconName | undefined, TeactNode, TeactNode][];
type OwnProps = {
contentClassName?: string;
isOpen?: boolean;
listItemData?: TableAboutData;
headerIconName?: IconName;
@ -36,11 +39,12 @@ const TableAboutModal = ({
withSeparator,
onClose,
onButtonClick,
contentClassName,
}: OwnProps) => {
return (
<Modal
isOpen={isOpen}
className={styles.root}
className={buildClassName(styles.root, contentClassName)}
contentClassName={styles.content}
hasAbsoluteCloseButton
absoluteCloseButtonColor={hasBackdrop ? 'translucent-white' : undefined}
@ -55,6 +59,7 @@ const TableAboutModal = ({
isStatic
multiline
icon={icon}
iconClassName={styles.listItemIcon}
>
<span className="title">{title}</span>
<span className="subtitle">{subtitle}</span>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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