Repository: gitify-app/gitify
Pull request #1103 (2024-05-10 20:46:32)

feat: use `last_read_at` timestamp when available
Prefer last_read_at time when available.  Helpful in conditions where you open a notification, then mark is as unread.  Most noticeable when a decent amount of time has lapsed between reading and unreading. 

**Before**
![Screenshot 2024-05-10 at 4 43 39 PM](https://github.com/gitify-app/gitify/assets/386277/ee06d651-1a9e-4117-a67d-bb9b37bfa87a)


**After**
![Screenshot 2024-05-10 at 4 43 18 PM](https://github.com/gitify-app/gitify/assets/386277/d483e194-6247-4245-a224-2366eed8baca)


setchy: Hard to tell if this is a regression in the GitHub API around how they set `updated_at` timestamps... the GQL queries to fetch discussions are also failing when previously I'm sure they were OK...  Perhaps we wait a bit to see how this pans out

Patch:
diff --git a/package.json b/package.json
index 1e444165..b695b6d5 100644
--- a/package.json
+++ b/package.json
@@ -130,7 +130,7 @@
     "@types/jest": "29.5.12",
     "@types/node": "20.12.11",
     "@types/nprogress": "0.2.3",
-    "@types/react": "18.3.1",
+    "@types/react": "18.3.2",
     "@types/react-router-dom": "5.3.3",
     "@types/react-test-renderer": "18.3.0",
     "@types/react-transition-group": "4.4.10",
@@ -146,7 +146,7 @@
     "postcss": "8.4.38",
     "postcss-loader": "8.1.1",
     "react-test-renderer": "18.3.1",
-    "rimraf": "5.0.6",
+    "rimraf": "5.0.7",
     "style-loader": "4.0.0",
     "tailwindcss": "3.4.3",
     "ts-jest": "29.1.2",
@@ -155,7 +155,7 @@
     "webpack-cli": "5.1.4",
     "webpack-merge": "5.10.0"
   },
-  "packageManager": "pnpm@9.1.0",
+  "packageManager": "pnpm@9.1.1",
   "lint-staged": {
     "*.{html,js,json,md,ts,tsx}": "biome format --write"
   }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6dbb90c3..462476df 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -62,7 +62,7 @@ importers:
         version: 2.3.0
       '@testing-library/react':
         specifier: 15.0.7
-        version: 15.0.7(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+        version: 15.0.7(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@types/jest':
         specifier: 29.5.12
         version: 29.5.12
@@ -73,8 +73,8 @@ importers:
         specifier: 0.2.3
         version: 0.2.3
       '@types/react':
-        specifier: 18.3.1
-        version: 18.3.1
+        specifier: 18.3.2
+        version: 18.3.2
       '@types/react-router-dom':
         specifier: 5.3.3
         version: 5.3.3
@@ -121,8 +121,8 @@ importers:
         specifier: 18.3.1
         version: 18.3.1(react@18.3.1)
       rimraf:
-        specifier: 5.0.6
-        version: 5.0.6
+        specifier: 5.0.7
+        version: 5.0.7
       style-loader:
         specifier: 4.0.0
         version: 4.0.0(webpack@5.91.0(webpack-cli@5.1.4))
@@ -702,8 +702,8 @@ packages:
   '@types/react-transition-group@4.4.10':
     resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==}
 
-  '@types/react@18.3.1':
-    resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==}
+  '@types/react@18.3.2':
+    resolution: {integrity: sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==}
 
   '@types/responselike@1.0.3':
     resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
@@ -2646,9 +2646,9 @@ packages:
     resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
     engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
 
-  rimraf@5.0.6:
-    resolution: {integrity: sha512-X72SgyOf+1lFnGM6gYcmZ4+jMOwuT4E4SajKQzUIlI7EoR5eFHMhS/wf8Ll0mN+w2bxcIVldrJQ6xT7HFQywjg==}
-    engines: {node: '>=14'}
+  rimraf@5.0.7:
+    resolution: {integrity: sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==}
+    engines: {node: '>=14.18'}
     hasBin: true
 
   roarr@2.15.4:
@@ -3777,7 +3777,7 @@ snapshots:
       lz-string: 1.5.0
       pretty-format: 27.5.1
 
-  '@testing-library/react@15.0.7(@types/react@18.3.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+  '@testing-library/react@15.0.7(@types/react@18.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
     dependencies:
       '@babel/runtime': 7.24.1
       '@testing-library/dom': 10.0.0
@@ -3785,7 +3785,7 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1(react@18.3.1)
     optionalDependencies:
-      '@types/react': 18.3.1
+      '@types/react': 18.3.2
 
   '@tootallnate/once@2.0.0': {}
 
@@ -3900,28 +3900,28 @@ snapshots:
 
   '@types/react-dom@18.2.22':
     dependencies:
-      '@types/react': 18.3.1
+      '@types/react': 18.3.2
 
   '@types/react-router-dom@5.3.3':
     dependencies:
       '@types/history': 4.7.11
-      '@types/react': 18.3.1
+      '@types/react': 18.3.2
       '@types/react-router': 5.1.20
 
   '@types/react-router@5.1.20':
     dependencies:
       '@types/history': 4.7.11
-      '@types/react': 18.3.1
+      '@types/react': 18.3.2
 
   '@types/react-test-renderer@18.3.0':
     dependencies:
-      '@types/react': 18.3.1
+      '@types/react': 18.3.2
 
   '@types/react-transition-group@4.4.10':
     dependencies:
-      '@types/react': 18.3.1
+      '@types/react': 18.3.2
 
-  '@types/react@18.3.1':
+  '@types/react@18.3.2':
     dependencies:
       '@types/prop-types': 15.7.12
       csstype: 3.1.3
@@ -6141,7 +6141,7 @@ snapshots:
 
   reusify@1.0.4: {}
 
-  rimraf@5.0.6:
+  rimraf@5.0.7:
     dependencies:
       glob: 10.3.10
 
diff --git a/src/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts
index f035be23..776a0787 100644
--- a/src/__mocks__/mockedData.ts
+++ b/src/__mocks__/mockedData.ts
@@ -419,6 +419,7 @@ export const mockDiscussionComments: DiscussionComments = {
       },
     },
   ],
