Repository: gitify-app/gitify (ts)

Issue:
Feature Request: Display who did the thing that caused the notification
what is shown:

"Updated X minutes ago"

what I'd like:

"Updated X minutes ago by <Author of the Pull Request>"

story: As a developer working with many teams on the same project, I currently get every notification for a repository. Currently I have to open every notification to see if it's something I need to pay attention to. It'd be nice if the gitify app showed me who made the change so I don't have to blow up my browser window with new tabs.

Pull request:
feat: show notification last update user
Closes #439

Shows the last update user when available.

![Screenshot 2024-03-18 at 2 12 27 PM](https://github.com/gitify-app/gitify/assets/386277/55222c51-7900-4933-a7fb-5370d450ee75)

Currently this logic is part of the subject enrichment process, which is controlled by `SettingsState.colors`.   Perhaps this setting should be renamed to something more suitable now that it handles fetching notification state/color and notification last updated user details




Patch:
diff --git a/src/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts
index 59f046f3..f57450b8 100644
--- a/src/__mocks__/mockedData.ts
+++ b/src/__mocks__/mockedData.ts
@@ -299,6 +299,9 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2215656,
                     createdAt: '2022-02-20T18:33:39Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [],
                     },
@@ -306,6 +309,9 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2217789,
                     createdAt: '2022-02-21T03:30:42Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [],
                     },
@@ -313,11 +319,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2223243,
                     createdAt: '2022-02-21T18:26:27Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2232922,
                           createdAt: '2022-02-23T00:57:58Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -325,6 +337,9 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2232921,
                     createdAt: '2022-02-23T00:57:49Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [],
                     },
@@ -332,11 +347,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2258799,
                     createdAt: '2022-02-27T01:22:20Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2300902,
                           createdAt: '2022-03-05T17:43:52Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -344,11 +365,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2297637,
                     createdAt: '2022-03-04T20:39:44Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2300893,
                           createdAt: '2022-03-05T17:41:04Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -356,11 +383,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2299763,
                     createdAt: '2022-03-05T11:05:42Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2300895,
                           createdAt: '2022-03-05T17:41:44Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -379,6 +412,9 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2215656,
                     createdAt: '2022-02-20T18:33:39Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [],
                     },
@@ -386,6 +422,9 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2217789,
                     createdAt: '2022-02-21T03:30:42Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [],
                     },
@@ -393,11 +432,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2223243,
                     createdAt: '2022-02-21T18:26:27Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2232922,
                           createdAt: '2022-02-23T00:57:58Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -405,6 +450,9 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2232921,
                     createdAt: '2022-02-23T00:57:49Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [],
                     },
@@ -412,11 +460,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2258799,
                     createdAt: '2022-02-27T01:22:20Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2300902,
                           createdAt: '2022-03-05T17:43:52Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -424,11 +478,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2297637,
                     createdAt: '2022-03-04T20:39:44Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2300893,
                           createdAt: '2022-03-05T17:41:04Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
