diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 525876325..2138269e2 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -1,7 +1,15 @@ import type { CallbackAction } from '../../global/types'; import type { IconName } from '../../types/icons'; import type { LangFnParameters, RegularLangFnParameters } from '../../util/localization'; -import type { ApiDocument, ApiFormattedText, ApiMessageEntity, ApiPhoto, ApiReaction, ApiVideo } from './messages'; +import type { + ApiContact, + ApiDocument, + ApiFormattedText, + ApiMessageEntity, + ApiPhoto, + ApiReaction, + ApiVideo, +} from './messages'; import type { ApiPremiumSection } from './payments'; import type { ApiBotVerification } from './peers'; import type { ApiStarsSubscriptionPricing } from './stars'; @@ -143,6 +151,27 @@ export type ApiNotification = { messageEntities?: undefined; }); +export type ApiDialogError = { + type: 'error'; +} & ApiError; + +export type ApiDialogMessage = { + type: 'message'; + text: ApiFormattedText; +}; + +export type ApiDialogContact = { + type: 'contact'; + contact: ApiContact; +}; + +export type ApiDialogLocalizedMessage = { + type: 'localized'; + text: LangFnParameters; +}; + +export type ApiDialog = ApiDialogError | ApiDialogMessage | ApiDialogContact | ApiDialogLocalizedMessage; + export type ApiError = { message: string; entities?: ApiMessageEntity[]; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 37ad714bf..693d75f67 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -663,7 +663,8 @@ "AttachDocument" = "File"; "Poll" = "Poll"; "SlowModePlaceholder" = "Next message in {timer}"; -"SlowModeHint" = "Slow Mode is active. You can send\nyour next message in {time}."; +"SlowModeHint" = "Slow Mode is active. You can send your next message in {time}."; +"NoVoiceMessagesAllowed" = "**{user}** doesn't accept voice messages."; "SendMessageAsTitle" = "Send message as..."; "Message" = "Message"; "RecentStickers" = "Recently Used"; @@ -737,6 +738,8 @@ "ErrorPasswordChanged" = "Password has been changed, please try again"; "ErrorPasswordMissing" = "You must set 2FA password to use this feature"; "ErrorPasskeyUnknown" = "This passkey is not assigned to any account"; +"ErrorMessageTooLong_one" = "The provided message is too long. Please remove {count} character."; +"ErrorMessageTooLong_other" = "The provided message is too long. Please remove {count} characters."; "ErrorUrlExpired" = "This link has expired"; "ErrorUnspecified" = "Error"; "NoStickers" = "No stickers yet"; diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 91e69eb8f..b662fb411 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -1050,12 +1050,16 @@ const Composer = ({ const extraLength = text.length - maxLength; showDialog({ data: { - message: 'MESSAGE_TOO_LONG_PLEASE_REMOVE_CHARACTERS', - textParams: { - '{EXTRA_CHARS_COUNT}': extraLength.toString(), - '{PLURAL_S}': extraLength > 1 ? 's' : '', + type: 'localized', + text: { + key: 'ErrorMessageTooLong', + variables: { + count: extraLength, + }, + options: { + pluralValue: extraLength, + }, }, - hasErrorKey: true, }, }); @@ -1080,11 +1084,16 @@ const Composer = ({ const secondsRemaining = nextSendDateNotReached ? slowMode.nextSendDate! - nowSeconds : slowMode.seconds - secondsSinceLastMessage!; + showDialog({ data: { - message: oldLang('SlowModeHint', formatMediaDuration(secondsRemaining)), - isSlowMode: true, - hasErrorKey: false, + type: 'localized', + text: { + key: 'SlowModeHint', + variables: { + time: formatMediaDuration(secondsRemaining), + }, + }, }, }); diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index 283d5bc4e..4b9c264f1 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -733,7 +733,7 @@ function processEntityAsHtml( case ApiMessageEntityTypes.TextUrl: return `${renderedContent}`; diff --git a/src/components/main/Dialogs.tsx b/src/components/main/Dialogs.tsx index 89ed0aeda..90e6cc99b 100644 --- a/src/components/main/Dialogs.tsx +++ b/src/components/main/Dialogs.tsx @@ -1,8 +1,13 @@ +import type { TeactNode } from '../../lib/teact/teact'; import { memo, useEffect } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { - ApiContact, ApiError, + ApiContact, + ApiDialog, + ApiDialogError, + ApiDialogLocalizedMessage, + ApiDialogMessage, } from '../../api/types'; import type { MessageList } from '../../types'; @@ -18,7 +23,7 @@ import Modal from '../ui/Modal'; type StateProps = { currentMessageList?: MessageList; - dialogs: (ApiError | ApiContact)[]; + dialogs: ApiDialog[]; }; const Dialogs = ({ dialogs, currentMessageList }: StateProps) => { @@ -81,17 +86,16 @@ const Dialogs = ({ dialogs, currentMessageList }: StateProps) => { ); }; - const renderError = (error: ApiError) => { + const renderTextDialog = (renderedText: TeactNode, title = 'Telegram') => { return ( - {error.hasErrorKey ? getReadableErrorText(error) - : renderTextWithEntities({ text: error.message, entities: error.entities })} + {renderedText}
@@ -99,9 +103,37 @@ const Dialogs = ({ dialogs, currentMessageList }: StateProps) => { ); }; - const renderDialog = (dialog: ApiError | ApiContact) => { - if ('phoneNumber' in dialog) { - return renderContactRequest(dialog); + const renderFormattedTextDialog = (dialog: ApiDialogMessage, title?: string) => { + const renderedText = renderTextWithEntities(dialog.text); + + return renderTextDialog(renderedText, title); + }; + + const renderLocalizedDialog = (dialog: ApiDialogLocalizedMessage, title?: string) => { + return renderTextDialog(lang.with(dialog.text), title); + }; + + const renderError = (error: ApiDialogError) => { + const renderedErrorMessage = error.hasErrorKey + ? getReadableErrorText(error) + : error.entities?.length + ? renderTextWithEntities({ text: error.message, entities: error.entities }) + : error.message; + + return renderTextDialog(renderedErrorMessage, getErrorHeader(error)); + }; + + const renderDialog = (dialog: ApiDialog) => { + if (dialog.type === 'contact') { + return renderContactRequest(dialog.contact); + } + + if (dialog.type === 'message') { + return renderFormattedTextDialog(dialog); + } + + if (dialog.type === 'localized') { + return renderLocalizedDialog(dialog); } return renderError(dialog); @@ -110,7 +142,7 @@ const Dialogs = ({ dialogs, currentMessageList }: StateProps) => { return Boolean(dialogs.length) && renderDialog(dialogs[dialogs.length - 1]); }; -function getErrorHeader(error: ApiError) { +function getErrorHeader(error: ApiDialogError) { if (error.isSlowMode) { return 'Slowmode enabled'; } diff --git a/src/components/middle/message/SponsoredContextMenuContainer.tsx b/src/components/middle/message/SponsoredContextMenuContainer.tsx index 08264de53..6cb8bea90 100644 --- a/src/components/middle/message/SponsoredContextMenuContainer.tsx +++ b/src/components/middle/message/SponsoredContextMenuContainer.tsx @@ -71,7 +71,10 @@ const SponsoredMessageContextMenuContainer: FC = ({ const handleSponsorInfo = useLastCallback(() => { showDialog({ data: { - message: [sponsorInfo, additionalInfo].filter(Boolean).join('\n'), + type: 'message', + text: { + text: [sponsorInfo, additionalInfo].filter(Boolean).join('\n'), + }, }, }); handleItemClick(); diff --git a/src/components/middle/message/_message-content.scss b/src/components/middle/message/_message-content.scss index e2f9cfc47..e2b0ea1f7 100644 --- a/src/components/middle/message/_message-content.scss +++ b/src/components/middle/message/_message-content.scss @@ -402,8 +402,9 @@ user-select: none; display: flex; - align-items: center; gap: 0.25rem; + align-items: center; + margin-left: 1rem; } @@ -428,10 +429,8 @@ .sender-boosts { user-select: none; - display: flex; align-items: center; - font-size: 0.75rem; } } diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index cdc1b103d..358e600cc 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -6,7 +6,6 @@ import type { } from '../../types'; import { type ApiChat, - type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult, @@ -138,11 +137,15 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur } actions.showDialog({ data: { - phoneNumber: user.phoneNumber, - firstName: user.firstName || '', - lastName: user.lastName || '', - userId: user.id, - } as ApiContact, + type: 'contact', + contact: { + mediaType: 'contact', + phoneNumber: user.phoneNumber, + firstName: user.firstName || '', + lastName: user.lastName || '', + userId: user.id, + }, + }, tabId, }); break; @@ -1438,7 +1441,7 @@ async function answerCallbackButton( const { message, alert: isError, url } = result; if (isError) { - showDialog({ data: { message: message || 'Error' }, tabId }); + showDialog({ data: { type: 'error', message: message || 'Error' }, tabId }); } else if (message) { showNotification({ message, tabId }); } else if (url) { diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 862f4f82c..f5a770df8 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -789,7 +789,7 @@ addActionHandler('createChannel', async (global, actions, payload): Promise if ((error as ApiError).message === 'CHANNELS_TOO_MUCH') { actions.openLimitReachedModal({ limit: 'channels', tabId }); } else { - actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + actions.showDialog({ data: { type: 'error', ...(error as ApiError), hasErrorKey: true }, tabId }); } } }); @@ -2651,7 +2651,7 @@ addActionHandler('toggleForum', async (global, actions, payload): Promise if ((error as ApiError).message === 'FLOOD') { actions.showNotification({ message: langProvider.oldTranslate('FloodWait'), tabId }); } else { - actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + actions.showDialog({ data: { type: 'error', ...(error as ApiError), hasErrorKey: true }, tabId }); } } @@ -2860,7 +2860,7 @@ addActionHandler('joinChatlistInvite', async (global, actions, payload): Promise if ((error as ApiError).message === 'CHATLISTS_TOO_MUCH') { actions.openLimitReachedModal({ limit: 'chatlistJoined', tabId }); } else { - actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + actions.showDialog({ data: { type: 'error', ...(error as ApiError), hasErrorKey: true }, tabId }); } } }); @@ -2946,7 +2946,7 @@ addActionHandler('createChatlistInvite', async (global, actions, payload): Promi actions.openLimitReachedModal({ limit: 'chatlistInvites', tabId }); actions.openSettingsScreen({ screen: SettingsScreens.Folders, tabId }); } else { - actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + actions.showDialog({ data: { type: 'error', ...(error as ApiError), hasErrorKey: true }, tabId }); } } @@ -3031,7 +3031,7 @@ addActionHandler('editChatlistInvite', async (global, actions, payload): Promise }; setGlobal(global); } catch (error) { - actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + actions.showDialog({ data: { type: 'error', ...(error as ApiError), hasErrorKey: true }, tabId }); } finally { global = getGlobal(); @@ -3532,7 +3532,7 @@ export async function migrateChat( if ((error as ApiError).message === 'CHANNELS_TOO_MUCH') { actions.openLimitReachedModal({ limit: 'channels', tabId }); } else { - actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + actions.showDialog({ data: { type: 'error', ...(error as ApiError), hasErrorKey: true }, tabId }); } return undefined; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 22f4d5917..6d9de7b4b 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -2567,7 +2567,17 @@ addActionHandler('setForwardChatOrTopic', async (global, actions, payload): Prom if (isSelectForwardsContainVoiceMessages && user && !await checkIfVoiceMessagesAllowed(global, user, chatId)) { actions.showDialog({ data: { - message: oldTranslate('VoiceMessagesRestrictedByPrivacy', getUserFullName(user)), + type: 'localized', + text: { + key: 'NoVoiceMessagesAllowed', + variables: { + user: getUserFullName(user), + }, + options: { + withNodes: true, + withMarkdown: true, + }, + }, }, tabId, }); diff --git a/src/global/actions/api/payments.ts b/src/global/actions/api/payments.ts index 0a7d6450b..1d75ed5e6 100644 --- a/src/global/actions/api/payments.ts +++ b/src/global/actions/api/payments.ts @@ -1329,5 +1329,5 @@ function handlePaymentFormError(error: string, tabId: number) { return; } - getActions().showDialog({ data: { message: error, hasErrorKey: true }, tabId }); + getActions().showDialog({ data: { type: 'error', message: error, hasErrorKey: true }, tabId }); } diff --git a/src/global/actions/apiUpdaters/initial.ts b/src/global/actions/apiUpdaters/initial.ts index d0554c81c..e980c49be 100644 --- a/src/global/actions/apiUpdaters/initial.ts +++ b/src/global/actions/apiUpdaters/initial.ts @@ -101,7 +101,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { } else if (shouldClosePaymentModal(update.error)) { actions.closePaymentModal({ tabId }); } else if (actions.showDialog) { - actions.showDialog({ data: update.error, tabId }); + actions.showDialog({ data: { type: 'error', ...update.error }, tabId }); } }); diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 316612e7f..a5c5179d5 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -1,7 +1,6 @@ import { addCallback } from '../../../lib/teact/teactn'; import type { ActionReturnType, GlobalState } from '../../types'; -import { type ApiError } from '../../../api/types'; import { ANIMATION_WAVE_MIN_INTERVAL, @@ -389,13 +388,15 @@ addActionHandler('showDialog', (global, actions, payload): ActionReturnType => { const { data, tabId = getCurrentTabId() } = payload; // Filter out errors that we don't want to show to the user - if ('message' in data && data.hasErrorKey && !getReadableErrorText(data)) { + if (data.type === 'error' && data.hasErrorKey && !getReadableErrorText(data)) { return global; } const newDialogs = [...selectTabState(global, tabId).dialogs]; - if ('message' in data) { - const existingErrorIndex = newDialogs.findIndex((err) => (err as ApiError).message === data.message); + if (data.type === 'error') { + const existingErrorIndex = newDialogs.findIndex((dialog) => { + return dialog.type === 'error' && dialog.message === data.message; + }); if (existingErrorIndex !== -1) { newDialogs.splice(existingErrorIndex, 1); } diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 1921ebfc3..76df72ebd 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -10,9 +10,8 @@ import type { ApiChatType, ApiCheckedGiftCode, ApiCollectibleInfo, - ApiContact, + ApiDialog, ApiEmojiStatusCollectible, - ApiError, ApiFormattedText, ApiGeoPoint, ApiGlobalMessageSearchType, @@ -490,7 +489,7 @@ export type TabState = { }; notifications: ApiNotification[]; - dialogs: (ApiError | ApiContact)[]; + dialogs: ApiDialog[]; safeLinkModalUrl?: string; mapModal?: { diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 3e7b5dc7f..375fc3f50 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -2222,6 +2222,9 @@ export interface LangPairWithVariables { 'SlowModeHint': { 'time': V; }; + 'NoVoiceMessagesAllowed': { + 'user': V; + }; 'ErrorFloodTime': { 'time': V; }; @@ -3663,6 +3666,9 @@ export interface LangPairPluralWithVariables { 'PreviewSenderSendFile': { 'count': V; }; + 'ErrorMessageTooLong': { + 'count': V; + }; 'PinnedMessageTitle': { 'index': V; }; diff --git a/src/util/setupServiceWorker.ts b/src/util/setupServiceWorker.ts index 148c5f184..a0972bb25 100644 --- a/src/util/setupServiceWorker.ts +++ b/src/util/setupServiceWorker.ts @@ -86,7 +86,7 @@ if (IS_SERVICE_WORKER_SUPPORTED) { } if (!IS_IOS && !IS_ANDROID && !IS_TEST) { - getActions().showDialog?.({ data: { message: 'SERVICE_WORKER_DISABLED', hasErrorKey: true } }); + getActions().showDialog?.({ data: { type: 'error', message: 'SERVICE_WORKER_DISABLED', hasErrorKey: true } }); } } } catch (err) {