+  totalCount: 2,
 };
 
 export const mockedGraphQLResponse: GraphQLSearch<Discussion> = {
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index bae5af4b..8ea831e1 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -1,10 +1,10 @@
 import {
   BellSlashIcon,
   CheckIcon,
+  CommentIcon,
   FeedPersonIcon,
   ReadIcon,
 } from '@primer/octicons-react';
-import { formatDistanceToNow, parseISO } from 'date-fns';
 import {
   type FC,
   type KeyboardEvent,
@@ -14,9 +14,14 @@ import {
 } from 'react';
 
 import { AppContext } from '../context/App';
+import { IconColor } from '../types';
 import type { Notification } from '../typesGitHub';
 import { openExternalLink } from '../utils/comms';
-import { formatForDisplay, openInBrowser } from '../utils/helpers';
+import {
+  formatForDisplay,
+  formatNotificationUpdatedAt,
+  openInBrowser,
+} from '../utils/helpers';
 import {
   getNotificationTypeIcon,
   getNotificationTypeIconColor,
@@ -71,9 +76,7 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
   const NotificationIcon = getNotificationTypeIcon(notification.subject);
   const iconColor = getNotificationTypeIconColor(notification.subject);
 
-  const updatedAt = formatDistanceToNow(parseISO(notification.updated_at), {
-    addSuffix: true,
-  });
+  const updatedAt = formatNotificationUpdatedAt(notification);
   const updatedLabel = notification.subject.user
     ? `${notification.subject.user.login} updated ${updatedAt}`
     : `Updated ${updatedAt}`;
@@ -83,6 +86,10 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
     notification.subject.type,
   ]);
 
+  const commentsLabel = `${notification.subject.comments} ${
+    notification.subject.comments > 1 ? 'comments' : 'comment'
+  }`;
+
   return (
     <div
       id={notification.id}
@@ -158,6 +165,15 @@ export const NotificationRow: FC<IProps> = ({ notification, hostname }) => {
                     );
                   })
                 : null}
+              {notification.subject?.comments > 0 && (
+                <span className="ml-1" title={commentsLabel}>
+                  <CommentIcon
+                    size={16}
+                    className={IconColor.GRAY}
+                    aria-label={commentsLabel}
+                  />
+                </span>
+              )}
             </span>
           </span>
         </div>
