Repository: gitify-app/gitify
Pull request #1419 (2024-07-27 21:59:54)

feat: gracefully handle partial failure for multiple accounts
Closes https://github.com/gitify-app/gitify/issues/423

simulated multiple account setup with a partial failure

<img width="500" alt="Screenshot 2024-07-28 at 7 51 40 AM" src="https://github.com/user-attachments/assets/05fbdcec-31c0-473b-9812-c7cc193bab6d">

<img width="497" alt="Screenshot 2024-07-28 at 10 09 37 AM" src="https://github.com/user-attachments/assets/e22201d6-b96e-4473-920b-de402bef24e2">

<img width="503" alt="Screenshot 2024-07-28 at 10 18 18 AM" src="https://github.com/user-attachments/assets/c9d335b9-999d-4443-904f-1a44c6c7d608">



setchy: updated to set the account header row background as red if an account has encountered errors

_Example_
<img width="497" alt="Screenshot 2024-07-28 at 4 34 16 PM" src="https://github.com/user-attachments/assets/84b215c5-a9ca-4575-9acb-bcb2190f0e91">


Patch:
diff --git a/package.json b/package.json
index 5da27591..7980ef93 100644
--- a/package.json
+++ b/package.json
@@ -130,10 +130,10 @@
     "@types/react-router-dom": "5.3.3",
     "autoprefixer": "10.4.19",
     "css-loader": "7.1.2",
-    "electron": "31.3.0",
+    "electron": "31.3.1",
     "electron-builder": "24.13.3",
     "graphql-tag": "2.12.6",
-    "husky": "9.1.3",
+    "husky": "9.1.4",
     "jest": "29.7.0",
     "jest-environment-jsdom": "29.7.0",
     "nock": "13.5.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fe5f56f1..6337c240 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -10,7 +10,7 @@ importers:
     dependencies:
       '@electron/remote':
         specifier: 2.1.2
-        version: 2.1.2(electron@31.3.0)
+        version: 2.1.2(electron@31.3.1)
       '@primer/octicons-react':
         specifier: 19.11.0
         version: 19.11.0(react@18.3.1)
@@ -37,7 +37,7 @@ importers:
         version: 4.20.10
       menubar:
         specifier: 9.5.0
-        version: 9.5.0(electron@31.3.0)
+        version: 9.5.0(electron@31.3.1)
       nprogress:
         specifier: 0.2.0
         version: 0.2.0
@@ -94,8 +94,8 @@ importers:
         specifier: 7.1.2
         version: 7.1.2(webpack@5.93.0(webpack-cli@5.1.4))
       electron:
-        specifier: 31.3.0
-        version: 31.3.0
+        specifier: 31.3.1
+        version: 31.3.1
       electron-builder:
         specifier: 24.13.3
         version: 24.13.3(electron-builder-squirrel-windows@24.13.3(dmg-builder@24.13.3))
@@ -103,8 +103,8 @@ importers:
         specifier: 2.12.6
         version: 2.12.6(graphql@16.8.1)
       husky:
-        specifier: 9.1.3
-        version: 9.1.3
+        specifier: 9.1.4
+        version: 9.1.4
       jest:
         specifier: 29.7.0
         version: 29.7.0(@types/node@20.14.13)(ts-node@10.9.2(@types/node@20.14.13)(typescript@5.5.4))
@@ -1392,8 +1392,8 @@ packages:
   electron-updater@6.3.1:
     resolution: {integrity: sha512-Et9t9LcH42q/A73yu398YGHR7cbdTnHbIyFN/p0FNZIJi10XxGshZBVVE6FsClMuoggw7FVhn1+kMUVMCqS7MA==}
 
-  electron@31.3.0:
-    resolution: {integrity: sha512-3LMRMmK4UK0A+jYSLGLYdfhc20TgY2v5jD3iGmhRZlDYj0gn7xBj/waRjlNalysZ0D2rgPvoes0wHuf5e/Bguw==}
+  electron@31.3.1:
+    resolution: {integrity: sha512-9fiuWlRhBfygtcT+auRd/WdBK/f8LZZcrpx0RjpXhH2DPTP/PfnkC4JB1PW55qCbGbh4wAgkYbf4ExIag8oGCA==}
     engines: {node: '>= 12.20.55'}
     hasBin: true
 
@@ -1748,8 +1748,8 @@ packages:
     resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
     engines: {node: '>=10.17.0'}
 
-  husky@9.1.3:
-    resolution: {integrity: sha512-ET3TQmQgdIu0pt+jKkpo5oGyg/4MQZpG6xcam5J5JyNJV+CBT23OBpCF15bKHKycRyMH9k6ONy8g2HdGIsSkMQ==}
+  husky@9.1.4:
+    resolution: {integrity: sha512-bho94YyReb4JV7LYWRWxZ/xr6TtOTt8cMfmQ39MQYJ7f/YE268s3GdghGwi+y4zAeqewE5zYLvuhV0M0ijsDEA==}
     engines: {node: '>=18'}
     hasBin: true
 
@@ -3531,9 +3531,9 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@electron/remote@2.1.2(electron@31.3.0)':
+  '@electron/remote@2.1.2(electron@31.3.1)':
     dependencies:
-      electron: 31.3.0
+      electron: 31.3.1
 
   '@electron/universal@1.5.1':
     dependencies:
@@ -4774,7 +4774,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  electron@31.3.0:
+  electron@31.3.1:
     dependencies:
       '@electron/get': 2.0.3
       '@types/node': 20.14.13
@@ -5145,7 +5145,7 @@ snapshots:
 
   human-signals@2.1.0: {}
 
-  husky@9.1.3: {}
+  husky@9.1.4: {}
 
   iconv-corefoundation@1.1.7:
     dependencies:
@@ -5767,9 +5767,9 @@ snapshots:
       escape-string-regexp: 4.0.0
     optional: true
 
-  menubar@9.5.0(electron@31.3.0):
+  menubar@9.5.0(electron@31.3.1):
     dependencies:
-      electron: 31.3.0
+      electron: 31.3.1
       electron-positioner: 4.1.0
 
   merge-stream@2.0.0: {}
diff --git a/src/__mocks__/notifications-mocks.ts b/src/__mocks__/notifications-mocks.ts
index 438ab37b..0abbe39c 100644
--- a/src/__mocks__/notifications-mocks.ts
+++ b/src/__mocks__/notifications-mocks.ts
@@ -12,10 +12,12 @@ export const mockAccountNotifications: AccountNotifications[] = [
   {
     account: mockGitHubCloudAccount,
     notifications: mockGitHubNotifications,
+    error: null,
   },
   {
     account: mockGitHubEnterpriseServerAccount,
     notifications: mockEnterpriseNotifications,
+    error: null,
   },
 ];
 
