Support pinned gifts (#5744)

This commit is contained in:
zubiden 2025-03-21 14:02:25 +04:00 committed by Alexander Zinchuk
parent 905124fe96
commit 440001b938
47 changed files with 1052 additions and 6145 deletions

7
package-lock.json generated
View File

@ -44,6 +44,7 @@
"@testing-library/jest-dom": "^6.4.5",
"@twbs/fantasticon": "^3.0.0",
"@types/croppie": "^2.6.4",
"@types/dom-view-transitions": "^1.0.6",
"@types/hast": "^3.0.4",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.1",
@ -5146,6 +5147,12 @@
"@types/ms": "*"
}
},
"node_modules/@types/dom-view-transitions": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/dom-view-transitions/-/dom-view-transitions-1.0.6.tgz",
"integrity": "sha512-Q5IoDouTOcZKEs4nDpmUti2MXP48MQDBkS2nUQKFrsDeTFIaArVmhWUon28vlDfNkbbaK4EROu4Fv0iKObWjSg==",
"dev": true
},
"node_modules/@types/eslint": {
"version": "8.56.10",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz",

View File

@ -66,6 +66,7 @@
"@testing-library/jest-dom": "^6.4.5",
"@twbs/fantasticon": "^3.0.0",
"@types/croppie": "^2.6.4",
"@types/dom-view-transitions": "^1.0.6",
"@types/hast": "^3.0.4",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.1",

View File

@ -87,6 +87,7 @@ export interface GramJsAppConfig extends LimitsConfig {
stargifts_convert_period_max?: number;
starref_start_param_prefixes?: string[];
ton_blockchain_explorer_url?: string;
stargifts_pinned_to_top_limit?: number;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -174,5 +175,6 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
starGiftMaxConvertPeriod: appConfig.stargifts_convert_period_max,
starRefStartPrefixes: appConfig.starref_start_param_prefixes,
tonExplorerUrl: appConfig.ton_blockchain_explorer_url,
savedGiftPinLimit: appConfig.stargifts_pinned_to_top_limit,
};
}

View File

@ -131,7 +131,7 @@ export function buildApiStarGiftAttribute(attribute: GramJs.TypeStarGiftAttribut
export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId: string): ApiSavedStarGift {
const {
gift, date, convertStars, fromId, message, msgId, nameHidden, unsaved, upgradeStars, transferStars, canUpgrade,
savedId, canExportAt,
savedId, canExportAt, pinnedToTop,
} = userStarGift;
const inputGift: ApiInputSavedStarGift | undefined = savedId && peerId
@ -153,5 +153,6 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId
inputGift,
savedId: savedId?.toString(),
canExportAt,
isPinned: pinnedToTop,
};
}

View File

@ -42,3 +42,5 @@ export * from './stories';
export * from './payments';
export * from './fragment';
export * from './stars';

View File

@ -1,25 +1,15 @@
import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import type {
GiftProfileFilterOptions,
} from '../../../types';
import type {
ApiChat,
ApiInputStorePaymentPurpose,
ApiPeer,
ApiRequestInputInvoice,
ApiRequestInputSavedStarGift,
ApiStarGiftRegular,
ApiThemeParameters,
} from '../../types';
import { DEBUG } from '../../../config';
import {
buildApiSavedStarGift,
buildApiStarGift,
buildApiStarGiftAttribute,
} from '../apiBuilders/gifts';
import {
buildApiBoost,
buildApiBoostsStatus,
@ -30,33 +20,24 @@ import {
buildApiPremiumGiftCodeOption,
buildApiPremiumPromo,
buildApiReceipt,
buildApiStarsAmount,
buildApiStarsGiftOptions,
buildApiStarsGiveawayOptions,
buildApiStarsSubscription,
buildApiStarsTransaction,
buildApiStarTopupOption,
buildShippingOptions,
} from '../apiBuilders/payments';
import { buildApiPeerId } from '../apiBuilders/peers';
import {
buildInputInvoice,
buildInputPeer,
buildInputSavedStarGift,
buildInputStorePaymentPurpose,
buildInputThemeParams,
buildShippingInfo,
} from '../gramjsBuilders';
import {
checkErrorType,
deserializeBytes,
serializeBytes,
wrapError,
} from '../helpers/misc';
import localDb from '../localDb';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { handleGramJsUpdate, invokeRequest } from './client';
import { getPassword, getTemporaryPaymentPassword } from './twoFaSettings';
import { getTemporaryPaymentPassword } from './twoFaSettings';
export async function validateRequestedInfo({
inputInvoice,
@ -413,107 +394,6 @@ export async function getPremiumGiftCodeOptions({
return result.map(buildApiPremiumGiftCodeOption);
}
export async function getStarsGiftOptions({
chat,
}: {
chat?: ApiChat;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarsGiftOptions({
userId: chat && buildInputPeer(chat.id, chat.accessHash),
}));
if (!result) {
return undefined;
}
return result.map(buildApiStarsGiftOptions);
}
export async function fetchStarsGiveawayOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsGiveawayOptions());
if (!result) {
return undefined;
}
return result.map(buildApiStarsGiveawayOptions);
}
export async function fetchStarGifts() {
const result = await invokeRequest(new GramJs.payments.GetStarGifts({}));
if (!result || result instanceof GramJs.payments.StarGiftsNotModified) {
return undefined;
}
// Right now, only regular star gifts can be bought, but API are not specific
return result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift');
}
export async function fetchSavedStarGifts({
peer,
offset = '',
limit,
filter,
}: {
peer: ApiPeer;
offset?: string;
limit?: number;
filter?: GiftProfileFilterOptions;
}) {
type GetSavedStarGiftsParams = ConstructorParameters<typeof GramJs.payments.GetSavedStarGifts>[0];
const params : GetSavedStarGiftsParams = {
peer: buildInputPeer(peer.id, peer.accessHash),
offset,
limit,
...(filter && {
sortByValue: filter.sortType === 'byValue' || undefined,
excludeUnlimited: !filter.shouldIncludeUnlimited || undefined,
excludeLimited: !filter.shouldIncludeLimited || undefined,
excludeUnique: !filter.shouldIncludeUnique || undefined,
excludeSaved: !filter.shouldIncludeDisplayed || undefined,
excludeUnsaved: !filter.shouldIncludeHidden || undefined,
} satisfies GetSavedStarGiftsParams),
};
const result = await invokeRequest(new GramJs.payments.GetSavedStarGifts(params));
if (!result) {
return undefined;
}
const gifts = result.gifts.map((g) => buildApiSavedStarGift(g, peer.id));
return {
gifts,
nextOffset: result.nextOffset,
};
}
export function saveStarGift({
inputGift,
shouldUnsave,
}: {
inputGift: ApiRequestInputSavedStarGift;
shouldUnsave?: boolean;
}) {
return invokeRequest(new GramJs.payments.SaveStarGift({
stargift: buildInputSavedStarGift(inputGift),
unsave: shouldUnsave || undefined,
}));
}
export function convertStarGift({
inputSavedGift,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
}) {
return invokeRequest(new GramJs.payments.ConvertStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
}));
}
export function launchPrepaidGiveaway({
chat,
giveawayId,
@ -531,234 +411,3 @@ export function launchPrepaidGiveaway({
shouldReturnTrue: true,
});
}
export async function fetchStarsStatus() {
const result = await invokeRequest(new GramJs.payments.GetStarsStatus({
peer: new GramJs.InputPeerSelf(),
}));
if (!result) {
return undefined;
}
return {
nextHistoryOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction),
nextSubscriptionOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions?.map(buildApiStarsSubscription),
balance: buildApiStarsAmount(result.balance),
};
}
export async function fetchStarsTransactions({
peer,
offset,
isInbound,
isOutbound,
}: {
peer?: ApiPeer;
offset?: string;
isInbound?: true;
isOutbound?: true;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsTransactions({
peer: inputPeer,
offset,
inbound: isInbound,
outbound: isOutbound,
}));
if (!result) {
return undefined;
}
return {
nextOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction),
balance: buildApiStarsAmount(result.balance),
};
}
export async function fetchStarsTransactionById({
id, peer,
}: {
id: string;
peer?: ApiPeer;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsTransactionsByID({
peer: inputPeer,
id: [new GramJs.InputStarsTransaction({
id,
})],
}));
if (!result?.history?.[0]) {
return undefined;
}
return {
transaction: buildApiStarsTransaction(result?.history[0]),
};
}
export async function fetchStarsSubscriptions({
offset, peer,
}: {
offset?: string;
peer?: ApiPeer;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsSubscriptions({
peer: inputPeer,
offset,
}));
if (!result?.subscriptions) {
return undefined;
}
return {
nextOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions.map(buildApiStarsSubscription),
balance: buildApiStarsAmount(result.balance),
};
}
export async function changeStarsSubscription({
peer, subscriptionId, isCancelled,
}: {
peer?: ApiPeer;
subscriptionId: string;
isCancelled: boolean;
}) {
const result = await invokeRequest(new GramJs.payments.ChangeStarsSubscription({
peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(),
subscriptionId,
canceled: isCancelled,
}));
return result;
}
export async function fulfillStarsSubscription({
peer, subscriptionId,
}: {
peer?: ApiPeer;
subscriptionId: string;
}) {
const result = await invokeRequest(new GramJs.payments.FulfillStarsSubscription({
peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(),
subscriptionId,
}));
return result;
}
export async function fetchStarsTopupOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions());
if (!result) {
return undefined;
}
return result.map(buildApiStarTopupOption);
}
export async function fetchUniqueStarGift({ slug }: {
slug: string;
}) {
const result = await invokeRequest(new GramJs.payments.GetUniqueStarGift({ slug }));
if (!result) return undefined;
const gift = buildApiStarGift(result.gift);
if (gift.type !== 'starGiftUnique') return undefined;
return gift;
}
export async function fetchStarGiftUpgradePreview({
giftId,
}: {
giftId: string;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarGiftUpgradePreview({
giftId: BigInt(giftId),
}));
if (!result) {
return undefined;
}
return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean);
}
export function upgradeStarGift({
inputSavedGift,
shouldKeepOriginalDetails,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
shouldKeepOriginalDetails?: true;
}) {
return invokeRequest(new GramJs.payments.UpgradeStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
keepOriginalDetails: shouldKeepOriginalDetails,
}), {
shouldReturnTrue: true,
});
}
export function transferStarGift({
inputSavedGift,
toPeer,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
toPeer: ApiPeer;
}) {
return invokeRequest(new GramJs.payments.TransferStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
toId: buildInputPeer(toPeer.id, toPeer.accessHash),
}), {
shouldReturnTrue: true,
});
}
export async function fetchStarGiftWithdrawalUrl({
inputGift,
password,
}: {
inputGift: ApiRequestInputSavedStarGift;
password: string;
}) {
try {
const passwordCheck = await getPassword(password);
if (!passwordCheck) {
return undefined;
}
if ('error' in passwordCheck) {
return passwordCheck;
}
const result = await invokeRequest(new GramJs.payments.GetStarGiftWithdrawalUrl({
stargift: buildInputSavedStarGift(inputGift),
password: passwordCheck,
}), {
shouldThrow: true,
});
if (!result) {
return undefined;
}
return { url: result.url };
} catch (err: unknown) {
if (!checkErrorType(err)) return undefined;
return wrapError(err);
}
return undefined;
}

