diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index a6e7270a6..67b183db1 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -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'; diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 93be9d8da..971b673ff 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -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'; diff --git a/src/api/gramjs/apiBuilders/gifts.ts b/src/api/gramjs/apiBuilders/gifts.ts index d5fa6d2d0..cb0597dc2 100644 --- a/src/api/gramjs/apiBuilders/gifts.ts +++ b/src/api/gramjs/apiBuilders/gifts.ts @@ -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, }; } diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 84b7c4a48..5e1887c5f 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -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, diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 4c4d7c838..3a40a81e8 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -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, diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 16c93c815..686ec064c 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -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'; diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 5634e37b3..a1afb5136 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -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'; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 241cc026b..f44885f4d 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -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) { diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers/localDb.ts similarity index 70% rename from src/api/gramjs/helpers.ts rename to src/api/gramjs/helpers/localDb.ts index d72ee3ae5..ed2b68e39 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers/localDb.ts @@ -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; 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(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; -} diff --git a/src/api/gramjs/helpers/misc.ts b/src/api/gramjs/helpers/misc.ts new file mode 100644 index 000000000..9a9dc50d4 --- /dev/null +++ b/src/api/gramjs/helpers/misc.ts @@ -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 = { + 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; +export type MediaRepairContext = MessageRepairContext; + +export type WrappedError = { + 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(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(error: T): WrappedError { + 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 }, + }; +} diff --git a/src/api/gramjs/methods/auth.ts b/src/api/gramjs/methods/auth.ts index 923f9e773..ed1a62a7d 100644 --- a/src/api/gramjs/methods/auth.ts +++ b/src/api/gramjs/methods/auth.ts @@ -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 = { - 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', diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index d348ff957..d44112d16 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -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'; diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 5acc8c53f..fbfe2040b 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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 { diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 8bab90fdb..0610cae3d 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -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 }) { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 7b2938583..231d03efc 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -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'; diff --git a/src/api/gramjs/methods/payments.ts b/src/api/gramjs/methods/payments.ts index 41d2e2980..b265eeb86 100644 --- a/src/api/gramjs/methods/payments.ts +++ b/src/api/gramjs/methods/payments.ts @@ -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; +} diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 22d349806..7d7b99c0e 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -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'; diff --git a/src/api/gramjs/methods/statistics.ts b/src/api/gramjs/methods/statistics.ts index c1f328888..b8f2f51c7 100644 --- a/src/api/gramjs/methods/statistics.ts +++ b/src/api/gramjs/methods/statistics.ts @@ -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; diff --git a/src/api/gramjs/methods/stories.ts b/src/api/gramjs/methods/stories.ts index 4f9275638..a6ca5d367 100644 --- a/src/api/gramjs/methods/stories.ts +++ b/src/api/gramjs/methods/stories.ts @@ -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({ diff --git a/src/api/gramjs/methods/twoFaSettings.ts b/src/api/gramjs/methods/twoFaSettings.ts index 3a6be255e..44e59ba4b 100644 --- a/src/api/gramjs/methods/twoFaSettings.ts +++ b/src/api/gramjs/methods/twoFaSettings.ts @@ -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, }); } diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 57716fa02..2c70e3775 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -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'; diff --git a/src/api/gramjs/updates/entityProcessor.ts b/src/api/gramjs/updates/entityProcessor.ts index 55dee5823..fc67ca380 100644 --- a/src/api/gramjs/updates/entityProcessor.ts +++ b/src/api/gramjs/updates/entityProcessor.ts @@ -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']); diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index e659c2496..2dd8f0948 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -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'; diff --git a/src/api/gramjs/worker/worker.ts b/src/api/gramjs/worker/worker.ts index 533274d88..cc440b670 100644 --- a/src/api/gramjs/worker/worker.ts +++ b/src/api/gramjs/worker/worker.ts @@ -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; diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index bf62bb3c2..ef0ae44b5 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -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 } diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index bba5a039a..656475000 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -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 | diff --git a/src/assets/font-icons/fragment.svg b/src/assets/font-icons/fragment.svg new file mode 100644 index 000000000..d473ad267 --- /dev/null +++ b/src/assets/font-icons/fragment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 4a6a7be8e..403dbdfe7 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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."; diff --git a/src/assets/localization/initialKeys.ts b/src/assets/localization/initialKeys.ts index 9d17a382b..0868f71cd 100644 --- a/src/assets/localization/initialKeys.ts +++ b/src/assets/localization/initialKeys.ts @@ -28,7 +28,7 @@ const INITIAL_KEYS: LangKey[] = [ 'ErrorIncorrectPassword', 'ErrorPasswordFlood', 'ErrorPhoneBanned', - 'ErrorFlood', + 'ErrorFloodTime', 'ErrorUnexpected', 'ErrorUnexpectedMessage', ]; diff --git a/src/assets/localization/initialStrings.ts b/src/assets/localization/initialStrings.ts index 7c42cf753..8ee3c2fbc 100644 --- a/src/assets/localization/initialStrings.ts +++ b/src/assets/localization/initialStrings.ts @@ -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; diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index 5a61b1974..c6437704f 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -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'; diff --git a/src/components/common/VerificationMonetizationModal.async.tsx b/src/components/common/VerificationMonetizationModal.async.tsx index e037427c2..62fbf73a2 100644 --- a/src/components/common/VerificationMonetizationModal.async.tsx +++ b/src/components/common/VerificationMonetizationModal.async.tsx @@ -8,8 +8,8 @@ import { Bundles } from '../../util/moduleLoader'; import useModuleLoader from '../../hooks/useModuleLoader'; const VerificationMonetizationModalAsync: FC = (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 ? : undefined; diff --git a/src/components/common/VerificationMonetizationModal.tsx b/src/components/common/VerificationMonetizationModal.tsx index b90fc92ac..8eac2932f 100644 --- a/src/components/common/VerificationMonetizationModal.tsx +++ b/src/components/common/VerificationMonetizationModal.tsx @@ -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 = ({ - 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 (
= ({ ); }; -export default memo(VerificationMonetizationModal); +export default memo(withGlobal( + (global): StateProps => { + const { + twoFaSettings: { + hint: passwordHint, + }, + } = global; + + return { + passwordHint, + }; + }, +)(VerificationMonetizationModal)); diff --git a/src/components/left/settings/twoFa/SettingsTwoFa.tsx b/src/components/left/settings/twoFa/SettingsTwoFa.tsx index 0d3502622..cddae79cd 100644 --- a/src/components/left/settings/twoFa/SettingsTwoFa.tsx +++ b/src/components/left/settings/twoFa/SettingsTwoFa.tsx @@ -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 = ({ state, hint, isLoading, - error, + errorKey, waitingEmailCodeLength, dispatch, isActive, @@ -49,6 +50,9 @@ const SettingsTwoFa: FC = ({ clearPassword, } = getActions(); + const lang = useLang(); + const oldLang = useOldLang(); + useEffect(() => { if (waitingEmailCodeLength) { if (currentScreen === SettingsScreens.TwoFaNewPasswordEmail) { @@ -153,8 +157,6 @@ const SettingsTwoFa: FC = ({ provideTwoFaEmailCode({ code }); }, [provideTwoFaEmailCode]); - const lang = useOldLang(); - switch (currentScreen) { case SettingsScreens.TwoFaDisabled: return ( @@ -175,8 +177,8 @@ const SettingsTwoFa: FC = ({ case SettingsScreens.TwoFaNewPassword: return ( = ({ return ( = ({ return ( = ({ 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 = ({ return ( = ({ return ( = ({ case SettingsScreens.TwoFaChangePasswordNew: return ( = ({ return ( = ({ return ( = ({ return ( = ({ return ( = ({ = ({ return ( ; 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; diff --git a/src/components/modals/gift/fragment/GiftWithdrawModal.async.tsx b/src/components/modals/gift/fragment/GiftWithdrawModal.async.tsx new file mode 100644 index 000000000..c87c42925 --- /dev/null +++ b/src/components/modals/gift/fragment/GiftWithdrawModal.async.tsx @@ -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 = (props) => { + const { modal } = props; + const GiftWithdrawModal = useModuleLoader(Bundles.Stars, 'GiftWithdrawModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return GiftWithdrawModal ? : undefined; +}; + +export default GiftWithdrawModalAsync; diff --git a/src/components/modals/gift/fragment/GiftWithdrawModal.module.scss b/src/components/modals/gift/fragment/GiftWithdrawModal.module.scss new file mode 100644 index 000000000..a38df4cf2 --- /dev/null +++ b/src/components/modals/gift/fragment/GiftWithdrawModal.module.scss @@ -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); +} diff --git a/src/components/modals/gift/fragment/GiftWithdrawModal.tsx b/src/components/modals/gift/fragment/GiftWithdrawModal.tsx new file mode 100644 index 000000000..5673083c7 --- /dev/null +++ b/src/components/modals/gift/fragment/GiftWithdrawModal.tsx @@ -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 ( + + {giftAttributes && ( + <> +
+
+ + +
+ + +
+

+ {lang('GiftWithdrawDescription', { + gift: `${gift.title} #${gift.number}`, + }, { + withNodes: true, + withMarkdown: true, + renderTextFilters: ['br'], + })} +

+ + )} + {Boolean(exportDelay) && ( +

+ {lang('GiftWithdrawWait', { days: getDays(exportDelay) }, { pluralValue: getDays(exportDelay) })} +

+ )} + {!hasPassword && {lang('ErrorPasswordMissing')}} + {hasPassword && !exportDelay && ( + + )} +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { + settings: { + byKey: { + hasPassword, + }, + }, + twoFaSettings: { + hint: passwordHint, + }, + } = global; + + return { + hasPassword, + passwordHint, + }; + }, +)(GiftWithdrawModal)); diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 3e80752a6..f7c131643 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -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')} + {canUpdate && isUniqueGift && ( + + {lang('GiftInfoWithdraw')} + + )} ); diff --git a/src/components/payment/PasswordConfirm.tsx b/src/components/payment/PasswordConfirm.tsx index 9b866c7b7..1fbaf7438 100644 --- a/src/components/payment/PasswordConfirm.tsx +++ b/src/components/payment/PasswordConfirm.tsx @@ -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 = ({ isActive, - error, + errorKey, state, savedCredentials, passwordHint, @@ -35,7 +37,9 @@ const PasswordConfirm: FC = ({ }) => { 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 = ({ = ({ export default memo(withGlobal((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, }; diff --git a/src/components/payment/PaymentInfo.tsx b/src/components/payment/PaymentInfo.tsx index eab2d40bc..9093aaab0 100644 --- a/src/components/payment/PaymentInfo.tsx +++ b/src/components/payment/PaymentInfo.tsx @@ -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 = ({ dispatch({ type: 'changeSaveCredentials', payload: e.target.value }); }, [dispatch]); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const { formErrors = {} } = state; return (
-
{lang('PaymentCardTitle')}
+
{oldLang('PaymentCardTitle')}
{needCardholderName && ( )}
{needCountry || needZip ? ( -
{lang('PaymentBillingAddress')}
+
{oldLang('PaymentBillingAddress')}
) : undefined}
{needCountry && ( @@ -159,52 +161,52 @@ const ShippingInfo: FC = ({
) : undefined} { needName || needEmail || needPhone ? ( -
{lang('PaymentShippingReceiver')}
+
{oldLang('PaymentShippingReceiver')}
) : undefined } { needName && ( ) } { needEmail && ( ) } { needPhone && ( ) } { - const { loadChannelMonetizationStatistics, loadPasswordInfo } = getActions(); + const { loadChannelMonetizationStatistics, openMonetizationVerificationModal, loadPasswordInfo } = getActions(); const oldLang = useOldLang(); const lang = useLang(); @@ -79,9 +72,6 @@ const MonetizationStatistics = ({ const loadedCharts = useRef([]); 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} /> - @@ -419,15 +420,3 @@ export default memo(withGlobal((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; -} diff --git a/src/global/actions/all.ts b/src/global/actions/all.ts index 0146942f3..d192c886e 100644 --- a/src/global/actions/all.ts +++ b/src/global/actions/all.ts @@ -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'; diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index aaeccbaf5..ab7f37340 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -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( 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 => { + 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 }); +}); diff --git a/src/global/actions/api/statistics.ts b/src/global/actions/api/statistics.ts index 0545ea9bc..3cb37f910 100644 --- a/src/global/actions/api/statistics.ts +++ b/src/global/actions/api/statistics.ts @@ -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 => { +addActionHandler('processMonetizationRevenueWithdrawalUrl', async (global, actions, payload): Promise => { 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 }); -}); diff --git a/src/global/actions/api/twoFaSettings.ts b/src/global/actions/api/twoFaSettings.ts index 59b02fd40..a2ea5dd4f 100644 --- a/src/global/actions/api/twoFaSettings.ts +++ b/src/global/actions/api/twoFaSettings.ts @@ -19,7 +19,7 @@ addActionHandler('loadPasswordInfo', async (global): Promise => { addActionHandler('checkPassword', async (global, actions, payload): Promise => { 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 => { 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 { - return updateTwoFaSettings(global, { error: undefined }); + return updateTwoFaSettings(global, { errorKey: undefined }); }); diff --git a/src/global/actions/apiUpdaters/password.ts b/src/global/actions/apiUpdaters/password.ts deleted file mode 100644 index 3601c5d73..000000000 --- a/src/global/actions/apiUpdaters/password.ts +++ /dev/null @@ -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; -}); diff --git a/src/global/actions/apiUpdaters/twoFaSettings.ts b/src/global/actions/apiUpdaters/twoFaSettings.ts index 5d47ce70c..833f7fba3 100644 --- a/src/global/actions/apiUpdaters/twoFaSettings.ts +++ b/src/global/actions/apiUpdaters/twoFaSettings.ts @@ -20,7 +20,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { ...global, twoFaSettings: { ...global.twoFaSettings, - error: update.message, + errorKey: update.messageKey, }, }; } diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index 83b32aef4..0c992eece 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -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); +}); diff --git a/src/global/actions/ui/statistics.ts b/src/global/actions/ui/statistics.ts new file mode 100644 index 000000000..85d89ce44 --- /dev/null +++ b/src/global/actions/ui/statistics.ts @@ -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); +}); diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index b74c1ce22..df0a35576 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -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); -}); diff --git a/src/global/helpers/users.ts b/src/global/helpers/users.ts index 0ab94b8b5..fadf7c72f 100644 --- a/src/global/helpers/users.ts +++ b/src/global/helpers/users.ts @@ -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; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 323e6d19e..6f1219447 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -313,8 +313,6 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { isMinimized: false, isHidden: false, }, - - monetizationInfo: {}, }; export const INITIAL_TAB_STATE: TabState = { diff --git a/src/global/reducers/index.ts b/src/global/reducers/index.ts index 3ed9c26fe..99507b0fd 100644 --- a/src/global/reducers/index.ts +++ b/src/global/reducers/index.ts @@ -13,5 +13,4 @@ export * from './statistics'; export * from './stories'; export * from './translations'; export * from './peers'; -export * from './password'; export * from './topics'; diff --git a/src/global/reducers/password.ts b/src/global/reducers/password.ts deleted file mode 100644 index 9e9a24d77..000000000 --- a/src/global/reducers/password.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { GlobalState } from '../types'; - -export function updateMonetizationInfo( - global: T, - update: GlobalState['monetizationInfo'], -): T { - return { - ...global, - monetizationInfo: { - ...global.monetizationInfo, - ...update, - }, - }; -} diff --git a/src/global/reducers/statistics.ts b/src/global/reducers/statistics.ts index 7466ef276..cd89d5f20 100644 --- a/src/global/reducers/statistics.ts +++ b/src/global/reducers/statistics.ts @@ -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( }, }, tabId); } + +export function updateVerifyMonetizationModal( + global: T, update: Partial, + ...[tabId = getCurrentTabId()]: TabArgs +): T { + const tabState = selectTabState(global, tabId); + if (!tabState.monetizationVerificationModal) { + return global; + } + + return updateTabState(global, { + monetizationVerificationModal: { + ...tabState.monetizationVerificationModal, + ...update, + }, + }, tabId); +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 4d7c141b5..61996b911 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -173,7 +173,11 @@ export interface ActionPayloads { updatePerformanceSettings: Partial; 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; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index da1446e0c..00651edaf 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -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; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 4e41c7a78..50830910a 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -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; }; diff --git a/src/hooks/reducers/usePaymentReducer.ts b/src/hooks/reducers/usePaymentReducer.ts index a0a45ec71..708d2e377 100644 --- a/src/hooks/reducers/usePaymentReducer.ts +++ b/src/hooks/reducers/usePaymentReducer.ts @@ -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; + formErrors: Partial>; tipAmount: number; savedCredentialId: string; }; diff --git a/src/lib/gramjs/client/2fa.ts b/src/lib/gramjs/client/2fa.ts index 3909335f8..7e9ff22c0 100644 --- a/src/lib/gramjs/client/2fa.ts +++ b/src/lib/gramjs/client/2fa.ts @@ -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 { 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(); } diff --git a/src/lib/gramjs/client/TelegramClient.ts b/src/lib/gramjs/client/TelegramClient.ts index 42cdb6b85..0ba7bca34 100644 --- a/src/lib/gramjs/client/TelegramClient.ts +++ b/src/lib/gramjs/client/TelegramClient.ts @@ -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 { - return getCurrentPassword(this, params); + getCurrentPassword(currentPassword?: string): Promise { + return getCurrentPassword(this, currentPassword); } // event region diff --git a/src/lib/gramjs/errors/RPCErrorList.ts b/src/lib/gramjs/errors/RPCErrorList.ts index 94d2d63f0..8e40d62b9 100644 --- a/src/lib/gramjs/errors/RPCErrorList.ts +++ b/src/lib/gramjs/errors/RPCErrorList.ts @@ -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([ [/USER_MIGRATE_(\d+)/, UserMigrateError], [/NETWORK_MIGRATE_(\d+)/, NetworkMigrateError], [/EMAIL_UNCONFIRMED_(\d+)/, EmailUnconfirmedError], - [/PASSWORD_TOO_FRESH_(\d+)/, PasswordModifiedError], + [/PASSWORD_TOO_FRESH_(\d+)/, PasswordFreshError], [/^Timeout$/, TimedOutError], ]); diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index c42e80eae..bc69afbd8 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -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; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 43c848019..5d247db2a 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -310,6 +310,7 @@ "payments.upgradeStarGift", "payments.transferStarGift", "payments.getUniqueStarGift", + "payments.getStarGiftWithdrawalUrl", "langpack.getLangPack", "langpack.getStrings", "langpack.getLanguages", diff --git a/src/styles/icons.scss b/src/styles/icons.scss index 9b6bfa377..7376f860e 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -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"); } diff --git a/src/styles/icons.woff b/src/styles/icons.woff index 48794b466..2a88b40d5 100644 Binary files a/src/styles/icons.woff and b/src/styles/icons.woff differ diff --git a/src/styles/icons.woff2 b/src/styles/icons.woff2 index 94e8bf132..9efed8b4e 100644 Binary files a/src/styles/icons.woff2 and b/src/styles/icons.woff2 differ diff --git a/src/types/icons/font.ts b/src/types/icons/font.ts index 46c5150ab..e0ccd605d 100644 --- a/src/types/icons/font.ts +++ b/src/types/icons/font.ts @@ -88,6 +88,7 @@ export type FontIconName = | 'fontsize' | 'forums' | 'forward' + | 'fragment' | 'fullscreen' | 'gifs' | 'gift' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 522a44ff1..8f3f58d55 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { @@ -1461,6 +1474,12 @@ export interface LangPairWithVariables { 'SlowModeHint': { 'time': V; }; + 'ErrorFloodTime': { + 'time': V; + }; + 'ErrorPasswordFresh': { + 'time': V; + }; 'ErrorUnexpectedMessage': { 'error': V; }; @@ -1707,6 +1726,9 @@ export interface LangPairWithVariables { 'peer': V; 'link': V; }; + 'GiftWithdrawDescription': { + 'gift': V; + }; 'StarsAmount': { 'amount': V; }; @@ -1829,9 +1851,6 @@ export interface LangPairPluralWithVariables { 'PreviewSenderSendFile': { 'count': V; }; - 'ErrorFlood': { - 'hour': V; - }; 'PinnedMessageTitle': { 'index': V; }; @@ -1958,6 +1977,9 @@ export interface LangPairPluralWithVariables { 'count': V; 'total': V; }; + 'GiftWithdrawWait': { + 'days': V; + }; 'StarsAmountText': { 'amount': V; }; diff --git a/src/util/dates/units.ts b/src/util/dates/units.ts new file mode 100644 index 000000000..cb1bdc1a6 --- /dev/null +++ b/src/util/dates/units.ts @@ -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); +} diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index f4243a997..6d794d2ad 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -361,7 +361,7 @@ function getString(langKey: LangKey, count: number) { function processTranslation( langKey: LangKey, - variables?: Record, + variables?: Record, 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); diff --git a/src/util/localization/types.ts b/src/util/localization/types.ts index 0a1dd412b..b5b3411ac 100644 --- a/src/util/localization/types.ts +++ b/src/util/localization/types.ts @@ -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 = { [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 = { [K in keyof T]: { key: K; - variables: T[K]; + variables: { + [key in keyof T[K]]: LangVariable | RegularLangFnParameters; + }; options: LangFnOptionsWithPlural; } }[keyof T];