Repository: gitify-app/gitify
Pull request #1038 (2024-04-17 21:06:24)

fix: duplicate system notifications
Closes: #1029 


Patch:
diff --git a/biome.json b/biome.json
index 20771d45..1cf48ce0 100644
--- a/biome.json
+++ b/biome.json
@@ -1,5 +1,5 @@
 {
-  "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
+  "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
   "organizeImports": {
     "enabled": true
   },
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 79b2449b..ed5b3f54 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -17,11 +17,11 @@ import { AppContext } from '../context/App';
 import type { Notification } from '../typesGithub';
 import { openExternalLink } from '../utils/comms';
 import {
-  formatReason,
   getNotificationTypeIcon,
   getNotificationTypeIconColor,
 } from '../utils/github-api';
 import { formatForDisplay, openInBrowser } from '../utils/helpers';
+import { formatReason } from '../utils/reason';
 
 interface IProps {
   hostname: string;
@@ -36,10 +36,11 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
     markNotificationRead,
     markNotificationDone,
     unsubscribeNotification,
+    notifications,
   } = useContext(AppContext);
 
-  const pressTitle = useCallback(() => {
-    openBrowser();
+  const openNotification = useCallback(() => {
+    openInBrowser(notification, accounts);
 
     if (settings.markAsDoneOnOpen) {
       markNotificationDone(notification.id, hostname);
@@ -47,12 +48,7 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
       // no need to mark as read, github does it by default when opening it
       removeNotificationFromState(notification.id, hostname);
     }
-  }, [settings]);
-
-  const openBrowser = useCallback(
-    () => openInBrowser(notification, accounts),
-    [notification],
-  );
+  }, [notifications, notification, accounts, settings]); // notifications required here to prevent weird state issues
 
   const unsubscribe = (event: MouseEvent<HTMLElement>) => {
     // Don't trigger onClick of parent element.
@@ -96,8 +92,8 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
 
       <div
         className="flex-1 overflow-hidden"
-        onClick={() => pressTitle()}
-        onKeyDown={() => pressTitle()}
+        onClick={() => openNotification()}
+        onKeyDown={() => openNotification()}
       >
         <div
           className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden cursor-pointer"
@@ -132,7 +128,7 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
                 </span>
               )}
               <span className="ml-1" title={reason.description}>
-                {reason.type}
+                {reason.title}
               </span>
               <span className="ml-1">{updatedAt}</span>
             </span>
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index 896c33a7..216e5b2a 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -6,14 +6,7 @@ import {
 } from '@primer/octicons-react';
 import { ipcRenderer } from 'electron';
 
-import {
-  type FC,
-  useCallback,
-  useContext,
-  useEffect,
-  useMemo,
-  useRef,
-} from 'react';
+import { type FC, useCallback, useContext, useMemo } from 'react';
 import { useLocation, useNavigate } from 'react-router-dom';
 
 import { Logo } from '../components/Logo';