View File

@ -0,0 +1,371 @@
import bigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs';
import type { GiftProfileFilterOptions } from '../../../types';
import type {
ApiChat,
ApiPeer,
ApiRequestInputSavedStarGift,
ApiStarGiftRegular,
} from '../../types';
import { buildApiSavedStarGift, buildApiStarGift, buildApiStarGiftAttribute } from '../apiBuilders/gifts';
import {
buildApiStarsAmount,
buildApiStarsGiftOptions,
buildApiStarsGiveawayOptions,
buildApiStarsSubscription,
buildApiStarsTransaction,
buildApiStarTopupOption,
} from '../apiBuilders/payments';
import { buildInputPeer, buildInputSavedStarGift } from '../gramjsBuilders';
import { checkErrorType, wrapError } from '../helpers/misc';
import { invokeRequest } from './client';
import { getPassword } from './twoFaSettings';
export async function fetchStarsGiveawayOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsGiveawayOptions());
if (!result) {
return undefined;
}
return result.map(buildApiStarsGiveawayOptions);
}
export async function fetchStarGifts() {
const result = await invokeRequest(new GramJs.payments.GetStarGifts({}));
if (!result || result instanceof GramJs.payments.StarGiftsNotModified) {
return undefined;
}
// Right now, only regular star gifts can be bought, but API are not specific
return result.gifts.map(buildApiStarGift).filter((gift): gift is ApiStarGiftRegular => gift.type === 'starGift');
}
export async function fetchSavedStarGifts({
peer,
offset = '',
limit,
filter,
}: {
peer: ApiPeer;
offset?: string;
limit?: number;
filter?: GiftProfileFilterOptions;
}) {
type GetSavedStarGiftsParams = ConstructorParameters<typeof GramJs.payments.GetSavedStarGifts>[0];
const params : GetSavedStarGiftsParams = {
peer: buildInputPeer(peer.id, peer.accessHash),
offset,
limit,
...(filter && {
sortByValue: filter.sortType === 'byValue' || undefined,
excludeUnlimited: !filter.shouldIncludeUnlimited || undefined,
excludeLimited: !filter.shouldIncludeLimited || undefined,
excludeUnique: !filter.shouldIncludeUnique || undefined,
excludeSaved: !filter.shouldIncludeDisplayed || undefined,
excludeUnsaved: !filter.shouldIncludeHidden || undefined,
} satisfies GetSavedStarGiftsParams),
};
const result = await invokeRequest(new GramJs.payments.GetSavedStarGifts(params));
if (!result) {
return undefined;
}
const gifts = result.gifts.map((g) => buildApiSavedStarGift(g, peer.id));
return {
gifts,
nextOffset: result.nextOffset,
};
}
export function saveStarGift({
inputGift,
shouldUnsave,
}: {
inputGift: ApiRequestInputSavedStarGift;
shouldUnsave?: boolean;
}) {
return invokeRequest(new GramJs.payments.SaveStarGift({
stargift: buildInputSavedStarGift(inputGift),
unsave: shouldUnsave || undefined,
}));
}
export function convertStarGift({
inputSavedGift,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
}) {
return invokeRequest(new GramJs.payments.ConvertStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
}));
}
export async function getStarsGiftOptions({
chat,
}: {
chat?: ApiChat;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarsGiftOptions({
userId: chat && buildInputPeer(chat.id, chat.accessHash),
}));
if (!result) {
return undefined;
}
return result.map(buildApiStarsGiftOptions);
}
export async function fetchStarsStatus() {
const result = await invokeRequest(new GramJs.payments.GetStarsStatus({
peer: new GramJs.InputPeerSelf(),
}));
if (!result) {
return undefined;
}
return {
nextHistoryOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction),
nextSubscriptionOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions?.map(buildApiStarsSubscription),
balance: buildApiStarsAmount(result.balance),
};
}
export async function fetchStarsTransactions({
peer,
offset,
isInbound,
isOutbound,
}: {
peer?: ApiPeer;
offset?: string;
isInbound?: true;
isOutbound?: true;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsTransactions({
peer: inputPeer,
offset,
inbound: isInbound,
outbound: isOutbound,
}));
if (!result) {
return undefined;
}
return {
nextOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction),
balance: buildApiStarsAmount(result.balance),
};
}
export async function fetchStarsTransactionById({
id, peer,
}: {
id: string;
peer?: ApiPeer;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsTransactionsByID({
peer: inputPeer,
id: [new GramJs.InputStarsTransaction({
id,
})],
}));
if (!result?.history?.[0]) {
return undefined;
}
return {
transaction: buildApiStarsTransaction(result?.history[0]),
};
}
export async function fetchStarsSubscriptions({
offset, peer,
}: {
offset?: string;
peer?: ApiPeer;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsSubscriptions({
peer: inputPeer,
offset,
}));
if (!result?.subscriptions) {
return undefined;
}
return {
nextOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions.map(buildApiStarsSubscription),
balance: buildApiStarsAmount(result.balance),
};
}
export async function changeStarsSubscription({
peer, subscriptionId, isCancelled,
}: {
peer?: ApiPeer;
subscriptionId: string;
isCancelled: boolean;
}) {
const result = await invokeRequest(new GramJs.payments.ChangeStarsSubscription({
peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(),
subscriptionId,
canceled: isCancelled,
}));
return result;
}
export async function fulfillStarsSubscription({
peer, subscriptionId,
}: {
peer?: ApiPeer;
subscriptionId: string;
}) {
const result = await invokeRequest(new GramJs.payments.FulfillStarsSubscription({
peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(),
subscriptionId,
}));
return result;
}
export async function fetchStarsTopupOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions());
if (!result) {
return undefined;
}
return result.map(buildApiStarTopupOption);
}
export async function fetchUniqueStarGift({ slug }: {
slug: string;
}) {
const result = await invokeRequest(new GramJs.payments.GetUniqueStarGift({ slug }));
if (!result) return undefined;
const gift = buildApiStarGift(result.gift);
if (gift.type !== 'starGiftUnique') return undefined;
return gift;
}
export async function fetchStarGiftUpgradePreview({
giftId,
}: {
giftId: string;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarGiftUpgradePreview({
giftId: bigInt(giftId),
}));
if (!result) {
return undefined;
}
return result.sampleAttributes.map(buildApiStarGiftAttribute).filter(Boolean);
}
export function upgradeStarGift({
inputSavedGift,
shouldKeepOriginalDetails,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
shouldKeepOriginalDetails?: true;
}) {
return invokeRequest(new GramJs.payments.UpgradeStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
keepOriginalDetails: shouldKeepOriginalDetails,
}), {
shouldReturnTrue: true,
});
}
export function transferStarGift({
inputSavedGift,
toPeer,
}: {
inputSavedGift: ApiRequestInputSavedStarGift;
toPeer: ApiPeer;
}) {
return invokeRequest(new GramJs.payments.TransferStarGift({
stargift: buildInputSavedStarGift(inputSavedGift),
toId: buildInputPeer(toPeer.id, toPeer.accessHash),
}), {
shouldReturnTrue: true,
});
}
export function toggleSavedGiftPinned({
inputSavedGifts,
peer,
}: {
inputSavedGifts: ApiRequestInputSavedStarGift[];
peer: ApiPeer;
}) {
return invokeRequest(new GramJs.payments.ToggleStarGiftsPinnedToTop({
stargift: inputSavedGifts.map(buildInputSavedStarGift),
peer: buildInputPeer(peer.id, peer.accessHash),
}), {
shouldReturnTrue: true,
});
}
export async function fetchStarGiftWithdrawalUrl({
inputGift,
password,
}: {
inputGift: ApiRequestInputSavedStarGift;
password: string;
}) {
try {
const passwordCheck = await getPassword(password);
if (!passwordCheck) {
return undefined;
}
if ('error' in passwordCheck) {
return passwordCheck;
}
const result = await invokeRequest(new GramJs.payments.GetStarGiftWithdrawalUrl({
stargift: buildInputSavedStarGift(inputGift),
password: passwordCheck,
}), {
shouldThrow: true,
});
if (!result) {
return undefined;
}
return { url: result.url };
} catch (err: unknown) {
if (!checkErrorType(err)) return undefined;
return wrapError(err);
}
return undefined;
}