diff --git a/src/typesGitHub.ts b/src/typesGitHub.ts
index 397dc2e9..cb099bff 100644
--- a/src/typesGitHub.ts
+++ b/src/typesGitHub.ts
@@ -259,6 +259,7 @@ export interface GitifySubject {
   state?: StateType;
   user?: SubjectUser;
   reviews?: GitifyPullRequestReview[];
+  comments?: number;
 }
 
 export interface PullRequest {
@@ -461,6 +462,7 @@ export interface Discussion {
 
 export interface DiscussionComments {
   nodes: DiscussionComment[];
+  totalCount: number;
 }
 
 export interface DiscussionComment {
diff --git a/src/utils/api/graphql/discussions.ts b/src/utils/api/graphql/discussions.ts
index 3679367d..405337ae 100644
--- a/src/utils/api/graphql/discussions.ts
+++ b/src/utils/api/graphql/discussions.ts
@@ -40,6 +40,7 @@ export const QUERY_SEARCH_DISCUSSIONS = gql`
             ...AuthorFields
           }
           comments(last: $lastComments){
+            totalCount
             nodes {
               ...CommentFields
               replies(last: $lastReplies) {
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index b6501bd6..535f1772 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -1,3 +1,4 @@
+import { formatDistanceToNow, parseISO } from 'date-fns';
 import type { AuthState } from '../types';
 import type {
   Discussion,
@@ -203,6 +204,16 @@ export function formatForDisplay(text: string[]): string {
     });
 }
 
+export function formatNotificationUpdatedAt(
+  notification: Notification,
+): string {
+  const date = notification.last_read_at ?? notification.updated_at;
+
+  return formatDistanceToNow(parseISO(date), {
+    addSuffix: true,
+  });
+}
+
 export async function openInBrowser(
   notification: Notification,
   accounts: AuthState,
diff --git a/src/utils/subject.ts b/src/utils/subject.ts
index 3a94b957..391cfe2c 100644
--- a/src/utils/subject.ts
+++ b/src/utils/subject.ts
@@ -180,6 +180,7 @@ async function getGitifySubjectForDiscussion(
   return {
     state: discussionState,
     user: discussionUser,
+    comments: discussion.comments.totalCount,
   };
 }
 
@@ -209,6 +210,7 @@ async function getGitifySubjectForIssue(
       avatar_url: issueCommentUser?.avatar_url ?? issue.user.avatar_url,
       type: issueCommentUser?.type ?? issue.user.type,
     },
+    comments: issue.comments,
   };
 }
 
@@ -251,6 +253,7 @@ async function getGitifySubjectForPullRequest(
       type: prCommentUser?.type ?? pr.user.type,
     },
     reviews: reviews,
+    comments: pr.comments,
   };
 }
 


Test patch:
diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx
index 3703b110..4677f92d 100644
--- a/src/components/NotificationRow.test.tsx
+++ b/src/components/NotificationRow.test.tsx
@@ -34,13 +34,13 @@ describe('components/NotificationRow.tsx', () => {
     expect(tree).toMatchSnapshot();
   });
 
-  it('should render itself & its children without avatar', async () => {
+  it('should render itself & its children when last_read_at is null', async () => {
     jest
       .spyOn(global.Date, 'now')
       .mockImplementation(() => new Date('2024').valueOf());
 
     const mockNotification = mockedSingleNotification;
-    mockNotification.subject.user = null;
+    mockNotification.last_read_at = null;
 
     const props = {
       notification: mockNotification,
@@ -51,183 +51,256 @@ describe('components/NotificationRow.tsx', () => {
     expect(tree).toMatchSnapshot();
   });
 
-  it('should open a notification in the browser - click', () => {
-    const removeNotificationFromState = jest.fn();
-
-    const props = {
-      notification: mockedSingleNotification,
-      hostname: 'github.com',
-    };
-
-    render(
-      <AppContext.Provider
-        value={{
-          settings: { ...mockSettings, markAsDoneOnOpen: false },
-          removeNotificationFromState,
-          accounts: mockAccounts,
-        }}
-      >
-        <NotificationRow {...props} />
-      </AppContext.Provider>,
-    );
-
-    fireEvent.click(screen.getByRole('main'));
-    expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
-    expect(removeNotificationFromState).toHaveBeenCalledTimes(1);
-  });
+  it('should render itself & its children without avatar', async () => {
+    jest
+      .spyOn(global.Date, 'now')
+      .mockImplementation(() => new Date('2024').valueOf());
 
-  it('should open a notification in the browser - key down', () => {
-    const removeNotificationFromState = jest.fn();
+    const mockNotification = mockedSingleNotification;
+    mockNotification.subject.user = null;
 
     const props = {
-      notification: mockedSingleNotification,
+      notification: mockNotification,
       hostname: 'github.com',
     };
 
-    render(
-      <AppContext.Provider
-        value={{
-          settings: { ...mockSettings, markAsDoneOnOpen: false },
-          removeNotificationFromState,
-          accounts: mockAccounts,
-        }}
-      >
-        <NotificationRow {...props} />
-      </AppContext.Provider>,
-    );
-
-    fireEvent.keyDown(screen.getByRole('main'));
-    expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
-    expect(removeNotificationFromState).toHaveBeenCalledTimes(1);
+    const tree = TestRenderer.create(<NotificationRow {...props} />);
+    expect(tree).toMatchSnapshot();
   });
 
-  it('should open a notification in browser & mark it as done', () => {
-    const markNotificationDone = jest.fn();
-
-    const props = {
-      notification: mockedSingleNotification,
-      hostname: 'github.com',
-    };
-
-    render(
-      <AppContext.Provider
-        value={{
-          settings: { ...mockSettings, markAsDoneOnOpen: true },
-          markNotificationDone,
-          accounts: mockAccounts,
-        }}
-      >
-        <NotificationRow {...props} />
-      </AppContext.Provider>,
-    );
-
-    fireEvent.click(screen.getByRole('main'));
-    expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
-    expect(markNotificationDone).toHaveBeenCalledTimes(1);
+  describe('rendering for notification comments count', () => {
+    it('should render when no comments', async () => {
+      jest
+        .spyOn(global.Date, 'now')
+        .mockImplementation(() => new Date('2024').valueOf());
+
+      const mockNotification = mockedSingleNotification;
+      mockNotification.subject.comments = null;
+
+      const props = {
+        notification: mockNotification,
+        hostname: 'github.com',
+      };
+
+      const tree = TestRenderer.create(<NotificationRow {...props} />);
+      expect(tree).toMatchSnapshot();
+    });
+
+    it('should render when 1 comment', async () => {
+      jest
+        .spyOn(global.Date, 'now')
+        .mockImplementation(() => new Date('2024').valueOf());
+
+      const mockNotification = mockedSingleNotification;
+      mockNotification.subject.comments = 1;
+
+      const props = {
+        notification: mockNotification,
+        hostname: 'github.com',
+      };
+
+      const tree = TestRenderer.create(<NotificationRow {...props} />);
+      expect(tree).toMatchSnapshot();
+    });
+
+    it('should render when more than 1 comments', async () => {
+      jest
+        .spyOn(global.Date, 'now')
+        .mockImplementation(() => new Date('2024').valueOf());
+
+      const mockNotification = mockedSingleNotification;
+      mockNotification.subject.comments = 2;
+
+      const props = {
+        notification: mockNotification,
+        hostname: 'github.com',
+      };
+
+      const tree = TestRenderer.create(<NotificationRow {...props} />);
+      expect(tree).toMatchSnapshot();
+    });
   });
 
-  it('should mark a notification as read', () => {
-    const markNotificationRead = jest.fn();
-
-    const props = {
-      notification: mockedSingleNotification,
-      hostname: 'github.com',
-    };
-
-    render(
-      <AppContext.Provider
-        value={{
-          settings: { ...mockSettings, markAsDoneOnOpen: false },
-          accounts: mockAccounts,
-        }}
-      >
-        <AppContext.Provider value={{ markNotificationRead }}>
+  describe('notification interactions', () => {
+    it('should open a notification in the browser - click', () => {
+      const removeNotificationFromState = jest.fn();
+
+      const props = {
+        notification: mockedSingleNotification,
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider
+          value={{
+            settings: { ...mockSettings, markAsDoneOnOpen: false },
+            removeNotificationFromState,
+            accounts: mockAccounts,
+          }}
+        >
           <NotificationRow {...props} />
-        </AppContext.Provider>
-      </AppContext.Provider>,
-    );
-
-    fireEvent.click(screen.getByTitle('Mark as Read'));
-    expect(markNotificationRead).toHaveBeenCalledTimes(1);
-  });
-
-  it('should mark a notification as done', () => {
-    const markNotificationDone = jest.fn();
-
-    const props = {
-      notification: mockedSingleNotification,
-      hostname: 'github.com',
-    };
-
-    render(
-      <AppContext.Provider
-        value={{
-          settings: { ...mockSettings },
-          accounts: mockAccounts,
-        }}
-      >
-        <AppContext.Provider value={{ markNotificationDone }}>
+        </AppContext.Provider>,
+      );
+
+      fireEvent.click(screen.getByRole('main'));
+      expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
+      expect(removeNotificationFromState).toHaveBeenCalledTimes(1);
+    });
+
+    it('should open a notification in the browser - key down', () => {
+      const removeNotificationFromState = jest.fn();
+
+      const props = {
+        notification: mockedSingleNotification,
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider
+          value={{
+            settings: { ...mockSettings, markAsDoneOnOpen: false },
+            removeNotificationFromState,
+            accounts: mockAccounts,
+          }}
+        >
           <NotificationRow {...props} />
-        </AppContext.Provider>
-      </AppContext.Provider>,
-    );
-
-    fireEvent.click(screen.getByTitle('Mark as Done'));
-    expect(markNotificationDone).toHaveBeenCalledTimes(1);
-  });
-
-  it('should unsubscribe from a notification thread', () => {
-    const unsubscribeNotification = jest.fn();
-
-    const props = {
-      notification: mockedSingleNotification,
-      hostname: 'github.com',
-    };
-
-    render(
-      <AppContext.Provider value={{}}>
-        <AppContext.Provider value={{ unsubscribeNotification }}>
+        </AppContext.Provider>,
+      );
+
+      fireEvent.keyDown(screen.getByRole('main'));
+      expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
+      expect(removeNotificationFromState).toHaveBeenCalledTimes(1);
+    });
+
+    it('should open a notification in browser & mark it as done', () => {
+      const markNotificationDone = jest.fn();
+
+      const props = {
+        notification: mockedSingleNotification,
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider
+          value={{
+            settings: { ...mockSettings, markAsDoneOnOpen: true },
+            markNotificationDone,
+            accounts: mockAccounts,
+          }}
+        >
           <NotificationRow {...props} />
-        </AppContext.Provider>
-      </AppContext.Provider>,
-    );
-    fireEvent.click(screen.getByTitle('Unsubscribe from Thread'));
-    expect(unsubscribeNotification).toHaveBeenCalledTimes(1);
-  });
-
-  it('should open notification user profile', () => {
-    const props = {
-      notification: {
-        ...mockedSingleNotification,
-        subject: {
-          ...mockedSingleNotification.subject,
-          user: {
-            login: 'some-user',
-            html_url: 'https://github.com/some-user',
-            avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4',
-            type: 'User' as UserType,
+        </AppContext.Provider>,
+      );
+
+      fireEvent.click(screen.getByRole('main'));
+      expect(helpers.openInBrowser).toHaveBeenCalledTimes(1);
+      expect(markNotificationDone).toHaveBeenCalledTimes(1);
+    });
+
+    it('should mark a notification as read', () => {
+      const markNotificationRead = jest.fn();
+
+      const props = {
+        notification: mockedSingleNotification,
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider
+          value={{
+            settings: { ...mockSettings, markAsDoneOnOpen: false },
+            accounts: mockAccounts,
+          }}
+        >
+          <AppContext.Provider value={{ markNotificationRead }}>
+            <NotificationRow {...props} />
+          </AppContext.Provider>
+        </AppContext.Provider>,
+      );
+
+      fireEvent.click(screen.getByTitle('Mark as Read'));
+      expect(markNotificationRead).toHaveBeenCalledTimes(1);
+    });
+
+    it('should mark a notification as done', () => {
+      const markNotificationDone = jest.fn();
+
+      const props = {
+        notification: mockedSingleNotification,
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider
+          value={{
+            settings: { ...mockSettings },
+            accounts: mockAccounts,
+          }}
+        >
+          <AppContext.Provider value={{ markNotificationDone }}>
+            <NotificationRow {...props} />
+          </AppContext.Provider>
+        </AppContext.Provider>,
+      );
+
+      fireEvent.click(screen.getByTitle('Mark as Done'));
+      expect(markNotificationDone).toHaveBeenCalledTimes(1);
+    });
+
+    it('should unsubscribe from a notification thread', () => {
+      const unsubscribeNotification = jest.fn();
+
+      const props = {
+        notification: mockedSingleNotification,
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider value={{}}>
+          <AppContext.Provider value={{ unsubscribeNotification }}>
+            <NotificationRow {...props} />
+          </AppContext.Provider>
+        </AppContext.Provider>,
+      );
+      fireEvent.click(screen.getByTitle('Unsubscribe from Thread'));
+      expect(unsubscribeNotification).toHaveBeenCalledTimes(1);
+    });
+
+    it('should open notification user profile', () => {
+      const props = {
+        notification: {
+          ...mockedSingleNotification,
+          subject: {
+            ...mockedSingleNotification.subject,
+            user: {
+              login: 'some-user',
+              html_url: 'https://github.com/some-user',
+              avatar_url:
+                'https://avatars.githubusercontent.com/u/123456789?v=4',
+              type: 'User' as UserType,
+            },
+            reviews: null,
           },
-          reviews: null,
         },
-      },
-      hostname: 'github.com',
-    };
-
-    render(
-      <AppContext.Provider
-        value={{
-          settings: { ...mockSettings },
-          accounts: mockAccounts,
-        }}
-      >
-        <NotificationRow {...props} />
-      </AppContext.Provider>,
-    );
-
-    fireEvent.click(screen.getByTitle('View User Profile'));
-    expect(shell.openExternal).toHaveBeenCalledTimes(1);
-    expect(shell.openExternal).toHaveBeenCalledWith(
-      props.notification.subject.user.html_url,
-    );
+        hostname: 'github.com',
+      };
+
+      render(
+        <AppContext.Provider
+          value={{
+            settings: { ...mockSettings },
+            accounts: mockAccounts,
+          }}
+        >
+          <NotificationRow {...props} />
+        </AppContext.Provider>,
+      );
+
+      fireEvent.click(screen.getByTitle('View User Profile'));
+      expect(shell.openExternal).toHaveBeenCalledTimes(1);
+      expect(shell.openExternal).toHaveBeenCalledWith(
+        props.notification.subject.user.html_url,
+      );
+    });
   });
 });
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap
index 2dd1b9f7..02855d65 100644
--- a/src/components/__snapshots__/NotificationRow.test.tsx.snap
+++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap
@@ -1,5 +1,788 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`components/NotificationRow.tsx rendering for notification comments count should render when 1 comment 1`] = `
+<div
+  className="flex space-x-3 py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+  id="138661096"
+>
+  <div
+    className="flex justify-center items-center w-5 text-green-500"
+    title="Open Issue"
+  >
+    <svg
+      aria-label="Issue"
+      className="octicon octicon-issue-opened"
+      fill="currentColor"
+      focusable="false"
+      height={18}
+      role="img"
+      style={
+        {
+          "display": "inline-block",
+          "overflow": "visible",
+          "userSelect": "none",
+          "verticalAlign": "text-bottom",
+        }
+      }
+      viewBox="0 0 16 16"
+      width={18}
+    >
+      <path
+        d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+      />
+      <path
+        d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+      />
+    </svg>
+  </div>
+  <div
+    className="flex-1 overflow-hidden"
+    onClick={[Function]}
+    onKeyDown={[Function]}
+  >
+    <div
+      className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden cursor-pointer"
+      role="main"
+      title="I am a robot and this is a test!"
+    >
+      I am a robot and this is a test!
+    </div>
+    <div
+      className="text-xs text-capitalize whitespace-nowrap overflow-ellipsis overflow-hidden"
+    >
+      <span
+        className="flex items-center"
+      >
+        <span
+          className="flex"
+          title="Updated over 6 years ago"
+        >
+          <span>
+            <svg
+              aria-hidden="true"
+              className="text-gray-500 dark:text-gray-300"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm.847-8.145a2.502 2.502 0 1 0-1.694 0C5.471 8.261 4 9.775 4 11c0 .395.145.995 1 .995h6c.855 0 1-.6 1-.995 0-1.224-1.47-2.74-3.153-3.145Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="You're watching the repository."
+          >
+            Updated
+          </span>
+          <span
+            className="ml-1"
+          >
+            over 6 years ago
+          </span>
+          <span
+            className="ml-1"
+            title="octocat approved these changes"
+          >
+            <svg
+              aria-label="octocat approved these changes"
+              className="text-green-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="gitify-app requested changes"
+          >
+            <svg
+              aria-label="gitify-app requested changes"
+              className="text-red-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="1 comment"
+          >
+            <svg
+              aria-label="1 comment"
+              className="text-gray-500 dark:text-gray-300"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
+              />
+            </svg>
+          </span>
+        </span>
+      </span>
+    </div>
+  </div>
+  <div
+    className="flex justify-center items-center gap-2 opacity-0 group-hover:opacity-80 transition-opacity"
+  >
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Done"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Done"
+        className="octicon octicon-check"
+        fill="currentColor"
+        focusable="false"
+        height={16}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={16}
+      >
+        <path
+          d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-red-500"
+      onClick={[Function]}
+      title="Unsubscribe from Thread"
+      type="button"
+    >
+      <svg
+        aria-label="Unsubscribe from Thread"
+        className="octicon octicon-bell-slash"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Read"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Read"
+        className="octicon octicon-read"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
+        />
+      </svg>
+    </button>
+  </div>
+</div>
+`;
+
+exports[`components/NotificationRow.tsx rendering for notification comments count should render when more than 1 comments 1`] = `
+<div
+  className="flex space-x-3 py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+  id="138661096"
+>
+  <div
+    className="flex justify-center items-center w-5 text-green-500"
+    title="Open Issue"
+  >
+    <svg
+      aria-label="Issue"
+      className="octicon octicon-issue-opened"
+      fill="currentColor"
+      focusable="false"
+      height={18}
+      role="img"
+      style={
+        {
+          "display": "inline-block",
+          "overflow": "visible",
+          "userSelect": "none",
+          "verticalAlign": "text-bottom",
+        }
+      }
+      viewBox="0 0 16 16"
+      width={18}
+    >
+      <path
+        d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+      />
+      <path
+        d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+      />
+    </svg>
+  </div>
+  <div
+    className="flex-1 overflow-hidden"
+    onClick={[Function]}
+    onKeyDown={[Function]}
+  >
+    <div
+      className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden cursor-pointer"
+      role="main"
+      title="I am a robot and this is a test!"
+    >
+      I am a robot and this is a test!
+    </div>
+    <div
+      className="text-xs text-capitalize whitespace-nowrap overflow-ellipsis overflow-hidden"
+    >
+      <span
+        className="flex items-center"
+      >
+        <span
+          className="flex"
+          title="Updated over 6 years ago"
+        >
+          <span>
+            <svg
+              aria-hidden="true"
+              className="text-gray-500 dark:text-gray-300"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm.847-8.145a2.502 2.502 0 1 0-1.694 0C5.471 8.261 4 9.775 4 11c0 .395.145.995 1 .995h6c.855 0 1-.6 1-.995 0-1.224-1.47-2.74-3.153-3.145Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="You're watching the repository."
+          >
+            Updated
+          </span>
+          <span
+            className="ml-1"
+          >
+            over 6 years ago
+          </span>
+          <span
+            className="ml-1"
+            title="octocat approved these changes"
+          >
+            <svg
+              aria-label="octocat approved these changes"
+              className="text-green-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="gitify-app requested changes"
+          >
+            <svg
+              aria-label="gitify-app requested changes"
+              className="text-red-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="2 comments"
+          >
+            <svg
+              aria-label="2 comments"
+              className="text-gray-500 dark:text-gray-300"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"
+              />
+            </svg>
+          </span>
+        </span>
+      </span>
+    </div>
+  </div>
+  <div
+    className="flex justify-center items-center gap-2 opacity-0 group-hover:opacity-80 transition-opacity"
+  >
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Done"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Done"
+        className="octicon octicon-check"
+        fill="currentColor"
+        focusable="false"
+        height={16}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={16}
+      >
+        <path
+          d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-red-500"
+      onClick={[Function]}
+      title="Unsubscribe from Thread"
+      type="button"
+    >
+      <svg
+        aria-label="Unsubscribe from Thread"
+        className="octicon octicon-bell-slash"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Read"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Read"
+        className="octicon octicon-read"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
+        />
+      </svg>
+    </button>
+  </div>
+</div>
+`;
+
+exports[`components/NotificationRow.tsx rendering for notification comments count should render when no comments 1`] = `
+<div
+  className="flex space-x-3 py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+  id="138661096"
+>
+  <div
+    className="flex justify-center items-center w-5 text-green-500"
+    title="Open Issue"
+  >
+    <svg
+      aria-label="Issue"
+      className="octicon octicon-issue-opened"
+      fill="currentColor"
+      focusable="false"
+      height={18}
+      role="img"
+      style={
+        {
+          "display": "inline-block",
+          "overflow": "visible",
+          "userSelect": "none",
+          "verticalAlign": "text-bottom",
+        }
+      }
+      viewBox="0 0 16 16"
+      width={18}
+    >
+      <path
+        d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+      />
+      <path
+        d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+      />
+    </svg>
+  </div>
+  <div
+    className="flex-1 overflow-hidden"
+    onClick={[Function]}
+    onKeyDown={[Function]}
+  >
+    <div
+      className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden cursor-pointer"
+      role="main"
+      title="I am a robot and this is a test!"
+    >
+      I am a robot and this is a test!
+    </div>
+    <div
+      className="text-xs text-capitalize whitespace-nowrap overflow-ellipsis overflow-hidden"
+    >
+      <span
+        className="flex items-center"
+      >
+        <span
+          className="flex"
+          title="Updated over 6 years ago"
+        >
+          <span>
+            <svg
+              aria-hidden="true"
+              className="text-gray-500 dark:text-gray-300"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16Zm.847-8.145a2.502 2.502 0 1 0-1.694 0C5.471 8.261 4 9.775 4 11c0 .395.145.995 1 .995h6c.855 0 1-.6 1-.995 0-1.224-1.47-2.74-3.153-3.145Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="You're watching the repository."
+          >
+            Updated
+          </span>
+          <span
+            className="ml-1"
+          >
+            over 6 years ago
+          </span>
+          <span
+            className="ml-1"
+            title="octocat approved these changes"
+          >
+            <svg
+              aria-label="octocat approved these changes"
+              className="text-green-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="gitify-app requested changes"
+          >
+            <svg
+              aria-label="gitify-app requested changes"
+              className="text-red-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"
+              />
+            </svg>
+          </span>
+        </span>
+      </span>
+    </div>
+  </div>
+  <div
+    className="flex justify-center items-center gap-2 opacity-0 group-hover:opacity-80 transition-opacity"
+  >
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Done"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Done"
+        className="octicon octicon-check"
+        fill="currentColor"
+        focusable="false"
+        height={16}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={16}
+      >
+        <path
+          d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-red-500"
+      onClick={[Function]}
+      title="Unsubscribe from Thread"
+      type="button"
+    >
+      <svg
+        aria-label="Unsubscribe from Thread"
+        className="octicon octicon-bell-slash"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Read"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Read"
+        className="octicon octicon-read"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
+        />
+      </svg>
+    </button>
+  </div>
+</div>
+`;
+
 exports[`components/NotificationRow.tsx should render itself & its children 1`] = `
 <div
   className="flex space-x-3 py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
@@ -232,6 +1015,238 @@ exports[`components/NotificationRow.tsx should render itself & its children 1`]
 </div>
 `;
 
+exports[`components/NotificationRow.tsx should render itself & its children when last_read_at is null 1`] = `
+<div
+  className="flex space-x-3 py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
+  id="138661096"
+>
+  <div
+    className="flex justify-center items-center w-5 text-green-500"
+    title="Open Issue"
+  >
+    <svg
+      aria-label="Issue"
+      className="octicon octicon-issue-opened"
+      fill="currentColor"
+      focusable="false"
+      height={18}
+      role="img"
+      style={
+        {
+          "display": "inline-block",
+          "overflow": "visible",
+          "userSelect": "none",
+          "verticalAlign": "text-bottom",
+        }
+      }
+      viewBox="0 0 16 16"
+      width={18}
+    >
+      <path
+        d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+      />
+      <path
+        d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+      />
+    </svg>
+  </div>
+  <div
+    className="flex-1 overflow-hidden"
+    onClick={[Function]}
+    onKeyDown={[Function]}
+  >
+    <div
+      className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden cursor-pointer"
+      role="main"
+      title="I am a robot and this is a test!"
+    >
+      I am a robot and this is a test!
+    </div>
+    <div
+      className="text-xs text-capitalize whitespace-nowrap overflow-ellipsis overflow-hidden"
+    >
+      <span
+        className="flex items-center"
+      >
+        <span
+          className="flex"
+          title="gitify-app updated over 6 years ago"
+        >
+          <span
+            onClick={[Function]}
+            onKeyDown={[Function]}
+            title="View User Profile"
+          >
+            <img
+              alt="gitify-app's avatar"
+              className="rounded-full w-4 h-4 cursor-pointer"
+              src="https://avatars.githubusercontent.com/u/133795385?s=200&v=4"
+              title="gitify-app"
+            />
+          </span>
+          <span
+            className="ml-1"
+            title="You're watching the repository."
+          >
+            Updated
+          </span>
+          <span
+            className="ml-1"
+          >
+            over 6 years ago
+          </span>
+          <span
+            className="ml-1"
+            title="octocat approved these changes"
+          >
+            <svg
+              aria-label="octocat approved these changes"
+              className="text-green-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+              />
+            </svg>
+          </span>
+          <span
+            className="ml-1"
+            title="gitify-app requested changes"
+          >
+            <svg
+              aria-label="gitify-app requested changes"
+              className="text-red-500"
+              fill="currentColor"
+              focusable="false"
+              height={16}
+              role="img"
+              style={
+                {
+                  "display": "inline-block",
+                  "overflow": "visible",
+                  "userSelect": "none",
+                  "verticalAlign": "text-bottom",
+                }
+              }
+              viewBox="0 0 16 16"
+              width={16}
+            >
+              <path
+                d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"
+              />
+            </svg>
+          </span>
+        </span>
+      </span>
+    </div>
+  </div>
+  <div
+    className="flex justify-center items-center gap-2 opacity-0 group-hover:opacity-80 transition-opacity"
+  >
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Done"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Done"
+        className="octicon octicon-check"
+        fill="currentColor"
+        focusable="false"
+        height={16}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={16}
+      >
+        <path
+          d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-red-500"
+      onClick={[Function]}
+      title="Unsubscribe from Thread"
+      type="button"
+    >
+      <svg
+        aria-label="Unsubscribe from Thread"
+        className="octicon octicon-bell-slash"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none h-full hover:text-green-500"
+      onClick={[Function]}
+      title="Mark as Read"
+      type="button"
+    >
+      <svg
+        aria-label="Mark as Read"
+        className="octicon octicon-read"
+        fill="currentColor"
+        focusable="false"
+        height={14}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={14}
+      >
+        <path
+          d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
+        />
+      </svg>
+    </button>
+  </div>
+</div>
+`;
+
 exports[`components/NotificationRow.tsx should render itself & its children without avatar 1`] = `
 <div
   className="flex space-x-3 py-2 px-3 bg-white dark:bg-gray-dark dark:text-white hover:bg-gray-100 dark:hover:bg-gray-darker border-b border-gray-100 dark:border-gray-darker group"
diff --git a/src/utils/subject.test.ts b/src/utils/subject.test.ts
index 560252ba..db1729ec 100644
--- a/src/utils/subject.test.ts
+++ b/src/utils/subject.test.ts
@@ -257,6 +257,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
 
@@ -284,6 +285,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
 
@@ -311,6 +313,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
 
@@ -338,6 +341,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
 
@@ -365,6 +369,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
 
@@ -392,6 +397,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
 
@@ -422,6 +428,7 @@ describe('utils/subject.ts', () => {
             avatar_url: mockDiscussionAuthor.avatar_url,
             type: mockDiscussionAuthor.type,
           },
+          comments: 0,
         });
       });
     });
@@ -1189,6 +1196,7 @@ function mockDiscussionNode(
     author: mockDiscussionAuthor,
     comments: {
       nodes: [],
+      totalCount: 0,
     },
   };
 }