@@ -23,5 +25,6 @@ export const mockSingleAccountNotifications: AccountNotifications[] = [
   {
     account: mockGitHubCloudAccount,
     notifications: [mockGitHubNotifications[0]],
+    error: null,
   },
 ];
diff --git a/src/components/AccountNotifications.tsx b/src/components/AccountNotifications.tsx
index 5d9f1552..3714bc64 100644
--- a/src/components/AccountNotifications.tsx
+++ b/src/components/AccountNotifications.tsx
@@ -3,14 +3,16 @@ import {
   ChevronLeftIcon,
   ChevronUpIcon,
 } from '@primer/octicons-react';
-import { type FC, type MouseEvent, useContext, useState } from 'react';
+import { type FC, type MouseEvent, useContext, useMemo, useState } from 'react';
 import { AppContext } from '../context/App';
-import { type Account, Opacity, Size } from '../types';
+import { type Account, type GitifyError, Opacity, Size } from '../types';
 import type { Notification } from '../typesGitHub';
 import { cn } from '../utils/cn';
 import { openAccountProfile } from '../utils/links';
+import { AllRead } from './AllRead';
 import { HoverGroup } from './HoverGroup';
 import { NotificationRow } from './NotificationRow';
+import { Oops } from './Oops';
 import { RepositoryNotifications } from './RepositoryNotifications';
 import { InteractionButton } from './buttons/InteractionButton';
 import { PlatformIcon } from './icons/PlatformIcon';
@@ -18,6 +20,7 @@ import { PlatformIcon } from './icons/PlatformIcon';
 interface IAccountNotifications {
   account: Account;
   notifications: Notification[];
+  error: GitifyError | null;
   showAccountHostname: boolean;
 }
 
@@ -40,6 +43,11 @@ export const AccountNotifications: FC<IAccountNotifications> = (
     ),
   );
 
+  const hasNotifications = useMemo(
+    () => notifications.length > 0,
+    [notifications],
+  );
+
   const [showAccountNotifications, setShowAccountNotifications] =
     useState(true);
 