View File

@ -11,3 +11,4 @@ export * from './calls';
export * from './statistics';
export * from './stories';
export * from './business';
export * from './stars';

View File

@ -1,6 +1,6 @@
import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls';
import type { ApiBotApp, ApiFormattedText, ApiPhoto } from './messages';
import type { ApiStarGiftRegular, ApiStarGiftUnique } from './payments';
import type { ApiStarGiftRegular, ApiStarGiftUnique } from './stars';
interface ActionMediaType {
mediaType: 'action';

View File

@ -8,8 +8,8 @@ import type { ApiPeerColor } from './chats';
import type { ApiMessageAction } from './messageActions';
import type {
ApiLabeledPrice,
ApiStarGiftUnique,
} from './payments';
import type { ApiStarGiftUnique } from './stars';
import type {
ApiMessageStoryData, ApiStory, ApiWebPageStickerData, ApiWebPageStoryData,
} from './stories';

View File

@ -4,7 +4,8 @@ import type { CallbackAction } from '../../global/types';
import type { IconName } from '../../types/icons';
import type { RegularLangFnParameters } from '../../util/localization';
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
import type { ApiPremiumSection, ApiStarsSubscriptionPricing } from './payments';
import type { ApiPremiumSection } from './payments';
import type { ApiStarsSubscriptionPricing } from './stars';
import type { ApiUser } from './users';
export interface ApiInitialArgs {
@ -235,6 +236,7 @@ export interface ApiAppConfig {
starGiftMaxConvertPeriod?: number;
starRefStartPrefixes?: string[];
tonExplorerUrl?: string;
savedGiftPinLimit?: number;
}
export interface ApiConfig {

View File

@ -7,9 +7,10 @@ import type {
ApiInvoice,
ApiMessageEntity,
ApiPaymentCredentials,
ApiSticker,
BoughtPaidMedia,
} from './messages';
import type {
ApiInputSavedStarGift, ApiRequestInputSavedStarGift, ApiStarsGiveawayWinnerOption,
} from './stars';
import type { StatisticsOverviewPercentage } from './statistics';
import type { ApiUser } from './users';
@ -190,114 +191,6 @@ export type ApiInputStorePaymentStarsGiveaway = {
export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode |
ApiInputStorePaymentStarsTopup | ApiInputStorePaymentStarsGift | ApiInputStorePaymentStarsGiveaway;
export interface ApiStarGiftRegular {
type: 'starGift';
isLimited?: true;
id: string;
sticker: ApiSticker;
stars: number;
availabilityRemains?: number;
availabilityTotal?: number;
starsToConvert: number;
isSoldOut?: true;
firstSaleDate?: number;
lastSaleDate?: number;
isBirthday?: true;
upgradeStars?: number;
}
export interface ApiStarGiftUnique {
type: 'starGiftUnique';
id: string;
title: string;
number: number;
ownerId?: string;
ownerName?: string;
ownerAddress?: string;
issuedCount: number;
totalCount: number;
attributes: ApiStarGiftAttribute[];
slug: string;
giftAddress?: string;
}
export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique;
export interface ApiStarGiftAttributeModel {
type: 'model';
name: string;
rarityPercent: number;
sticker: ApiSticker;
}
export interface ApiStarGiftAttributePattern {
type: 'pattern';
name: string;
rarityPercent: number;
sticker: ApiSticker;
}
export interface ApiStarGiftAttributeBackdrop {
type: 'backdrop';
name: string;
centerColor: string;
edgeColor: string;
patternColor: string;
textColor: string;
rarityPercent: number;
}
export interface ApiStarGiftAttributeOriginalDetails {
type: 'originalDetails';
senderId?: string;
recipientId: string;
date: number;
message?: ApiFormattedText;
}
export type ApiStarGiftAttribute = ApiStarGiftAttributeModel | ApiStarGiftAttributePattern
| ApiStarGiftAttributeBackdrop | ApiStarGiftAttributeOriginalDetails;
export interface ApiSavedStarGift {
isNameHidden?: boolean;
isUnsaved?: boolean;
fromId?: string;
date: number;
gift: ApiStarGift;
inputGift?: ApiInputSavedStarGift;
savedId?: string;
message?: ApiFormattedText;
messageId?: number;
starsToConvert?: number;
canUpgrade?: true;
alreadyPaidUpgradeStars?: number;
transferStars?: number;
canExportAt?: number;
isConverted?: boolean; // Local field, used for Action Message
upgradeMsgId?: number; // Local field, used for Action Message
}
export interface ApiInputSavedStarGiftUser {
type: 'user';
messageId: number;
}
export interface ApiInputSavedStarGiftChat {
type: 'chat';
chatId: string;
savedId: string;
}
export type ApiInputSavedStarGift = ApiInputSavedStarGiftUser | ApiInputSavedStarGiftChat;
export type ApiRequestInputSavedStarGiftUser = ApiInputSavedStarGiftUser;
export type ApiRequestInputSavedStarGiftChat = {
type: 'chat';
chat: ApiChat;
savedId: string;
};
export type ApiRequestInputSavedStarGift = ApiRequestInputSavedStarGiftUser | ApiRequestInputSavedStarGiftChat;
export interface ApiPremiumGiftCodeOption {
users: number;
months: number;
@ -387,110 +280,6 @@ export type ApiCheckedGiftCode = {
usedAt?: number;
};
export interface ApiStarsAmount {
amount: number;
nanos: number;
}
export interface ApiStarsTransactionPeerUnsupported {
type: 'unsupported';
}
export interface ApiStarsTransactionPeerAppStore {
type: 'appStore';
}
export interface ApiStarsTransactionPeerPlayMarket {
type: 'playMarket';
}
export interface ApiStarsTransactionPeerPremiumBot {
type: 'premiumBot';
}
export interface ApiStarsTransactionPeerFragment {
type: 'fragment';
}
export interface ApiStarsTransactionPeerAds {
type: 'ads';
}
export interface ApiStarsTransactionApi {
type: 'api';
}
export interface ApiStarsTransactionPeerPeer {
type: 'peer';
id: string;
}
export type ApiStarsTransactionPeer =
| ApiStarsTransactionPeerUnsupported
| ApiStarsTransactionPeerAppStore
| ApiStarsTransactionPeerPlayMarket
| ApiStarsTransactionPeerPremiumBot
| ApiStarsTransactionPeerFragment
| ApiStarsTransactionPeerAds
| ApiStarsTransactionApi
| ApiStarsTransactionPeerPeer;
export interface ApiStarsTransaction {
id?: string;
peer: ApiStarsTransactionPeer;
messageId?: number;
stars: ApiStarsAmount;
isRefund?: true;
isGift?: true;
starGift?: ApiStarGift;
giveawayPostId?: number;
isMyGift?: true; // Used only for outgoing star gift messages
isReaction?: true;
hasFailed?: true;
isPending?: true;
date: number;
title?: string;
description?: string;
photo?: ApiWebDocument;
extendedMedia?: BoughtPaidMedia[];
subscriptionPeriod?: number;
starRefCommision?: number;
isGiftUpgrade?: true;
}
export interface ApiStarsSubscription {
id: string;
peerId: string;
until: number;
pricing: ApiStarsSubscriptionPricing;
isCancelled?: true;
canRefulfill?: true;
hasMissingBalance?: true;
chatInviteHash?: string;
hasBotCancelled?: true;
title?: string;
photo?: ApiWebDocument;
invoiceSlug?: string;
}
export type ApiStarsSubscriptionPricing = {
period: number;
amount: number;
};
export interface ApiStarTopupOption {
isExtended?: true;
stars: number;
currency: string;
amount: number;
}
export interface ApiStarsGiveawayWinnerOption {
isDefault?: true;
users: number;
perUserStars: number;
}
export interface ApiStarGiveawayOption {
isExtended?: true;
isDefault?: true;

216
src/api/types/stars.ts Normal file
View File

@ -0,0 +1,216 @@
import type { ApiWebDocument } from './bots';
import type { ApiChat } from './chats';
import type { ApiFormattedText, ApiSticker, BoughtPaidMedia } from './messages';
export interface ApiStarGiftRegular {
type: 'starGift';
isLimited?: true;
id: string;
sticker: ApiSticker;
stars: number;
availabilityRemains?: number;
availabilityTotal?: number;
starsToConvert: number;
isSoldOut?: true;
firstSaleDate?: number;
lastSaleDate?: number;
isBirthday?: true;
upgradeStars?: number;
}
export interface ApiStarGiftUnique {
type: 'starGiftUnique';
id: string;
title: string;
number: number;
ownerId?: string;
ownerName?: string;
ownerAddress?: string;
issuedCount: number;
totalCount: number;
attributes: ApiStarGiftAttribute[];
slug: string;
giftAddress?: string;
}
export type ApiStarGift = ApiStarGiftRegular | ApiStarGiftUnique;
export interface ApiStarGiftAttributeModel {
type: 'model';
name: string;
rarityPercent: number;
sticker: ApiSticker;
}
export interface ApiStarGiftAttributePattern {
type: 'pattern';
name: string;
rarityPercent: number;
sticker: ApiSticker;
}
export interface ApiStarGiftAttributeBackdrop {
type: 'backdrop';
name: string;
centerColor: string;
edgeColor: string;
patternColor: string;
textColor: string;
rarityPercent: number;
}
export interface ApiStarGiftAttributeOriginalDetails {
type: 'originalDetails';
senderId?: string;
recipientId: string;
date: number;
message?: ApiFormattedText;
}
export type ApiStarGiftAttribute = ApiStarGiftAttributeModel | ApiStarGiftAttributePattern
| ApiStarGiftAttributeBackdrop | ApiStarGiftAttributeOriginalDetails;
export interface ApiSavedStarGift {
isNameHidden?: boolean;
isUnsaved?: boolean;
fromId?: string;
date: number;
gift: ApiStarGift;
inputGift?: ApiInputSavedStarGift;
savedId?: string;
message?: ApiFormattedText;
messageId?: number;
starsToConvert?: number;
canUpgrade?: true;
alreadyPaidUpgradeStars?: number;
transferStars?: number;
canExportAt?: number;
isPinned?: boolean;
isConverted?: boolean; // Local field, used for Action Message
upgradeMsgId?: number; // Local field, used for Action Message
}
export interface ApiInputSavedStarGiftUser {
type: 'user';
messageId: number;
}
export interface ApiInputSavedStarGiftChat {
type: 'chat';
chatId: string;
savedId: string;
}
export type ApiInputSavedStarGift = ApiInputSavedStarGiftUser | ApiInputSavedStarGiftChat;
export type ApiRequestInputSavedStarGiftUser = ApiInputSavedStarGiftUser;
export type ApiRequestInputSavedStarGiftChat = {
type: 'chat';
chat: ApiChat;
savedId: string;
};
export type ApiRequestInputSavedStarGift = ApiRequestInputSavedStarGiftUser | ApiRequestInputSavedStarGiftChat;
export interface ApiStarsAmount {
amount: number;
nanos: number;
}
export interface ApiStarsTransactionPeerUnsupported {
type: 'unsupported';
}
export interface ApiStarsTransactionPeerAppStore {
type: 'appStore';
}
export interface ApiStarsTransactionPeerPlayMarket {
type: 'playMarket';
}
export interface ApiStarsTransactionPeerPremiumBot {
type: 'premiumBot';
}
export interface ApiStarsTransactionPeerFragment {
type: 'fragment';
}
export interface ApiStarsTransactionPeerAds {
type: 'ads';
}
export interface ApiStarsTransactionApi {
type: 'api';
}
export interface ApiStarsTransactionPeerPeer {
type: 'peer';
id: string;
}
export type ApiStarsTransactionPeer =
| ApiStarsTransactionPeerUnsupported
| ApiStarsTransactionPeerAppStore
| ApiStarsTransactionPeerPlayMarket
| ApiStarsTransactionPeerPremiumBot
| ApiStarsTransactionPeerFragment
| ApiStarsTransactionPeerAds
| ApiStarsTransactionApi
| ApiStarsTransactionPeerPeer;
export interface ApiStarsTransaction {
id?: string;
peer: ApiStarsTransactionPeer;
messageId?: number;
stars: ApiStarsAmount;
isRefund?: true;
isGift?: true;
starGift?: ApiStarGift;
giveawayPostId?: number;
isMyGift?: true; // Used only for outgoing star gift messages
isReaction?: true;
hasFailed?: true;
isPending?: true;
date: number;
title?: string;
description?: string;
photo?: ApiWebDocument;
extendedMedia?: BoughtPaidMedia[];
subscriptionPeriod?: number;
starRefCommision?: number;
isGiftUpgrade?: true;
}
export interface ApiStarsSubscription {
id: string;
peerId: string;
until: number;
pricing: ApiStarsSubscriptionPricing;
isCancelled?: true;
canRefulfill?: true;
hasMissingBalance?: true;
chatInviteHash?: string;
hasBotCancelled?: true;
title?: string;
photo?: ApiWebDocument;
invoiceSlug?: string;
}
export type ApiStarsSubscriptionPricing = {
period: number;
amount: number;
};
export interface ApiStarTopupOption {
isExtended?: true;
stars: number;
currency: string;
amount: number;
}
export interface ApiStarsGiveawayWinnerOption {
isDefault?: true;
users: number;
perUserStars: number;
}

View File

@ -41,8 +41,8 @@ import type {
ApiPeerNotifySettings,
ApiSessionData,
} from './misc';
import type { ApiStarsAmount } from './payments';
import type { ApiPrivacyKey, LangPackStringValue, PrivacyVisibility } from './settings';
import type { ApiStarsAmount } from './stars';
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
import type {
ApiEmojiStatusType, ApiUser, ApiUserFullInfo, ApiUserStatus,

View File

@ -4,7 +4,7 @@ import type { ApiBusinessIntro, ApiBusinessLocation, ApiBusinessWorkHours } from
import type { ApiPeerColor } from './chats';
import type { ApiDocument, ApiPhoto } from './messages';
import type { ApiBotVerification } from './misc';
import type { ApiSavedStarGift } from './payments';
import type { ApiSavedStarGift } from './stars';
export interface ApiUser {
id: string;

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 625 B

After

Width:  |  Height:  |  Size: 625 B

View File

@ -1413,6 +1413,8 @@
"GiftInfoChannelHidden" = "This gift is hidden from visitors of your channel. {link}";
"GiftInfoSavedHide" = "Hide >";
"GiftInfoSavedShow" = "Show >";
"GiftActionShow" = "Show";
"GiftActionHide" = "Hide";
"GiftInfoTonText" = "This gift is on the TON Blockchain. {link}";
"GiftInfoTonLinkText" = "View >";
"GiftInfoAvailability" = "Availability";

View File

@ -29,5 +29,11 @@
}
.transition {
width: 1.5rem;
height: 1.5em;
width: 1.5em;
}
.transitionSlide {
display: flex;
align-items: center;
}

View File

@ -141,10 +141,11 @@ const FullNameTitle: FC<OwnProps> = ({
{canShowEmojiStatus && emojiStatus && (
<Transition
className={styles.transition}
slideClassName={styles.transitionSlide}
activeKey={Number(emojiStatus.documentId)}
name="fade"
name="slideFade"
direction={-1}
shouldCleanup
shouldRestoreHeight
>
<CustomEmoji
forceAlways

View File

@ -150,7 +150,7 @@ const PasswordForm: FC<OwnProps> = ({
title="Toggle password visibility"
aria-label="Toggle password visibility"
>
<Icon name={isPasswordVisible ? 'eye' : 'eye-closed'} />
<Icon name={isPasswordVisible ? 'eye' : 'eye-crossed'} />
</div>
</div>
{description && <p className="description">{description}</p>}

View File

@ -0,0 +1,145 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type {
ApiEmojiStatusCollectible, ApiEmojiStatusType, ApiSavedStarGift, ApiStarGift,
} from '../../../api/types';
import { DEFAULT_STATUS_ICON_ID, TME_LINK_PREFIX } from '../../../config';
import { copyTextToClipboard } from '../../../util/clipboard';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import MenuItem from '../../ui/MenuItem';
type OwnProps = {
peerId: string;
canManage?: boolean;
gift: ApiSavedStarGift | ApiStarGift;
currentUserEmojiStatus?: ApiEmojiStatusType;
collectibleEmojiStatuses?: ApiEmojiStatusType[];
};
const GiftMenuItems = ({
peerId,
canManage,
gift: typeGift,
currentUserEmojiStatus,
collectibleEmojiStatuses,
}: OwnProps) => {
const {
showNotification,
openChatWithDraft,
openGiftTransferModal,
openGiftStatusInfoModal,
setEmojiStatus,
toggleSavedGiftPinned,
changeGiftVisibility,
} = getActions();
const lang = useLang();
const isSavedGift = typeGift && 'gift' in typeGift;
const savedGift = isSavedGift ? typeGift : undefined;
const gift = isSavedGift ? typeGift.gift : typeGift;
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 currenUniqueEmojiStatusSlug = currentUserEmojiStatus?.type === 'collectible'
? currentUserEmojiStatus.slug : undefined;
const isGiftUnique = gift && gift.type === 'starGiftUnique';
const canTakeOff = isGiftUnique && currenUniqueEmojiStatusSlug === gift.slug;
const canWear = userCollectibleStatus && !canTakeOff;
const hasPinOptions = canManage && savedGift && !savedGift.isUnsaved && isGiftUnique;
const handleTriggerVisibility = useLastCallback(() => {
const { inputGift, isUnsaved } = savedGift!;
changeGiftVisibility({ gift: inputGift!, shouldUnsave: !isUnsaved });
});
const handleCopyLink = useLastCallback(() => {
if (!starGiftUniqueLink) return;
copyTextToClipboard(starGiftUniqueLink);
showNotification({
message: lang('LinkCopied'),
});
});
const handleLinkShare = useLastCallback(() => {
if (!starGiftUniqueLink) return;
openChatWithDraft({ text: { text: starGiftUniqueLink } });
});
const handleTransfer = useLastCallback(() => {
if (savedGift?.gift.type !== 'starGiftUnique') return;
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 handleTogglePin = useLastCallback(() => {
toggleSavedGiftPinned({ peerId, gift: savedGift! });
});
return (
<>
{hasPinOptions && (
<MenuItem icon={savedGift.isPinned ? 'unpin' : 'pin'} onClick={handleTogglePin}>
{lang(savedGift.isPinned ? 'UnpinFromTop' : 'PinToTop')}
</MenuItem>
)}
<MenuItem icon="link-badge" onClick={handleCopyLink}>
{lang('CopyLink')}
</MenuItem>
<MenuItem icon="forward" onClick={handleLinkShare}>
{lang('Share')}
</MenuItem>
{canManage && isGiftUnique && (
<MenuItem icon="diamond" onClick={handleTransfer}>
{lang('GiftInfoTransfer')}
</MenuItem>
)}
{canManage && savedGift && (
<MenuItem icon={savedGift.isUnsaved ? 'eye-outline' : 'eye-crossed-outline'} onClick={handleTriggerVisibility}>
{lang(savedGift.isUnsaved ? 'GiftActionShow' : 'GiftActionHide')}
</MenuItem>
)}
{canWear && (
<MenuItem icon="crown-wear" onClick={handleWear}>
{lang('GiftInfoWear')}
</MenuItem>
)}
{canTakeOff && (
<MenuItem icon="crown-take-off" onClick={handleTakeOff}>
{lang('GiftInfoTakeOff')}
</MenuItem>
)}
</>
);
};
export default memo(GiftMenuItems);

View File

@ -29,10 +29,11 @@
}
}
.avatar {
.topIcon {
position: absolute;
top: 0.25rem;
left: 0.25rem;
color: white;
}
.hiddenGift {

View File

@ -1,23 +1,27 @@
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiPeer, ApiSavedStarGift } from '../../../api/types';
import type { ApiEmojiStatusType, ApiPeer, ApiSavedStarGift } from '../../../api/types';
import { selectPeer } from '../../../global/selectors';
import { getHasAdminRight } from '../../../global/helpers';
import { selectChat, selectPeer, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { CUSTOM_PEER_HIDDEN } from '../../../util/objects/customPeer';
import { formatIntegerCompact } from '../../../util/textFormat';
import { getGiftAttributes, getStickerFromGift, getTotalGiftAvailability } from '../helpers/gifts';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useFlag from '../../../hooks/useFlag';
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Menu from '../../ui/Menu';
import AnimatedIconFromSticker from '../AnimatedIconFromSticker';
import Avatar from '../Avatar';
import Icon from '../icons/Icon';
import RadialPatternBackground from '../profile/RadialPatternBackground';
import GiftMenuItems from './GiftMenuItems';
import GiftRibbon from './GiftRibbon';
import styles from './SavedGift.module.scss';
@ -25,11 +29,16 @@ import styles from './SavedGift.module.scss';
type OwnProps = {
peerId: string;
gift: ApiSavedStarGift;
style?: string;
observeIntersection?: ObserveFn;
};
type StateProps = {
fromPeer?: ApiPeer;
currentUserId?: string;
hasAdminRights?: boolean;
currentUserEmojiStatus?: ApiEmojiStatusType;
collectibleEmojiStatuses?: ApiEmojiStatusType[];
};
const GIFT_STICKER_SIZE = 90;
@ -37,7 +46,12 @@ const GIFT_STICKER_SIZE = 90;
const SavedGift = ({
peerId,
gift,
style,
fromPeer,
currentUserId,
hasAdminRights,
collectibleEmojiStatuses,
currentUserEmojiStatus,
observeIntersection,
}: OwnProps & StateProps) => {
const { openGiftInfoModal } = getActions();
@ -49,6 +63,21 @@ const SavedGift = ({
const oldLang = useOldLang();
const canManage = peerId === currentUserId || hasAdminRights;
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref);
const getTriggerElement = useLastCallback(() => ref.current);
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll'));
const getMenuElement = useLastCallback(() => (
document.querySelector('#portals')?.querySelector('.saved-gift-context-menu .bubble')
));
const getLayout = useLastCallback(() => ({ withPortal: true }));
const handleClick = useLastCallback(() => {
openGiftInfoModal({
peerId,
@ -94,10 +123,14 @@ const SavedGift = ({
<div
ref={ref}
className={buildClassName(styles.root, 'scroll-item')}
style={style}
onClick={handleClick}
onContextMenu={handleContextMenu}
onMouseDown={handleBeforeContextMenu}
>
{radialPatternBackdrop}
{!radialPatternBackdrop && <Avatar className={styles.avatar} peer={avatarPeer} size="micro" />}
{!radialPatternBackdrop && <Avatar className={styles.topIcon} peer={avatarPeer} size="micro" />}
{gift.isPinned && <Icon name="pinned-message" className={styles.topIcon} />}
<AnimatedIconFromSticker
sticker={sticker}
noLoop
@ -107,7 +140,7 @@ const SavedGift = ({
/>
{gift.isUnsaved && (
<div className={styles.hiddenGift}>
<Icon name="eye-closed-outline" />
<Icon name="eye-crossed-outline" />
</div>
)}
{totalIssued && (
@ -116,16 +149,50 @@ const SavedGift = ({
text={oldLang('Gift2Limited1OfRibbon', formatIntegerCompact(totalIssued))}
/>
)}
{contextMenuAnchor !== undefined && (
<Menu
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
className="saved-gift-context-menu"
autoClose
withPortal
getMenuElement={getMenuElement}
getTriggerElement={getTriggerElement}
getRootElement={getRootElement}
getLayout={getLayout}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
>
<GiftMenuItems
peerId={peerId}
gift={gift}
canManage={canManage}
collectibleEmojiStatuses={collectibleEmojiStatuses}
currentUserEmojiStatus={currentUserEmojiStatus}
/>
</Menu>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { gift }): StateProps => {
(global, { peerId, gift }): StateProps => {
const fromPeer = gift.fromId ? selectPeer(global, gift.fromId) : undefined;
const chat = selectChat(global, peerId);
const hasAdminRights = chat && getHasAdminRight(chat, 'postMessages');
const currentUserId = global.currentUserId;
const currentUser = currentUserId ? selectUser(global, currentUserId) : undefined;
const currentUserEmojiStatus = currentUser?.emojiStatus;
const collectibleEmojiStatuses = global.collectibleEmojiStatuses?.statuses;
return {
fromPeer,
hasAdminRights,
currentUserId,
currentUserEmojiStatus,
collectibleEmojiStatuses,
};
},
)(SavedGift));

View File

@ -44,7 +44,7 @@ const STORY_FEATURE_DESCRIPTIONS = {
const STORY_FEATURE_ICONS: Record<string, IconName> = {
stories_order: 'story-priority',
stories_stealth: 'eye-closed-outline',
stories_stealth: 'eye-crossed-outline',
stories_views: 'eye-outline',
stories_timer: 'timer',
stories_save: 'arrow-down-circle',

View File

@ -422,7 +422,7 @@ const TextFormatter: FC<OwnProps> = ({
className={getFormatButtonClassName('spoiler')}
onClick={handleSpoilerText}
>
<Icon name="eye-closed" />
<Icon name="eye-crossed" />
</Button>
<div className="TextFormatter-divider" />
<Button

View File

@ -33,7 +33,7 @@ export type StateProps = {
isPremium?: boolean;
};
const INTERVAL = 3000;
const INTERVAL = 3200;
const EmojiStatusAccessModal: FC<OwnProps & StateProps> = ({
modal,

View File

@ -3,13 +3,11 @@ 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 { DEFAULT_STATUS_ICON_ID, TME_LINK_PREFIX } from '../../../../config';
import { getHasAdminRight, getPeerTitle } from '../../../../global/helpers';
import { isApiPeerChat } from '../../../../global/helpers/peers';
import { selectPeer, selectUser } from '../../../../global/selectors';
@ -32,13 +30,13 @@ import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import BadgeButton from '../../../common/BadgeButton';
import GiftMenuItems from '../../../common/gift/GiftMenuItems';
import Icon from '../../../common/icons/Icon';
import SafeLink from '../../../common/SafeLink';
import Button from '../../../ui/Button';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import DropdownMenu from '../../../ui/DropdownMenu';
import Link from '../../../ui/Link';
import MenuItem from '../../../ui/MenuItem';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import UniqueGiftHeader from '../UniqueGiftHeader';
@ -80,10 +78,6 @@ const GiftInfoModal = ({
focusMessage,
openGiftUpgradeModal,
showNotification,
openChatWithDraft,
openGiftStatusInfoModal,
setEmojiStatus,
openGiftTransferModal,
} = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
@ -111,27 +105,10 @@ const GiftInfoModal = ({
const giftSticker = gift && getStickerFromGift(gift);
const hasConvertOption = canConvertDifference > 0 && Boolean(savedGift?.starsToConvert);
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 && (
const canManage = !canFocusUpgrade && savedGift?.inputGift && (
isTargetChat ? hasAdminRights : renderingTargetPeer?.id === currentUserId
);
@ -139,38 +116,6 @@ const GiftInfoModal = ({
closeGiftInfoModal();
});
const handleCopyLink = useLastCallback(() => {
if (!starGiftUniqueLink) return;
copyTextToClipboard(starGiftUniqueLink);
showNotification({
message: lang('LinkCopied'),
});
});
const handleLinkShare = useLastCallback(() => {
if (!starGiftUniqueLink) return;
openChatWithDraft({ text: { text: starGiftUniqueLink } });
handleClose();
});
const handleTransfer = useLastCallback(() => {
if (savedGift?.gift.type !== 'starGiftUnique') return;
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 giftChat = isSender ? renderingTargetPeer : renderingFromPeer;
if (!savedGift?.upgradeMsgId || !giftChat) return;
@ -225,7 +170,7 @@ const GiftInfoModal = ({
);
}
if (canUpdate && savedGift?.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) {
if (canManage && savedGift?.alreadyPaidUpgradeStars && !savedGift.upgradeMsgId) {
return (
<Button size="smaller" isShiny onClick={handleOpenUpgradeModal}>
{lang('GiftInfoUpgradeForFree')}
@ -258,13 +203,13 @@ const GiftInfoModal = ({
if (savedGift.upgradeMsgId) return lang('GiftInfoDescriptionUpgraded');
if (savedGift.canUpgrade && savedGift.alreadyPaidUpgradeStars) {
return canUpdate
return canManage
? lang('GiftInfoDescriptionFreeUpgrade')
: lang('GiftInfoPeerDescriptionFreeUpgradeOut', { peer: getPeerTitle(lang, renderingTargetPeer!)! });
}
if (!canUpdate && !isSender) return undefined;
if (!canManage && !isSender) return undefined;
if (isConverted && canConvert) {
return canUpdate
return canManage
? lang('GiftInfoDescriptionConverted', {
amount: starsToConvert,
}, {
@ -282,7 +227,7 @@ const GiftInfoModal = ({
});
}
if (savedGift.canUpgrade && canUpdate) {
if (savedGift.canUpgrade && canManage) {
if (canConvert) {
return lang('GiftInfoDescriptionUpgrade', {
amount: starsToConvert,
@ -296,7 +241,7 @@ const GiftInfoModal = ({
return lang('GiftInfoDescriptionUpgradeRegular');
}
if (canUpdate) {
if (canManage) {
if (canConvert) {
return lang('GiftInfoDescription', {
amount: starsToConvert,
@ -328,7 +273,7 @@ const GiftInfoModal = ({
if (isGiftUnique) return gift.title;
if (!savedGift) return lang('GiftInfoSoldOutTitle');
return canUpdate ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
return canManage ? lang('GiftInfoReceived') : lang('GiftInfoTitle');
}
const uniqueGiftContextMenu = (
@ -337,27 +282,13 @@ const GiftInfoModal = ({
trigger={SettingsMenuButton}
positionX="right"
>
<MenuItem icon="link-badge" onClick={handleCopyLink}>
{lang('CopyLink')}
</MenuItem>
<MenuItem icon="forward" onClick={handleLinkShare}>
{lang('Share')}
</MenuItem>
{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>
)}
<GiftMenuItems
peerId={renderingModal!.peerId!}
gift={typeGift}
canManage={canManage}
collectibleEmojiStatuses={collectibleEmojiStatuses}
currentUserEmojiStatus={currentUserEmojiStatus}
/>
</DropdownMenu>
);
@ -452,7 +383,7 @@ const GiftInfoModal = ({
lang('GiftInfoValue'),
<div className={styles.giftValue}>
{formatStarsAsIcon(lang, starsValue, { className: styles.starAmountIcon })}
{canUpdate && hasConvertOption && Boolean(starsToConvert) && (
{canManage && hasConvertOption && Boolean(starsToConvert) && (
<BadgeButton onClick={openConvertConfirm}>
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
</BadgeButton>
@ -477,7 +408,7 @@ const GiftInfoModal = ({
lang('GiftInfoStatus'),
<div className={styles.giftValue}>
{lang('GiftInfoStatusNonUnique')}
{canUpdate && <BadgeButton onClick={handleOpenUpgradeModal}>{lang('GiftInfoUpgradeBadge')}</BadgeButton>}
{canManage && <BadgeButton onClick={handleOpenUpgradeModal}>{lang('GiftInfoUpgradeBadge')}</BadgeButton>}
</div>,
]);
}
@ -627,7 +558,7 @@ const GiftInfoModal = ({
const footer = (
<div className={styles.footer}>
{(canUpdate || tonLink) && (
{(canManage || tonLink) && (
<div className={styles.footerDescription}>
{tonLink && (
<div>
@ -636,7 +567,7 @@ const GiftInfoModal = ({
}, { withNodes: true })}
</div>
)}
{canUpdate && (
{canManage && (
<div>
{lang(`GiftInfo${isTargetChat ? 'Channel' : ''}${isUnsaved ? 'Hidden' : 'Saved'}`, {
link: (
@ -668,9 +599,10 @@ const GiftInfoModal = ({
};
}, [
typeGift, savedGift, renderingTargetPeer, giftSticker, lang,
canUpdate, hasConvertOption, isSender, oldLang, tonExplorerUrl,
canManage, hasConvertOption, isSender, oldLang, tonExplorerUrl,
gift, giftAttributes, renderFooterButton, isTargetChat,
SettingsMenuButton, isOpen, isGiftUnique, canWear, canTakeOff,
SettingsMenuButton, isOpen, isGiftUnique, renderingModal,
collectibleEmojiStatuses, currentUserEmojiStatus,
]);
return (

View File

@ -67,6 +67,7 @@ import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
import renderText from '../common/helpers/renderText';
import { getSenderName } from '../left/search/helpers/getSenderName';
import { useViewTransition } from '../../hooks/animations/useViewTransition';
import usePeerStoriesPolling from '../../hooks/polling/usePeerStoriesPolling';
import useCacheBuster from '../../hooks/useCacheBuster';
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
@ -130,7 +131,6 @@ type StateProps = {
hasPreviewMediaTab?: boolean;
hasGiftsTab?: boolean;
gifts?: ApiSavedStarGift[];
giftsTransitionKey: number;
areMembersHidden?: boolean;
canAddMembers?: boolean;
canDeleteMembers?: boolean;
@ -198,7 +198,6 @@ const Profile: FC<OwnProps & StateProps> = ({
hasPreviewMediaTab,
hasGiftsTab,
gifts,
giftsTransitionKey,
botPreviewMedia,
areMembersHidden,
canAddMembers,
@ -356,9 +355,13 @@ const Profile: FC<OwnProps & StateProps> = ({
}
}, [chatId, isBot, similarBots, isSynced]);
const giftIds = useMemo(() => {
return gifts?.map(({ date, gift, fromId }) => `${date}-${fromId}-${gift.id}`);
}, [gifts]);
const [renderingGifts, setRenderingGifts] = useState(gifts);
const { startViewTransition, shouldApplyVtn } = useViewTransition();
const getGiftId = useLastCallback((gift: ApiSavedStarGift) => (
`${gift.date}-${gift.fromId}-${gift.gift.id}`
));
const giftIds = useMemo(() => renderingGifts?.map(getGiftId), [renderingGifts]);
const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab;
const tabType = tabs[renderingActiveTab].type as ProfileTabType;
@ -375,6 +378,25 @@ const Profile: FC<OwnProps & StateProps> = ({
loadPeerSavedGifts({ peerId: chatId });
}, [chatId]);
useEffectWithPrevDeps(([prevGifts]) => {
if (!gifts || !prevGifts) {
setRenderingGifts(gifts);
return;
}
const prevGiftIds = prevGifts.map(getGiftId);
const newGiftIds = gifts.map(getGiftId);
const hasOrderChanged = prevGiftIds.some((id, index) => id !== newGiftIds[index]);
if (hasOrderChanged) {
startViewTransition(() => {
setRenderingGifts(gifts);
});
} else {
setRenderingGifts(gifts);
}
}, [gifts, startViewTransition]);
const [resultType, viewportIds, getMore, noProfileInfo] = useProfileViewportIds({
loadMoreMembers,
searchMessages: searchSharedMediaMessages,
@ -816,38 +838,24 @@ const Profile: FC<OwnProps & StateProps> = ({
)}
</div>
) : resultType === 'gifts' ? (
(gifts?.map((gift) => (
<SavedGift
peerId={chatId}
key={`${gift.date}-${gift.fromId}-${gift.gift.id}`}
gift={gift}
observeIntersection={observeIntersectionForMedia}
/>
)))
(renderingGifts?.map((gift) => {
return (
<SavedGift
peerId={chatId}
key={getGiftId(gift)}
style={shouldApplyVtn ? `view-transition-name: vt${getGiftId(gift)}` : undefined}
gift={gift}
observeIntersection={observeIntersectionForMedia}
/>
);
}))
) : undefined}
</div>
);
}
const shouldUseTransitionForContent = resultType === 'gifts';
const contentTransitionKey = giftsTransitionKey;
function renderContentWithTransition() {
return (
<Transition
className={`${resultType}-list`}
activeKey={contentTransitionKey}
name="fade"
>
{renderContent()}
</Transition>
);
}
const activeListSelector = `.shared-media-transition > .Transition_slide-active.${resultType}-list`;
const itemSelector = !shouldUseTransitionForContent
? `${activeListSelector} > .scroll-item`
: `${activeListSelector} > .Transition_slide-active > .content > .scroll-item`;
const itemSelector = `${activeListSelector} > .scroll-item`;
return (
<InfiniteScroll
@ -881,7 +889,7 @@ const Profile: FC<OwnProps & StateProps> = ({
onStart={applyTransitionFix}
onStop={handleTransitionStop}
>
{shouldUseTransitionForContent ? renderContentWithTransition() : renderContent()}
{renderContent()}
</Transition>
<TabList activeTab={renderingActiveTab} tabs={tabs} onSwitchTab={handleSwitchTab} />
</div>
@ -974,7 +982,6 @@ export default memo(withGlobal<OwnProps>(
const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedDialog;
const peerGifts = selectTabState(global).savedGifts.giftsByPeerId[chatId];
const giftsTransitionKey = selectTabState(global).savedGifts.transitionKey || 0;
return {
theme: selectTheme(global),
@ -1000,7 +1007,6 @@ export default memo(withGlobal<OwnProps>(
storyIds,
hasGiftsTab,
gifts: peerGifts?.gifts,
giftsTransitionKey,
pinnedStoryIds,
archiveStoryIds,
storyByIds,

View File

@ -84,7 +84,7 @@ const StealthModeModal = ({ isOpen, stealthMode, isCurrentUserPremium } : StateP
<Icon name="close" />
</Button>
<div className={styles.stealthIcon}>
<Icon name="eye-closed-outline" />
<Icon name="eye-crossed-outline" />
</div>
<div className={styles.title}>{lang('StealthMode')}</div>
<div className={styles.description}>

View File

@ -730,7 +730,7 @@ function Story({
</MenuItem>
)}
{!isOut && isUserStory && (
<MenuItem icon="eye-closed-outline" onClick={handleOpenStealthModal}>
<MenuItem icon="eye-crossed-outline" onClick={handleOpenStealthModal}>
{lang('StealthMode')}
</MenuItem>
)}

View File

@ -33,6 +33,7 @@
&.open {
transform: scale(1);
view-transition-name: open-menu-bubble;
}
&.closing {
@ -115,3 +116,8 @@
position: absolute;
}
}
// Hacky way to fix z-index issues with overlays in View Transitions
html::view-transition-group(open-menu-bubble) {
z-index: var(--z-portal-menu);
}

View File

@ -1,4 +1,4 @@
import type { ApiSavedStarGift } from '../../../api/types';
import type { ApiSavedStarGift, ApiStarGiftUnique } from '../../../api/types';
import type { StarGiftCategory } from '../../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -138,7 +138,7 @@ addActionHandler('loadStarGifts', async (global): Promise<void> => {
addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise<void> => {
const {
peerId, shouldRefresh, withTransition, tabId = getCurrentTabId(),
peerId, shouldRefresh, tabId = getCurrentTabId(),
} = payload;
const peer = selectPeer(global, peerId);
@ -167,17 +167,6 @@ addActionHandler('loadPeerSavedGifts', async (global, actions, payload): Promise
const newGifts = currentGifts && !shouldRefresh ? currentGifts.gifts.concat(result.gifts) : result.gifts;
const tabState = selectTabState(global, tabId);
if (withTransition) {
global = updateTabState(global, {
savedGifts: {
...tabState.savedGifts,
transitionKey: (tabState?.savedGifts.transitionKey || 0) + 1,
},
}, tabId);
}
global = replacePeerSavedGifts(global, peerId, newGifts, result.nextOffset, tabId);
setGlobal(global);
});
@ -295,7 +284,7 @@ addActionHandler('convertGiftToStars', async (global, actions, payload): Promise
const peerId = gift.type === 'user' ? global.currentUserId! : gift.chatId;
Object.values(global.byTabId).forEach((tabState) => {
if (selectPeerSavedGifts(global, peerId, tabId)) {
if (selectPeerSavedGifts(global, peerId, tabState.id)) {
actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id });
}
});
@ -325,3 +314,36 @@ addActionHandler('openGiftUpgradeModal', async (global, actions, payload): Promi
setGlobal(global);
});
addActionHandler('toggleSavedGiftPinned', async (global, actions, payload): Promise<void> => {
const { gift, peerId, tabId = getCurrentTabId() } = payload;
const peer = selectPeer(global, peerId);
if (!peer) return;
const savedGifts = selectPeerSavedGifts(global, peerId, tabId);
if (!savedGifts) return;
const pinLimit = global.appConfig?.savedGiftPinLimit;
const currentPinnedGifts = savedGifts.gifts.filter((g) => g.isPinned);
const newPinnedGifts = gift.isPinned
? currentPinnedGifts.filter((g) => (g.gift as ApiStarGiftUnique).slug !== (gift.gift as ApiStarGiftUnique).slug)
: [...currentPinnedGifts, gift];
const trimmedPinnedGifts = pinLimit ? newPinnedGifts.slice(-pinLimit) : newPinnedGifts;
const inputSavedGifts = trimmedPinnedGifts.map((g) => getRequestInputSavedStarGift(global, g.inputGift!))
.filter(Boolean);
const result = await callApi('toggleSavedGiftPinned', {
inputSavedGifts,
peer,
});
if (!result) return;
Object.values(global.byTabId).forEach((tabState) => {
if (selectPeerSavedGifts(global, peerId, tabState.id)) {
actions.loadPeerSavedGifts({ peerId, shouldRefresh: true, tabId: tabState.id });
}
});
});

View File

@ -110,7 +110,7 @@ addActionHandler('updateGiftProfileFilter', (global, actions, payload): ActionRe
setGlobal(global);
actions.loadPeerSavedGifts({
peerId, shouldRefresh: true, withTransition: true, tabId: tabState.id,
peerId, shouldRefresh: true, tabId: tabState.id,
});
});
@ -132,6 +132,6 @@ addActionHandler('resetGiftProfileFilter', (global, actions, payload): ActionRet
setGlobal(global);
actions.loadPeerSavedGifts({
peerId, shouldRefresh: true, withTransition: true, tabId: tabState.id,
peerId, shouldRefresh: true, tabId: tabState.id,
});
});

View File

@ -2412,7 +2412,6 @@ export interface ActionPayloads {
loadPeerSavedGifts: {
peerId: string;
shouldRefresh?: boolean;
withTransition?: boolean;
} & WithTabId;
changeGiftVisibility: {
gift: ApiInputSavedStarGift;
@ -2421,6 +2420,10 @@ export interface ActionPayloads {
convertGiftToStars: {
gift: ApiInputSavedStarGift;
} & WithTabId;
toggleSavedGiftPinned: {
peerId: string;
gift: ApiSavedStarGift;
} & WithTabId;
openStarsGiftModal: ({
chatId?: string;

View File

@ -210,7 +210,6 @@ export type TabState = {
savedGifts: {
giftsByPeerId: Record<string, ApiSavedGifts>;
filter: GiftProfileFilterOptions;
transitionKey?: number;
};
globalSearch: {

View File

@ -0,0 +1,75 @@
import {
useEffect,
useRef,
useState,
} from '../../lib/teact/teact';
import { requestNextMutation } from '../../lib/fasterdom/fasterdom';
import Deferred from '../../util/Deferred';
import { IS_VIEW_TRANSITION_SUPPORTED } from '../../util/windowEnvironment';
type TransitionFunction = () => Promise<void> | void;
type TransitionState = 'idle' | 'capturing-old' | 'capturing-new' | 'animating' | 'skipped';
interface ViewTransitionController {
transitionState: TransitionState;
shouldApplyVtn?: boolean;
startViewTransition: (domUpdateCallback?: TransitionFunction) => PromiseLike<void> | void;
}
let hasActiveTransition = false;
export function hasActiveViewTransition(): boolean {
return hasActiveTransition;
}
export function useViewTransition(): ViewTransitionController {
const domUpdaterFn = useRef<TransitionFunction>();
const [transitionState, setTransitionState] = useState<TransitionState>('idle');
useEffect(() => {
if (transitionState !== 'capturing-old') return;
const transition = document.startViewTransition(async () => {
setTransitionState('capturing-new');
if (domUpdaterFn.current) await domUpdaterFn.current();
const deferred = new Deferred<void>();
requestNextMutation(() => {
deferred.resolve();
});
return deferred.promise;
});
transition.finished.then(() => {
setTransitionState('idle');
hasActiveTransition = false;
});
transition.ready.then(() => {
setTransitionState('animating');
}).catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
setTransitionState('skipped');
hasActiveTransition = false;
});
}, [transitionState]);
function startViewTransition(updateCallback?: TransitionFunction): PromiseLike<void> | void {
// Fallback: simply run the callback immediately if view transitions aren't supported.
if (!IS_VIEW_TRANSITION_SUPPORTED) {
if (updateCallback) updateCallback();
return;
}
domUpdaterFn.current = updateCallback;
setTransitionState('capturing-old');
hasActiveTransition = true;
}
return {
shouldApplyVtn: transitionState === 'capturing-old'
|| transitionState === 'capturing-new' || transitionState === 'animating',
transitionState,
startViewTransition,
};
}

View File

@ -1,6 +1,8 @@
import type { RefObject } from 'react';
import { useEffect } from '../lib/teact/teact';
import { hasActiveViewTransition } from './animations/useViewTransition';
const BACKDROP_CLASSNAME = 'backdrop';
// This effect implements closing menus by clicking outside of them
@ -20,7 +22,7 @@ export default function useVirtualBackdrop(
const handleEvent = (e: MouseEvent) => {
const container = containerRef.current;
const target = e.target as HTMLElement | null;
if (!container || !target || (ignoreRightClick && e.button === 2)) {
if (!container || !target || (ignoreRightClick && e.button === 2) || hasActiveViewTransition()) {
return;
}

View File

@ -1729,6 +1729,7 @@ payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer =
payments.getUniqueStarGift#a1974d72 slug:string = payments.UniqueStarGift;
payments.getSavedStarGifts#23830de9 flags:# exclude_unsaved:flags.0?true exclude_saved:flags.1?true exclude_unlimited:flags.2?true exclude_limited:flags.3?true exclude_unique:flags.4?true sort_by_value:flags.5?true peer:InputPeer offset:string limit:int = payments.SavedStarGifts;
payments.getStarGiftWithdrawalUrl#d06e93a8 stargift:InputSavedStarGift password:InputCheckPasswordSRP = payments.StarGiftWithdrawalUrl;
payments.toggleStarGiftsPinnedToTop#1513e7b0 peer:InputPeer stargift:Vector<InputSavedStarGift> = Bool;
phone.requestCall#a6c4600c flags:# video:flags.0?true user_id:InputUser conference_call:flags.1?InputGroupCall random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;

View File

@ -314,6 +314,7 @@
"payments.transferStarGift",
"payments.getUniqueStarGift",
"payments.getStarGiftWithdrawalUrl",
"payments.toggleStarGiftsPinnedToTop",
"langpack.getLangPack",
"langpack.getStrings",
"langpack.getLanguages",

File diff suppressed because one or more lines are too long

View File

@ -113,8 +113,8 @@ $icons-map: (
"enter": "\f14c",
"expand-modal": "\f14d",
"expand": "\f14e",
"eye-closed-outline": "\f14f",
"eye-closed": "\f150",
"eye-crossed-outline": "\f14f",
"eye-crossed": "\f150",
"eye-outline": "\f151",
"eye": "\f152",
"favorite-filled": "\f153",
@ -525,11 +525,11 @@ $icons-map: (
.icon-expand::before {
content: map.get($icons-map, "expand");
}
.icon-eye-closed-outline::before {
content: map.get($icons-map, "eye-closed-outline");
.icon-eye-crossed-outline::before {
content: map.get($icons-map, "eye-crossed-outline");
}
.icon-eye-closed::before {
content: map.get($icons-map, "eye-closed");
.icon-eye-crossed::before {
content: map.get($icons-map, "eye-crossed");
}
.icon-eye-outline::before {
content: map.get($icons-map, "eye-outline");

Binary file not shown.

Binary file not shown.

View File

@ -77,8 +77,8 @@ export type FontIconName =
| 'enter'
| 'expand-modal'
| 'expand'
| 'eye-closed-outline'
| 'eye-closed'
| 'eye-crossed-outline'
| 'eye-crossed'
| 'eye-outline'
| 'eye'
| 'favorite-filled'

View File

@ -1183,6 +1183,8 @@ export interface LangPair {
'GiftInfoConvertDescription2': undefined;
'GiftInfoSavedHide': undefined;
'GiftInfoSavedShow': undefined;
'GiftActionShow': undefined;
'GiftActionHide': undefined;
'GiftInfoTonLinkText': undefined;
'GiftInfoAvailability': undefined;
'GiftInfoFirstSale': undefined;

View File

@ -114,6 +114,7 @@ export const IS_MULTITAB_SUPPORTED = 'BroadcastChannel' in window;
export const IS_OPEN_IN_NEW_TAB_SUPPORTED = IS_MULTITAB_SUPPORTED && !(IS_PWA && IS_MOBILE);
export const IS_TRANSLATION_SUPPORTED = !IS_TEST;
export const IS_INTL_LIST_FORMAT_SUPPORTED = 'ListFormat' in Intl;
export const IS_VIEW_TRANSITION_SUPPORTED = 'ViewTransition' in window;
export const IS_BAD_URL_PARSER = new URL('tg://host').host !== 'host';