@@ -436,11 +496,17 @@ export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultNode> =
                   {
                     databaseId: 2299763,
                     createdAt: '2022-03-05T11:05:42Z',
+                    author: {
+                      login: 'comment-user',
+                    },
                     replies: {
                       nodes: [
                         {
                           databaseId: 2300895,
                           createdAt: '2022-03-05T17:41:44Z',
+                          author: {
+                            login: 'reply-user',
+                          },
                         },
                       ],
                     },
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 8f884cef..17c553c8 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -61,6 +61,10 @@ export const NotificationRow: React.FC<IProps> = ({
   const updatedAt = formatDistanceToNow(parseISO(notification.updated_at), {
     addSuffix: true,
   });
+  const updatedBy = notification.subject.user
+    ? ` by ${notification.subject.user}`
+    : '';
+  const updatedLabel = `Updated ${updatedAt}${updatedBy}`;
   const notificationTitle = formatForDisplay([
     notification.subject.state,
     notification.subject.type,
@@ -80,13 +84,16 @@ export const NotificationRow: React.FC<IProps> = ({
         onClick={() => pressTitle()}
         role="main"
       >
-        <div className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden">
+        <div
+          className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden"
+          title={notification.subject.title}
+        >
           {notification.subject.title}
         </div>
 
-        <div className="text-xs text-capitalize">
-          <span title={reason.description}>{reason.type}</span> - Updated{' '}
-          {updatedAt}
+        <div className="text-xs text-capitalize whitespace-nowrap overflow-ellipsis overflow-hidden">
+          <span title={reason.description}>{reason.type}</span> -{' '}
+          <span title={updatedLabel}>{updatedLabel}</span>
         </div>
       </div>
 
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 82ca9f4d..72f43459 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -17,7 +17,7 @@ import {
 } from '../utils/notifications';
 import Constants from '../utils/constants';
 import { removeNotifications } from '../utils/remove-notifications';
-import { getNotificationState } from '../utils/state';
+import { getGitifySubjectDetails } from '../utils/subject';
 
 interface NotificationsState {
   notifications: AccountNotifications[];
@@ -141,16 +141,14 @@ export const useNotifications = (colors: boolean): NotificationsState => {
                               )
                             : accounts.token;
 
-                          const notificationState = await getNotificationState(
-                            notification,
-                            token,
-                          );
+                          const additionalSubjectDetails =
+                            await getGitifySubjectDetails(notification, token);
 
                           return {
                             ...notification,
                             subject: {
                               ...notification.subject,
-                              state: notificationState,
+                              ...additionalSubjectDetails,
                             },
                           };
                         },
diff --git a/src/typesGithub.ts b/src/typesGithub.ts
index d2654a8d..49f3354f 100644
--- a/src/typesGithub.ts
+++ b/src/typesGithub.ts
@@ -157,14 +157,21 @@ export interface Owner {
   site_admin: boolean;
 }
 
-export interface Subject {
+export type Subject = GitHubSubject & GitifySubject;
+
+interface GitHubSubject {
   title: string;
   url: string | null;
-  state?: StateType; // This is not in the GitHub API, but we add it to the type to make it easier to work with
   latest_comment_url: string | null;
   type: SubjectType;
 }
 
+// This is not in the GitHub API, but we add it to the type to make it easier to work with
+export interface GitifySubject {
+  state?: StateType;
+  user?: string;
+}
+
 export interface PullRequest {
   url: string;
   id: number;
@@ -262,6 +269,7 @@ export interface DiscussionSearchResultNode {
 export interface DiscussionCommentNode {
   databaseId: string | number;
   createdAt: string;
+  author: { login: string };
   replies: {
     nodes: DiscussionSubcommentNode[];
   };
@@ -270,6 +278,7 @@ export interface DiscussionCommentNode {
 export interface DiscussionSubcommentNode {
   databaseId: string | number;
   createdAt: string;
+  author: { login: string };
 }
 
 export interface CheckSuiteAttributes {
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 62a72a74..6da0b252 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -7,11 +7,12 @@ import {
   PullRequest,
   Issue,
   IssueComments,
+  DiscussionSubcommentNode,
 } from '../typesGithub';
 import { apiRequestAuth } from '../utils/api-requests';
 import { openExternalLink } from '../utils/comms';
 import { Constants } from './constants';
-import { getWorkflowRunAttributes, getCheckSuiteAttributes } from './state';
+import { getWorkflowRunAttributes, getCheckSuiteAttributes } from './subject';
 
 export function getEnterpriseAccountToken(
   hostname: string,
@@ -126,7 +127,7 @@ async function getDiscussionUrl(
 ): Promise<string> {
   let url = `${notification.repository.html_url}/discussions`;
 
-  const discussion = await fetchDiscussion(notification, token, true);
+  const discussion = await fetchDiscussion(notification, token);
 
   if (discussion) {
     url = discussion.url;
@@ -136,7 +137,7 @@ async function getDiscussionUrl(
     let latestCommentId: string | number;
 
     if (comments?.length) {
-      latestCommentId = getLatestDiscussionCommentId(comments);
+      latestCommentId = getLatestDiscussionComment(comments)?.databaseId;
       url += `#discussioncomment-${latestCommentId}`;
     }
   }
@@ -147,12 +148,10 @@ async function getDiscussionUrl(
 export async function fetchDiscussion(
   notification: Notification,
   token: string,
-  includeComments: boolean,
 ): Promise<DiscussionSearchResultNode | null> {
   const response: GraphQLSearch<DiscussionSearchResultNode> =
     await apiRequestAuth(`https://api.github.com/graphql`, 'POST', token, {
       query: `query fetchDiscussions(
-          $includeComments: Boolean!,
           $queryStatement: String!,
           $type: SearchType!,
           $firstDiscussions: Int,
@@ -167,14 +166,20 @@ export async function fetchDiscussion(
                 stateReason
                 isAnswered
                 url
-                comments(last: $lastComments) @include(if: $includeComments){
+                comments(last: $lastComments){
                   nodes {
                     databaseId
                     createdAt
+                    author {
+                      login
+                    }
                     replies(last: $firstReplies) {
                       nodes {
                         databaseId
                         createdAt
+                        author {
+                          login
+                        }
                       }
                     }
                   }
@@ -194,7 +199,6 @@ export async function fetchDiscussion(
         firstDiscussions: 10,
         lastComments: 100,
         firstReplies: 1,
-        includeComments: includeComments,
       },
     });
 
@@ -211,13 +215,13 @@ export async function fetchDiscussion(
   return discussions[0];
 }
 
-export function getLatestDiscussionCommentId(
+export function getLatestDiscussionComment(
   comments: DiscussionCommentNode[],
-) {
+): DiscussionSubcommentNode | null {
   return comments
     .flatMap((comment) => comment.replies.nodes)
     .concat([comments.at(-1)])
-    .reduce((a, b) => (a.createdAt > b.createdAt ? a : b))?.databaseId;
+    .reduce((a, b) => (a.createdAt > b.createdAt ? a : b));
 }
 
 export async function generateGitHubWebUrl(
diff --git a/src/utils/state.ts b/src/utils/subject.ts
similarity index 55%
rename from src/utils/state.ts
rename to src/utils/subject.ts
index 0e750573..c58f54fd 100644
--- a/src/utils/state.ts
+++ b/src/utils/subject.ts
@@ -1,34 +1,33 @@
-import { fetchDiscussion } from './helpers';
+import { fetchDiscussion, getLatestDiscussionComment } from './helpers';
 import {
   CheckSuiteAttributes,
   CheckSuiteStatus,
   DiscussionStateType,
+  GitifySubject,
   Issue,
-  IssueStateReasonType,
-  IssueStateType,
+  IssueComments,
   Notification,
   PullRequest,
   PullRequestStateType,
-  StateType,
   WorkflowRunAttributes,
 } from '../typesGithub';
 import { apiRequestAuth } from './api-requests';
 
-export async function getNotificationState(
+export async function getGitifySubjectDetails(
   notification: Notification,
   token: string,
-): Promise<StateType> {
+): Promise<GitifySubject> {
   switch (notification.subject.type) {
     case 'CheckSuite':
-      return getCheckSuiteAttributes(notification)?.status;
+      return getGitifySubjectForCheckSuite(notification);
     case 'Discussion':
-      return await getDiscussionState(notification, token);
+      return await getGitifySubjectForDiscussion(notification, token);
     case 'Issue':
-      return await getIssueState(notification, token);
+      return await getGitifySubjectForIssue(notification, token);
     case 'PullRequest':
-      return await getPullRequestState(notification, token);
+      return await getGitifySubjectForPullRequest(notification, token);
     case 'WorkflowRun':
-      return getWorkflowRunAttributes(notification)?.status;
+      return getGitifySubjectForWorkflowRun(notification);
     default:
       return null;
   }
@@ -78,51 +77,113 @@ function getCheckSuiteStatus(statusDisplayName: string): CheckSuiteStatus {
   }
 }
 
-export async function getDiscussionState(
+export function getGitifySubjectForCheckSuite(
+  notification: Notification,
+): GitifySubject {
+  return {
+    state: getCheckSuiteAttributes(notification)?.status,
+    user: null,
+  };
+}
+
+export async function getGitifySubjectForDiscussion(
   notification: Notification,
   token: string,
-): Promise<DiscussionStateType> {
-  const discussion = await fetchDiscussion(notification, token, false);
+): Promise<GitifySubject> {
+  const discussion = await fetchDiscussion(notification, token);
+  let discussionState: DiscussionStateType = 'OPEN';
 
   if (discussion) {
     if (discussion.isAnswered) {
-      return 'ANSWERED';
+      discussionState = 'ANSWERED';
     }
 
     if (discussion.stateReason) {
-      return discussion.stateReason;
+      discussionState = discussion.stateReason;
     }
   }
 
-  return 'OPEN';
+  let comments = discussion.comments.nodes;
+  let discussionUser = null;
+
+  if (comments?.length) {
+    discussionUser = getLatestDiscussionComment(comments)?.author.login;
+  }
+
+  return {
+    state: discussionState,
+    user: discussionUser,
+  };
 }
 
-export async function getIssueState(
+export async function getGitifySubjectForIssue(
   notification: Notification,
   token: string,
-): Promise<IssueStateType | IssueStateReasonType> {
+): Promise<GitifySubject> {
   const issue: Issue = (
     await apiRequestAuth(notification.subject.url, 'GET', token)
   ).data;
 
-  return issue.state_reason ?? issue.state;
+  let issueCommentUser = null;
+  if (notification.subject.latest_comment_url) {
+    const issueComment: IssueComments = (
+      await apiRequestAuth(
+        notification.subject.latest_comment_url,
+        'GET',
+        token,
+      )
+    ).data;
+
+    issueCommentUser = issueComment.user.login;
+  }
+
+  return {
+    state: issue.state_reason ?? issue.state,
+    user: issueCommentUser,
+  };
 }
 
-export async function getPullRequestState(
+export async function getGitifySubjectForPullRequest(
   notification: Notification,
   token: string,
-): Promise<PullRequestStateType> {
+): Promise<GitifySubject> {
   const pr: PullRequest = (
     await apiRequestAuth(notification.subject.url, 'GET', token)
   ).data;
 
+  let prCommentUser = null;
+  if (notification.subject.latest_comment_url) {
+    const prComment: IssueComments = (
+      await apiRequestAuth(
+        notification.subject.latest_comment_url,
+        'GET',
+        token,
+      )
+    ).data;
+
+    prCommentUser = prComment.user.login;
+  }
+
+  let prState: PullRequestStateType = pr.state;
   if (pr.merged) {
-    return 'merged';
+    prState = 'merged';
   } else if (pr.draft) {
-    return 'draft';
+    prState = 'draft';
   }
 
-  return pr.state;
+  return {
+    state: prState,
+    user: prCommentUser,
+  };
+}
+
+export function getGitifySubjectForWorkflowRun(
+  notification: Notification,
+): GitifySubject {
+  return {
+    state: getWorkflowRunAttributes(notification)?.status,
+    user: null,
+  };
 }
 
 /**


Test patch:
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap
index 3dfac90d..0f107e1f 100644
--- a/src/components/__snapshots__/NotificationRow.test.tsx.snap
+++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap
@@ -42,20 +42,25 @@ exports[`components/Notification.js should render itself & its children 1`] = `
   >
     <div
       className="mb-1 text-sm whitespace-nowrap overflow-ellipsis overflow-hidden"
+      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"
+      className="text-xs text-capitalize whitespace-nowrap overflow-ellipsis overflow-hidden"
     >
       <span
         title="You're watching the repository."
       >
         Subscribed
       </span>
-       - Updated
+       -
        
-      in over 3 years
+      <span
+        title="Updated in over 3 years"
+      >
+        Updated in over 3 years
+      </span>
     </div>
   </div>
   <div
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 4b020c8f..516b49be 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -219,6 +219,7 @@ describe('hooks/useNotifications.ts', () => {
               title: 'This is an Issue.',
               type: 'Issue',
               url: 'https://api.github.com/3',
+              latest_comment_url: 'https://api.github.com/3/comments',
             },
           },
           {
@@ -227,6 +228,7 @@ describe('hooks/useNotifications.ts', () => {
               title: 'This is a Pull Request.',
               type: 'PullRequest',
               url: 'https://api.github.com/4',
+              latest_comment_url: 'https://api.github.com/4/comments',
             },
           },
           {
@@ -258,21 +260,43 @@ describe('hooks/useNotifications.ts', () => {
               search: {
                 nodes: [
                   {
-                    title: 'This is an answered discussion',
+                    title: 'This is a Discussion.',
                     viewerSubscription: 'SUBSCRIBED',
                     stateReason: null,
                     isAnswered: true,
+                    url: 'https://github.com/manosim/notifications-test/discussions/612',
+                    comments: {
+                      nodes: [
+                        {
+                          databaseId: 2297637,
+                          createdAt: '2022-03-04T20:39:44Z',
+                          author: {
+                            login: 'comment-user',
+                          },
+                          replies: {
+                            nodes: [],
+                          },
+                        },
+                      ],
+                    },
                   },
                 ],
               },
             },
           });
+
         nock('https://api.github.com')
           .get('/3')
           .reply(200, { state: 'closed', merged: true });
+        nock('https://api.github.com')
+          .get('/3/comments')
+          .reply(200, { user: { login: 'some-user' } });
         nock('https://api.github.com')
           .get('/4')
           .reply(200, { state: 'closed', merged: false });
+        nock('https://api.github.com')
+          .get('/4/comments')
+          .reply(200, { user: { login: 'some-user' } });
         nock('https://api.github.com')
           .get('/5')
           .reply(200, { state: 'open', draft: false });
diff --git a/src/utils/state.test.ts b/src/utils/subject.test.ts
similarity index 73%
rename from src/utils/state.test.ts
rename to src/utils/subject.test.ts
index 89a62318..d2eb357c 100644
--- a/src/utils/state.test.ts
+++ b/src/utils/subject.test.ts
@@ -5,11 +5,11 @@ import { mockAccounts } from '../__mocks__/mock-state';
 import { mockedSingleNotification } from '../__mocks__/mockedData';
 import {
   getCheckSuiteAttributes,
-  getDiscussionState,
-  getIssueState,
-  getPullRequestState,
+  getGitifySubjectForDiscussion,
+  getGitifySubjectForIssue,
+  getGitifySubjectForPullRequest,
   getWorkflowRunAttributes,
-} from './state';
+} from './subject';
 describe('utils/state.ts', () => {
   beforeEach(() => {
     // axios will default to using the XHR adapter which can't be intercepted
@@ -135,7 +135,7 @@ describe('utils/state.ts', () => {
     });
   });
 
-  describe('getDiscussionState', () => {
+  describe('getGitifySubjectForDiscussion', () => {
     it('answered discussion state', async () => {
       const mockNotification = {
         ...mockedSingleNotification,
@@ -156,18 +156,22 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: null,
                   isAnswered: true,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('ANSWERED');
+      expect(result.state).toBe('ANSWERED');
+      expect(result.user).toBe(null);
     });
 
     it('duplicate discussion state', async () => {
@@ -190,18 +194,22 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: 'DUPLICATE',
                   isAnswered: false,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('DUPLICATE');
+      expect(result.state).toBe('DUPLICATE');
+      expect(result.user).toBe(null);
     });
 
     it('open discussion state', async () => {
@@ -224,18 +232,22 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: null,
                   isAnswered: false,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('OPEN');
+      expect(result.state).toBe('OPEN');
+      expect(result.user).toBe(null);
     });
 
     it('outdated discussion state', async () => {
@@ -258,18 +270,22 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: 'OUTDATED',
                   isAnswered: false,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('OUTDATED');
+      expect(result.state).toBe('OUTDATED');
+      expect(result.user).toBe(null);
     });
 
     it('reopened discussion state', async () => {
@@ -292,18 +308,22 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: 'REOPENED',
                   isAnswered: false,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('REOPENED');
+      expect(result.state).toBe('REOPENED');
+      expect(result.user).toBe(null);
     });
 
     it('resolved discussion state', async () => {
@@ -326,18 +346,22 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: 'RESOLVED',
                   isAnswered: false,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('RESOLVED');
+      expect(result.state).toBe('RESOLVED');
+      expect(result.user).toBe(null);
     });
 
     it('filtered response by subscribed', async () => {
@@ -360,41 +384,53 @@ describe('utils/state.ts', () => {
                   viewerSubscription: 'SUBSCRIBED',
                   stateReason: null,
                   isAnswered: false,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
                 {
                   title: 'This is a discussion',
                   viewerSubscription: 'IGNORED',
                   stateReason: null,
                   isAnswered: true,
+                  comments: {
+                    nodes: [], //TODO - Update this to have real data
+                  },
                 },
               ],
             },
           },
         });
 
-      const result = await getDiscussionState(
+      const result = await getGitifySubjectForDiscussion(
         mockNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('OPEN');
+      expect(result.state).toBe('OPEN');
+      expect(result.user).toBe(null);
     });
 
     it('handles unknown or missing results', async () => {});
   });
 
-  describe('getIssueState', () => {
+  describe('getGitifySubjectForIssue', () => {
     it('open issue state', async () => {
       nock('https://api.github.com')
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'open' });
 
-      const result = await getIssueState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForIssue(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('open');
+      expect(result.state).toBe('open');
+      expect(result.user).toBe('some-user');
     });
 
     it('closed issue state', async () => {
@@ -402,12 +438,17 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'closed' });
 
-      const result = await getIssueState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForIssue(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('closed');
+      expect(result.state).toBe('closed');
+      expect(result.user).toBe('some-user');
     });
 
     it('completed issue state', async () => {
@@ -415,12 +456,17 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'closed', state_reason: 'completed' });
 
-      const result = await getIssueState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForIssue(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('completed');
+      expect(result.state).toBe('completed');
+      expect(result.user).toBe('some-user');
     });
 
     it('not_planned issue state', async () => {
@@ -428,12 +474,17 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'open', state_reason: 'not_planned' });
 
-      const result = await getIssueState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForIssue(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('not_planned');
+      expect(result.state).toBe('not_planned');
+      expect(result.user).toBe('some-user');
     });
 
     it('reopened issue state', async () => {
@@ -441,27 +492,37 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'open', state_reason: 'reopened' });
 
-      const result = await getIssueState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForIssue(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('reopened');
+      expect(result.state).toBe('reopened');
+      expect(result.user).toBe('some-user');
     });
   });
 
-  describe('getPullRequestState', () => {
+  describe('getGitifySubjectForPullRequest', () => {
     it('closed pull request state', async () => {
       nock('https://api.github.com')
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'closed', draft: false, merged: false });
 
-      const result = await getPullRequestState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForPullRequest(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('closed');
+      expect(result.state).toBe('closed');
+      expect(result.user).toBe('some-user');
     });
 
     it('draft pull request state', async () => {
@@ -469,12 +530,17 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'open', draft: true, merged: false });
 
-      const result = await getPullRequestState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForPullRequest(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('draft');
+      expect(result.state).toBe('draft');
+      expect(result.user).toBe('some-user');
     });
 
     it('merged pull request state', async () => {
@@ -482,12 +548,17 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'open', draft: false, merged: true });
 
-      const result = await getPullRequestState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForPullRequest(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('merged');
+      expect(result.state).toBe('merged');
+      expect(result.user).toBe('some-user');
     });
 
     it('open pull request state', async () => {
@@ -495,12 +566,17 @@ describe('utils/state.ts', () => {
         .get('/repos/manosim/notifications-test/issues/1')
         .reply(200, { state: 'open', draft: false, merged: false });
 
-      const result = await getPullRequestState(
+      nock('https://api.github.com')
+        .get('/repos/manosim/notifications-test/issues/comments/302888448')
+        .reply(200, { user: { login: 'some-user' } });
+
+      const result = await getGitifySubjectForPullRequest(
         mockedSingleNotification,
         mockAccounts.token,
       );
 
-      expect(result).toBe('open');
+      expect(result.state).toBe('open');
+      expect(result.user).toBe('some-user');
     });
   });
 
@@ -516,8 +592,8 @@ describe('utils/state.ts', () => {
 
       const result = getWorkflowRunAttributes(mockNotification);
 
-      expect(result.user).toBe('some-user');
       expect(result.status).toBe('waiting');
+      expect(result.user).toBe('some-user');
     });
 
     it('unknown workflow run state', async () => {
@@ -532,8 +608,8 @@ describe('utils/state.ts', () => {
 
       const result = getWorkflowRunAttributes(mockNotification);
 
-      expect(result.user).toBe('some-user');
       expect(result.status).toBeNull();
+      expect(result.user).toBe('some-user');
     });
 
     it('unhandled workflow run title', async () => {