@@ -68,8 +76,10 @@ export const AccountNotifications: FC<IAccountNotifications> = (
       {showAccountHostname && (
         <div
           className={cn(
-            'group flex items-center justify-between px-3 py-1.5 text-sm font-semibold',
-            'bg-gray-300 dark:bg-gray-darkest dark:text-white',
+            'group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white',
+            props.error
+              ? 'bg-red-300 dark:bg-red-500'
+              : 'bg-gray-300 dark:bg-gray-darkest',
             Opacity.LOW,
           )}
           onClick={toggleAccountNotifications}
@@ -103,6 +113,8 @@ export const AccountNotifications: FC<IAccountNotifications> = (
 
       {showAccountNotifications && (
         <>
+          {props.error && <Oops error={props.error} />}
+          {!hasNotifications && !props.error && <AllRead />}
           {groupByRepository
             ? Object.values(groupedNotifications).map((repoNotifications) => {
                 const repoSlug = repoNotifications[0].repository.full_name;
diff --git a/src/components/AllRead.tsx b/src/components/AllRead.tsx
index 4ba62b0e..b34c932b 100644
--- a/src/components/AllRead.tsx
+++ b/src/components/AllRead.tsx
@@ -12,7 +12,7 @@ export const AllRead: FC = () => {
 
   return (
     <div className="flex flex-1 flex-col items-center justify-center bg-white p-4 text-black dark:bg-gray-dark dark:text-white">
-      <h1 className="mb-5 text-5xl">{emoji}</h1>
+      <h1 className="mt-2 mb-5 text-5xl">{emoji}</h1>
 
       <h2 className="mb-2 text-xl font-semibold">No new notifications.</h2>
     </div>
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 54bac2ab..c69f29d7 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -124,34 +124,36 @@ export const NotificationRow: FC<INotificationRow> = ({
         <NotificationFooter notification={notification} />
       </div>
 
-      <HoverGroup>
-        <InteractionButton
-          title="Mark as Done"
-          icon={CheckIcon}
-          size={Size.MEDIUM}
-          onClick={() => {
-            setAnimateExit(!settings.delayNotificationState);
-            setShowAsRead(settings.delayNotificationState);
-            markNotificationDone(notification);
-          }}
-        />
-        <InteractionButton
-          title="Mark as Read"
-          icon={ReadIcon}
-          size={Size.SMALL}
-          onClick={() => {
-            setAnimateExit(!settings.delayNotificationState);
-            setShowAsRead(settings.delayNotificationState);
-            markNotificationRead(notification);
-          }}
-        />
-        <InteractionButton
-          title="Unsubscribe from Thread"
-          icon={BellSlashIcon}
-          size={Size.SMALL}
-          onClick={unsubscribeFromThread}
-        />
-      </HoverGroup>
+      {!animateExit && (
+        <HoverGroup>
+          <InteractionButton
+            title="Mark as Done"
+            icon={CheckIcon}
+            size={Size.MEDIUM}
+            onClick={() => {
+              setAnimateExit(!settings.delayNotificationState);
+              setShowAsRead(settings.delayNotificationState);
+              markNotificationDone(notification);
+            }}
+          />
+          <InteractionButton
+            title="Mark as Read"
+            icon={ReadIcon}
+            size={Size.SMALL}
+            onClick={() => {
+              setAnimateExit(!settings.delayNotificationState);
+              setShowAsRead(settings.delayNotificationState);
+              markNotificationRead(notification);
+            }}
+          />
+          <InteractionButton
+            title="Unsubscribe from Thread"
+            icon={BellSlashIcon}
+            size={Size.SMALL}
+            onClick={unsubscribeFromThread}
+          />
+        </HoverGroup>
+      )}
     </div>
   );
 };
diff --git a/src/components/Oops.tsx b/src/components/Oops.tsx
index 791424c8..7f7b21ea 100644
--- a/src/components/Oops.tsx
+++ b/src/components/Oops.tsx
@@ -13,7 +13,7 @@ export const Oops: FC<IOops> = ({ error }: IOops) => {
 
   return (
     <div className="flex flex-1 flex-col items-center justify-center p-4">
-      <h1 className="mb-5 text-5xl">{emoji}</h1>
+      <h1 className="mt-2 mb-5 text-5xl">{emoji}</h1>
 
       <h2 className="mb-2 text-xl font-semibold">{error.title}</h2>
       {error.descriptions.map((description, i) => {
diff --git a/src/components/RepositoryNotifications.tsx b/src/components/RepositoryNotifications.tsx
index 65a22939..27b50ad4 100644
--- a/src/components/RepositoryNotifications.tsx
+++ b/src/components/RepositoryNotifications.tsx
@@ -78,38 +78,40 @@ export const RepositoryNotifications: FC<IRepositoryNotifications> = ({
           </span>
         </div>
 
-        <HoverGroup>
-          <InteractionButton
-            title="Mark Repository as Done"
-            icon={CheckIcon}
-            size={Size.MEDIUM}
-            onClick={(event: MouseEvent<HTMLElement>) => {
-              // Don't trigger onClick of parent element.
-              event.stopPropagation();
-              setAnimateExit(!settings.delayNotificationState);
-              setShowAsRead(settings.delayNotificationState);
-              markRepoNotificationsDone(repoNotifications[0]);
-            }}
-          />
-          <InteractionButton
-            title="Mark Repository as Read"
-            icon={ReadIcon}
-            size={Size.SMALL}
-            onClick={(event: MouseEvent<HTMLElement>) => {
-              // Don't trigger onClick of parent element.
-              event.stopPropagation();
-              setAnimateExit(!settings.delayNotificationState);
-              setShowAsRead(settings.delayNotificationState);
-              markRepoNotificationsRead(repoNotifications[0]);
-            }}
-          />
-          <InteractionButton
-            title={toggleRepositoryNotificationsLabel}
-            icon={ChevronIcon}
-            size={Size.SMALL}
-            onClick={toggleRepositoryNotifications}
-          />
-        </HoverGroup>
+        {!animateExit && (
+          <HoverGroup>
+            <InteractionButton
+              title="Mark Repository as Done"
+              icon={CheckIcon}
+              size={Size.MEDIUM}
+              onClick={(event: MouseEvent<HTMLElement>) => {
+                // Don't trigger onClick of parent element.
+                event.stopPropagation();
+                setAnimateExit(!settings.delayNotificationState);
+                setShowAsRead(settings.delayNotificationState);
+                markRepoNotificationsDone(repoNotifications[0]);
+              }}
+            />
+            <InteractionButton
+              title="Mark Repository as Read"
+              icon={ReadIcon}
+              size={Size.SMALL}
+              onClick={(event: MouseEvent<HTMLElement>) => {
+                // Don't trigger onClick of parent element.
+                event.stopPropagation();
+                setAnimateExit(!settings.delayNotificationState);
+                setShowAsRead(settings.delayNotificationState);
+                markRepoNotificationsRead(repoNotifications[0]);
+              }}
+            />
+            <InteractionButton
+              title={toggleRepositoryNotificationsLabel}
+              icon={ChevronIcon}
+              size={Size.SMALL}
+              onClick={toggleRepositoryNotifications}
+            />
+          </HoverGroup>
+        )}
       </div>
 
       {showRepositoryNotifications &&
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index e5c007f4..b9f8f156 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -12,6 +12,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
 import { AppContext } from '../context/App';
 import { Size } from '../types';
 import { quitApp } from '../utils/comms';
+import Constants from '../utils/constants';
 import { getFilterCount } from '../utils/helpers';
 import {
   openGitHubIssues,
@@ -27,8 +28,18 @@ export const Sidebar: FC = () => {
   const navigate = useNavigate();
   const location = useLocation();
 
-  const { notifications, fetchNotifications, isLoggedIn, status, settings } =
-    useContext(AppContext);
+  const {
+    notifications,
+    fetchNotifications,
+    isLoggedIn,
+    status,
+    settings,
+    auth,
+  } = useContext(AppContext);
+
+  // We naively assume that the first account is the primary account for the purposes of our sidebar quick links
+  const primaryAccountHostname =
+    auth.accounts[0]?.hostname ?? Constants.DEFAULT_AUTH_OPTIONS.hostname;
 
   const toggleFilters = () => {
     if (location.pathname.startsWith('/filters')) {
@@ -77,19 +88,19 @@ export const Sidebar: FC = () => {
           title={`${notificationsCount} Unread Notifications`}
           metric={notificationsCount}
           icon={BellIcon}
-          onClick={() => openGitHubNotifications()}
+          onClick={() => openGitHubNotifications(primaryAccountHostname)}
         />
 
         <SidebarButton
           title="My Issues"
           icon={IssueOpenedIcon}
-          onClick={() => openGitHubIssues()}
+          onClick={() => openGitHubIssues(primaryAccountHostname)}
         />
 
         <SidebarButton
           title="My Pull Requests"
           icon={GitPullRequestIcon}
-          onClick={() => openGitHubPulls()}
+          onClick={() => openGitHubPulls(primaryAccountHostname)}
         />
       </div>
 
diff --git a/src/context/App.tsx b/src/context/App.tsx
index 5bd6ddad..90cf29a1 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -100,7 +100,7 @@ interface AppContextState {
 
   notifications: AccountNotifications[];
   status: Status;
-  errorDetails: GitifyError;
+  globalError: GitifyError;
   fetchNotifications: () => Promise<void>;
   markNotificationRead: (notification: Notification) => Promise<void>;
   markNotificationDone: (notification: Notification) => Promise<void>;
@@ -122,7 +122,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
   const {
     fetchNotifications,
     notifications,
-    errorDetails,
+    globalError,
     status,
     markNotificationRead,
     markNotificationDone,
@@ -311,7 +311,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
 
         notifications,
         status,
-        errorDetails,
+        globalError,
         fetchNotifications: fetchNotificationsWithAccounts,
         markNotificationRead: markNotificationReadWithAccounts,
         markNotificationDone: markNotificationDoneWithAccounts,
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index b8fb2849..76415d38 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -12,7 +12,6 @@ import {
   markNotificationThreadAsRead,
   markRepositoryNotificationsAsRead,
 } from '../utils/api/client';
-import { determineFailureType } from '../utils/api/errors';
 import { getAccountUUID } from '../utils/auth/utils';
 import {
   getAllNotifications,
@@ -46,12 +45,12 @@ interface NotificationsState {
     notification: Notification,
   ) => Promise<void>;
   status: Status;
-  errorDetails: GitifyError;
+  globalError: GitifyError;
 }
 
 export const useNotifications = (): NotificationsState => {
   const [status, setStatus] = useState<Status>('success');
-  const [errorDetails, setErrorDetails] = useState<GitifyError>();
+  const [globalError, setGlobalError] = useState<GitifyError>();
 
   const [notifications, setNotifications] = useState<AccountNotifications[]>(
     [],
@@ -61,16 +60,30 @@ export const useNotifications = (): NotificationsState => {
     async (state: GitifyState) => {
       setStatus('loading');
 
-      try {
-        const fetchedNotifications = await getAllNotifications(state);
+      const fetchedNotifications = await getAllNotifications(state);
+
+      // Set Global Error if all accounts have the same error
+      const allAccountsHaveErrors = fetchedNotifications.every((account) => {
+        return account.error !== null;
+      });
+      let accountErrorsAreAllSame = true;
+      const accountError = fetchedNotifications[0]?.error;
+      for (const fetchedNotification of fetchedNotifications) {
+        if (accountError !== fetchedNotification.error) {
+          accountErrorsAreAllSame = false;
+          break;
+        }
+      }
 
-        setNotifications(fetchedNotifications);
-        triggerNativeNotifications(notifications, fetchedNotifications, state);
-        setStatus('success');
-      } catch (err) {
+      if (allAccountsHaveErrors) {
         setStatus('error');
-        setErrorDetails(determineFailureType(err));
+        setGlobalError(accountErrorsAreAllSame ? accountError : null);
+        return;
       }
+
+      setNotifications(fetchedNotifications);
+      triggerNativeNotifications(notifications, fetchedNotifications, state);
+      setStatus('success');
     },
     [notifications],
   );
@@ -223,7 +236,7 @@ export const useNotifications = (): NotificationsState => {
 
   return {
     status,
-    errorDetails,
+    globalError,
     notifications,
 
     fetchNotifications,
diff --git a/src/routes/Notifications.tsx b/src/routes/Notifications.tsx
index 6d8019c2..17543e54 100644
--- a/src/routes/Notifications.tsx
+++ b/src/routes/Notifications.tsx
@@ -4,31 +4,31 @@ import { AllRead } from '../components/AllRead';
 import { Oops } from '../components/Oops';
 import { AppContext } from '../context/App';
 import { getAccountUUID } from '../utils/auth/utils';
-import { Errors } from '../utils/constants';
 import { getNotificationCount } from '../utils/notifications';
 
 export const NotificationsRoute: FC = () => {
-  const { notifications, status, errorDetails, settings } =
-    useContext(AppContext);
+  const { notifications, globalError, settings } = useContext(AppContext);
 
   const hasMultipleAccounts = useMemo(
     () => notifications.length > 1,
     [notifications],
   );
-  const notificationsCount = useMemo(() => {
-    return getNotificationCount(notifications);
-  }, [notifications]);
+
+  const hasNoAccountErrors = useMemo(
+    () => notifications.every((account) => account.error === null),
+    [notifications],
+  );
 
   const hasNotifications = useMemo(
-    () => notificationsCount > 0,
-    [notificationsCount],
+    () => getNotificationCount(notifications) > 0,
+    [notifications],
   );
 
-  if (status === 'error') {
-    return <Oops error={errorDetails ?? Errors.UNKNOWN} />;
+  if (globalError) {
+    return <Oops error={globalError} />;
   }
 
-  if (!hasNotifications) {
+  if (!hasNotifications && hasNoAccountErrors) {
     return <AllRead />;
   }
 
@@ -39,6 +39,7 @@ export const NotificationsRoute: FC = () => {
           key={getAccountUUID(accountNotifications.account)}
           account={accountNotifications.account}
           notifications={accountNotifications.notifications}
+          error={accountNotifications.error}
           showAccountHostname={
             hasMultipleAccounts || settings.showAccountHostname
           }
diff --git a/src/types.ts b/src/types.ts
index a51c648e..e7cac771 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -123,6 +123,7 @@ export type RadioGroupItem = {
 export interface AccountNotifications {
   account: Account;
   notifications: Notification[];
+  error: GitifyError | null;
 }
 
 export interface GitifyUser {
diff --git a/src/utils/auth/migration.ts b/src/utils/auth/migration.ts
index 018b3d9f..643edcb3 100644
--- a/src/utils/auth/migration.ts
+++ b/src/utils/auth/migration.ts
@@ -13,7 +13,6 @@ export async function migrateAuthenticatedAccounts() {
   const existing = loadState();
 
   if (!hasAccountsToMigrate(existing.auth)) {
-    log.info('Account Migration: No accounts need migrating');
     return;
   }
 
diff --git a/src/utils/links.ts b/src/utils/links.ts
index 6edd4477..e19aaadd 100644
--- a/src/utils/links.ts
+++ b/src/utils/links.ts
@@ -15,16 +15,22 @@ export function openGitifyReleaseNotes(version: string) {
   );
 }
 
-export function openGitHubNotifications() {
-  openExternalLink('https://github.com/notifications' as Link);
+export function openGitHubNotifications(hostname: Hostname) {
+  const url = new URL(`https://${hostname}`);
+  url.pathname = 'notifications';
+  openExternalLink(url.toString() as Link);
 }
 
-export function openGitHubIssues() {
-  openExternalLink('https://github.com/issues' as Link);
+export function openGitHubIssues(hostname: Hostname) {
+  const url = new URL(`https://${hostname}`);
+  url.pathname = 'issues';
+  openExternalLink(url.toString() as Link);
 }
 
-export function openGitHubPulls() {
-  openExternalLink('https://github.com/pulls' as Link);
+export function openGitHubPulls(hostname: Hostname) {
+  const url = new URL(`https://${hostname}`);
+  url.pathname = 'pulls';
+  openExternalLink(url.toString() as Link);
 }
 
 export function openAccountProfile(account: Account) {
diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts
index 44ffcc0e..35ad9d2a 100644
--- a/src/utils/notifications.ts
+++ b/src/utils/notifications.ts
@@ -5,6 +5,7 @@ import type {
 } from '../types';
 import { Notification } from '../typesGitHub';
 import { listNotificationsForAuthenticatedUser } from './api/client';
+import { determineFailureType } from './api/errors';
 import { getAccountUUID } from './auth/utils';
 import { hideWindow, showWindow, updateTrayIcon } from './comms';
 import { openNotification } from './links';
@@ -129,21 +130,30 @@ export async function getAllNotifications(
     responses
       .filter((response) => !!response)
       .map(async (accountNotifications) => {
-        let notifications = (await accountNotifications.notifications).data.map(
-          (notification: Notification) => ({
+        try {
+          let notifications = (
+            await accountNotifications.notifications
+          ).data.map((notification: Notification) => ({
             ...notification,
             account: accountNotifications.account,
-          }),
-        );
+          }));
 
-        notifications = await enrichNotifications(notifications, state);
+          notifications = await enrichNotifications(notifications, state);
 
-        notifications = filterNotifications(notifications, state.settings);
+          notifications = filterNotifications(notifications, state.settings);
 
-        return {
-          account: accountNotifications.account,
-          notifications: notifications,
-        };
+          return {
+            account: accountNotifications.account,
+            notifications: notifications,
+            error: null,
+          };
+        } catch (error) {
+          return {
+            account: accountNotifications.account,
+            notifications: [],
+            error: determineFailureType(error),
+          };
+        }
       }),
   );
 


Test patch:
diff --git a/src/components/AccountNotifications.test.tsx b/src/components/AccountNotifications.test.tsx
index 3b970f4b..7df67540 100644
--- a/src/components/AccountNotifications.test.tsx
+++ b/src/components/AccountNotifications.test.tsx
@@ -11,11 +11,18 @@ jest.mock('./RepositoryNotifications', () => ({
 }));
 
 describe('components/AccountNotifications.tsx', () => {
-  it('should render itself (github.com with notifications) - group by repositories', () => {
+  // The read emoji randomly rotates, but then the snapshots would never match
+  // Have to make it consistent so the emojis are always the same
+  beforeEach(() => {
+    global.Math.random = jest.fn(() => 0.1);
+  });
+
+  it('should render itself - group notifications by repositories', () => {
     const props = {
       account: mockGitHubCloudAccount,
       notifications: mockGitHubNotifications,
       showAccountHostname: true,
+      error: null,
     };
 
     const tree = render(
@@ -28,11 +35,12 @@ describe('components/AccountNotifications.tsx', () => {
     expect(tree).toMatchSnapshot();
   });
 
-  it('should render itself (github.com with notifications) - group by date', () => {
+  it('should render itself - group notifications by date', () => {
     const props = {
       account: mockGitHubCloudAccount,
       notifications: mockGitHubNotifications,
       showAccountHostname: true,
+      error: null,
     };
 
     const tree = render(
@@ -45,10 +53,31 @@ describe('components/AccountNotifications.tsx', () => {
     expect(tree).toMatchSnapshot();
   });
 
-  it('should render itself (github.com without notifications)', () => {
+  it('should render itself - no notifications', () => {
+    const props = {
+      account: mockGitHubCloudAccount,
+      notifications: [],
+      showAccountHostname: true,
+      error: null,
+    };
+
+    const tree = render(
+      <AppContext.Provider value={{ settings: mockSettings }}>
+        <AccountNotifications {...props} />
+      </AppContext.Provider>,
+    );
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should render itself - account error', () => {
     const props = {
       account: mockGitHubCloudAccount,
       notifications: [],
+      error: {
+        title: 'Error title',
+        descriptions: ['Error description'],
+        emojis: ['🔥'],
+      },
       showAccountHostname: true,
     };
 
@@ -69,6 +98,7 @@ describe('components/AccountNotifications.tsx', () => {
       account: mockGitHubCloudAccount,
       notifications: [],
       showAccountHostname: true,
+      error: null,
     };
 
     await act(async () => {
@@ -90,6 +120,7 @@ describe('components/AccountNotifications.tsx', () => {
       account: mockGitHubCloudAccount,
       notifications: mockGitHubNotifications,
       showAccountHostname: true,
+      error: null,
     };
 
     await act(async () => {
diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx
index 7958fd2e..3ecf154e 100644
--- a/src/components/Sidebar.test.tsx
+++ b/src/components/Sidebar.test.tsx
@@ -1,7 +1,7 @@
 import { fireEvent, render, screen } from '@testing-library/react';
 import { MemoryRouter } from 'react-router-dom';
 import { mockAccountNotifications } from '../__mocks__/notifications-mocks';
-import { mockSettings } from '../__mocks__/state-mocks';
+import { mockAuth, mockSettings } from '../__mocks__/state-mocks';
 import { AppContext } from '../context/App';
 import { IconColor } from '../types';
 import * as comms from '../utils/comms';
@@ -27,8 +27,9 @@ describe('components/Sidebar.tsx', () => {
     const tree = render(
       <AppContext.Provider
         value={{
-          settings: mockSettings,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
+          settings: mockSettings,
         }}
       >
         <MemoryRouter>
@@ -46,6 +47,7 @@ describe('components/Sidebar.tsx', () => {
         value={{
           isLoggedIn: false,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
           settings: mockSettings,
         }}
       >
@@ -61,7 +63,12 @@ describe('components/Sidebar.tsx', () => {
   it('should open the gitify repository', () => {
     render(
       <AppContext.Provider
-        value={{ isLoggedIn: false, notifications: [], settings: mockSettings }}
+        value={{
+          isLoggedIn: false,
+          notifications: [],
+          auth: mockAuth,
+          settings: mockSettings,
+        }}
       >
         <MemoryRouter>
           <Sidebar />
@@ -85,6 +92,7 @@ describe('components/Sidebar.tsx', () => {
             value={{
               isLoggedIn: true,
               notifications: [],
+              auth: mockAuth,
               settings: mockSettings,
             }}
           >
@@ -114,6 +122,7 @@ describe('components/Sidebar.tsx', () => {
             value={{
               isLoggedIn: true,
               notifications: mockAccountNotifications,
+              auth: mockAuth,
               settings: mockSettings,
             }}
           >
@@ -146,6 +155,7 @@ describe('components/Sidebar.tsx', () => {
         value={{
           isLoggedIn: true,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
           settings: mockSettings,
         }}
       >
@@ -169,6 +179,7 @@ describe('components/Sidebar.tsx', () => {
         value={{
           isLoggedIn: true,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
           settings: mockSettings,
         }}
       >
@@ -193,6 +204,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
             fetchNotifications,
             status: 'success',
@@ -215,6 +227,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
             fetchNotifications,
             status: 'loading',
@@ -239,6 +252,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
           }}
         >
@@ -257,6 +271,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
           }}
         >
@@ -277,6 +292,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
           }}
         >
@@ -297,6 +313,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
             fetchNotifications,
           }}
@@ -322,6 +339,7 @@ describe('components/Sidebar.tsx', () => {
         value={{
           isLoggedIn: true,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
           settings: mockSettings,
         }}
       >
@@ -345,6 +363,7 @@ describe('components/Sidebar.tsx', () => {
         value={{
           isLoggedIn: true,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
           settings: mockSettings,
         }}
       >
@@ -368,6 +387,7 @@ describe('components/Sidebar.tsx', () => {
         value={{
           isLoggedIn: true,
           notifications: mockAccountNotifications,
+          auth: mockAuth,
           settings: mockSettings,
         }}
       >
@@ -388,7 +408,12 @@ describe('components/Sidebar.tsx', () => {
 
     render(
       <AppContext.Provider
-        value={{ isLoggedIn: false, notifications: [], settings: mockSettings }}
+        value={{
+          isLoggedIn: false,
+          notifications: [],
+          auth: mockAuth,
+          settings: mockSettings,
+        }}
       >
         <MemoryRouter>
           <Sidebar />
@@ -406,7 +431,12 @@ describe('components/Sidebar.tsx', () => {
 
     render(
       <AppContext.Provider
-        value={{ isLoggedIn: false, notifications: [], settings: mockSettings }}
+        value={{
+          isLoggedIn: false,
+          notifications: [],
+          auth: mockAuth,
+          settings: mockSettings,
+        }}
       >
         <MemoryRouter>
           <Sidebar />
@@ -427,6 +457,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: [],
+            auth: mockAuth,
             settings: mockSettings,
           }}
         >
@@ -448,6 +479,7 @@ describe('components/Sidebar.tsx', () => {
           value={{
             isLoggedIn: true,
             notifications: mockAccountNotifications,
+            auth: mockAuth,
             settings: mockSettings,
           }}
         >
diff --git a/src/components/__snapshots__/AccountNotifications.test.tsx.snap b/src/components/__snapshots__/AccountNotifications.test.tsx.snap
index a466615a..f37cb8bc 100644
--- a/src/components/__snapshots__/AccountNotifications.test.tsx.snap
+++ b/src/components/__snapshots__/AccountNotifications.test.tsx.snap
@@ -1,12 +1,239 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`components/AccountNotifications.tsx should render itself (github.com with notifications) - group by date 1`] = `
+exports[`components/AccountNotifications.tsx should render itself - account error 1`] = `
 {
   "asFragment": [Function],
   "baseElement": <body>
     <div>
       <div
-        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-red-300 dark:bg-red-500 opacity-70"
+      >
+        <div
+          class="flex"
+        >
+          <div
+            class="mr-3 flex items-center justify-center"
+          >
+            <span
+              class="mr-1"
+              title="GitHub Cloud"
+            >
+              <svg
+                aria-hidden="true"
+                class="octicon octicon-mark-github"
+                fill="currentColor"
+                focusable="false"
+                height="16"
+                style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+                viewBox="0 0 16 16"
+                width="16"
+              >
+                <path
+                  d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+                />
+              </svg>
+            </span>
+          </div>
+          <button
+            title="Open Profile"
+            type="button"
+          >
+            @
+            octocat
+          </button>
+        </div>
+        <div
+          class="flex items-center justify-center gap-2 opacity-0 transition-opacity group-hover:opacity-80"
+        >
+          <button
+            class="h-full hover:text-green-500 focus:outline-none"
+            title="No notifications for account"
+            type="button"
+          >
+            <svg
+              aria-label="No notifications for account"
+              class="octicon octicon-chevron-left"
+              fill="currentColor"
+              focusable="false"
+              height="14"
+              role="img"
+              style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+              viewBox="0 0 12 12"
+              width="14"
+            >
+              <path
+                d="M3.587 6.025c0 .2.1.4.2.5l3.3 3.3c.3.3.8.3 1.1 0 .3-.3.3-.8 0-1.1l-2.7-2.7 2.7-2.7c.3-.3.3-.8 0-1.1-.3-.3-.8-.3-1.1 0l-3.2 3.2c-.2.2-.3.4-.3.6Z"
+              />
+            </svg>
+          </button>
+        </div>
+      </div>
+      <div
+        class="flex flex-1 flex-col items-center justify-center p-4"
+      >
+        <h1
+          class="mt-2 mb-5 text-5xl"
+        >
+          🔥
+        </h1>
+        <h2
+          class="mb-2 text-xl font-semibold"
+        >
+          Error title
+        </h2>
+        <div
+          class="mb-2 text-center"
+        >
+          Error description
+        </div>
+      </div>
+    </div>
+  </body>,
+  "container": <div>
+    <div
+      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-red-300 dark:bg-red-500 opacity-70"
+    >
+      <div
+        class="flex"
+      >
+        <div
+          class="mr-3 flex items-center justify-center"
+        >
+          <span
+            class="mr-1"
+            title="GitHub Cloud"
+          >
+            <svg
+              aria-hidden="true"
+              class="octicon octicon-mark-github"
+              fill="currentColor"
+              focusable="false"
+              height="16"
+              style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+              viewBox="0 0 16 16"
+              width="16"
+            >
+              <path
+                d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"
+              />
+            </svg>
+          </span>
+        </div>
+        <button
+          title="Open Profile"
+          type="button"
+        >
+          @
+          octocat
+        </button>
+      </div>
+      <div
+        class="flex items-center justify-center gap-2 opacity-0 transition-opacity group-hover:opacity-80"
+      >
+        <button
+          class="h-full hover:text-green-500 focus:outline-none"
+          title="No notifications for account"
+          type="button"
+        >
+          <svg
+            aria-label="No notifications for account"
+            class="octicon octicon-chevron-left"
+            fill="currentColor"
+            focusable="false"
+            height="14"
+            role="img"
+            style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+            viewBox="0 0 12 12"
+            width="14"
+          >
+            <path
+              d="M3.587 6.025c0 .2.1.4.2.5l3.3 3.3c.3.3.8.3 1.1 0 .3-.3.3-.8 0-1.1l-2.7-2.7 2.7-2.7c.3-.3.3-.8 0-1.1-.3-.3-.8-.3-1.1 0l-3.2 3.2c-.2.2-.3.4-.3.6Z"
+            />
+          </svg>
+        </button>
+      </div>
+    </div>
+    <div
+      class="flex flex-1 flex-col items-center justify-center p-4"
+    >
+      <h1
+        class="mt-2 mb-5 text-5xl"
+      >
+        🔥
+      </h1>
+      <h2
+        class="mb-2 text-xl font-semibold"
+      >
+        Error title
+      </h2>
+      <div
+        class="mb-2 text-center"
+      >
+        Error description
+      </div>
+    </div>
+  </div>,
+  "debug": [Function],
+  "findAllByAltText": [Function],
+  "findAllByDisplayValue": [Function],
+  "findAllByLabelText": [Function],
+  "findAllByPlaceholderText": [Function],
+  "findAllByRole": [Function],
+  "findAllByTestId": [Function],
+  "findAllByText": [Function],
+  "findAllByTitle": [Function],
+  "findByAltText": [Function],
+  "findByDisplayValue": [Function],
+  "findByLabelText": [Function],
+  "findByPlaceholderText": [Function],
+  "findByRole": [Function],
+  "findByTestId": [Function],
+  "findByText": [Function],
+  "findByTitle": [Function],
+  "getAllByAltText": [Function],
+  "getAllByDisplayValue": [Function],
+  "getAllByLabelText": [Function],
+  "getAllByPlaceholderText": [Function],
+  "getAllByRole": [Function],
+  "getAllByTestId": [Function],
+  "getAllByText": [Function],
+  "getAllByTitle": [Function],
+  "getByAltText": [Function],
+  "getByDisplayValue": [Function],
+  "getByLabelText": [Function],
+  "getByPlaceholderText": [Function],
+  "getByRole": [Function],
+  "getByTestId": [Function],
+  "getByText": [Function],
+  "getByTitle": [Function],
+  "queryAllByAltText": [Function],
+  "queryAllByDisplayValue": [Function],
+  "queryAllByLabelText": [Function],
+  "queryAllByPlaceholderText": [Function],
+  "queryAllByRole": [Function],
+  "queryAllByTestId": [Function],
+  "queryAllByText": [Function],
+  "queryAllByTitle": [Function],
+  "queryByAltText": [Function],
+  "queryByDisplayValue": [Function],
+  "queryByLabelText": [Function],
+  "queryByPlaceholderText": [Function],
+  "queryByRole": [Function],
+  "queryByTestId": [Function],
+  "queryByText": [Function],
+  "queryByTitle": [Function],
+  "rerender": [Function],
+  "unmount": [Function],
+}
+`;
+
+exports[`components/AccountNotifications.tsx should render itself - group notifications by date 1`] = `
+{
+  "asFragment": [Function],
+  "baseElement": <body>
+    <div>
+      <div
+        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
       >
         <div
           class="flex"
@@ -438,7 +665,7 @@ exports[`components/AccountNotifications.tsx should render itself (github.com wi
   </body>,
   "container": <div>
     <div
-      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
     >
       <div
         class="flex"
@@ -921,13 +1148,13 @@ exports[`components/AccountNotifications.tsx should render itself (github.com wi
 }
 `;
 
-exports[`components/AccountNotifications.tsx should render itself (github.com with notifications) - group by repositories 1`] = `
+exports[`components/AccountNotifications.tsx should render itself - group notifications by repositories 1`] = `
 {
   "asFragment": [Function],
   "baseElement": <body>
     <div>
       <div
-        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
       >
         <div
           class="flex"
@@ -996,7 +1223,7 @@ exports[`components/AccountNotifications.tsx should render itself (github.com wi
   </body>,
   "container": <div>
     <div
-      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
     >
       <div
         class="flex"
@@ -1116,13 +1343,13 @@ exports[`components/AccountNotifications.tsx should render itself (github.com wi
 }
 `;
 
-exports[`components/AccountNotifications.tsx should render itself (github.com without notifications) 1`] = `
+exports[`components/AccountNotifications.tsx should render itself - no notifications 1`] = `
 {
   "asFragment": [Function],
   "baseElement": <body>
     <div>
       <div
-        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
       >
         <div
           class="flex"
@@ -1184,11 +1411,25 @@ exports[`components/AccountNotifications.tsx should render itself (github.com wi
           </button>
         </div>
       </div>
+      <div
+        class="flex flex-1 flex-col items-center justify-center bg-white p-4 text-black dark:bg-gray-dark dark:text-white"
+      >
+        <h1
+          class="mt-2 mb-5 text-5xl"
+        >
+          🎊
+        </h1>
+        <h2
+          class="mb-2 text-xl font-semibold"
+        >
+          No new notifications.
+        </h2>
+      </div>
     </div>
   </body>,
   "container": <div>
     <div
-      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
     >
       <div
         class="flex"
@@ -1250,6 +1491,20 @@ exports[`components/AccountNotifications.tsx should render itself (github.com wi
         </button>
       </div>
     </div>
+    <div
+      class="flex flex-1 flex-col items-center justify-center bg-white p-4 text-black dark:bg-gray-dark dark:text-white"
+    >
+      <h1
+        class="mt-2 mb-5 text-5xl"
+      >
+        🎊
+      </h1>
+      <h2
+        class="mb-2 text-xl font-semibold"
+      >
+        No new notifications.
+      </h2>
+    </div>
   </div>,
   "debug": [Function],
   "findAllByAltText": [Function],
@@ -1311,7 +1566,7 @@ exports[`components/AccountNotifications.tsx should toggle account notifications
   "baseElement": <body>
     <div>
       <div
-        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
       >
         <div
           class="flex"
@@ -1376,7 +1631,7 @@ exports[`components/AccountNotifications.tsx should toggle account notifications
     </div>
     <div>
       <div
-        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+        class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
       >
         <div
           class="flex"
@@ -1445,7 +1700,7 @@ exports[`components/AccountNotifications.tsx should toggle account notifications
   </body>,
   "container": <div>
     <div
-      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold bg-gray-300 dark:bg-gray-darkest dark:text-white opacity-70"
+      class="group flex items-center justify-between px-3 py-1.5 text-sm font-semibold dark:text-white bg-gray-300 dark:bg-gray-darkest opacity-70"
     >
       <div
         class="flex"
diff --git a/src/components/__snapshots__/AllRead.test.tsx.snap b/src/components/__snapshots__/AllRead.test.tsx.snap
index 0e2e61cb..484584b5 100644
--- a/src/components/__snapshots__/AllRead.test.tsx.snap
+++ b/src/components/__snapshots__/AllRead.test.tsx.snap
@@ -9,7 +9,7 @@ exports[`components/AllRead.tsx should render itself & its children 1`] = `
         class="flex flex-1 flex-col items-center justify-center bg-white p-4 text-black dark:bg-gray-dark dark:text-white"
       >
         <h1
-          class="mb-5 text-5xl"
+          class="mt-2 mb-5 text-5xl"
         >
           🎊
         </h1>
@@ -26,7 +26,7 @@ exports[`components/AllRead.tsx should render itself & its children 1`] = `
       class="flex flex-1 flex-col items-center justify-center bg-white p-4 text-black dark:bg-gray-dark dark:text-white"
     >
       <h1
-        class="mb-5 text-5xl"
+        class="mt-2 mb-5 text-5xl"
       >
         🎊
       </h1>
diff --git a/src/components/__snapshots__/Oops.test.tsx.snap b/src/components/__snapshots__/Oops.test.tsx.snap
index 3c3a7b93..654158f4 100644
--- a/src/components/__snapshots__/Oops.test.tsx.snap
+++ b/src/components/__snapshots__/Oops.test.tsx.snap
@@ -9,7 +9,7 @@ exports[`components/Oops.tsx should render itself & its children 1`] = `
         class="flex flex-1 flex-col items-center justify-center p-4"
       >
         <h1
-          class="mb-5 text-5xl"
+          class="mt-2 mb-5 text-5xl"
         >
           🔥
         </h1>
@@ -31,7 +31,7 @@ exports[`components/Oops.tsx should render itself & its children 1`] = `
       class="flex flex-1 flex-col items-center justify-center p-4"
     >
       <h1
-        class="mb-5 text-5xl"
+        class="mt-2 mb-5 text-5xl"
       >
         🔥
       </h1>
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index e4b3af7c..730364ea 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -252,10 +252,10 @@ describe('hooks/useNotifications.ts', () => {
       expect(result.current.notifications[0].notifications.length).toBe(6);
     });
 
-    it('should fetch notifications with failures', async () => {
+    it('should fetch notifications with same failures', async () => {
       const code = AxiosError.ERR_BAD_REQUEST;
-      const status = 400;
-      const message = 'Oops! Something went wrong.';
+      const status = 401;
+      const message = 'Bad credentials';
 
       nock('https://api.github.com/')
         .get('/notifications?participating=false')
@@ -293,7 +293,49 @@ describe('hooks/useNotifications.ts', () => {
         expect(result.current.status).toBe('error');
       });
 
-      expect(result.current.errorDetails).toBe(Errors.UNKNOWN);
+      expect(result.current.globalError).toBe(Errors.BAD_CREDENTIALS);
+    });
+
+    it('should fetch notifications with different failures', async () => {
+      const code = AxiosError.ERR_BAD_REQUEST;
+
+      nock('https://api.github.com/')
+        .get('/notifications?participating=false')
+        .replyWithError({
+          code,
+          response: {
+            status: 400,
+            data: {
+              message: 'Oops! Something went wrong.',
+            },
+          },
+        });
+
+      nock('https://github.gitify.io/api/v3/')
+        .get('/notifications?participating=false')
+        .replyWithError({
+          code,
+          response: {
+            status: 401,
+            data: {
+              message: 'Bad credentials',
+            },
+          },
+        });
+
+      const { result } = renderHook(() => useNotifications());
+
+      act(() => {
+        result.current.fetchNotifications(mockState);
+      });
+
+      expect(result.current.status).toBe('loading');
+
+      await waitFor(() => {
+        expect(result.current.status).toBe('error');
+      });
+
+      expect(result.current.globalError).toBeNull();
     });
   });
 
diff --git a/src/routes/Notifications.test.tsx b/src/routes/Notifications.test.tsx
index 6fb0edc3..207083b4 100644
--- a/src/routes/Notifications.test.tsx
+++ b/src/routes/Notifications.test.tsx
@@ -58,7 +58,7 @@ describe('routes/Notifications.tsx', () => {
           value={{
             notifications: [],
             status: 'error',
-            errorDetails: Errors.BAD_CREDENTIALS,
+            globalError: Errors.BAD_CREDENTIALS,
           }}
         >
           <NotificationsRoute />
@@ -74,7 +74,7 @@ describe('routes/Notifications.tsx', () => {
           value={{
             notifications: [],
             status: 'error',
-            errorDetails: Errors.MISSING_SCOPES,
+            globalError: Errors.MISSING_SCOPES,
           }}
         >
           <NotificationsRoute />
@@ -90,7 +90,7 @@ describe('routes/Notifications.tsx', () => {
           value={{
             notifications: [],
             status: 'error',
-            errorDetails: Errors.RATE_LIMITED,
+            globalError: Errors.RATE_LIMITED,
           }}
         >
           <NotificationsRoute />
@@ -106,7 +106,7 @@ describe('routes/Notifications.tsx', () => {
           value={{
             notifications: [],
             status: 'error',
-            errorDetails: Errors.UNKNOWN,
+            globalError: Errors.UNKNOWN,
           }}
         >
           <NotificationsRoute />
@@ -122,7 +122,7 @@ describe('routes/Notifications.tsx', () => {
           value={{
             notifications: [],
             status: 'error',
-            errorDetails: null,
+            globalError: null,
           }}
         >
           <NotificationsRoute />
diff --git a/src/routes/__snapshots__/Notifications.test.tsx.snap b/src/routes/__snapshots__/Notifications.test.tsx.snap
index 8bb99d30..5dbd7314 100644
--- a/src/routes/__snapshots__/Notifications.test.tsx.snap
+++ b/src/routes/__snapshots__/Notifications.test.tsx.snap
@@ -144,13 +144,13 @@ exports[`routes/Notifications.tsx should render itself & its children (error con
   "baseElement": <body>
     <div>
       <p>
-        Oops
+        AllRead
       </p>
     </div>
   </body>,
   "container": <div>
     <p>
-      Oops
+      AllRead
     </p>
   </div>,
   "debug": [Function],
diff --git a/src/utils/links.test.ts b/src/utils/links.test.ts
index ee1bb5a5..d4c4f656 100644
--- a/src/utils/links.test.ts
+++ b/src/utils/links.test.ts
@@ -46,21 +46,21 @@ describe('utils/links.ts', () => {
   });
 
   it('openGitHubNotifications', () => {
-    openGitHubNotifications();
+    openGitHubNotifications(mockGitHubCloudAccount.hostname);
     expect(openExternalLinkMock).toHaveBeenCalledWith(
       'https://github.com/notifications',
     );
   });
 
   it('openGitHubIssues', () => {
-    openGitHubIssues();
+    openGitHubIssues(mockGitHubCloudAccount.hostname);
     expect(openExternalLinkMock).toHaveBeenCalledWith(
       'https://github.com/issues',
     );
   });
 
   it('openGitHubPulls', () => {
-    openGitHubPulls();
+    openGitHubPulls(mockGitHubCloudAccount.hostname);
     expect(openExternalLinkMock).toHaveBeenCalledWith(
       'https://github.com/pulls',
     );