@@ -29,38 +22,6 @@ export const Sidebar: FC = () => {
   const { notifications, fetchNotifications, isLoggedIn, isFetching } =
     useContext(AppContext);
 
-  const useFetchInterval = (callback, delay: number) => {
-    const savedCallback = useRef(callback);
-    const intervalRef = useRef(null);
-
-    useEffect(() => {
-      savedCallback.current = callback;
-    }, [callback]);
-
-    useEffect(() => {
-      if (delay !== null) {
-        const id = setInterval(savedCallback.current, delay);
-        intervalRef.current = id;
-        return () => clearInterval(id);
-      }
-    }, [delay]);
-
-    const resetFetchInterval = useCallback(() => {
-      if (intervalRef.current !== null) {
-        clearInterval(intervalRef.current);
-        intervalRef.current = setInterval(savedCallback.current, delay);
-      }
-    }, [delay]);
-
-    return { resetFetchInterval };
-  };
-
-  const { resetFetchInterval } = useFetchInterval(() => {
-    if (isLoggedIn) {
-      fetchNotifications();
-    }
-  }, Constants.FETCH_INTERVAL);
-
   const onOpenBrowser = useCallback(() => {
     openExternalLink(`https://github.com/${Constants.REPO_SLUG}`);
   }, []);
@@ -119,7 +80,6 @@ export const Sidebar: FC = () => {
               onClick={() => {
                 navigate('/', { replace: true });
                 fetchNotifications();
-                resetFetchInterval();
               }}
               disabled={isFetching}
             >
diff --git a/src/context/App.tsx b/src/context/App.tsx
index 48d811a2..3907fb97 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -7,6 +7,7 @@ import {
   useState,
 } from 'react';
 
+import { useInterval } from '../hooks/useInterval';
 import { useNotifications } from '../hooks/useNotifications';
 import {
   type AccountNotifications,
@@ -110,6 +111,10 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     accounts.enterpriseAccounts.length,
   ]);
 
+  useInterval(() => {
+    fetchNotifications(accounts, settings);
+  }, Constants.FETCH_INTERVAL);
+
   // biome-ignore lint/correctness/useExhaustiveDependencies: We need to update tray title when settings or notifications changes.
   useEffect(() => {
     const count = getNotificationCount(notifications);
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index 0e0de767..77c0e00e 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -36,6 +36,7 @@ export const SettingsRoute: FC = () => {
   const navigate = useNavigate();
 
   const [isLinux, setIsLinux] = useState<boolean>(false);
+  const [isMacOS, setIsMacOS] = useState<boolean>(false);
   const [appVersion, setAppVersion] = useState<string | null>(null);
   const [repoScope, setRepoScope] = useState<boolean>(false);
 
@@ -57,6 +58,7 @@ export const SettingsRoute: FC = () => {
   useEffect(() => {
     ipcRenderer.invoke('get-platform').then((result: string) => {
       setIsLinux(result === 'linux');
+      setIsMacOS(result === 'darwin');
     });
 
     ipcRenderer.invoke('get-app-version').then((result: string) => {
@@ -240,14 +242,19 @@ export const SettingsRoute: FC = () => {
           <legend id="system" className="font-semibold mt-2 mb-1">
             System
           </legend>
-          <Checkbox
-            name="showNotificationsCountInTray"
-            label="Show notifications count in tray"
-            checked={settings.showNotificationsCountInTray}
-            onChange={(evt) =>
-              updateSetting('showNotificationsCountInTray', evt.target.checked)
-            }
-          />
+          {isMacOS && (
+            <Checkbox
+              name="showNotificationsCountInTray"
+              label="Show notifications count in tray"
+              checked={settings.showNotificationsCountInTray}
+              onChange={(evt) =>
+                updateSetting(
+                  'showNotificationsCountInTray',
+                  evt.target.checked,
+                )
+              }
+            />
+          )}
           <Checkbox
             name="showNotifications"
             label="Show system notifications"
diff --git a/src/types.ts b/src/types.ts
index 77c9e77c..1b318fb8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -88,3 +88,8 @@ export type ErrorType =
   | 'NETWORK'
   | 'RATE_LIMITED'
   | 'UNKNOWN';
+
+export interface FormattedReason {
+  title: string;
+  description: string;
+}
diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts
index 9898080e..f392fdfe 100644
--- a/src/utils/github-api.ts
+++ b/src/utils/github-api.ts
@@ -24,99 +24,7 @@ import {
   XIcon,
 } from '@primer/octicons-react';
 import type { FC } from 'react';
-import type { Reason, Subject } from '../typesGithub';
-
-const DESCRIPTIONS = {
-  APPROVAL_REQUESTED: 'You were requested to review and approve a deployment.',
-  ASSIGN: 'You were assigned to the issue.',
-  AUTHOR: 'You created the thread.',
-  CI_ACTIVITY:
-    'A GitHub Actions workflow run was triggered for your repository',
-  COMMENT: 'You commented on the thread.',
-  INVITATION: 'You accepted an invitation to contribute to the repository.',
-  MANUAL: 'You subscribed to the thread (via an issue or pull request).',
-  MEMBER_FEATURE_REQUESTED:
-    'Organization members have requested to enable a feature such as Draft Pull Requests or CoPilot.',
-  MENTION: 'You were specifically @mentioned in the content.',
-  REVIEW_REQUESTED:
-    "You, or a team you're a member of, were requested to review a pull request.",
-  SECURITY_ADVISORY_CREDIT:
-    'You were credited for contributing to a security advisory.',
-  SECURITY_ALERT:
-    'GitHub discovered a security vulnerability in your repository.',
-  STATE_CHANGE:
-    'You changed the thread state (for example, closing an issue or merging a pull request).',
-  SUBSCRIBED: "You're watching the repository.",
-  TEAM_MENTION: 'You were on a team that was mentioned.',
-  UNKNOWN: 'The reason for this notification is not supported by the app.',
-};
-
-export function formatReason(reason: Reason): {
-  type: string;
-  description: string;
-} {
-  switch (reason) {
-    case 'approval_requested':
-      return {
-        type: 'Approval Requested',
-        description: DESCRIPTIONS.APPROVAL_REQUESTED,
-      };
-    case 'assign':
-      return { type: 'Assigned', description: DESCRIPTIONS.ASSIGN };
-    case 'author':
-      return { type: 'Authored', description: DESCRIPTIONS.AUTHOR };
-    case 'ci_activity':
-      return {
-        type: 'Workflow Run Completed',
-        description: DESCRIPTIONS.CI_ACTIVITY,
-      };
-    case 'comment':
-      return { type: 'Commented', description: DESCRIPTIONS.COMMENT };
-    case 'invitation':
-      return {
-        type: 'Invitation Received',
-        description: DESCRIPTIONS.INVITATION,
-      };
-    case 'manual':
-      return { type: 'Updated', description: DESCRIPTIONS.MANUAL };
-    case 'member_feature_requested':
-      return {
-        type: 'Member Feature Requested',
-        description: DESCRIPTIONS.MEMBER_FEATURE_REQUESTED,
-      };
-    case 'mention':
-      return { type: 'Mentioned', description: DESCRIPTIONS.MENTION };
-    case 'review_requested':
-      return {
-        type: 'Review Requested',
-        description: DESCRIPTIONS.REVIEW_REQUESTED,
-      };
-    case 'security_advisory_credit':
-      return {
-        type: 'Security Advisory Credit Recevied',
-        description: DESCRIPTIONS.SECURITY_ADVISORY_CREDIT,
-      };
-    case 'security_alert':
-      return {
-        type: 'Security Alert Received',
-        description: DESCRIPTIONS.SECURITY_ALERT,
-      };
-    case 'state_change':
-      return {
-        type: 'State Changed',
-        description: DESCRIPTIONS.STATE_CHANGE,
-      };
-    case 'subscribed':
-      return { type: 'Updated', description: DESCRIPTIONS.SUBSCRIBED };
-    case 'team_mention':
-      return {
-        type: 'Team Mentioned',
-        description: DESCRIPTIONS.TEAM_MENTION,
-      };
-    default:
-      return { type: 'Unknown', description: DESCRIPTIONS.UNKNOWN };
-  }
-}
+import type { Subject } from '../typesGithub';
 
 export function getNotificationTypeIcon(subject: Subject): FC<OcticonProps> {
   switch (subject.type) {
diff --git a/src/utils/reason.ts b/src/utils/reason.ts
new file mode 100644
index 00000000..1eab5491
--- /dev/null
+++ b/src/utils/reason.ts
@@ -0,0 +1,79 @@
+import type { FormattedReason } from '../types';
+import type { Reason } from '../typesGithub';
+
+const FORMATTED_REASONS: Record<Reason, FormattedReason> = {
+  approval_requested: {
+    title: 'Approval Requested',
+    description: 'You were requested to review and approve a deployment.',
+  },
+  assign: {
+    title: 'Assigned',
+    description: 'You were assigned to the issue.',
+  },
+  author: {
+    title: 'Authored',
+    description: 'You created the thread.',
+  },
+  ci_activity: {
+    title: 'Workflow Run Completed',
+    description:
+      'A GitHub Actions workflow run was triggered for your repository.',
+  },
+  comment: {
+    title: 'Commented',
+    description: 'You commented on the thread.',
+  },
+  invitation: {
+    title: 'Invitation Received',
+    description: 'You accepted an invitation to contribute to the repository.',
+  },
+  manual: {
+    title: 'Updated',
+    description: 'You subscribed to the thread (via an issue or pull request).',
+  },
+  member_feature_requested: {
+    title: 'Member Feature Requested',
+    description:
+      'Organization members have requested to enable a feature such as Draft Pull Requests or Copilot.',
+  },
+  mention: {
+    title: 'Mentioned',
+    description: 'You were specifically @mentioned in the content.',
+  },
+  review_requested: {
+    title: 'Review Requested',
+    description:
+      "You, or a team you're a member of, were requested to review a pull request.",
+  },
+  security_advisory_credit: {
+    title: 'Security Advisory Credit Received',
+    description: 'You were credited for contributing to a security advisory.',
+  },
+  security_alert: {
+    title: 'Security Alert Received',
+    description:
+      'GitHub discovered a security vulnerability in your repository.',
+  },
+  state_change: {
+    title: 'State Changed',
+    description:
+      'You changed the thread state (for example, closing an issue or merging a pull request).',
+  },
+  subscribed: {
+    title: 'Updated',
+    description: "You're watching the repository.",
+  },
+  team_mention: {
+    title: 'Team Mentioned',
+    description: 'You were on a team that was mentioned.',
+  },
+};
+
+const UNKNOWN_REASON: FormattedReason = {
+  title: 'Unknown',
+  description: 'The reason for this notification is not supported by the app.',
+};
+
+export function formatReason(reason: Reason): FormattedReason {
+  return FORMATTED_REASONS[reason] || UNKNOWN_REASON;
+}


Test patch:
diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx
index 270cdbb3..281ea73c 100644
--- a/src/components/Sidebar.test.tsx
+++ b/src/components/Sidebar.test.tsx
@@ -1,11 +1,10 @@
-import { act, fireEvent, render, screen } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
 import { MemoryRouter } from 'react-router-dom';
 import * as TestRenderer from 'react-test-renderer';
 const { shell, ipcRenderer } = require('electron');
 import { mockSettings } from '../__mocks__/mock-state';
 import { mockedAccountNotifications } from '../__mocks__/mockedData';
 import { AppContext } from '../context/App';
-import Constants from '../utils/constants';
 import { Sidebar } from './Sidebar';
 
 const mockNavigate = jest.fn();
@@ -20,15 +19,12 @@ describe('components/Sidebar.tsx', () => {
   beforeEach(() => {
     fetchNotifications.mockReset();
 
-    jest.useFakeTimers();
-
     jest.spyOn(ipcRenderer, 'send');
     jest.spyOn(shell, 'openExternal');
     jest.spyOn(window, 'clearInterval');
   });
 
   afterEach(() => {
-    jest.clearAllTimers();
     jest.clearAllMocks();
   });
 
@@ -61,37 +57,6 @@ describe('components/Sidebar.tsx', () => {
     expect(tree).toMatchSnapshot();
   });
 
-  it('should fetch notifications every minute', async () => {
-    render(
-      <AppContext.Provider
-        value={{ isLoggedIn: true, notifications: [], fetchNotifications }}
-      >
-        <MemoryRouter>
-          <Sidebar />
-        </MemoryRouter>
-      </AppContext.Provider>,
-    );
-    fetchNotifications.mockReset();
-
-    act(() => {
-      jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
-      return;
-    });
-    expect(fetchNotifications).toHaveBeenCalledTimes(1);
-
-    act(() => {
-      jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
-      return;
-    });
-    expect(fetchNotifications).toHaveBeenCalledTimes(2);
-
-    act(() => {
-      jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
-      return;
-    });
-    expect(fetchNotifications).toHaveBeenCalledTimes(3);
-  });
-
   it('should refresh the notifications', () => {
     render(
       <AppContext.Provider
@@ -103,19 +68,9 @@ describe('components/Sidebar.tsx', () => {
       </AppContext.Provider>,
     );
     fetchNotifications.mockReset();
-
-    const enabledRefreshButton = 'Refresh Notifications';
-
-    fireEvent.click(screen.getByTitle(enabledRefreshButton));
+    fireEvent.click(screen.getByTitle('Refresh Notifications'));
 
     expect(fetchNotifications).toHaveBeenCalledTimes(1);
-
-    act(() => {
-      jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
-      return;
-    });
-
-    expect(fetchNotifications).toHaveBeenCalledTimes(2);
   });
 
   it('go to the settings route', () => {
diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx
index dde50505..3280d577 100644
--- a/src/context/App.test.tsx
+++ b/src/context/App.test.tsx
@@ -6,6 +6,7 @@ import { useNotifications } from '../hooks/useNotifications';
 import type { AuthState, SettingsState } from '../types';
 import * as apiRequests from '../utils/api-requests';
 import * as comms from '../utils/comms';
+import Constants from '../utils/constants';
 import * as notifications from '../utils/notifications';
 import * as storage from '../utils/storage';
 import { AppContext, AppProvider } from './App';
@@ -58,6 +59,36 @@ describe('context/App.tsx', () => {
       });
     });
 
+    it('fetch notifications every minute', async () => {
+      customRender(null);
+
+      // Wait for the useEffects, for settings.participating and accounts, to run.
+      // Those aren't what we're testing
+      await waitFor(() =>
+        expect(fetchNotificationsMock).toHaveBeenCalledTimes(1),
+      );
+
+      fetchNotificationsMock.mockReset();
+
+      act(() => {
+        jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
+        return;
+      });
+      expect(fetchNotificationsMock).toHaveBeenCalledTimes(1);
+
+      act(() => {
+        jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
+        return;
+      });
+      expect(fetchNotificationsMock).toHaveBeenCalledTimes(2);
+
+      act(() => {
+        jest.advanceTimersByTime(Constants.FETCH_INTERVAL);
+        return;
+      });
+      expect(fetchNotificationsMock).toHaveBeenCalledTimes(3);
+    });
+
     it('should call fetchNotifications', async () => {
       const TestComponent = () => {
         const { fetchNotifications } = useContext(AppContext);
diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap
index 146e409f..2c5335a0 100644
--- a/src/utils/__snapshots__/github-api.test.ts.snap
+++ b/src/utils/__snapshots__/github-api.test.ts.snap
@@ -1,117 +1,5 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`utils/github-api.ts formatReason - should format the notification reason 1`] = `
-{
-  "description": "You were requested to review and approve a deployment.",
-  "type": "Approval Requested",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 2`] = `
-{
-  "description": "You were assigned to the issue.",
-  "type": "Assigned",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 3`] = `
-{
-  "description": "You created the thread.",
-  "type": "Authored",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 4`] = `
-{
-  "description": "A GitHub Actions workflow run was triggered for your repository",
-  "type": "Workflow Run Completed",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 5`] = `
-{
-  "description": "You commented on the thread.",
-  "type": "Commented",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 6`] = `
-{
-  "description": "You accepted an invitation to contribute to the repository.",
-  "type": "Invitation Received",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 7`] = `
-{
-  "description": "You subscribed to the thread (via an issue or pull request).",
-  "type": "Updated",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 8`] = `
-{
-  "description": "Organization members have requested to enable a feature such as Draft Pull Requests or CoPilot.",
-  "type": "Member Feature Requested",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 9`] = `
-{
-  "description": "You were specifically @mentioned in the content.",
-  "type": "Mentioned",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 10`] = `
-{
-  "description": "You, or a team you're a member of, were requested to review a pull request.",
-  "type": "Review Requested",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 11`] = `
-{
-  "description": "You were credited for contributing to a security advisory.",
-  "type": "Security Advisory Credit Recevied",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 12`] = `
-{
-  "description": "GitHub discovered a security vulnerability in your repository.",
-  "type": "Security Alert Received",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 13`] = `
-{
-  "description": "You changed the thread state (for example, closing an issue or merging a pull request).",
-  "type": "State Changed",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 14`] = `
-{
-  "description": "You're watching the repository.",
-  "type": "Updated",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 15`] = `
-{
-  "description": "You were on a team that was mentioned.",
-  "type": "Team Mentioned",
-}
-`;
-
-exports[`utils/github-api.ts formatReason - should format the notification reason 16`] = `
-{
-  "description": "The reason for this notification is not supported by the app.",
-  "type": "Unknown",
-}
-`;
-
 exports[`utils/github-api.ts getNotificationTypeIconColor should format the notification color for check suite 1`] = `"text-gray-500 dark:text-gray-300"`;
 
 exports[`utils/github-api.ts getNotificationTypeIconColor should format the notification color for check suite 2`] = `"text-red-500"`;
diff --git a/src/utils/__snapshots__/reason.test.ts.snap b/src/utils/__snapshots__/reason.test.ts.snap
new file mode 100644
index 00000000..2bda4473
--- /dev/null
+++ b/src/utils/__snapshots__/reason.test.ts.snap
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`utils/reason.ts formatReason - should format the notification reason 1`] = `
+{
+  "description": "You were requested to review and approve a deployment.",
+  "title": "Approval Requested",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 2`] = `
+{
+  "description": "You were assigned to the issue.",
+  "title": "Assigned",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 3`] = `
+{
+  "description": "You created the thread.",
+  "title": "Authored",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 4`] = `
+{
+  "description": "A GitHub Actions workflow run was triggered for your repository.",
+  "title": "Workflow Run Completed",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 5`] = `
+{
+  "description": "You commented on the thread.",
+  "title": "Commented",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 6`] = `
+{
+  "description": "You accepted an invitation to contribute to the repository.",
+  "title": "Invitation Received",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 7`] = `
+{
+  "description": "You subscribed to the thread (via an issue or pull request).",
+  "title": "Updated",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 8`] = `
+{
+  "description": "Organization members have requested to enable a feature such as Draft Pull Requests or Copilot.",
+  "title": "Member Feature Requested",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 9`] = `
+{
+  "description": "You were specifically @mentioned in the content.",
+  "title": "Mentioned",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 10`] = `
+{
+  "description": "You, or a team you're a member of, were requested to review a pull request.",
+  "title": "Review Requested",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 11`] = `
+{
+  "description": "You were credited for contributing to a security advisory.",
+  "title": "Security Advisory Credit Received",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 12`] = `
+{
+  "description": "GitHub discovered a security vulnerability in your repository.",
+  "title": "Security Alert Received",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 13`] = `
+{
+  "description": "You changed the thread state (for example, closing an issue or merging a pull request).",
+  "title": "State Changed",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 14`] = `
+{
+  "description": "You're watching the repository.",
+  "title": "Updated",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 15`] = `
+{
+  "description": "You were on a team that was mentioned.",
+  "title": "Team Mentioned",
+}
+`;
+
+exports[`utils/reason.ts formatReason - should format the notification reason 16`] = `
+{
+  "description": "The reason for this notification is not supported by the app.",
+  "title": "Unknown",
+}
+`;
diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts
index 588e36c5..70ebd6b3 100644
--- a/src/utils/github-api.test.ts
+++ b/src/utils/github-api.test.ts
@@ -1,30 +1,10 @@
-import type { Reason, StateType, Subject, SubjectType } from '../typesGithub';
+import type { StateType, Subject, SubjectType } from '../typesGithub';
 import {
-  formatReason,
   getNotificationTypeIcon,
   getNotificationTypeIconColor,
 } from './github-api';
 
 describe('utils/github-api.ts', () => {
-  it('formatReason - should format the notification reason', () => {
-    expect(formatReason('approval_requested')).toMatchSnapshot();
-    expect(formatReason('assign')).toMatchSnapshot();
-    expect(formatReason('author')).toMatchSnapshot();
-    expect(formatReason('ci_activity')).toMatchSnapshot();
-    expect(formatReason('comment')).toMatchSnapshot();
-    expect(formatReason('invitation')).toMatchSnapshot();
-    expect(formatReason('manual')).toMatchSnapshot();
-    expect(formatReason('member_feature_requested')).toMatchSnapshot();
-    expect(formatReason('mention')).toMatchSnapshot();
-    expect(formatReason('review_requested')).toMatchSnapshot();
-    expect(formatReason('security_advisory_credit')).toMatchSnapshot();
-    expect(formatReason('security_alert')).toMatchSnapshot();
-    expect(formatReason('state_change')).toMatchSnapshot();
-    expect(formatReason('subscribed')).toMatchSnapshot();
-    expect(formatReason('team_mention')).toMatchSnapshot();
-    expect(formatReason('something_else_unknown' as Reason)).toMatchSnapshot();
-  });
-
   describe('getNotificationTypeIcon - should get the notification type icon', () => {
     expect(
       getNotificationTypeIcon(
diff --git a/src/utils/reason.test.ts b/src/utils/reason.test.ts
new file mode 100644
index 00000000..10ed1c50
--- /dev/null
+++ b/src/utils/reason.test.ts
@@ -0,0 +1,23 @@
+import type { Reason } from '../typesGithub';
+import { formatReason } from './reason';
+
+describe('utils/reason.ts', () => {
+  it('formatReason - should format the notification reason', () => {
+    expect(formatReason('approval_requested')).toMatchSnapshot();
+    expect(formatReason('assign')).toMatchSnapshot();
+    expect(formatReason('author')).toMatchSnapshot();
+    expect(formatReason('ci_activity')).toMatchSnapshot();
+    expect(formatReason('comment')).toMatchSnapshot();
+    expect(formatReason('invitation')).toMatchSnapshot();
+    expect(formatReason('manual')).toMatchSnapshot();
+    expect(formatReason('member_feature_requested')).toMatchSnapshot();
+    expect(formatReason('mention')).toMatchSnapshot();
+    expect(formatReason('review_requested')).toMatchSnapshot();
+    expect(formatReason('security_advisory_credit')).toMatchSnapshot();
+    expect(formatReason('security_alert')).toMatchSnapshot();
+    expect(formatReason('state_change')).toMatchSnapshot();
+    expect(formatReason('subscribed')).toMatchSnapshot();
+    expect(formatReason('team_mention')).toMatchSnapshot();
+    expect(formatReason('something_else_unknown' as Reason)).toMatchSnapshot();
+  });
+});