From 7bec240999a38a1d798d9dfe1d49bf50fb1607e7 Mon Sep 17 00:00:00 2001 From: Alexander Harding <2166114+aeharding@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:28:59 -0500 Subject: [PATCH] Refactor error toast messages (#1352) Resolves #1349 --- src/core/Auth.tsx | 2 +- src/features/auth/login/login/Login.tsx | 5 +- src/features/auth/login/login/Totp.tsx | 5 +- .../comment/compose/reply/CommentReply.tsx | 2 +- src/features/comment/useCommentActions.ts | 17 +++++- src/features/inbox/inboxSlice.ts | 2 +- src/features/labels/Vote.tsx | 9 ++- src/features/post/postSlice.ts | 27 ++------- src/features/post/shared/VoteButton.tsx | 7 ++- src/features/post/shared/usePostActions.tsx | 12 +++- src/features/report/Report.tsx | 2 +- src/features/resolve/resolveSlice.tsx | 2 +- .../shared/sliding/BaseSlidingVote.tsx | 9 ++- src/features/shared/useLemmyUrlHandler.ts | 2 +- src/features/user/AsyncProfile.tsx | 2 +- src/helpers/lemmy.ts | 33 ----------- src/helpers/lemmyErrors.ts | 58 +++++++++++++++++++ src/helpers/toastMessages.ts | 5 -- src/routes/pages/inbox/ComposeButton.tsx | 3 +- .../search/results/SearchCommunitiesPage.tsx | 2 +- 20 files changed, 124 insertions(+), 82 deletions(-) create mode 100644 src/helpers/lemmyErrors.ts diff --git a/src/core/Auth.tsx b/src/core/Auth.tsx index 41ca390816..74464c2a6d 100644 --- a/src/core/Auth.tsx +++ b/src/core/Auth.tsx @@ -6,7 +6,7 @@ import { getInboxCounts, syncMessages } from "../features/inbox/inboxSlice"; import { useInterval } from "usehooks-ts"; import usePageVisibility from "../helpers/usePageVisibility"; import { getDefaultServer } from "../services/app"; -import { isLemmyError } from "../helpers/lemmy"; +import { isLemmyError } from "../helpers/lemmyErrors"; import useAppToast from "../helpers/useAppToast"; import BackgroundReportSync from "../features/moderation/BackgroundReportSync"; import { getSiteIfNeeded, isAdminSelector } from "../features/auth/siteSlice"; diff --git a/src/features/auth/login/login/Login.tsx b/src/features/auth/login/login/Login.tsx index 9aeceb7531..33354da266 100644 --- a/src/features/auth/login/login/Login.tsx +++ b/src/features/auth/login/login/Login.tsx @@ -18,7 +18,10 @@ import { import useAppToast from "../../../../helpers/useAppToast"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { addGuestInstance, login } from "../../authSlice"; -import { getLoginErrorMessage, isLemmyError } from "../../../../helpers/lemmy"; +import { + getLoginErrorMessage, + isLemmyError, +} from "../../../../helpers/lemmyErrors"; import Totp from "./Totp"; import { DynamicDismissableModalContext } from "../../../shared/DynamicDismissableModal"; import InAppExternalLink from "../../../shared/InAppExternalLink"; diff --git a/src/features/auth/login/login/Totp.tsx b/src/features/auth/login/login/Totp.tsx index 6ff4ecbc9c..dfde15ec9d 100644 --- a/src/features/auth/login/login/Totp.tsx +++ b/src/features/auth/login/login/Totp.tsx @@ -14,7 +14,10 @@ import { import useAppToast from "../../../../helpers/useAppToast"; import { useAppDispatch } from "../../../../store"; import { login } from "../../authSlice"; -import { getLoginErrorMessage, isLemmyError } from "../../../../helpers/lemmy"; +import { + getLoginErrorMessage, + isLemmyError, +} from "../../../../helpers/lemmyErrors"; import { DynamicDismissableModalContext } from "../../../shared/DynamicDismissableModal"; import { loginSuccess } from "../../../../helpers/toastMessages"; import AppHeader from "../../../shared/AppHeader"; diff --git a/src/features/comment/compose/reply/CommentReply.tsx b/src/features/comment/compose/reply/CommentReply.tsx index 81dc3b2406..07a4e84271 100644 --- a/src/features/comment/compose/reply/CommentReply.tsx +++ b/src/features/comment/compose/reply/CommentReply.tsx @@ -27,7 +27,7 @@ import { import { receivedComments } from "../../commentSlice"; import CommentEditorContent from "../CommentEditorContent"; import useAppToast from "../../../../helpers/useAppToast"; -import { isLemmyError } from "../../../../helpers/lemmy"; +import { isLemmyError } from "../../../../helpers/lemmyErrors"; import AccountSwitcher from "../../../auth/AccountSwitcher"; import { getClient } from "../../../../services/lemmy"; import AppHeader from "../../../shared/AppHeader"; diff --git a/src/features/comment/useCommentActions.ts b/src/features/comment/useCommentActions.ts index 5e1151edcc..2ff2cdb103 100644 --- a/src/features/comment/useCommentActions.ts +++ b/src/features/comment/useCommentActions.ts @@ -30,7 +30,6 @@ import { postLocked, saveError, saveSuccess, - voteError, } from "../../helpers/toastMessages"; import store, { useAppDispatch } from "../../store"; import { PageContext } from "../auth/PageContext"; @@ -45,6 +44,7 @@ import { useOptimizedIonRouter } from "../../helpers/useOptimizedIonRouter"; import { isDownvoteEnabledSelector } from "../auth/siteSlice"; import { compact } from "lodash"; import { isStubComment } from "./CommentHeader"; +import { getVoteErrorMessage } from "../../helpers/lemmyErrors"; export interface CommentActionsProps { comment: CommentView | PersonMentionView | CommentReplyView; @@ -125,7 +125,12 @@ export default function useCommentActions({ try { await dispatch(voteOnComment(comment.id, myVote === 1 ? 0 : 1)); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); + + throw error; } })(); }, @@ -143,7 +148,12 @@ export default function useCommentActions({ voteOnComment(comment.id, myVote === -1 ? 0 : -1), ); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); + + throw error; } })(); }, @@ -162,6 +172,7 @@ export default function useCommentActions({ if (!mySaved) presentToast(saveSuccess); } catch (error) { presentToast(saveError); + throw error; } })(); }, diff --git a/src/features/inbox/inboxSlice.ts b/src/features/inbox/inboxSlice.ts index 1afede33ab..01e6b2442b 100644 --- a/src/features/inbox/inboxSlice.ts +++ b/src/features/inbox/inboxSlice.ts @@ -5,7 +5,7 @@ import { logoutAccount } from "../auth/authSlice"; import { InboxItemView } from "./InboxItem"; import { differenceBy, uniqBy } from "lodash"; import { receivedUsers } from "../user/userSlice"; -import { isLemmyError } from "../../helpers/lemmy"; +import { isLemmyError } from "../../helpers/lemmyErrors"; import { clientSelector, userHandleSelector, diff --git a/src/features/labels/Vote.tsx b/src/features/labels/Vote.tsx index 027c539daa..6345f21bfe 100644 --- a/src/features/labels/Vote.tsx +++ b/src/features/labels/Vote.tsx @@ -4,7 +4,7 @@ import { arrowDownSharp, arrowUpSharp } from "ionicons/icons"; import { voteOnPost } from "../post/postSlice"; import React, { useContext } from "react"; import { voteOnComment } from "../comment/commentSlice"; -import { downvotesDisabled, voteError } from "../../helpers/toastMessages"; +import { downvotesDisabled } from "../../helpers/toastMessages"; import { PageContext } from "../auth/PageContext"; import { calculateTotalScore, @@ -18,6 +18,7 @@ import useHapticFeedback from "../../helpers/useHapticFeedback"; import useAppToast from "../../helpers/useAppToast"; import { formatNumber } from "../../helpers/number"; import { styled } from "@linaria/react"; +import { getVoteErrorMessage } from "../../helpers/lemmyErrors"; const Container = styled.div<{ vote?: 1 | -1 | 0; @@ -91,7 +92,11 @@ export default function Vote({ try { await dispatch(dispatcherFn(id, vote)); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); + throw error; } } diff --git a/src/features/post/postSlice.ts b/src/features/post/postSlice.ts index 9d04115bdd..80d8541b23 100644 --- a/src/features/post/postSlice.ts +++ b/src/features/post/postSlice.ts @@ -7,7 +7,7 @@ import { jwtSelector, } from "../auth/authSelectors"; import { IPostMetadata, db } from "../../services/db"; -import { isLemmyError } from "../../helpers/lemmy"; +import { isLemmyError } from "../../helpers/lemmyErrors"; import { resolvePostReport } from "../moderation/modSlice"; interface PostHiddenData { @@ -324,10 +324,9 @@ export const getPost = id, }); } catch (error) { - // I think there is a bug in lemmy-js-client where it tries to parse 404 with non-json body if ( isLemmyError(error, "couldnt_find_post") || - error instanceof SyntaxError + isLemmyError(error, "unknown") ) { dispatch(receivedPostNotFound(id)); } @@ -341,24 +340,10 @@ export const getPost = export const deletePost = (id: number) => async (dispatch: AppDispatch, getState: () => RootState) => { - try { - await clientSelector(getState()).deletePost({ - post_id: id, - deleted: true, - }); - } catch (error) { - // I think there is a bug in lemmy-js-client where it tries to parse 404 with non-json body - if ( - isLemmyError(error, "couldnt_find_post") || - error instanceof SyntaxError - ) { - dispatch(receivedPostNotFound(id)); - - return; - } - - throw error; - } + await clientSelector(getState()).deletePost({ + post_id: id, + deleted: true, + }); dispatch(postDeleted(id)); }; diff --git a/src/features/post/shared/VoteButton.tsx b/src/features/post/shared/VoteButton.tsx index ec054179d3..62493ad37a 100644 --- a/src/features/post/shared/VoteButton.tsx +++ b/src/features/post/shared/VoteButton.tsx @@ -4,7 +4,6 @@ import { useAppDispatch, useAppSelector } from "../../../store"; import { voteOnPost } from "../postSlice"; import { arrowDownSharp, arrowUpSharp } from "ionicons/icons"; import { ActionButton } from "../actions/ActionButton"; -import { voteError } from "../../../helpers/toastMessages"; import { PageContext } from "../../auth/PageContext"; import { isDownvoteEnabledSelector } from "../../auth/siteSlice"; import { bounceAnimationOnTransition, bounceMs } from "../../shared/animations"; @@ -13,6 +12,7 @@ import { ImpactStyle } from "@capacitor/haptics"; import useHapticFeedback from "../../../helpers/useHapticFeedback"; import useAppToast from "../../../helpers/useAppToast"; import { styled } from "@linaria/react"; +import { getVoteErrorMessage } from "../../../helpers/lemmyErrors"; const InactiveItem = styled(ActionButton)` ${bounceAnimationOnTransition} @@ -103,7 +103,10 @@ export function VoteButton({ type, postId }: VoteButtonProps) { voteOnPost(postId, myVote === selectedVote ? 0 : selectedVote), ); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); throw error; } diff --git a/src/features/post/shared/usePostActions.tsx b/src/features/post/shared/usePostActions.tsx index 1fad3d2092..dabdf43ae1 100644 --- a/src/features/post/shared/usePostActions.tsx +++ b/src/features/post/shared/usePostActions.tsx @@ -39,7 +39,6 @@ import { postLocked, saveError, saveSuccess, - voteError, } from "../../../helpers/toastMessages"; import { userHandleSelector } from "../../auth/authSelectors"; import useAppToast from "../../../helpers/useAppToast"; @@ -50,6 +49,7 @@ import { isDownvoteEnabledSelector } from "../../auth/siteSlice"; import { resolveObject } from "../../resolve/resolveSlice"; import { compact } from "lodash"; import { InFeedContext } from "../../feed/Feed"; +import { getVoteErrorMessage } from "../../../helpers/lemmyErrors"; export default function usePostActions(post: PostView) { const inFeed = useContext(InFeedContext); @@ -105,7 +105,10 @@ export default function usePostActions(post: PostView) { try { await dispatch(voteOnPost(post.post.id, myVote === 1 ? 0 : 1)); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); throw error; } @@ -124,7 +127,10 @@ export default function usePostActions(post: PostView) { voteOnPost(post.post.id, myVote === -1 ? 0 : -1), ); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); throw error; } diff --git a/src/features/report/Report.tsx b/src/features/report/Report.tsx index afd546d307..c6cbf0118e 100644 --- a/src/features/report/Report.tsx +++ b/src/features/report/Report.tsx @@ -4,7 +4,7 @@ import { forwardRef, useImperativeHandle, useState } from "react"; import useClient from "../../helpers/useClient"; import { IonAlertCustomEvent, OverlayEventDetail } from "@ionic/core"; import useAppToast from "../../helpers/useAppToast"; -import { isLemmyError } from "../../helpers/lemmy"; +import { isLemmyError } from "../../helpers/lemmyErrors"; export type ReportableItem = CommentView | PostView | PrivateMessageView; diff --git a/src/features/resolve/resolveSlice.tsx b/src/features/resolve/resolveSlice.tsx index 2cd65f1970..4abf5a153e 100644 --- a/src/features/resolve/resolveSlice.tsx +++ b/src/features/resolve/resolveSlice.tsx @@ -6,7 +6,7 @@ import { receivedCommunity } from "../community/communitySlice"; import { receivedPosts } from "../post/postSlice"; import { receivedUsers } from "../user/userSlice"; import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { isLemmyError } from "../../helpers/lemmy"; +import { isLemmyError } from "../../helpers/lemmyErrors"; import { getClient } from "../../services/lemmy"; import { COMMENT_PATH, diff --git a/src/features/shared/sliding/BaseSlidingVote.tsx b/src/features/shared/sliding/BaseSlidingVote.tsx index 1e12c8d5ce..45e3f50936 100644 --- a/src/features/shared/sliding/BaseSlidingVote.tsx +++ b/src/features/shared/sliding/BaseSlidingVote.tsx @@ -28,7 +28,6 @@ import { postLocked, replyStubError, saveSuccess, - voteError, } from "../../../helpers/toastMessages"; import { saveComment, @@ -46,6 +45,7 @@ import { scrollCommentIntoViewIfNeeded } from "../../comment/inTree/CommentTree" import { AppContext } from "../../auth/AppContext"; import { getCanModerate } from "../../moderation/useCanModerate"; import { isStubComment } from "../../comment/CommentHeader"; +import { getVoteErrorMessage } from "../../../helpers/lemmyErrors"; const StyledItemContainer = styled.div` --ion-item-border-color: transparent; @@ -151,7 +151,12 @@ function BaseSlidingVoteInternal({ if (isPost) await dispatch(voteOnPost(item.post.id, score)); else await dispatch(voteOnComment(item.comment.id, score)); } catch (error) { - presentToast(voteError); + presentToast({ + color: "danger", + message: getVoteErrorMessage(error), + }); + + throw error; } }, [presentLoginIfNeeded, isPost, dispatch, item, presentToast], diff --git a/src/features/shared/useLemmyUrlHandler.ts b/src/features/shared/useLemmyUrlHandler.ts index c6d4ddb8d5..8a7e8ae918 100644 --- a/src/features/shared/useLemmyUrlHandler.ts +++ b/src/features/shared/useLemmyUrlHandler.ts @@ -6,7 +6,7 @@ import { useBuildGeneralBrowseLink } from "../../helpers/routes"; import { normalizeObjectUrl, resolveObject } from "../resolve/resolveSlice"; import { MouseEvent } from "react"; import useAppToast from "../../helpers/useAppToast"; -import { isLemmyError } from "../../helpers/lemmy"; +import { isLemmyError } from "../../helpers/lemmyErrors"; import { useOptimizedIonRouter } from "../../helpers/useOptimizedIonRouter"; export const POST_PATH = /^\/post\/(\d+)$/; diff --git a/src/features/user/AsyncProfile.tsx b/src/features/user/AsyncProfile.tsx index f0d31dcceb..3059b46832 100644 --- a/src/features/user/AsyncProfile.tsx +++ b/src/features/user/AsyncProfile.tsx @@ -10,7 +10,7 @@ import { GetPersonDetailsResponse } from "lemmy-js-client"; import { useAppDispatch } from "../../store"; import { getUser } from "../../features/user/userSlice"; import { useBuildGeneralBrowseLink } from "../../helpers/routes"; -import { isLemmyError } from "../../helpers/lemmy"; +import { isLemmyError } from "../../helpers/lemmyErrors"; import { useOptimizedIonRouter } from "../../helpers/useOptimizedIonRouter"; import { styled } from "@linaria/react"; diff --git a/src/helpers/lemmy.ts b/src/helpers/lemmy.ts index 771c9829cb..9a1205a216 100644 --- a/src/helpers/lemmy.ts +++ b/src/helpers/lemmy.ts @@ -4,7 +4,6 @@ import { Community, CommunityModeratorView, GetSiteResponse, - LemmyErrorType, Post, PostView, } from "lemmy-js-client"; @@ -279,13 +278,6 @@ export function keywordFoundInSentence( return pattern.test(sentence); } -export type LemmyErrorValue = LemmyErrorType["error"]; - -export function isLemmyError(error: unknown, lemmyErrorValue: LemmyErrorValue) { - if (!(error instanceof Error)) return; - return error.message === lemmyErrorValue; -} - export function canModerateCommunity( communityId: number | undefined, moderates: CommunityModeratorView[] | undefined, @@ -320,31 +312,6 @@ export function buildCrosspostBody(post: Post): string { return `${header}\n>\n${quote(post.body)}`; } -export function getLoginErrorMessage( - error: unknown, - instanceActorId: string, -): string { - if (!(error instanceof Error)) - return "Unknown error occurred, please try again."; - - switch (error.message as LemmyErrorValue) { - case "incorrect_totp_token": - return "Incorrect 2nd factor code. Please try again."; - case "couldnt_find_person": - return `User not found. Is your account on ${instanceActorId}?`; - case "incorrect_login": - return `Incorrect login credentials for ${instanceActorId}. Please try again.`; - case "email_not_verified": - return `Email not verified. Please check your inbox. Request a new verification email from https://${instanceActorId}.`; - case "site_ban": - return "You have been banned."; - case "deleted": - return "Account deleted."; - default: - return "Connection error, please try again."; - } -} - export function isPost(item: PostView | CommentView): item is PostView { return !isComment(item); } diff --git a/src/helpers/lemmyErrors.ts b/src/helpers/lemmyErrors.ts new file mode 100644 index 0000000000..e10035db8e --- /dev/null +++ b/src/helpers/lemmyErrors.ts @@ -0,0 +1,58 @@ +import { LemmyErrorType } from "lemmy-js-client"; + +type LemmyErrorValue = LemmyErrorType["error"]; + +export function isLemmyError(error: unknown, lemmyErrorValue: LemmyErrorValue) { + if (!(error instanceof Error)) return; + return error.message === lemmyErrorValue; +} + +function getErrorMessage( + error: unknown, + customErrorMap: (message: LemmyErrorValue) => string | undefined, + unknownLemmyError?: string, +): string { + if (!(error instanceof Error)) + return "Unknown error occurred, please try again."; + + const message = customErrorMap(error.message as LemmyErrorValue); + + if (message) return message; + + return unknownLemmyError ?? "Connection error, please try again."; +} + +export function getLoginErrorMessage( + error: unknown, + instanceActorId: string, +): string { + return getErrorMessage(error, (message) => { + switch (message) { + case "incorrect_totp_token": + return "Incorrect 2nd factor code. Please try again."; + case "couldnt_find_person": + return `User not found. Is your account on ${instanceActorId}?`; + case "incorrect_login": + return `Incorrect login credentials for ${instanceActorId}. Please try again.`; + case "email_not_verified": + return `Email not verified. Please check your inbox. Request a new verification email from https://${instanceActorId}.`; + case "site_ban": + return "You have been banned."; + case "deleted": + return "Account deleted."; + } + }); +} + +export function getVoteErrorMessage(error: unknown): string { + return getErrorMessage( + error, + (message) => { + switch (message) { + case "invalid_bot_action": + return "You marked your account as a bot, so you can't vote."; + } + }, + "Problem voting, please try again.", + ); +} diff --git a/src/helpers/toastMessages.ts b/src/helpers/toastMessages.ts index 78b7206595..58744b0dfd 100644 --- a/src/helpers/toastMessages.ts +++ b/src/helpers/toastMessages.ts @@ -1,11 +1,6 @@ import { checkmark, close } from "ionicons/icons"; import { AppToastOptions } from "./useAppToast"; -export const voteError: AppToastOptions = { - message: "Problem voting. Please try again.", - color: "danger", -}; - export const downvotesDisabled: AppToastOptions = { message: "Downvotes have been disabled by your server admins.", color: "warning", diff --git a/src/routes/pages/inbox/ComposeButton.tsx b/src/routes/pages/inbox/ComposeButton.tsx index 0c9674c268..205f441508 100644 --- a/src/routes/pages/inbox/ComposeButton.tsx +++ b/src/routes/pages/inbox/ComposeButton.tsx @@ -3,9 +3,10 @@ import { createOutline } from "ionicons/icons"; import { useState } from "react"; import { useAppDispatch } from "../../../store"; import { getUser } from "../../../features/user/userSlice"; -import { getHandle, isLemmyError } from "../../../helpers/lemmy"; +import { getHandle } from "../../../helpers/lemmy"; import useAppToast from "../../../helpers/useAppToast"; import { useOptimizedIonRouter } from "../../../helpers/useOptimizedIonRouter"; +import { isLemmyError } from "../../../helpers/lemmyErrors"; export default function ComposeButton() { const [loading, setLoading] = useState(false); diff --git a/src/routes/pages/search/results/SearchCommunitiesPage.tsx b/src/routes/pages/search/results/SearchCommunitiesPage.tsx index 3ec6ea4733..1314e4a649 100644 --- a/src/routes/pages/search/results/SearchCommunitiesPage.tsx +++ b/src/routes/pages/search/results/SearchCommunitiesPage.tsx @@ -15,7 +15,7 @@ import { useParams } from "react-router"; import PostSort from "../../../../features/feed/PostSort"; import { CommunityView, LemmyHttp } from "lemmy-js-client"; import CommunityFeed from "../../../../features/feed/CommunityFeed"; -import { isLemmyError } from "../../../../helpers/lemmy"; +import { isLemmyError } from "../../../../helpers/lemmyErrors"; import useFeedSort from "../../../../features/feed/sort/useFeedSort"; import { compact } from "lodash"; import AppHeader from "../../../../features/shared/AppHeader";