Unique Gift: Support withdrawal (#5534)

This commit is contained in:
zubiden 2025-01-27 23:51:10 +01:00 committed by Alexander Zinchuk
parent e40d669ea3
commit d5f214fa9a
77 changed files with 1121 additions and 662 deletions

View File

@ -19,7 +19,7 @@ import type {
import { numberToHexColor } from '../../../util/colors';
import { pick } from '../../../util/iteratees';
import { addDocumentToLocalDb } from '../helpers';
import { addDocumentToLocalDb } from '../helpers/localDb';
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messageContent';

View File

@ -25,7 +25,8 @@ import type {
import { pick, pickTruthy } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers';
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc';
import {
buildApiBotVerification, buildApiFormattedText, buildApiPhoto, buildApiUsernames, buildAvatarPhotoId,
} from './common';

View File

@ -8,7 +8,7 @@ import type {
} from '../../types';
import { numberToHexColor } from '../../../util/colors';
import { addDocumentToLocalDb } from '../helpers';
import { addDocumentToLocalDb } from '../helpers/localDb';
import { buildApiFormattedText } from './common';
import { getApiChatIdFromMtpPeer } from './peers';
import { buildStickerFromDocument } from './symbols';
@ -129,7 +129,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,
savedId, canExportAt,
} = userStarGift;
const inputGift: ApiInputSavedStarGift | undefined = savedId && peerId
@ -150,5 +150,6 @@ export function buildApiSavedStarGift(userStarGift: GramJs.SavedStarGift, peerId
transferStars: transferStars?.toJSNumber(),
inputGift,
savedId: savedId?.toString(),
canExportAt,
};
}

View File

@ -32,8 +32,9 @@ import { SUPPORTED_PHOTO_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES, VIDEO_WEB
import { generateWaveform } from '../../../util/generateWaveform';
import { pick } from '../../../util/iteratees';
import {
addMediaToLocalDb, addStoryToLocalDb, type MediaRepairContext, serializeBytes,
} from '../helpers';
addMediaToLocalDb, addStoryToLocalDb, type MediaRepairContext,
} from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc';
import {
buildApiFormattedText,
buildApiMessageEntity,

View File

@ -58,9 +58,8 @@ import { buildPeer } from '../gramjsBuilders';
import {
addPhotoToLocalDb,
type MediaRepairContext,
resolveMessageApiChatId,
serializeBytes,
} from '../helpers';
} from '../helpers/localDb';
import { resolveMessageApiChatId, serializeBytes } from '../helpers/misc';
import { buildApiCallDiscardReason } from './calls';
import {
buildApiFormattedText,

View File

@ -22,7 +22,7 @@ import {
buildCollectionByCallback, omit, omitUndefined, pick,
} from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { addUserToLocalDb } from '../helpers';
import { addUserToLocalDb } from '../helpers/localDb';
import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildMessageTextContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';

View File

@ -29,7 +29,7 @@ import type {
BoughtPaidMedia,
} from '../../types';
import { addWebDocumentToLocalDb } from '../helpers';
import { addWebDocumentToLocalDb } from '../helpers/localDb';
import { buildApiStarsSubscriptionPricing } from './chats';
import { buildApiMessageEntity } from './common';
import { buildApiStarGift } from './gifts';

View File

@ -37,7 +37,7 @@ import {
import { CHANNEL_ID_LENGTH, DEFAULT_STATUS_ICON_ID } from '../../../config';
import { pick } from '../../../util/iteratees';
import { deserializeBytes } from '../helpers';
import { deserializeBytes } from '../helpers/misc';
import localDb from '../localDb';
function checkIfChannelId(id: string) {

View File

@ -1,42 +1,11 @@
import { Api as GramJs } from '../../lib/gramjs';
import { Api as GramJs } from '../../../lib/gramjs';
import type { RepairInfo } from './localDb';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers';
import localDb from './localDb';
const LOG_BACKGROUND = '#111111DD';
const LOG_PREFIX_COLOR = '#E4D00A';
const LOG_SUFFIX = {
INVOKE: '#49DBF5',
BEACON: '#F549DB',
RESPONSE: '#6887F7',
CONNECTING: '#E4D00A',
CONNECTED: '#26D907',
'CONNECTING ERROR': '#D1191C',
'INVOKE ERROR': '#D1191C',
UPDATE: '#0DD151',
'UNEXPECTED UPDATE': '#9C9C9C',
'UNEXPECTED RESPONSE': '#D1191C',
};
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import localDb, { type RepairInfo } from '../localDb';
export type MessageRepairContext = Pick<GramJs.TypeMessage, 'peerId' | 'id'>;
export type MediaRepairContext = MessageRepairContext;
export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) {
if (!(mtpMessage instanceof GramJs.Message || mtpMessage instanceof GramJs.MessageService)) {
return undefined;
}
return getApiChatIdFromMtpPeer(mtpMessage.peerId);
}
export function isChatFolder(
filter?: GramJs.TypeDialogFilter,
): filter is GramJs.DialogFilter | GramJs.DialogFilterChatlist {
return filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist;
}
export function addMessageToLocalDb(message: GramJs.TypeMessage | GramJs.TypeSponsoredMessage) {
if (message instanceof GramJs.Message) {
if (message.media) addMediaToLocalDb(message.media, message);
@ -204,33 +173,3 @@ export function addUserToLocalDb(user: GramJs.User) {
export function addWebDocumentToLocalDb(webDocument: GramJs.TypeWebDocument) {
localDb.webDocuments[webDocument.url] = webDocument;
}
export function serializeBytes(value: Buffer) {
return String.fromCharCode(...value);
}
export function deserializeBytes(value: string) {
return Buffer.from(value, 'binary');
}
export function log(suffix: keyof typeof LOG_SUFFIX, ...data: any) {
/* eslint-disable max-len */
/* eslint-disable no-console */
const func = suffix === 'UNEXPECTED RESPONSE' ? console.error
: suffix === 'INVOKE ERROR' || suffix === 'UNEXPECTED UPDATE' ? console.warn : console.log;
/* eslint-enable no-console */
func(
`%cGramJS%c${suffix}`,
`color: ${LOG_PREFIX_COLOR}; background: ${LOG_BACKGROUND}; padding: 0.25rem; border-radius: 0.25rem;`,
`color: ${LOG_SUFFIX[suffix]}; background: ${LOG_BACKGROUND}; padding: 0.25rem; border-radius: 0.25rem; margin-left: 0.25rem;`,
...data,
);
/* eslint-enable max-len */
}
export function isResponseUpdate<T extends GramJs.AnyRequest>(result: T['__response']): result is GramJs.TypeUpdate {
return result instanceof GramJs.UpdatesTooLong || result instanceof GramJs.UpdateShortMessage
|| result instanceof GramJs.UpdateShortChatMessage || result instanceof GramJs.UpdateShort
|| result instanceof GramJs.UpdatesCombined || result instanceof GramJs.Updates
|| result instanceof GramJs.UpdateShortSentMessage;
}

View File

@ -0,0 +1,179 @@
import { Api as GramJs, errors } from '../../../lib/gramjs';
import type { RegularLangKey } from '../../../types/language';
import type { RegularLangFnParameters } from '../../../util/localization';
import { DEBUG } from '../../../config';
import {
DAY, getDays, getHours, getMinutes, HOUR, MINUTE,
} from '../../../util/dates/units';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
const LOG_BACKGROUND = '#111111DD';
const LOG_PREFIX_COLOR = '#E4D00A';
const LOG_SUFFIX = {
INVOKE: '#49DBF5',
BEACON: '#F549DB',
RESPONSE: '#6887F7',
CONNECTING: '#E4D00A',
CONNECTED: '#26D907',
'CONNECTING ERROR': '#D1191C',
'INVOKE ERROR': '#D1191C',
UPDATE: '#0DD151',
'UNEXPECTED UPDATE': '#9C9C9C',
'UNEXPECTED RESPONSE': '#D1191C',
};
const ERROR_KEYS: Record<string, RegularLangKey> = {
PHONE_NUMBER_INVALID: 'ErrorPhoneNumberInvalid',
PHONE_CODE_INVALID: 'ErrorCodeInvalid',
PASSWORD_HASH_INVALID: 'ErrorIncorrectPassword',
PHONE_PASSWORD_FLOOD: 'ErrorPasswordFlood',
PHONE_NUMBER_BANNED: 'ErrorPhoneBanned',
EMAIL_UNCONFIRMED: 'ErrorEmailUnconfirmed',
EMAIL_HASH_EXPIRED: 'ErrorEmailHashExpired',
NEW_SALT_INVALID: 'ErrorNewSaltInvalid',
SRP_PASSWORD_CHANGED: 'ErrorPasswordChanged',
CODE_INVALID: 'ErrorEmailCodeInvalid',
PASSWORD_MISSING: 'ErrorPasswordMissing',
};
export type MessageRepairContext = Pick<GramJs.TypeMessage, 'peerId' | 'id'>;
export type MediaRepairContext = MessageRepairContext;
export type WrappedError<T extends Error = Error> = {
messageKey: RegularLangFnParameters;
errorMessage?: string;
error: T;
};
export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) {
if (!(mtpMessage instanceof GramJs.Message || mtpMessage instanceof GramJs.MessageService)) {
return undefined;
}
return getApiChatIdFromMtpPeer(mtpMessage.peerId);
}
export function isChatFolder(
filter?: GramJs.TypeDialogFilter,
): filter is GramJs.DialogFilter | GramJs.DialogFilterChatlist {
return filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist;
}
export function serializeBytes(value: Buffer) {
return String.fromCharCode(...value);
}
export function deserializeBytes(value: string) {
return Buffer.from(value, 'binary');
}
export function log(suffix: keyof typeof LOG_SUFFIX, ...data: any) {
/* eslint-disable max-len */
/* eslint-disable no-console */
const func = suffix === 'UNEXPECTED RESPONSE' ? console.error
: suffix === 'INVOKE ERROR' || suffix === 'UNEXPECTED UPDATE' ? console.warn : console.log;
/* eslint-enable no-console */
func(
`%cGramJS%c${suffix}`,
`color: ${LOG_PREFIX_COLOR}; background: ${LOG_BACKGROUND}; padding: 0.25rem; border-radius: 0.25rem;`,
`color: ${LOG_SUFFIX[suffix]}; background: ${LOG_BACKGROUND}; padding: 0.25rem; border-radius: 0.25rem; margin-left: 0.25rem;`,
...data,
);
/* eslint-enable max-len */
}
export function isResponseUpdate<T extends GramJs.AnyRequest>(result: T['__response']): result is GramJs.TypeUpdate {
return result instanceof GramJs.UpdatesTooLong || result instanceof GramJs.UpdateShortMessage
|| result instanceof GramJs.UpdateShortChatMessage || result instanceof GramJs.UpdateShort
|| result instanceof GramJs.UpdatesCombined || result instanceof GramJs.Updates
|| result instanceof GramJs.UpdateShortSentMessage;
}
export function checkErrorType(error: unknown): error is Error {
if (!(error instanceof Error)) {
// eslint-disable-next-line no-console
if (DEBUG) console.warn('Unexpected error type', error);
return false;
}
return true;
}
export function wrapError<T extends Error>(error: T): WrappedError<T> {
let messageKey: RegularLangFnParameters | undefined;
const errorMessage = error instanceof errors.RPCError ? error.errorMessage : undefined;
if (error instanceof errors.FloodWaitError) {
messageKey = {
key: 'ErrorFloodTime',
variables: { time: formatWait(error.seconds) },
};
} else if (error instanceof errors.PasswordFreshError) {
messageKey = {
key: 'ErrorPasswordFresh',
variables: { time: formatWait(error.seconds) },
};
} else if (error instanceof errors.RPCError) {
messageKey = {
key: ERROR_KEYS[error.errorMessage],
};
}
if (!messageKey) {
if (error.message) {
messageKey = {
key: 'ErrorUnexpectedMessage',
variables: { error: error.message },
};
} else {
messageKey = {
key: 'ErrorUnexpected',
};
}
}
return {
messageKey,
errorMessage,
error,
};
}
function formatWait(seconds: number): RegularLangFnParameters {
if (seconds < MINUTE) {
return {
key: 'Seconds',
variables: { count: seconds },
options: { pluralValue: seconds },
};
}
if (seconds < HOUR) {
const minutes = getMinutes(seconds);
return {
key: 'Minutes',
variables: { count: minutes },
options: { pluralValue: minutes },
};
}
if (seconds < DAY) {
const hours = getHours(seconds);
return {
key: 'Hours',
variables: { count: hours },
options: { pluralValue: hours },
};
}
const days = getDays(seconds);
return {
key: 'Days',
variables: { count: days },
options: { pluralValue: days },
};
}

View File

@ -1,7 +1,3 @@
import { FloodWaitError, RPCError } from '../../../lib/gramjs/errors';
import type { RegularLangKey } from '../../../types/language';
import type { RegularLangFnParameters } from '../../../util/localization';
import type {
ApiUpdateAuthorizationState,
ApiUpdateAuthorizationStateType,
@ -9,17 +5,9 @@ import type {
ApiUserFullInfo,
} from '../../types';
import { DEBUG } from '../../../config';
import { wrapError } from '../helpers/misc';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
const ApiErrors: Record<string, RegularLangKey> = {
PHONE_NUMBER_INVALID: 'ErrorPhoneNumberInvalid',
PHONE_CODE_INVALID: 'ErrorCodeInvalid',
PASSWORD_HASH_INVALID: 'ErrorIncorrectPassword',
PHONE_PASSWORD_FLOOD: 'ErrorPasswordFlood',
PHONE_NUMBER_BANNED: 'ErrorPhoneBanned',
};
const authController: {
resolve?: Function;
reject?: Function;
@ -87,36 +75,7 @@ export function onRequestQrCode(qrCode: { token: Buffer; expires: number }) {
}
export function onAuthError(err: Error) {
let messageKey: RegularLangFnParameters | undefined;
if (err instanceof FloodWaitError) {
const hours = Math.ceil(Number(err.seconds) / 60 / 60);
messageKey = {
key: 'ErrorFlood',
variables: { hour: hours },
options: { pluralValue: hours },
};
} else if (err instanceof RPCError) {
messageKey = {
key: ApiErrors[err.errorMessage],
};
} else if (err.message) {
messageKey = {
key: 'ErrorUnexpectedMessage',
variables: { error: err.message },
};
}
if (!messageKey) {
messageKey = {
key: 'ErrorUnexpected',
};
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
}
const { messageKey } = wrapError(err);
sendApiUpdate({
'@type': 'updateAuthorizationError',

View File

@ -39,8 +39,8 @@ import {
addPhotoToLocalDb,
addUserToLocalDb,
addWebDocumentToLocalDb,
deserializeBytes,
} from '../helpers';
} from '../helpers/localDb';
import { deserializeBytes } from '../helpers/misc';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { invokeRequest } from './client';

View File

@ -69,8 +69,8 @@ import {
} from '../gramjsBuilders';
import {
addPhotoToLocalDb,
isChatFolder,
} from '../helpers';
} from '../helpers/localDb';
import { isChatFolder } from '../helpers/misc';
import { scheduleMutedChatUpdate } from '../scheduleUnmute';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import {

View File

@ -3,7 +3,7 @@ import {
sessions,
type Update,
} from '../../../lib/gramjs';
import type { TwoFaParams, TwoFaPasswordParams } from '../../../lib/gramjs/client/2fa';
import type { TwoFaParams } from '../../../lib/gramjs/client/2fa';
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
import { RPCError } from '../../../lib/gramjs/errors';
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
@ -30,8 +30,11 @@ import { buildApiStory } from '../apiBuilders/stories';
import { buildApiUser, buildApiUserFullInfo } from '../apiBuilders/users';
import { buildInputPeerFromLocalDb, getEntityTypeById } from '../gramjsBuilders';
import {
addStoryToLocalDb, addUserToLocalDb, isResponseUpdate, log,
} from '../helpers';
addStoryToLocalDb, addUserToLocalDb,
} from '../helpers/localDb';
import {
isResponseUpdate, log,
} from '../helpers/misc';
import localDb, { clearLocalDb, type RepairInfo } from '../localDb';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { processAndUpdateEntities, processMessageAndUpdateThreadInfo } from '../updates/entityProcessor';
@ -376,8 +379,8 @@ export function getTmpPassword(currentPassword: string, ttl?: number) {
return client.getTmpPassword(currentPassword, ttl);
}
export function getCurrentPassword(params: TwoFaPasswordParams) {
return client.getCurrentPassword(params);
export function getCurrentPassword(currentPassword?: string) {
return client.getCurrentPassword(currentPassword);
}
export function abortChatRequests(params: { chatId: string; threadId?: ThreadId }) {

View File

@ -89,7 +89,7 @@ import {
import {
deserializeBytes,
resolveMessageApiChatId,
} from '../helpers';
} from '../helpers/misc';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { processMessageAndUpdateThreadInfo } from '../updates/entityProcessor';
import { processAffectedHistory, updateChannelState } from '../updates/updateManager';

View File

@ -45,13 +45,15 @@ import {
buildShippingInfo,
} from '../gramjsBuilders';
import {
checkErrorType,
deserializeBytes,
serializeBytes,
} from '../helpers';
wrapError,
} from '../helpers/misc';
import localDb from '../localDb';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { handleGramJsUpdate, invokeRequest } from './client';
import { getTemporaryPaymentPassword } from './twoFaSettings';
import { getPassword, getTemporaryPaymentPassword } from './twoFaSettings';
export async function validateRequestedInfo({
inputInvoice,
@ -689,3 +691,42 @@ export function upgradeGift({
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

@ -43,7 +43,7 @@ import {
buildInputPrivacyKey,
buildInputPrivacyRules,
} from '../gramjsBuilders';
import { addPhotoToLocalDb } from '../helpers';
import { addPhotoToLocalDb } from '../helpers/localDb';
import localDb from '../localDb';
import { getClient, invokeRequest, uploadFile } from './client';

View File

@ -16,8 +16,9 @@ import {
buildStoryPublicForwards,
} from '../apiBuilders/statistics';
import { buildInputEntity, buildInputPeer } from '../gramjsBuilders';
import { checkErrorType, wrapError } from '../helpers/misc';
import { invokeRequest } from './client';
import { getPassword, onPasswordError } from './twoFaSettings';
import { getPassword } from './twoFaSettings';
export async function fetchChannelStatistics({
chat, dcId,
@ -216,7 +217,7 @@ export async function fetchStoryPublicForwards({
};
}
export async function loadMonetizationRevenueWithdrawalUrl({
export async function fetchMonetizationRevenueWithdrawalUrl({
peer, currentPassword,
}: {
peer: ApiPeer;
@ -225,10 +226,14 @@ export async function loadMonetizationRevenueWithdrawalUrl({
try {
const password = await getPassword(currentPassword);
if (!password || 'error' in password) {
if (!password) {
return undefined;
}
if ('error' in password) {
return password;
}
const result = await invokeRequest(new GramJs.stats.GetBroadcastRevenueWithdrawalUrl({
peer: buildInputPeer(peer.id, peer.accessHash),
password,
@ -240,9 +245,10 @@ export async function loadMonetizationRevenueWithdrawalUrl({
return undefined;
}
return result;
} catch (err: any) {
onPasswordError(err);
return { url: result.url };
} catch (err: unknown) {
if (!checkErrorType(err)) return undefined;
return wrapError(err);
}
return undefined;

View File

@ -26,7 +26,8 @@ import {
buildInputPrivacyRules,
buildInputReaction,
} from '../gramjsBuilders';
import { addStoryToLocalDb, deserializeBytes } from '../helpers';
import { addStoryToLocalDb } from '../helpers/localDb';
import { deserializeBytes } from '../helpers/misc';
import { invokeRequest } from './client';
export async function fetchAllStories({

View File

@ -1,21 +1,11 @@
import { Api as GramJs, errors } from '../../../lib/gramjs';
import { Api as GramJs } from '../../../lib/gramjs';
import { DEBUG } from '../../../config';
import { checkErrorType, wrapError } from '../helpers/misc';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import {
getCurrentPassword, getTmpPassword, invokeRequest, updateTwoFaSettings,
} from './client';
const ApiErrors: { [k: string]: string } = {
EMAIL_UNCONFIRMED: 'Email unconfirmed',
EMAIL_HASH_EXPIRED: 'Email hash expired',
NEW_SALT_INVALID: 'The new salt is invalid',
NEW_SETTINGS_INVALID: 'The new password settings are invalid',
CODE_INVALID: 'Invalid Code',
PASSWORD_HASH_INVALID: 'Invalid Password',
PASSWORD_MISSING: 'You must enable 2FA before executing this operation',
};
const emailCodeController: {
resolve?: Function;
reject?: Function;
@ -45,18 +35,22 @@ function onRequestEmailCode(length: number) {
}
export function getTemporaryPaymentPassword(password: string, ttl?: number) {
return getTmpPassword(password, ttl);
try {
return getTmpPassword(password, ttl);
} catch (err: unknown) {
if (!checkErrorType(err)) return undefined;
return Promise.resolve(wrapError(err));
}
}
export function getPassword(password: string) {
try {
return getCurrentPassword({
currentPassword: password,
onPasswordCodeError: onPasswordError,
});
} catch (err: any) {
onPasswordError(err);
return undefined;
return getCurrentPassword(password);
} catch (err: unknown) {
if (!checkErrorType(err)) return undefined;
return Promise.resolve(wrapError(err));
}
}
@ -126,51 +120,10 @@ export function provideRecoveryEmailCode(code: string) {
}
function onError(err: Error) {
let message: string;
if (err instanceof errors.FloodWaitError) {
const hours = Math.ceil(Number(err.seconds) / 60 / 60);
message = `Too many attempts. Try again in ${hours > 1 ? `${hours} hours` : 'an hour'}`;
} else {
message = ApiErrors[err.message];
}
if (!message) {
message = 'Unexpected Error';
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
}
const wrappedError = wrapError(err);
sendApiUpdate({
'@type': 'updateTwoFaError',
message,
});
}
export function onPasswordError(err: Error) {
let message: string;
if (err instanceof errors.PasswordModifiedError) {
const hours = Math.ceil(Number(err.seconds) / 60 / 60);
message = `Too many attempts. Try again in ${hours > 1 ? `${hours} hours` : 'an hour'}`;
} else {
message = ApiErrors[err.message];
}
if (!message) {
message = 'Unexpected Error';
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
}
sendApiUpdate({
'@type': 'updatePasswordError',
error: message,
messageKey: wrappedError.messageKey,
});
}

View File

@ -17,7 +17,7 @@ import {
buildMtpPeerId,
getEntityTypeById,
} from '../gramjsBuilders';
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers';
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb';
import localDb from '../localDb';
import { sendApiUpdate } from '../updates/apiUpdateEmitter';
import { invokeRequest } from './client';

View File

@ -9,7 +9,7 @@ import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { buildPollFromMedia } from '../apiBuilders/messageContent';
import { buildApiThreadInfoFromMessage } from '../apiBuilders/messages';
import { buildApiUser } from '../apiBuilders/users';
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers';
import { addChatToLocalDb, addMessageToLocalDb, addUserToLocalDb } from '../helpers/localDb';
import { sendImmediateApiUpdate } from './apiUpdateEmitter';
const TYPE_USER = new Set(['User', 'UserEmpty']);

View File

@ -71,11 +71,13 @@ import {
import {
addPhotoToLocalDb,
addStoryToLocalDb,
} from '../helpers/localDb';
import {
isChatFolder,
log,
resolveMessageApiChatId,
serializeBytes,
} from '../helpers';
} from '../helpers/misc';
import localDb from '../localDb';
import { scheduleMutedChatUpdate, scheduleMutedTopicUpdate } from '../scheduleUnmute';
import { sendApiUpdate } from './apiUpdateEmitter';

View File

@ -7,7 +7,7 @@ import type { OriginMessageEvent, WorkerPayload } from './types';
import { DEBUG } from '../../../config';
import { DEBUG_LEVELS } from '../../../util/debugConsole';
import { throttleWithTickEnd } from '../../../util/schedulers';
import { log } from '../helpers';
import { log } from '../helpers/misc';
import { callApi, cancelApiProgress, initApi } from '../methods/init';
declare const self: WorkerGlobalScope;

View File

@ -271,6 +271,7 @@ export interface ApiSavedStarGift {
canUpgrade?: true;
alreadyPaidUpgradeStars?: number;
transferStars?: number;
canExportAt?: number;
isConverted?: boolean; // Local field, used for Action Message
upgradeMsgId?: number; // Local field, used for Action Message
}

View File

@ -491,12 +491,7 @@ export type ApiUpdateSavedGifs = {
export type ApiUpdateTwoFaError = {
'@type': 'updateTwoFaError';
message: string;
};
export type ApiUpdatePasswordError = {
'@type': 'updatePasswordError';
error: string;
messageKey: RegularLangFnParameters;
};
export type ApiUpdateNotifySettings = {
@ -819,7 +814,7 @@ export type ApiUpdate = (
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path d="M2.344 7.91c-.495 0-.639.78-.188.982l13.346 5.882c.33.149.71.149 1.041 0l13.346-5.882c.45-.203.307-.983-.188-.983zm-.64 3.034c-.568-.028-.882.494-.597.963l1.133 1.756.006.01.197.305.233.36L13.514 31.13c.514.75 1.411.586 1.425-.315l.028-13.37c0-.197-.02-.35-.086-.477a.75.75 0 0 0-.32-.303l-.067-.035-12.361-5.57a1.15 1.15 0 0 0-.43-.116m28.593 0q-.198.009-.43.115l-12.361 5.57c-.39.195-.473.415-.473.815l.028 13.371c.014.901.91 1.065 1.425.315l12.407-19.223c.285-.469-.029-.991-.596-.963" style="fill:#000;fill-opacity:1;stroke-width:0;stroke-dasharray:none"/></svg>

After

Width:  |  Height:  |  Size: 650 B

View File

@ -668,16 +668,23 @@
"FavoriteStickers" = "Favorites";
"PremiumStickers" = "Premium Stickers";
"GroupStickers" = "Group Stickers";
"ErrorSendRestrictedStickersAll" = "Sorry, sending stickers is not allowed in this group.";
"ErrorPhoneNumberInvalid" = "Invalid phone number, please try again.";
"ErrorCodeInvalid" = "Invalid code, please try again.";
"ErrorIncorrectPassword" = "Invalid password, please try again.";
"ErrorPasswordFlood" = "Too many attempts, please try again later.";
"ErrorPhoneBanned" = "This phone number is banned.";
"ErrorFlood_one" = "Too many attempts, please try again in {hour} hour";
"ErrorFlood_other" = "Too many attempts, please try again in {hour} hours";
"ErrorSendRestrictedStickersAll" = "Sorry, sending stickers is not allowed in this group";
"ErrorPhoneNumberInvalid" = "Invalid phone number, please try again";
"ErrorCodeInvalid" = "Invalid code, please try again";
"ErrorEmailCodeInvalid" = "Invalid code, please try again";
"ErrorIncorrectPassword" = "Invalid password, please try again";
"ErrorPasswordFlood" = "Too many attempts, please try again later";
"ErrorPhoneBanned" = "This phone number is banned";
"ErrorFloodTime" = "Too many attempts, please try again in {time}";
"ErrorPasswordFresh" = "The password was modified less than 24 hours ago, try again in {time}";
"ErrorUnexpected" = "Unexpected error";
"ErrorUnexpectedMessage" = "Unexpected error: {error}";
"ErrorEmailUnconfirmed" = "Email not confirmed";
"ErrorEmailHashExpired" = "Failed to confirm email, please try again";
"ErrorNewSaltInvalid" = "Error validating password, please try again";
"ErrorPasswordChanged" = "Password has been changed, please try again";
"ErrorPasswordMissing" = "You must set 2FA password to use this feature";
"ErrorUnspecified" = "Error";
"NoStickers" = "No stickers yet";
"ClearRecentEmoji" = "Clear recent emoji?";
"TextFormatAddLinkTitle" = "Add Link";
@ -1429,6 +1436,7 @@
"GiftInfoViewUpgraded" = "View Upgraded Gift";
"GiftInfoUpgradeBadge" = "upgrade";
"GiftInfoUpgradeForFree" = "Upgrade For Free";
"GiftInfoWithdraw" = "Withdraw";
"GiftUpgradeUniqueTitle" = "Unique";
"GiftUpgradeUniqueDescription" = "Turn your gift into a unique collectible that you can transfer or auction.";
"GiftUpgradeTransferableTitle" = "Transferable";
@ -1447,6 +1455,11 @@
"GiftMakeUniqueDescription" = "Enable this to let {user} turn your gift into a unique collectible. {link}";
"GiftMakeUniqueDescriptionChannel" = "Enable this to let admins of {peer} to turn your gift into a unique collectible. {link}";
"GiftMakeUniqueLink" = "Learn More >";
"GiftWithdrawTitle" = "Manage with Fragment";
"GiftWithdrawDescription" = "You can use Fragment, a third-party service, to transfer **{gift}** to the TON blockchain. After that, you can manage it as an NFT with any TON wallet application.\n\nYou can also move such NFTs back to your Telegram account via Fragment.";
"GiftWithdrawSubmit" = "Open Fragment";
"GiftWithdrawWait_one" = "In {days} day, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction.";
"GiftWithdrawWait_other" = "In {days} days, you'll be able to send this collectible to any TON blockchain address outside Telegram for sale or auction.";
"StarsAmount" = "⭐️{amount}";
"StarsAmountText_one" = "{amount} Star";
"StarsAmountText_other" = "{amount} Stars";
@ -1586,3 +1599,6 @@
"ViewButtonGiftUnique" = "VIEW COLLECTIBLE";
"AuthContinueOnThisLanguage" = "Continue in English";
"Share" = "Share";
"CheckPasswordTitle" = "Enter Password";
"CheckPasswordPlaceholder" = "Password";
"CheckPasswordDescription" = "Please enter your password to continue.";

View File

@ -28,7 +28,7 @@ const INITIAL_KEYS: LangKey[] = [
'ErrorIncorrectPassword',
'ErrorPasswordFlood',
'ErrorPhoneBanned',
'ErrorFlood',
'ErrorFloodTime',
'ErrorUnexpected',
'ErrorUnexpectedMessage',
];

View File

@ -31,10 +31,7 @@ export default {
"ErrorIncorrectPassword": "Invalid password, please try again.",
"ErrorPasswordFlood": "Too many attempts, please try again later.",
"ErrorPhoneBanned": "This phone number is banned.",
"ErrorFlood": {
"one": "Too many attempts, please try again in {hour} hour",
"other": "Too many attempts, please try again in {hour} hours"
},
"ErrorFloodTime": "Too many attempts, please try again in {time}.",
"ErrorUnexpected": "Unexpected error",
"ErrorUnexpectedMessage": "Unexpected error: {error}"
} as Record<LangKey, LangPackStringValue>;

View File

@ -9,3 +9,4 @@ 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 GiftWithdrawModal } from '../components/modals/gift/fragment/GiftWithdrawModal';

View File

@ -8,8 +8,8 @@ import { Bundles } from '../../util/moduleLoader';
import useModuleLoader from '../../hooks/useModuleLoader';
const VerificationMonetizationModalAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const VerificationMonetizationModal = useModuleLoader(Bundles.Extra, 'VerificationMonetizationModal', !isOpen);
const { modal } = props;
const VerificationMonetizationModal = useModuleLoader(Bundles.Extra, 'VerificationMonetizationModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return VerificationMonetizationModal ? <VerificationMonetizationModal {...props} /> : undefined;

View File

@ -1,14 +1,16 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo,
useState,
} from '../../lib/teact/teact';
import { getActions } from '../../global';
import { getActions, withGlobal } from '../../global';
import type { TabState } from '../../global/types';
import buildClassName from '../../util/buildClassName';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Modal from '../ui/Modal';
import PasswordForm from './PasswordForm';
@ -16,62 +18,63 @@ import PasswordForm from './PasswordForm';
import styles from './VerificationMonetizationModal.module.scss';
export type OwnProps = {
isOpen: boolean;
onClose: NoneToVoidFunction;
chatId: string;
passwordHint?: string;
error?: string;
isLoading?: boolean;
modal: TabState['monetizationVerificationModal'];
};
const VerificationMonetizationModal: FC<OwnProps> = ({
isOpen,
chatId,
onClose,
type StateProps = {
passwordHint?: string;
};
const VerificationMonetizationModal = ({
modal,
passwordHint,
error,
isLoading,
}) => {
}: OwnProps & StateProps) => {
const {
clearMonetizationInfo, loadMonetizationRevenueWithdrawalUrl,
closeMonetizationVerificationModal, clearMonetizationVerificationError, processMonetizationRevenueWithdrawalUrl,
} = getActions();
const lang = useOldLang();
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const lang = useLang();
const [shouldShowPassword, setShouldShowPassword] = useState(false);
const handleSubmit = useLastCallback((password: string) => {
loadMonetizationRevenueWithdrawalUrl({
peerId: chatId,
if (!renderingModal) return;
processMonetizationRevenueWithdrawalUrl({
peerId: renderingModal.chatId,
currentPassword: password,
onSuccess: () => {
onClose();
},
});
});
const handleClearError = useLastCallback(() => {
clearMonetizationInfo();
clearMonetizationVerificationError();
});
const handleClose = useLastCallback(() => {
closeMonetizationVerificationModal();
});
return (
<Modal
isOpen={isOpen}
hasCloseButton
title={lang('EnterPassword')}
title={lang('CheckPasswordTitle')}
className={styles.root}
contentClassName={styles.content}
onClose={onClose}
onClose={handleClose}
>
<div className={buildClassName(styles.content, 'settings-content password-form custom-scroll')}>
<div className="settings-item pt-0">
<PasswordForm
shouldShowSubmit
placeholder={lang('Password')}
error={error && lang(error)}
description={lang('Channel.OwnershipTransfer.EnterPasswordText')}
placeholder={lang('CheckPasswordPlaceholder')}
error={renderingModal?.errorKey && lang.withRegular(renderingModal.errorKey)}
description={lang('CheckPasswordDescription')}
clearError={handleClearError}
isLoading={isLoading}
isLoading={renderingModal?.isLoading}
hint={passwordHint}
isPasswordVisible={shouldShowPassword}
shouldResetValue={isOpen}
@ -84,4 +87,16 @@ const VerificationMonetizationModal: FC<OwnProps> = ({
);
};
export default memo(VerificationMonetizationModal);
export default memo(withGlobal(
(global): StateProps => {
const {
twoFaSettings: {
hint: passwordHint,
},
} = global;
return {
passwordHint,
};
},
)(VerificationMonetizationModal));

View File

@ -6,6 +6,7 @@ import type { GlobalState } from '../../../../global/types';
import type { TwoFaDispatch, TwoFaState } from '../../../../hooks/reducers/useTwoFaReducer';
import { SettingsScreens } from '../../../../types';
import useLang from '../../../../hooks/useLang';
import useOldLang from '../../../../hooks/useOldLang';
import SettingsTwoFaPassword from '../SettingsPasswordForm';
@ -33,7 +34,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
state,
hint,
isLoading,
error,
errorKey,
waitingEmailCodeLength,
dispatch,
isActive,
@ -49,6 +50,9 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
clearPassword,
} = getActions();
const lang = useLang();
const oldLang = useOldLang();
useEffect(() => {
if (waitingEmailCodeLength) {
if (currentScreen === SettingsScreens.TwoFaNewPasswordEmail) {
@ -153,8 +157,6 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
provideTwoFaEmailCode({ code });
}, [provideTwoFaEmailCode]);
const lang = useOldLang();
switch (currentScreen) {
case SettingsScreens.TwoFaDisabled:
return (
@ -175,8 +177,8 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
case SettingsScreens.TwoFaNewPassword:
return (
<SettingsTwoFaPassword
placeholder={lang('PleaseEnterPassword')}
submitLabel={lang('Continue')}
placeholder={oldLang('PleaseEnterPassword')}
submitLabel={oldLang('Continue')}
onSubmit={handleNewPassword}
isActive={isActive || [
SettingsScreens.TwoFaNewPasswordConfirm,
@ -193,8 +195,8 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaPassword
expectedPassword={state.password}
placeholder={lang('PleaseReEnterPassword')}
submitLabel={lang('Continue')}
placeholder={oldLang('PleaseReEnterPassword')}
submitLabel={oldLang('Continue')}
onSubmit={handleNewPasswordConfirm}
isActive={isActive || [
SettingsScreens.TwoFaNewPasswordHint,
@ -210,7 +212,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaSkippableForm
icon="hint"
placeholder={lang('PasswordHintPlaceholder')}
placeholder={oldLang('PasswordHintPlaceholder')}
onSubmit={handleNewPasswordHint}
isActive={isActive || [
SettingsScreens.TwoFaNewPasswordEmail,
@ -227,9 +229,9 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
icon="email"
type="email"
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
placeholder={lang('RecoveryEmailTitle')}
placeholder={oldLang('RecoveryEmailTitle')}
shouldConfirm
onSubmit={handleNewPasswordEmail}
isActive={isActive || [
@ -244,7 +246,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaEmailCode
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
onSubmit={handleEmailCode}
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
@ -284,7 +286,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaPassword
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
hint={hint}
onSubmit={handleChangePasswordCurrent}
@ -301,7 +303,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
case SettingsScreens.TwoFaChangePasswordNew:
return (
<SettingsTwoFaPassword
placeholder={lang('PleaseEnterNewFirstPassword')}
placeholder={oldLang('PleaseEnterNewFirstPassword')}
onSubmit={handleChangePasswordNew}
isActive={isActive || [
SettingsScreens.TwoFaChangePasswordConfirm,
@ -316,7 +318,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaPassword
expectedPassword={state.password}
placeholder={lang('PleaseReEnterPassword')}
placeholder={oldLang('PleaseReEnterPassword')}
onSubmit={handleChangePasswordConfirm}
isActive={isActive || [
SettingsScreens.TwoFaChangePasswordHint,
@ -330,10 +332,10 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaSkippableForm
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
icon="hint"
placeholder={lang('PasswordHintPlaceholder')}
placeholder={oldLang('PasswordHintPlaceholder')}
onSubmit={handleChangePasswordHint}
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}
onReset={onReset}
@ -344,7 +346,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaPassword
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
hint={hint}
onSubmit={handleTurnOff}
@ -357,7 +359,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaPassword
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
hint={hint}
onSubmit={handleRecoveryEmailCurrentPassword}
@ -375,7 +377,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
<SettingsTwoFaSkippableForm
icon="email"
type="email"
placeholder={lang('RecoveryEmailTitle')}
placeholder={oldLang('RecoveryEmailTitle')}
onSubmit={handleRecoveryEmail}
isActive={isActive || [
SettingsScreens.TwoFaRecoveryEmailCode,
@ -389,7 +391,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps> = ({
return (
<SettingsTwoFaEmailCode
isLoading={isLoading}
error={error}
error={errorKey && lang.withRegular(errorKey)}
clearError={clearTwoFaError}
onSubmit={handleEmailCode}
isActive={isActive || shownScreen === SettingsScreens.TwoFaCongratulations}

View File

@ -245,6 +245,7 @@ const Main = ({
loadAvailableEffects,
loadTopBotApps,
loadPaidReactionPrivacy,
loadPasswordInfo,
} = getActions();
if (DEBUG && !DEBUG_isLogged) {
@ -333,6 +334,7 @@ const Main = ({
loadAuthorizations();
loadTopBotApps();
loadPaidReactionPrivacy();
loadPasswordInfo();
}
}, [isMasterTab, isSynced]);

View File

@ -6,6 +6,7 @@ import type { TabState } from '../../global/types';
import { selectTabState } from '../../global/selectors';
import { pick } from '../../util/iteratees';
import VerificationMonetizationModal from '../common/VerificationMonetizationModal.async';
import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async';
import AboutAdsModal from './aboutAds/AboutAdsModal.async';
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
@ -14,6 +15,7 @@ import ChatInviteModal from './chatInvite/ChatInviteModal.async';
import ChatlistModal from './chatlist/ChatlistModal.async';
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import EmojiStatusAccessModal from './emojiStatusAccess/EmojiStatusAccessModal.async';
import GiftWithdrawModal from './gift/fragment/GiftWithdrawModal.async';
import PremiumGiftModal from './gift/GiftModal.async';
import GiftInfoModal from './gift/info/GiftInfoModal.async';
import GiftRecipientPicker from './gift/recipient/GiftRecipientPicker.async';
@ -65,7 +67,9 @@ type ModalKey = keyof Pick<TabState,
'emojiStatusAccessModal' |
'locationAccessModal' |
'aboutAdsModal' |
'giftUpgradeModal'
'giftUpgradeModal' |
'monetizationVerificationModal' |
'giftWithdrawModal'
>;
type StateProps = {
@ -109,6 +113,8 @@ const MODALS: ModalRegistry = {
locationAccessModal: LocationAccessModal,
aboutAdsModal: AboutAdsModal,
giftUpgradeModal: GiftUpgradeModal,
monetizationVerificationModal: VerificationMonetizationModal,
giftWithdrawModal: GiftWithdrawModal,
};
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

@ -0,0 +1,18 @@
import type { FC } from '../../../../lib/teact/teact';
import React from '../../../../lib/teact/teact';
import type { OwnProps } from './GiftWithdrawModal';
import { Bundles } from '../../../../util/moduleLoader';
import useModuleLoader from '../../../../hooks/useModuleLoader';
const GiftWithdrawModalAsync: FC<OwnProps> = (props) => {
const { modal } = props;
const GiftWithdrawModal = useModuleLoader(Bundles.Stars, 'GiftWithdrawModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading
return GiftWithdrawModal ? <GiftWithdrawModal {...props} /> : undefined;
};
export default GiftWithdrawModalAsync;

View File

@ -0,0 +1,39 @@
.header {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
.giftPreview {
width: 5.625rem;
height: 5.625rem;
position: relative;
display: grid;
place-items: center;
overflow: hidden;
border-radius: 0.625rem;
}
.backdrop {
position: absolute;
inset: 0;
}
.description {
text-wrap: pretty;
margin-bottom: 1.5rem;
}
.arrow {
font-size: 3rem;
color: var(--color-text-secondary);
}
.noPassword {
font-weight: var(--font-weight-semibold);
}

View File

@ -0,0 +1,157 @@
import React, {
memo,
useState,
} from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { ApiStarGiftUnique } from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import type { CustomPeer } from '../../../../types';
import { getDays } from '../../../../util/dates/units';
import { getServerTime } from '../../../../util/serverTime';
import { getGiftAttributes } from '../../../common/helpers/gifts';
import { REM } from '../../../common/helpers/mediaDimensions';
import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import Icon from '../../../common/icons/Icon';
import PasswordForm from '../../../common/PasswordForm';
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
import Modal from '../../../ui/Modal';
import styles from './GiftWithdrawModal.module.scss';
export type OwnProps = {
modal: TabState['giftWithdrawModal'];
};
type StateProps = {
hasPassword?: boolean;
passwordHint?: string;
};
const FRAGMENT_PEER: CustomPeer = {
isCustomPeer: true,
avatarIcon: 'fragment',
title: '',
customPeerAvatarColor: '#000000',
};
const GIFT_STICKER_SIZE = 4.5 * REM;
const GiftWithdrawModal = ({ modal, hasPassword, passwordHint }: OwnProps & StateProps) => {
const { closeGiftWithdrawModal, clearGiftWithdrawError, processStarGiftWithdrawal } = getActions();
const isOpen = Boolean(modal);
const [shouldShowPassword, setShouldShowPassword] = useState(false);
const lang = useLang();
const renderingModal = useCurrentOrPrev(modal);
const gift = renderingModal?.gift?.gift as ApiStarGiftUnique;
const giftAttributes = gift && getGiftAttributes(gift);
const exportDelay = renderingModal?.gift?.canExportAt
? Math.max(renderingModal.gift.canExportAt - getServerTime(), 0) : undefined;
const handleClose = useLastCallback(() => {
closeGiftWithdrawModal();
});
const handleSubmit = useLastCallback((password: string) => {
processStarGiftWithdrawal({
gift: renderingModal!.gift.inputGift!,
password,
});
});
return (
<Modal
isOpen={isOpen}
title={lang('GiftWithdrawTitle')}
hasCloseButton
isSlim
onClose={handleClose}
>
{giftAttributes && (
<>
<div className={styles.header}>
<div className={styles.giftPreview}>
<RadialPatternBackground
className={styles.backdrop}
backgroundColors={[giftAttributes.backdrop!.centerColor, giftAttributes.backdrop!.edgeColor]}
patternColor={giftAttributes.backdrop?.patternColor}
patternIcon={giftAttributes.pattern?.sticker}
/>
<AnimatedIconFromSticker
className={styles.sticker}
size={GIFT_STICKER_SIZE}
sticker={giftAttributes.model?.sticker}
/>
</div>
<Icon name="next" className={styles.arrow} />
<Avatar
peer={FRAGMENT_PEER}
size="giant"
className={styles.avatar}
/>
</div>
<p className={styles.description}>
{lang('GiftWithdrawDescription', {
gift: `${gift.title} #${gift.number}`,
}, {
withNodes: true,
withMarkdown: true,
renderTextFilters: ['br'],
})}
</p>
</>
)}
{Boolean(exportDelay) && (
<p className={styles.exportHint}>
{lang('GiftWithdrawWait', { days: getDays(exportDelay) }, { pluralValue: getDays(exportDelay) })}
</p>
)}
{!hasPassword && <span className={styles.noPassword}>{lang('ErrorPasswordMissing')}</span>}
{hasPassword && !exportDelay && (
<PasswordForm
shouldShowSubmit
placeholder={lang('CheckPasswordPlaceholder')}
error={renderingModal?.errorKey && lang.withRegular(renderingModal?.errorKey)}
description={lang('CheckPasswordDescription')}
clearError={clearGiftWithdrawError}
isLoading={renderingModal?.isLoading}
hint={passwordHint}
isPasswordVisible={shouldShowPassword}
shouldResetValue={isOpen}
onChangePasswordVisibility={setShouldShowPassword}
submitLabel={lang('GiftWithdrawSubmit')}
onSubmit={handleSubmit}
/>
)}
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
settings: {
byKey: {
hasPassword,
},
},
twoFaSettings: {
hint: passwordHint,
},
} = global;
return {
hasPassword,
passwordHint,
};
},
)(GiftWithdrawModal));

View File

@ -72,6 +72,7 @@ const GiftInfoModal = ({
openGiftUpgradeModal,
showNotification,
openChatWithDraft,
openGiftWithdrawModal,
} = getActions();
const [isConvertConfirmOpen, openConvertConfirm, closeConvertConfirm] = useFlag();
@ -127,6 +128,11 @@ const GiftInfoModal = ({
handleClose();
});
const handleWithdraw = useLastCallback(() => {
if (savedGift?.gift.type !== 'starGiftUnique') return;
openGiftWithdrawModal({ gift: savedGift });
});
const handleFocusUpgraded = useLastCallback(() => {
if (!savedGift?.upgradeMsgId || !renderingTargetPeer) return;
const { upgradeMsgId } = savedGift;
@ -291,6 +297,11 @@ const GiftInfoModal = ({
>
{lang('Share')}
</MenuItem>
{canUpdate && isUniqueGift && (
<MenuItem icon="diamond" onClick={handleWithdraw}>
{lang('GiftInfoWithdraw')}
</MenuItem>
)}
</DropdownMenu>
);

View File

@ -4,9 +4,11 @@ import { getActions, withGlobal } from '../../global';
import type { ApiPaymentCredentials } from '../../api/types';
import type { FormState } from '../../hooks/reducers/usePaymentReducer';
import type { RegularLangFnParameters } from '../../util/localization';
import { selectTabState } from '../../global/selectors';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import PasswordForm from '../common/PasswordForm';
@ -20,14 +22,14 @@ interface OwnProps {
}
interface StateProps {
error?: string;
errorKey?: RegularLangFnParameters;
passwordHint?: string;
savedCredentials?: ApiPaymentCredentials[];
}
const PasswordConfirm: FC<OwnProps & StateProps> = ({
isActive,
error,
errorKey,
state,
savedCredentials,
passwordHint,
@ -35,7 +37,9 @@ const PasswordConfirm: FC<OwnProps & StateProps> = ({
}) => {
const { clearPaymentError } = getActions();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const [shouldShowPassword, setShouldShowPassword] = useState(false);
const cardName = useMemo(() => {
return savedCredentials?.length && state.savedCredentialId
@ -48,10 +52,10 @@ const PasswordConfirm: FC<OwnProps & StateProps> = ({
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
<PasswordForm
error={error ? lang(error) : undefined}
error={errorKey && lang.withRegular(errorKey)}
hint={passwordHint}
description={lang('PaymentConfirmationMessage', cardName)}
placeholder={lang('Password')}
description={oldLang('PaymentConfirmationMessage', cardName)}
placeholder={oldLang('Password')}
clearError={clearPaymentError}
shouldShowSubmit={false}
shouldResetValue={isActive}
@ -66,7 +70,7 @@ const PasswordConfirm: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((global): StateProps => {
const { payment } = selectTabState(global);
return {
error: payment.error?.message,
errorKey: payment.error?.messageKey,
passwordHint: global.twoFaSettings.hint,
savedCredentials: payment.form?.type === 'regular' ? payment.form.savedCredentials : undefined,
};

View File

@ -7,6 +7,7 @@ import React, {
import type { ApiCountry } from '../../api/types';
import type { FormEditDispatch, FormState } from '../../hooks/reducers/usePaymentReducer';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import Checkbox from '../ui/Checkbox';
@ -75,58 +76,59 @@ const PaymentInfo: FC<OwnProps> = ({
dispatch({ type: 'changeSaveCredentials', payload: e.target.value });
}, [dispatch]);
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const { formErrors = {} } = state;
return (
<div className="PaymentInfo">
<form>
<h5>{lang('PaymentCardTitle')}</h5>
<h5>{oldLang('PaymentCardTitle')}</h5>
<CardInput
onChange={handleCardNumberChange}
value={state.cardNumber}
error={formErrors.cardNumber}
error={formErrors.cardNumber && lang.withRegular(formErrors.cardNumber)}
/>
{needCardholderName && (
<InputText
label={lang('Checkout.NewCard.CardholderNamePlaceholder')}
label={oldLang('Checkout.NewCard.CardholderNamePlaceholder')}
onChange={handleCardholderChange}
value={state.cardholder}
inputMode="text"
tabIndex={0}
error={formErrors.cardholder}
error={formErrors.cardholder && lang.withRegular(formErrors.cardholder)}
/>
)}
<section className="inline-inputs">
<ExpiryInput
value={state.expiry}
onChange={handleExpiryChange}
error={formErrors.expiry}
error={formErrors.expiry && lang.withRegular(formErrors.expiry)}
/>
<InputText
label={lang('lng_payments_card_cvc')}
label={oldLang('lng_payments_card_cvc')}
onChange={handleCvvChange}
value={state.cvv}
inputMode="numeric"
maxLength={3}
tabIndex={0}
error={formErrors.cvv}
error={formErrors.cvv && lang.withRegular(formErrors.cvv)}
teactExperimentControlled
/>
</section>
{needCountry || needZip ? (
<h5>{lang('PaymentBillingAddress')}</h5>
<h5>{oldLang('PaymentBillingAddress')}</h5>
) : undefined}
<section className="inline-inputs">
{needCountry && (
<Select
label={lang('PaymentShippingCountry')}
label={oldLang('PaymentShippingCountry')}
onChange={handleCountryChange}
value={state.billingCountry}
hasArrow={Boolean(true)}
id="billing-country"
error={formErrors.billingCountry}
error={formErrors.billingCountry && lang.withRegular(formErrors.billingCountry)}
tabIndex={0}
ref={selectCountryRef}
>
@ -145,22 +147,22 @@ const PaymentInfo: FC<OwnProps> = ({
)}
{needZip && (
<InputText
label={lang('PaymentShippingZipPlaceholder')}
label={oldLang('PaymentShippingZipPlaceholder')}
onChange={handleBillingPostCodeChange}
value={state.billingZip}
inputMode="text"
tabIndex={0}
maxLength={12}
error={formErrors.billingZip}
error={formErrors.billingZip && lang.withRegular(formErrors.billingZip)}
/>
)}
</section>
<div className="checkbox">
<Checkbox
label={lang('PaymentCardSavePaymentInformation')}
label={oldLang('PaymentCardSavePaymentInformation')}
checked={canSaveCredentials ? state.saveCredentials : false}
tabIndex={0}
subLabel={lang(canSaveCredentials ? 'Checkout.NewCard.SaveInfoHelp' : 'Checkout.2FA.Text')}
subLabel={oldLang(canSaveCredentials ? 'Checkout.NewCard.SaveInfoHelp' : 'Checkout.2FA.Text')}
onChange={handleChangeSaveCredentials}
disabled={!canSaveCredentials}
/>

View File

@ -171,7 +171,7 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
paymentDispatch({
type: 'setFormErrors',
payload: {
[error.field]: error.message,
[error.field]: error.messageKey,
},
});
}
@ -250,8 +250,7 @@ const PaymentModal: FC<OwnProps & StateProps> = ({
isOpen={Boolean(error)}
onClose={handleErrorModalClose}
>
<h4>{error.description || 'Error'}</h4>
<p>{error.description || 'Error'}</p>
<h4>{error.descriptionKey ? lang.withRegular(error.descriptionKey) : lang('ErrorUnspecified')}</h4>
<div className="dialog-buttons mt-2">
<Button
isText

View File

@ -8,6 +8,7 @@ import type { ApiCountry } from '../../api/types';
import type { FormEditDispatch, FormState } from '../../hooks/reducers/usePaymentReducer';
import useFocusAfterAnimation from '../../hooks/useFocusAfterAnimation';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import Checkbox from '../ui/Checkbox';
@ -49,7 +50,8 @@ const ShippingInfo: FC<OwnProps> = ({
}
}, [state.countryIso2]);
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
useFocusAfterAnimation(inputRef);
@ -104,46 +106,46 @@ const ShippingInfo: FC<OwnProps> = ({
<form>
{needAddress ? (
<div>
<h5>{lang('PaymentShippingAddress')}</h5>
<h5>{oldLang('PaymentShippingAddress')}</h5>
<InputText
ref={inputRef}
label={lang('PaymentShippingAddress1Placeholder')}
label={oldLang('PaymentShippingAddress1Placeholder')}
onChange={handleAddress1Change}
value={state.streetLine1}
inputMode="text"
tabIndex={0}
error={formErrors.streetLine1}
error={formErrors.streetLine1 && lang.withRegular(formErrors.streetLine1)}
/>
<InputText
label={lang('PaymentShippingAddress2Placeholder')}
label={oldLang('PaymentShippingAddress2Placeholder')}
onChange={handleAddress2Change}
value={state.streetLine2}
inputMode="text"
tabIndex={0}
error={formErrors.streetLine2}
error={formErrors.streetLine2 && lang.withRegular(formErrors.streetLine2)}
/>
<InputText
label={lang('PaymentShippingCityPlaceholder')}
label={oldLang('PaymentShippingCityPlaceholder')}
onChange={handleCityChange}
value={state.city}
inputMode="text"
tabIndex={0}
error={formErrors.city}
error={formErrors.city && lang.withRegular(formErrors.city)}
/>
<InputText
label={lang('PaymentShippingStatePlaceholder')}
label={oldLang('PaymentShippingStatePlaceholder')}
onChange={handleStateChange}
value={state.state}
inputMode="text"
error={formErrors.state}
error={formErrors.state && lang.withRegular(formErrors.state)}
/>
<Select
label={lang('PaymentShippingCountry')}
label={oldLang('PaymentShippingCountry')}
onChange={handleCountryChange}
value={state.countryIso2}
hasArrow={Boolean(true)}
id="shipping-country"
error={formErrors.countryIso2}
error={formErrors.countryIso2 && lang.withRegular(formErrors.countryIso2)}
ref={selectCountryRef}
tabIndex={0}
>
@ -159,52 +161,52 @@ const ShippingInfo: FC<OwnProps> = ({
</Select>
<InputText
label={lang('PaymentShippingZipPlaceholder')}
label={oldLang('PaymentShippingZipPlaceholder')}
onChange={handlePostCodeChange}
value={state.postCode}
inputMode="text"
tabIndex={0}
error={formErrors.postCode}
error={formErrors.postCode && lang.withRegular(formErrors.postCode)}
/>
</div>
) : undefined}
{ needName || needEmail || needPhone ? (
<h5>{lang('PaymentShippingReceiver')}</h5>
<h5>{oldLang('PaymentShippingReceiver')}</h5>
) : undefined }
{ needName && (
<InputText
label={lang('PaymentShippingName')}
label={oldLang('PaymentShippingName')}
onChange={handleFullNameChange}
value={state.fullName}
inputMode="text"
tabIndex={0}
error={formErrors.fullName}
error={formErrors.fullName && lang.withRegular(formErrors.fullName)}
/>
) }
{ needEmail && (
<InputText
label={lang('PaymentShippingEmailPlaceholder')}
label={oldLang('PaymentShippingEmailPlaceholder')}
onChange={handleEmailChange}
value={state.email}
inputMode="email"
tabIndex={0}
error={formErrors.email}
error={formErrors.email && lang.withRegular(formErrors.email)}
/>
) }
{ needPhone && (
<InputText
label={lang('PaymentShippingPhoneNumber')}
label={oldLang('PaymentShippingPhoneNumber')}
onChange={handlePhoneChange}
value={state.phone}
inputMode="tel"
tabIndex={0}
error={formErrors.phone}
error={formErrors.phone && lang.withRegular(formErrors.phone)}
ref={phoneRef}
/>
) }
<Checkbox
label={lang('PaymentShippingSave')}
subLabel={lang('PaymentShippingSaveInfo')}
label={oldLang('PaymentShippingSave')}
subLabel={oldLang('PaymentShippingSaveInfo')}
checked={Boolean(state.saveInfo)}
tabIndex={0}
onChange={handleSaveInfoChange}

View File

@ -18,7 +18,6 @@ import useOldLang from '../../../hooks/useOldLang';
import AboutMonetizationModal from '../../common/AboutMonetizationModal.async';
import Icon from '../../common/icons/Icon';
import SafeLink from '../../common/SafeLink';
import VerificationMonetizationModal from '../../common/VerificationMonetizationModal.async';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Link from '../../ui/Link';
@ -53,9 +52,6 @@ type StateProps = {
isCreator?: boolean;
isChannelRevenueWithdrawalEnabled?: boolean;
hasPassword?: boolean;
passwordHint?: string;
error?: string;
isLoading?: boolean;
};
const MonetizationStatistics = ({
@ -65,11 +61,8 @@ const MonetizationStatistics = ({
isCreator,
isChannelRevenueWithdrawalEnabled,
hasPassword,
passwordHint,
error,
isLoading,
}: StateProps) => {
const { loadChannelMonetizationStatistics, loadPasswordInfo } = getActions();
const { loadChannelMonetizationStatistics, openMonetizationVerificationModal, loadPasswordInfo } = getActions();
const oldLang = useOldLang();
const lang = useLang();
@ -79,9 +72,6 @@ const MonetizationStatistics = ({
const loadedCharts = useRef<string[]>([]);
const forceUpdate = useForceUpdate();
const [isAboutMonetizationModalOpen, openAboutMonetizationModal, closeAboutMonetizationModal] = useFlag(false);
const [
isVerificationMonetizationModalOpen, openVerificationMonetizationModal, closeVerificationMonetizationModal,
] = useFlag(false);
const [isConfirmPasswordDialogOpen, openConfirmPasswordDialogOpen, closeConfirmPasswordDialogOpen] = useFlag();
const availableBalance = statistics?.balances?.availableBalance;
const isWithdrawalEnabled = statistics?.balances?.isWithdrawalEnabled;
@ -206,7 +196,9 @@ const MonetizationStatistics = ({
const verificationMonetizationHandler = useLastCallback(() => {
if (hasPassword) {
openVerificationMonetizationModal();
openMonetizationVerificationModal({
chatId,
});
} else {
openConfirmPasswordDialogOpen();
}
@ -259,14 +251,6 @@ const MonetizationStatistics = ({
isOpen={isAboutMonetizationModalOpen}
onClose={closeAboutMonetizationModal}
/>
<VerificationMonetizationModal
chatId={chatId}
isOpen={isVerificationMonetizationModalOpen}
onClose={closeVerificationMonetizationModal}
passwordHint={passwordHint}
error={error}
isLoading={isLoading}
/>
<ConfirmDialog
isOnlyConfirm
isOpen={isConfirmPasswordDialogOpen}
@ -289,12 +273,7 @@ export default memo(withGlobal(
hasPassword,
},
},
twoFaSettings: {
hint: passwordHint,
},
} = global;
const isLoading = global.monetizationInfo?.isLoading;
const error = global.monetizationInfo?.error;
const monetizationStatistics = tabState.monetizationStatistics;
const chatId = monetizationStatistics && monetizationStatistics.chatId;
const chat = chatId ? selectChat(global, chatId) : undefined;
@ -312,9 +291,6 @@ export default memo(withGlobal(
isCreator,
isChannelRevenueWithdrawalEnabled,
hasPassword,
passwordHint,
error,
isLoading,
};
},
)(MonetizationStatistics));

View File

@ -11,6 +11,7 @@ import type { IconName } from '../../types/icons';
import { getPeerTitle, getUserFullName } from '../../global/helpers';
import { selectPeerStory, selectTabState } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { getHours } from '../../util/dates/units';
import stopEvent from '../../util/stopEvent';
import useFlag from '../../hooks/useFlag';
@ -299,7 +300,7 @@ function StorySettings({
}
function renderPrivacyList() {
const storyLifeTime = story ? convertSecondsToHours(story.expireDate - story.date) : 0;
const storyLifeTime = story ? getHours(story.expireDate - story.date) : 0;
return (
<>
@ -419,15 +420,3 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
currentUserId: global.currentUserId!,
};
})(StorySettings));
function convertSecondsToHours(seconds: number): number {
const secondsInHour = 3600;
const minutesInHour = 60;
const hours = Math.floor(seconds / secondsInHour);
const remainingSeconds = seconds % secondsInHour;
const remainingMinutes = Math.floor(remainingSeconds / minutesInHour);
// If remaining minutes are greater than or equal to 30, round up the hours
return remainingMinutes >= 30 ? hours + 1 : hours;
}

View File

@ -33,6 +33,7 @@ import './ui/passcode';
import './ui/stars';
import './ui/reactions';
import './ui/stories';
import './ui/statistics';
import './apiUpdaters/initial';
import './apiUpdaters/chats';
import './apiUpdaters/messages';
@ -41,6 +42,5 @@ import './apiUpdaters/symbols';
import './apiUpdaters/misc';
import './apiUpdaters/settings';
import './apiUpdaters/twoFaSettings';
import './apiUpdaters/password';
import './apiUpdaters/calls';
import './apiUpdaters/payments';

View File

@ -2,6 +2,7 @@ import type {
ApiInputInvoice, ApiInputInvoiceStarGift, ApiInputInvoiceStarGiftUpgrade, ApiRequestInputInvoice,
} from '../../../api/types';
import type { ApiCredentials } from '../../../components/payment/PaymentModal';
import type { RegularLangFnParameters } from '../../../util/localization';
import type {
ActionReturnType, GlobalState, TabArgs,
} from '../../types';
@ -397,7 +398,7 @@ async function sendSmartGlocalCredentials<T extends GlobalState>(
if (result.status !== 'ok') {
// TODO после получения документации сделать аналог getStripeError(result.error);
const error = { description: 'payment error' };
const error = { descriptionKey: { key: 'ErrorUnexpected' } satisfies RegularLangFnParameters };
global = getGlobal();
global = updateTabState(global, {
payment: {
@ -554,9 +555,9 @@ addActionHandler('validatePaymentPassword', async (global, actions, payload): Pr
global = getGlobal();
if (!result) {
global = updatePayment(global, { error: { message: 'Unknown Error', field: 'password' } }, tabId);
global = updatePayment(global, { error: { messageKey: { key: 'ErrorUnexpected' }, field: 'password' } }, tabId);
} else if ('error' in result) {
global = updatePayment(global, { error: { message: result.error, field: 'password' } }, tabId);
global = updatePayment(global, { error: { messageKey: result.messageKey, field: 'password' } }, tabId);
} else {
global = updatePayment(global, { temporaryPassword: result, step: PaymentStep.Checkout }, tabId);
}
@ -1063,3 +1064,51 @@ addActionHandler('openUniqueGiftBySlug', async (global, actions, payload): Promi
actions.openGiftInfoModal({ gift, tabId });
});
addActionHandler('processStarGiftWithdrawal', async (global, actions, payload): Promise<void> => {
const {
gift, password, tabId = getCurrentTabId(),
} = payload;
let giftWithdrawModal = selectTabState(global, tabId).giftWithdrawModal;
if (!giftWithdrawModal) return;
global = updateTabState(global, {
giftWithdrawModal: {
...giftWithdrawModal,
isLoading: true,
errorKey: undefined,
},
}, tabId);
setGlobal(global);
const inputGift = getRequestInputSavedStarGift(global, gift);
if (!inputGift) {
return;
}
const result = await callApi('fetchStarGiftWithdrawalUrl', { inputGift, password });
if (!result) {
return;
}
global = getGlobal();
giftWithdrawModal = selectTabState(global, tabId).giftWithdrawModal;
if (!giftWithdrawModal) return;
if ('error' in result) {
global = updateTabState(global, {
giftWithdrawModal: {
...giftWithdrawModal,
isLoading: false,
errorKey: result.messageKey,
},
}, tabId);
setGlobal(global);
return;
}
actions.openUrl({ url: result.url, shouldSkipModal: true, tabId });
actions.closeGiftWithdrawModal({ tabId });
});

View File

@ -1,5 +1,3 @@
import type { ActionReturnType } from '../../types';
import { areDeepEqual } from '../../../util/areDeepEqual';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { callApi } from '../../../api/gramjs';
@ -7,10 +5,10 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
import {
updateChannelMonetizationStatistics,
updateMessageStatistics,
updateMonetizationInfo,
updateStatistics,
updateStatisticsGraph,
updateStoryStatistics,
updateVerifyMonetizationModal,
} from '../../reducers';
import {
selectChat,
@ -229,12 +227,14 @@ addActionHandler('loadStoryPublicForwards', async (global, actions, payload): Pr
setGlobal(global);
});
addActionHandler('loadMonetizationRevenueWithdrawalUrl', async (global, actions, payload): Promise<void> => {
addActionHandler('processMonetizationRevenueWithdrawalUrl', async (global, actions, payload): Promise<void> => {
const {
peerId, currentPassword, onSuccess, tabId = getCurrentTabId(),
peerId, currentPassword, tabId = getCurrentTabId(),
} = payload;
global = updateMonetizationInfo(global, { isLoading: true, error: undefined });
global = updateVerifyMonetizationModal(global, {
isLoading: true,
}, tabId);
setGlobal(global);
const peer = selectPeer(global, peerId);
@ -242,27 +242,26 @@ addActionHandler('loadMonetizationRevenueWithdrawalUrl', async (global, actions,
return;
}
const result = await callApi('loadMonetizationRevenueWithdrawalUrl', { peer, currentPassword });
const result = await callApi('fetchMonetizationRevenueWithdrawalUrl', { peer, currentPassword });
if (!result) {
return;
}
global = getGlobal();
global = updateMonetizationInfo(global, { isLoading: false });
global = updateVerifyMonetizationModal(global, {
isLoading: false,
errorKey: 'error' in result ? result.messageKey : undefined,
}, tabId);
setGlobal(global);
if (result) {
onSuccess();
if ('url' in result) {
actions.openUrl({
url: result.url,
shouldSkipModal: true,
tabId,
ignoreDeepLinks: true,
});
actions.closeMonetizationVerificationModal({ tabId });
}
});
addActionHandler('clearMonetizationInfo', (global): ActionReturnType => {
return updateMonetizationInfo(global, { error: undefined });
});

View File

@ -19,7 +19,7 @@ addActionHandler('loadPasswordInfo', async (global): Promise<void> => {
addActionHandler('checkPassword', async (global, actions, payload): Promise<void> => {
const { currentPassword, onSuccess } = payload;
global = updateTwoFaSettings(global, { isLoading: true, error: undefined });
global = updateTwoFaSettings(global, { isLoading: true, errorKey: undefined });
setGlobal(global);
const isSuccess = await callApi('checkPassword', currentPassword);
@ -36,7 +36,7 @@ addActionHandler('checkPassword', async (global, actions, payload): Promise<void
addActionHandler('clearPassword', async (global, actions, payload): Promise<void> => {
const { currentPassword, onSuccess } = payload;
global = updateTwoFaSettings(global, { isLoading: true, error: undefined });
global = updateTwoFaSettings(global, { isLoading: true, errorKey: undefined });
setGlobal(global);
const isSuccess = await callApi('clearPassword', currentPassword);
@ -55,7 +55,7 @@ addActionHandler('updatePassword', async (global, actions, payload): Promise<voi
currentPassword, password, hint, email, onSuccess,
} = payload;
global = updateTwoFaSettings(global, { isLoading: true, error: undefined });
global = updateTwoFaSettings(global, { isLoading: true, errorKey: undefined });
setGlobal(global);
const isSuccess = await callApi('updatePassword', currentPassword, password, hint, email);
@ -74,7 +74,7 @@ addActionHandler('updateRecoveryEmail', async (global, actions, payload): Promis
currentPassword, email, onSuccess,
} = payload;
global = updateTwoFaSettings(global, { isLoading: true, error: undefined });
global = updateTwoFaSettings(global, { isLoading: true, errorKey: undefined });
setGlobal(global);
const isSuccess = await callApi('updateRecoveryEmail', currentPassword, email);
@ -95,5 +95,5 @@ addActionHandler('provideTwoFaEmailCode', (global, actions, payload): ActionRetu
});
addActionHandler('clearTwoFaError', (global): ActionReturnType => {
return updateTwoFaSettings(global, { error: undefined });
return updateTwoFaSettings(global, { errorKey: undefined });
});

View File

@ -1,20 +0,0 @@
import type { ActionReturnType } from '../../types';
import { addActionHandler } from '../../index';
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) {
case 'updatePasswordError': {
return {
...global,
monetizationInfo: {
...global.monetizationInfo,
isLoading: false,
error: update.error,
},
};
}
}
return undefined;
});

View File

@ -20,7 +20,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
...global,
twoFaSettings: {
...global.twoFaSettings,
error: update.message,
errorKey: update.messageKey,
},
};
}

View File

@ -9,7 +9,7 @@ import {
clearStarPayment, openStarsTransactionModal,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import { selectChatMessage, selectStarsPayment } from '../../selectors';
import { selectChatMessage, selectStarsPayment, selectTabState } from '../../selectors';
addActionHandler('processOriginStarsPayment', (global, actions, payload): ActionReturnType => {
const { originData, status, tabId = getCurrentTabId() } = payload;
@ -302,3 +302,35 @@ addActionHandler('closeGiftUpgradeModal', (global, actions, payload): ActionRetu
giftUpgradeModal: undefined,
}, tabId);
});
addActionHandler('openGiftWithdrawModal', (global, actions, payload): ActionReturnType => {
const { gift, tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftWithdrawModal: {
gift,
},
}, tabId);
});
addActionHandler('closeGiftWithdrawModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
giftWithdrawModal: undefined,
}, tabId);
});
addActionHandler('clearGiftWithdrawError', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const tabState = selectTabState(global, tabId);
const giftWithdrawModal = tabState?.giftWithdrawModal;
if (!giftWithdrawModal) return undefined;
return updateTabState(global, {
giftWithdrawModal: {
...giftWithdrawModal,
errorKey: undefined,
},
}, tabId);
});

View File

@ -0,0 +1,38 @@
import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { addActionHandler } from '../..';
import { updateVerifyMonetizationModal } from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
addActionHandler('openMonetizationVerificationModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), chatId } = payload || {};
return updateTabState(global, {
monetizationVerificationModal: {
chatId,
},
}, tabId);
});
addActionHandler('closeMonetizationVerificationModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
monetizationVerificationModal: undefined,
}, tabId);
});
addActionHandler('clearMonetizationVerificationError', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateVerifyMonetizationModal(global, { errorKey: undefined }, tabId);
});
addActionHandler('closeMonetizationStatistics', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
monetizationStatistics: undefined,
}, tabId);
});

View File

@ -428,11 +428,3 @@ addActionHandler('closeBoostStatistics', (global, actions, payload): ActionRetur
boostStatistics: undefined,
}, tabId);
});
addActionHandler('closeMonetizationStatistics', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
monetizationStatistics: undefined,
}, tabId);
});

View File

@ -3,6 +3,7 @@ import type { OldLangFn } from '../../hooks/useOldLang';
import { ANONYMOUS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { formatFullDate, formatTime } from '../../util/dates/dateFormat';
import { DAY } from '../../util/dates/units';
import { orderBy } from '../../util/iteratees';
import { formatPhoneNumber } from '../../util/phoneNumber';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
@ -227,11 +228,11 @@ export function sortUserIds(
switch (userStatus.type) {
case 'userStatusRecently':
return now - 60 * 60 * 24;
return now - DAY;
case 'userStatusLastWeek':
return now - 60 * 60 * 24 * 7;
return now - DAY * 7;
case 'userStatusLastMonth':
return now - 60 * 60 * 24 * 7 * 30;
return now - DAY * 7 * 30;
default:
return 0;
}

View File

@ -313,8 +313,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
isMinimized: false,
isHidden: false,
},
monetizationInfo: {},
};
export const INITIAL_TAB_STATE: TabState = {

View File

@ -13,5 +13,4 @@ export * from './statistics';
export * from './stories';
export * from './translations';
export * from './peers';
export * from './password';
export * from './topics';

View File

@ -1,14 +0,0 @@
import type { GlobalState } from '../types';
export function updateMonetizationInfo<T extends GlobalState>(
global: T,
update: GlobalState['monetizationInfo'],
): T {
return {
...global,
monetizationInfo: {
...global.monetizationInfo,
...update,
},
};
}

View File

@ -2,7 +2,7 @@ import type {
ApiChannelMonetizationStatistics,
ApiChannelStatistics, ApiGroupStatistics, ApiPostStatistics, StatisticsGraph,
} from '../../api/types';
import type { GlobalState, TabArgs } from '../types';
import type { GlobalState, TabArgs, TabState } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { selectTabState } from '../selectors';
@ -78,3 +78,20 @@ export function updateChannelMonetizationStatistics<T extends GlobalState>(
},
}, tabId);
}
export function updateVerifyMonetizationModal<T extends GlobalState>(
global: T, update: Partial<TabState['monetizationVerificationModal']>,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const tabState = selectTabState(global, tabId);
if (!tabState.monetizationVerificationModal) {
return global;
}
return updateTabState(global, {
monetizationVerificationModal: {
...tabState.monetizationVerificationModal,
...update,
},
}, tabId);
}

View File

@ -173,7 +173,11 @@ export interface ActionPayloads {
updatePerformanceSettings: Partial<PerformanceType>;
loadPasswordInfo: undefined;
clearTwoFaError: undefined;
clearMonetizationInfo: undefined;
openMonetizationVerificationModal: {
chatId: string;
} & WithTabId;
clearMonetizationVerificationError: WithTabId | undefined;
closeMonetizationVerificationModal: WithTabId | undefined;
updatePassword: {
currentPassword: string;
password: string;
@ -756,10 +760,9 @@ export interface ActionPayloads {
peerId: string;
} & WithTabId;
loadMonetizationRevenueWithdrawalUrl: {
processMonetizationRevenueWithdrawalUrl: {
peerId: string;
currentPassword: string;
onSuccess: VoidFunction;
} & WithTabId;
// ui
@ -2323,6 +2326,15 @@ export interface ActionPayloads {
shouldKeepOriginalDetails?: boolean;
upgradeStars?: number;
} & WithTabId;
openGiftWithdrawModal: {
gift: ApiSavedStarGift;
} & WithTabId;
clearGiftWithdrawError: WithTabId | undefined;
closeGiftWithdrawModal: WithTabId | undefined;
processStarGiftWithdrawal: {
gift: ApiInputSavedStarGift;
password: string;
} & WithTabId;
loadPeerSavedGifts: {
peerId: string;
shouldRefresh?: boolean;

View File

@ -109,15 +109,10 @@ export type GlobalState = {
twoFaSettings: {
hint?: string;
isLoading?: boolean;
error?: string;
errorKey?: RegularLangFnParameters;
waitingEmailCodeLength?: number;
};
monetizationInfo: {
isLoading?: boolean;
error?: string;
};
attachmentSettings: {
shouldCompress: boolean;
shouldSendGrouped: boolean;

View File

@ -82,6 +82,7 @@ import type {
} from '../../types';
import type { WebApp, WebAppModalStateType } from '../../types/webapp';
import type { SearchResultKey } from '../../util/keys/searchResultKey';
import type { RegularLangFnParameters } from '../../util/localization';
import type { CallbackAction } from './actions';
export type TabState = {
@ -377,8 +378,8 @@ export type TabState = {
receipt?: ApiReceiptRegular;
error?: {
field?: string;
message?: string;
description?: string;
messageKey?: RegularLangFnParameters;
descriptionKey?: RegularLangFnParameters;
};
isPaymentModalOpen?: boolean;
isExtendedMedia?: boolean;
@ -720,6 +721,12 @@ export type TabState = {
gift?: ApiSavedStarGift;
};
giftWithdrawModal?: {
gift: ApiSavedStarGift;
isLoading?: boolean;
errorKey?: RegularLangFnParameters;
};
suggestedStatusModal?: {
botId: string;
webAppKey?: string;
@ -727,5 +734,11 @@ export type TabState = {
duration?: number;
};
monetizationVerificationModal?: {
chatId: string;
isLoading?: boolean;
errorKey?: RegularLangFnParameters;
};
isWaitingForStarGiftUpgrade?: true;
};

View File

@ -1,3 +1,4 @@
import type { RegularLangFnParameters } from '../../util/localization';
import type { Dispatch, StateReducer } from '../useReducer';
import useReducer from '../useReducer';
@ -21,7 +22,7 @@ export type FormState = {
billingZip: string;
saveInfo: boolean;
saveCredentials: boolean;
formErrors: Record<string, string>;
formErrors: Partial<Record<string, RegularLangFnParameters>>;
tipAmount: number;
savedCredentialId: string;
};

View File

@ -1,6 +1,7 @@
import type TelegramClient from './TelegramClient';
import type { WrappedError } from '../../../api/gramjs/helpers/misc';
import { EmailUnconfirmedError, PasswordModifiedError, RPCError } from '../errors';
import { EmailUnconfirmedError } from '../errors';
import Api from '../tl/api';
import { generateRandomBytes } from '../Helpers';
@ -16,13 +17,8 @@ export interface TwoFaParams {
onEmailCodeError?: (err: Error) => void;
}
export interface TwoFaPasswordParams {
currentPassword?: string;
onPasswordCodeError?: (err: Error) => void;
}
export type TmpPasswordResult = Api.account.TmpPassword | { error: string } | undefined;
export type PasswordResult = Api.TypeInputCheckPasswordSRP | { error: string } | undefined;
export type TmpPasswordResult = Api.account.TmpPassword | WrappedError | undefined;
export type PasswordResult = Api.TypeInputCheckPasswordSRP | WrappedError | undefined;
/**
* Changes the 2FA settings of the logged in user.
@ -143,28 +139,17 @@ export async function getTmpPassword(client: TelegramClient, currentPassword: st
}
const inputPassword = await computeCheck(pwd, currentPassword);
try {
const result = await client.invoke(new Api.account.GetTmpPassword({
password: inputPassword,
period: ttl,
}));
const result = await client.invoke(new Api.account.GetTmpPassword({
password: inputPassword,
period: ttl,
}));
return result;
} catch (err: unknown) {
if (err instanceof RPCError && err.errorMessage === 'PASSWORD_HASH_INVALID') {
return { error: err.errorMessage };
}
throw err;
}
return result;
}
export async function getCurrentPassword(
client: TelegramClient,
{
currentPassword,
onPasswordCodeError,
}: TwoFaPasswordParams,
currentPassword?: string,
): Promise<PasswordResult> {
const pwd = await client.invoke(new Api.account.GetPassword());
@ -172,16 +157,5 @@ export async function getCurrentPassword(
return undefined;
}
try {
return currentPassword ? await computeCheck(pwd, currentPassword!) : new Api.InputCheckPasswordEmpty();
} catch (err: any) {
if (err instanceof PasswordModifiedError) {
onPasswordCodeError!(err);
return undefined;
} else if (err instanceof RPCError && err.errorMessage ==='PASSWORD_HASH_INVALID') {
return { error: err.errorMessage };
} else {
throw err;
}
}
return currentPassword ? await computeCheck(pwd, currentPassword!) : new Api.InputCheckPasswordEmpty();
}

View File

@ -24,7 +24,6 @@ import {
TwoFaParams,
TmpPasswordResult,
PasswordResult,
TwoFaPasswordParams,
} from './2fa';
import RequestState from '../network/RequestState';
import Deferred from '../../../util/Deferred';
@ -1264,8 +1263,8 @@ class TelegramClient {
return getTmpPassword(this, currentPassword, ttl);
}
getCurrentPassword(params: TwoFaPasswordParams): Promise<PasswordResult | undefined> {
return getCurrentPassword(this, params);
getCurrentPassword(currentPassword?: string): Promise<PasswordResult | undefined> {
return getCurrentPassword(this, currentPassword);
}
// event region

View File

@ -136,7 +136,7 @@ export class EmailUnconfirmedError extends BadRequestError {
}
}
export class PasswordModifiedError extends BadRequestError {
export class PasswordFreshError extends BadRequestError {
public seconds: number;
constructor(args: any) {
@ -159,6 +159,6 @@ export const rpcErrorRe = new Map<RegExp, any>([
[/USER_MIGRATE_(\d+)/, UserMigrateError],
[/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError],
[/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError],
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordModifiedError],
[/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError],
[/^Timeout$/, TimedOutError],
]);

View File

@ -1722,6 +1722,7 @@ payments.upgradeStarGift#aed6e4f5 flags:# keep_original_details:flags.0?true sta
payments.transferStarGift#7f18176a stargift:InputSavedStarGift to_id:InputPeer = Updates;
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;
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

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

View File

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

Binary file not shown.

Binary file not shown.

View File

@ -88,6 +88,7 @@ export type FontIconName =
| 'fontsize'
| 'forums'
| 'forward'
| 'fragment'
| 'fullscreen'
| 'gifs'
| 'gift'

View File

@ -590,10 +590,17 @@ export interface LangPair {
'ErrorSendRestrictedStickersAll': undefined;
'ErrorPhoneNumberInvalid': undefined;
'ErrorCodeInvalid': undefined;
'ErrorEmailCodeInvalid': undefined;
'ErrorIncorrectPassword': undefined;
'ErrorPasswordFlood': undefined;
'ErrorPhoneBanned': undefined;
'ErrorUnexpected': undefined;
'ErrorEmailUnconfirmed': undefined;
'ErrorEmailHashExpired': undefined;
'ErrorNewSaltInvalid': undefined;
'ErrorPasswordChanged': undefined;
'ErrorPasswordMissing': undefined;
'ErrorUnspecified': undefined;
'NoStickers': undefined;
'ClearRecentEmoji': undefined;
'TextFormatAddLinkTitle': undefined;
@ -1190,6 +1197,7 @@ export interface LangPair {
'GiftInfoViewUpgraded': undefined;
'GiftInfoUpgradeBadge': undefined;
'GiftInfoUpgradeForFree': undefined;
'GiftInfoWithdraw': undefined;
'GiftUpgradeUniqueTitle': undefined;
'GiftUpgradeUniqueDescription': undefined;
'GiftUpgradeTransferableTitle': undefined;
@ -1203,6 +1211,8 @@ export interface LangPair {
'GiftUpgradedDescription': undefined;
'GiftMakeUniqueAcc': undefined;
'GiftMakeUniqueLink': undefined;
'GiftWithdrawTitle': undefined;
'GiftWithdrawSubmit': undefined;
'AllGiftsCategory': undefined;
'LimitedGiftsCategory': undefined;
'StockGiftsCategory': undefined;
@ -1296,6 +1306,9 @@ export interface LangPair {
'ViewButtonGiftUnique': undefined;
'AuthContinueOnThisLanguage': undefined;
'Share': undefined;
'CheckPasswordTitle': undefined;
'CheckPasswordPlaceholder': undefined;
'CheckPasswordDescription': undefined;
}
export interface LangPairWithVariables<V extends unknown = LangVariable> {
@ -1461,6 +1474,12 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'SlowModeHint': {
'time': V;
};
'ErrorFloodTime': {
'time': V;
};
'ErrorPasswordFresh': {
'time': V;
};
'ErrorUnexpectedMessage': {
'error': V;
};
@ -1707,6 +1726,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'peer': V;
'link': V;
};
'GiftWithdrawDescription': {
'gift': V;
};
'StarsAmount': {
'amount': V;
};
@ -1829,9 +1851,6 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'PreviewSenderSendFile': {
'count': V;
};
'ErrorFlood': {
'hour': V;
};
'PinnedMessageTitle': {
'index': V;
};
@ -1958,6 +1977,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'count': V;
'total': V;
};
'GiftWithdrawWait': {
'days': V;
};
'StarsAmountText': {
'amount': V;
};

19
src/util/dates/units.ts Normal file
View File

@ -0,0 +1,19 @@
/// In seconds
export const MINUTE = 60;
export const HOUR = 3600;
export const DAY = 86400;
export function getMinutes(seconds: number, roundDown?: boolean) {
const roundFunc = roundDown ? Math.floor : Math.ceil;
return roundFunc(seconds / MINUTE);
}
export function getHours(seconds: number, roundDown?: boolean) {
const roundFunc = roundDown ? Math.floor : Math.ceil;
return roundFunc(seconds / HOUR);
}
export function getDays(seconds: number, roundDown?: boolean) {
const roundFunc = roundDown ? Math.floor : Math.ceil;
return roundFunc(seconds / DAY);
}

View File

@ -361,7 +361,7 @@ function getString(langKey: LangKey, count: number) {
function processTranslation(
langKey: LangKey,
variables?: Record<string, LangVariable>,
variables?: Record<string, LangVariable | RegularLangFnParameters>,
options?: LangFnOptions | LangFnOptionsWithPlural,
): string {
const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`;
@ -377,6 +377,9 @@ function processTranslation(
const variableEntries = variables ? Object.entries(variables) : [];
const finalString = variableEntries.reduce((result, [key, value]) => {
if (value === undefined) return result;
if (typeof value === 'object') { // Allow recursive variables in basic `lang.with`
value = processTranslation(value.key, value.variables, value.options);
}
const valueAsString = Number.isFinite(value) ? formatters!.number.format(value as number) : String(value);
return result.replace(`{${key}}`, valueAsString);

View File

@ -11,6 +11,7 @@ import type { TextFilter } from '../../components/common/helpers/renderText';
import type {
LangPairPluralWithVariables,
LangPairWithVariables,
LangVariable,
PluralLangKey,
PluralLangKeyWithVariables,
RegularLangKey,
@ -55,7 +56,9 @@ type RegularLangFnParametersWithoutVariables = {
type RegularLangFnParametersWithVariables<T = LangPairWithVariables> = {
[K in keyof T]: {
key: K;
variables: T[K];
variables: {
[key in keyof T[K]]: LangVariable | RegularLangFnParameters;
};
options?: LangFnOptions;
}
}[keyof T];
@ -69,7 +72,9 @@ type RegularLangFnPluralParameters = {
type RegularLangFnPluralParametersWithVariables<T = LangPairPluralWithVariables> = {
[K in keyof T]: {
key: K;
variables: T[K];
variables: {
[key in keyof T[K]]: LangVariable | RegularLangFnParameters;
};
options: LangFnOptionsWithPlural;
}
}[keyof T];