From 4decf22241f55574c3a070bee5fa1229f62321d5 Mon Sep 17 00:00:00 2001 From: William Grant Date: Tue, 23 Apr 2024 13:45:22 +1000 Subject: [PATCH] feat: improved error messaging when starting a new message --- ts/components/inputs/SessionInput.tsx | 4 ++ .../leftpane/overlay/OverlayMessage.tsx | 71 ++++++++++++++----- ts/session/apis/snode_api/batchRequest.ts | 3 +- ts/session/apis/snode_api/onions.ts | 3 +- ts/session/apis/snode_api/onsResolve.ts | 3 +- ts/session/apis/snode_api/retrieveRequest.ts | 3 +- ts/session/types/PubKey.ts | 2 +- ts/session/utils/errors.ts | 8 +++ ts/types/LocalizerKeys.ts | 1 + 9 files changed, 77 insertions(+), 21 deletions(-) diff --git a/ts/components/inputs/SessionInput.tsx b/ts/components/inputs/SessionInput.tsx index b328f24ed..2c3805ea7 100644 --- a/ts/components/inputs/SessionInput.tsx +++ b/ts/components/inputs/SessionInput.tsx @@ -229,6 +229,10 @@ export const SessionInput = (props: Props) => { }, onKeyDown: (event: KeyboardEvent) => { if (event.key === 'Enter' && onEnterPressed) { + if (isSpecial && event.shiftKey) { + return; + } + event.preventDefault(); onEnterPressed(inputValue); setErrorString(''); } diff --git a/ts/components/leftpane/overlay/OverlayMessage.tsx b/ts/components/leftpane/overlay/OverlayMessage.tsx index 984ee4501..46eaaf3ef 100644 --- a/ts/components/leftpane/overlay/OverlayMessage.tsx +++ b/ts/components/leftpane/overlay/OverlayMessage.tsx @@ -13,6 +13,7 @@ import { SessionButton } from '../../basic/SessionButton'; import { SessionSpinner } from '../../loading'; import { ONSResolve } from '../../../session/apis/snode_api/onsResolve'; +import { NotFoundError, SnodeResponseError } from '../../../session/utils/errors'; import { Flex } from '../../basic/Flex'; import { SpacerLG, SpacerMD } from '../../basic/Text'; import { YourSessionIDPill, YourSessionIDSelectable } from '../../basic/YourSessionIDPill'; @@ -28,6 +29,21 @@ const SessionIDDescription = styled.div` text-align: center; `; +const StyledLeftPaneOverlay = styled(Flex)` + background: var(--background-primary-color); + overflow-y: auto; + overflow-x: hidden; + padding-top: var(--margins-md); + + .session-button { + min-width: 160px; + width: fit-content; + margin-top: 1rem; + margin-bottom: 3rem; + flex-shrink: 0; + } +`; + function copyOurSessionID() { const ourSessionId = UserUtils.getOurPubKeyStrFromCache(); if (!ourSessionId) { @@ -46,11 +62,9 @@ export const OverlayMessage = () => { useKey('Escape', closeOverlay); const [pubkeyOrOns, setPubkeyOrOns] = useState(''); + const [pubkeyOrOnsError, setPubkeyOrOnsError] = useState(undefined); const [loading, setLoading] = useState(false); - const buttonText = window.i18n('next'); - const placeholder = window.i18n('accountIdOrOnsEnter'); - const disableNextButton = !pubkeyOrOns || loading; async function openConvoOnceResolved(resolvedSessionID: string) { @@ -76,41 +90,65 @@ export const OverlayMessage = () => { } async function handleMessageButtonClick() { + setPubkeyOrOnsError(undefined); + if ((!pubkeyOrOns && !pubkeyOrOns.length) || !pubkeyOrOns.trim().length) { - ToastUtils.pushToastError('invalidPubKey', window.i18n('onsErrorNotRecognised')); // or ons name + setPubkeyOrOnsError(window.i18n('accountIdErrorInvalid')); return; } + const pubkeyorOnsTrimmed = pubkeyOrOns.trim(); + const validationError = PubKey.validateWithErrorNoBlinding(pubkeyorOnsTrimmed); - if (!PubKey.validateWithErrorNoBlinding(pubkeyorOnsTrimmed)) { + if (!validationError) { await openConvoOnceResolved(pubkeyorOnsTrimmed); return; } + const isPubkey = PubKey.validate(pubkeyorOnsTrimmed); + if (isPubkey && validationError) { + setPubkeyOrOnsError(validationError); + return; + } + // this might be an ONS, validate the regex first const mightBeOnsName = new RegExp(ONSResolve.onsNameRegex, 'g').test(pubkeyorOnsTrimmed); if (!mightBeOnsName) { - ToastUtils.pushToastError('invalidPubKey', window.i18n('onsErrorNotRecognised')); + setPubkeyOrOnsError(window.i18n('onsErrorNotRecognised')); return; } + setLoading(true); try { const resolvedSessionID = await ONSResolve.getSessionIDForOnsName(pubkeyorOnsTrimmed); - if (PubKey.validateWithErrorNoBlinding(resolvedSessionID)) { - throw new Error('Got a resolved ONS but the returned entry is not a valid SessionID'); + const idValidationError = PubKey.validateWithErrorNoBlinding(resolvedSessionID); + + if (idValidationError) { + setPubkeyOrOnsError(window.i18n('onsErrorNotRecognised')); + return; } - // this is a pubkey + await openConvoOnceResolved(resolvedSessionID); } catch (e) { - window?.log?.warn('failed to resolve ons name', pubkeyorOnsTrimmed, e); - ToastUtils.pushToastError('invalidPubKey', window.i18n('failedResolveOns')); + setPubkeyOrOnsError( + e instanceof SnodeResponseError + ? window.i18n('onsErrorUnableToSearch') + : e instanceof NotFoundError + ? window.i18n('onsErrorNotRecognised') + : window.i18n('failedResolveOns') + ); } finally { setLoading(false); } } return ( -
+ {/* TODO[epic=893] Replace everywhere and test new error handling */} {/* {
@@ -155,11 +194,11 @@ export const OverlayMessage = () => { - + ); }; diff --git a/ts/session/apis/snode_api/batchRequest.ts b/ts/session/apis/snode_api/batchRequest.ts index fd5e3ae34..671f6da6f 100644 --- a/ts/session/apis/snode_api/batchRequest.ts +++ b/ts/session/apis/snode_api/batchRequest.ts @@ -1,5 +1,6 @@ import { isArray } from 'lodash'; import { Snode } from '../../../data/data'; +import { SnodeResponseError } from '../../utils/errors'; import { processOnionRequestErrorAtDestination, SnodeResponse } from './onions'; import { snodeRpc } from './sessionRpc'; import { @@ -44,7 +45,7 @@ export async function doSnodeBatchRequest( window?.log?.warn( `doSnodeBatchRequest - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}` ); - throw new Error( + throw new SnodeResponseError( `doSnodeBatchRequest - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}` ); } diff --git a/ts/session/apis/snode_api/onions.ts b/ts/session/apis/snode_api/onions.ts index 1478bce85..f7856770a 100644 --- a/ts/session/apis/snode_api/onions.ts +++ b/ts/session/apis/snode_api/onions.ts @@ -17,6 +17,7 @@ import { toHex } from '../../utils/String'; import { Snode } from '../../../data/data'; import { callUtilsWorker } from '../../../webworker/workers/browser/util_worker_interface'; import { encodeV4Request } from '../../onions/onionv4'; +import { SnodeResponseError } from '../../utils/errors'; import { fileServerHost } from '../file_server_api/FileServerApi'; import { hrefPnServerProd } from '../push_notification_api/PnServer'; import { ERROR_CODE_NO_CONNECT } from './SNodeAPI'; @@ -1158,7 +1159,7 @@ async function lokiOnionFetch({ window?.log?.warn('onionFetchRetryable failed ', e.message); if (e?.errno === 'ENETUNREACH') { // better handle the no connection state - throw new Error(ERROR_CODE_NO_CONNECT); + throw new SnodeResponseError(ERROR_CODE_NO_CONNECT); } if (e?.message === CLOCK_OUT_OF_SYNC_MESSAGE_ERROR) { window?.log?.warn('Its a clock out of sync error '); diff --git a/ts/session/apis/snode_api/onsResolve.ts b/ts/session/apis/snode_api/onsResolve.ts index c7555266e..f15c2054b 100644 --- a/ts/session/apis/snode_api/onsResolve.ts +++ b/ts/session/apis/snode_api/onsResolve.ts @@ -7,6 +7,7 @@ import { stringToUint8Array, toHex, } from '../../utils/String'; +import { NotFoundError } from '../../utils/errors'; import { OnsResolveSubRequest } from './SnodeRequestTypes'; import { doSnodeBatchRequest } from './batchRequest'; import { GetNetworkTime } from './getNetworkTime'; @@ -57,7 +58,7 @@ async function getSessionIDForOnsName(onsNameCase: string) { const intermediate = parsedBody?.result; if (!intermediate || !intermediate?.encrypted_value) { - throw new Error('ONSresolve: no encrypted_value'); + throw new NotFoundError('ONSresolve: no encrypted_value'); } const hexEncodedCipherText = intermediate?.encrypted_value; diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 477f1b85c..8f258cc36 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -8,6 +8,7 @@ import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { TTL_DEFAULT } from '../../constants'; import { UserUtils } from '../../utils'; import { sleepFor } from '../../utils/Promise'; +import { SnodeResponseError } from '../../utils/errors'; import { RetrieveLegacyClosedGroupSubRequestType, RetrieveSubRequestType, @@ -136,7 +137,7 @@ async function retrieveNextMessages( window?.log?.warn( `_retrieveNextMessages - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}` ); - throw new Error( + throw new SnodeResponseError( `_retrieveNextMessages - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}` ); } diff --git a/ts/session/types/PubKey.ts b/ts/session/types/PubKey.ts index 41e68c8aa..a6bb3a82f 100644 --- a/ts/session/types/PubKey.ts +++ b/ts/session/types/PubKey.ts @@ -145,7 +145,7 @@ export class PubKey { // dev pubkey on testnet are now 66 chars too with the prefix, so every sessionID needs 66 chars and the prefix to be valid if (!isProdOrDevValid) { - return window.i18n('invalidPubkeyFormat'); + return window.i18n('accountIdErrorInvalid'); } return undefined; } diff --git a/ts/session/utils/errors.ts b/ts/session/utils/errors.ts index 9e5f0029d..9ede6802a 100644 --- a/ts/session/utils/errors.ts +++ b/ts/session/utils/errors.ts @@ -66,3 +66,11 @@ export class HTTPError extends Error { } } } + +export class SnodeResponseError extends Error { + constructor(message = 'sessionRpc could not talk to node') { + super(message); + // restore prototype chain + Object.setPrototypeOf(this, SnodeResponseError.prototype); + } +} diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 6b0a2b4d9..8f054d13a 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -103,6 +103,7 @@ export type LocalizerKeys = | 'contactsHeader' | 'contextMenuNoSuggestions' | 'continue' + | 'conversationId' | 'conversationsHeader' | 'conversationsNone' | 'conversationsSettingsTitle'