Repository: gitify-app/gitify
Pull request #827 (2024-02-26 14:38:53)

feat: support discussion state types
Closes: #813

Uses the GraphQL API to fetch the Discussion `stateReason`

![Screenshot 2024-02-27 at 6 58 00 AM](https://github.com/gitify-app/gitify/assets/386277/f88549ab-6876-4c49-99f9-43611bb7873c)



setchy: In a separate PR, i think it would be wise to move all of this `state` enrichment logic out of `hooks/useNotifications` and into a separate file. 

Patch:
diff --git a/src/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts
index 228ae155..24f87f73 100644
--- a/src/__mocks__/mockedData.ts
+++ b/src/__mocks__/mockedData.ts
@@ -1,5 +1,11 @@
 import { AccountNotifications, EnterpriseAccount } from '../types';
-import { Notification, Repository, User, GraphQLSearch } from '../typesGithub';
+import {
+  Notification,
+  Repository,
+  User,
+  GraphQLSearch,
+  DiscussionSearchResultEdge,
+} from '../typesGithub';
 
 export const mockedEnterpriseAccounts: EnterpriseAccount[] = [
   {
@@ -276,217 +282,298 @@ export const mockedSingleAccountNotifications: AccountNotifications[] = [
   },
 ];
 
-export const mockedGraphQLResponse: GraphQLSearch = {
-  data: {
+export const mockedGraphQLResponse: GraphQLSearch<DiscussionSearchResultEdge> =
+  {
     data: {
-      search: {
-        edges: [
-          {
-            node: {
-              viewerSubscription: 'SUBSCRIBED',
-              title: '1.16.0',
-              url: 'https://github.com/manosim/notifications-test/discussions/612',
-              comments: {
-                edges: [
-                  {
-                    node: {
-                      databaseId: 2215656,
-                      createdAt: '2022-02-20T18:33:39Z',
-                      replies: {
-                        edges: [],
+      data: {
+        search: {
+          edges: [
+            {
+              node: {
+                viewerSubscription: 'SUBSCRIBED',
+                title: '1.16.0',
+                url: 'https://github.com/manosim/notifications-test/discussions/612',
+                comments: {
+                  edges: [
+                    {
+                      node: {
+                        databaseId: 2215656,
+                        createdAt: '2022-02-20T18:33:39Z',
+                        replies: {
+                          edges: [],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2217789,
-                      createdAt: '2022-02-21T03:30:42Z',
-                      replies: {
-                        edges: [],
+                    {
+                      node: {
+                        databaseId: 2217789,
+                        createdAt: '2022-02-21T03:30:42Z',
+                        replies: {
+                          edges: [],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2223243,
-                      createdAt: '2022-02-21T18:26:27Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2232922,
-                              createdAt: '2022-02-23T00:57:58Z',
+                    {
+                      node: {
+                        databaseId: 2223243,
+                        createdAt: '2022-02-21T18:26:27Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2232922,
+                                createdAt: '2022-02-23T00:57:58Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2232921,
-                      createdAt: '2022-02-23T00:57:49Z',
-                      replies: {
-                        edges: [],
+                    {
+                      node: {
+                        databaseId: 2232921,
+                        createdAt: '2022-02-23T00:57:49Z',
+                        replies: {
+                          edges: [],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2258799,
-                      createdAt: '2022-02-27T01:22:20Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2300902,
-                              createdAt: '2022-03-05T17:43:52Z',
+                    {
+                      node: {
+                        databaseId: 2258799,
+                        createdAt: '2022-02-27T01:22:20Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2300902,
+                                createdAt: '2022-03-05T17:43:52Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2297637,
-                      createdAt: '2022-03-04T20:39:44Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2300893,
-                              createdAt: '2022-03-05T17:41:04Z',
+                    {
+                      node: {
+                        databaseId: 2297637,
+                        createdAt: '2022-03-04T20:39:44Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2300893,
+                                createdAt: '2022-03-05T17:41:04Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2299763,
-                      createdAt: '2022-03-05T11:05:42Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2300895,
-                              createdAt: '2022-03-05T17:41:44Z',
+                    {
+                      node: {
+                        databaseId: 2299763,
+                        createdAt: '2022-03-05T11:05:42Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2300895,
+                                createdAt: '2022-03-05T17:41:44Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                ],
+                  ],
+                },
               },
             },
-          },
-          {
-            node: {
-              viewerSubscription: 'IGNORED',
-              title: '1.16.0',
-              url: 'https://github.com/manosim/notifications-test/discussions/612',
-              comments: {
-                edges: [
-                  {
-                    node: {
-                      databaseId: 2215656,
-                      createdAt: '2022-02-20T18:33:39Z',
-                      replies: {
-                        edges: [],
+            {
+              node: {
+                viewerSubscription: 'IGNORED',
+                title: '1.16.0',
+                url: 'https://github.com/manosim/notifications-test/discussions/612',
+                comments: {
+                  edges: [
+                    {
+                      node: {
+                        databaseId: 2215656,
+                        createdAt: '2022-02-20T18:33:39Z',
+                        replies: {
+                          edges: [],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2217789,
-                      createdAt: '2022-02-21T03:30:42Z',
-                      replies: {
-                        edges: [],
+                    {
+                      node: {
+                        databaseId: 2217789,
+                        createdAt: '2022-02-21T03:30:42Z',
+                        replies: {
+                          edges: [],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2223243,
-                      createdAt: '2022-02-21T18:26:27Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2232922,
-                              createdAt: '2022-02-23T00:57:58Z',
+                    {
+                      node: {
+                        databaseId: 2223243,
+                        createdAt: '2022-02-21T18:26:27Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2232922,
+                                createdAt: '2022-02-23T00:57:58Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2232921,
-                      createdAt: '2022-02-23T00:57:49Z',
-                      replies: {
-                        edges: [],
+                    {
+                      node: {
+                        databaseId: 2232921,
+                        createdAt: '2022-02-23T00:57:49Z',
+                        replies: {
+                          edges: [],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2258799,
-                      createdAt: '2022-02-27T01:22:20Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2300902,
-                              createdAt: '2022-03-05T17:43:52Z',
+                    {
+                      node: {
+                        databaseId: 2258799,
+                        createdAt: '2022-02-27T01:22:20Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2300902,
+                                createdAt: '2022-03-05T17:43:52Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2297637,
-                      createdAt: '2022-03-04T20:39:44Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2300893,
-                              createdAt: '2022-03-05T17:41:04Z',
+                    {
+                      node: {
+                        databaseId: 2297637,
+                        createdAt: '2022-03-04T20:39:44Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2300893,
+                                createdAt: '2022-03-05T17:41:04Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                  {
-                    node: {
-                      databaseId: 2299763,
-                      createdAt: '2022-03-05T11:05:42Z',
-                      replies: {
-                        edges: [
-                          {
-                            node: {
-                              databaseId: 2300895,
-                              createdAt: '2022-03-05T17:41:44Z',
+                    {
+                      node: {
+                        databaseId: 2299763,
+                        createdAt: '2022-03-05T11:05:42Z',
+                        replies: {
+                          edges: [
+                            {
+                              node: {
+                                databaseId: 2300895,
+                                createdAt: '2022-03-05T17:41:44Z',
+                              },
                             },
-                          },
-                        ],
+                          ],
+                        },
                       },
                     },
-                  },
-                ],
+                  ],
+                },
               },
             },
-          },
-        ],
+          ],
+        },
       },
     },
+  };
+
+export const mockedDiscussionNotifications = [
+  {
+    id: 1,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is an answered discussion',
+      type: 'Discussion',
+    },
   },
-};
+  {
+    id: 2,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is a duplicate discussion',
+      type: 'Discussion',
+    },
+  },
+  {
+    id: 3,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is an open discussion',
+      type: 'Discussion',
+    },
+  },
+  {
+    id: 4,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is nm outdated discussion',
+      type: 'Discussion',
+    },
+  },
+  {
+    id: 5,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is a reopened discussion',
+      type: 'Discussion',
+    },
+  },
+  {
+    id: 6,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is a resolved discussion',
+      type: 'Discussion',
+    },
+  },
+  {
+    id: 7,
+    updated_at: '2024-02-26T00:00:00Z',
+    repository: {
+      full_name: 'some/repo',
+    },
+    subject: {
+      title: 'This is a default discussion',
+      type: 'Discussion',
+    },
+  },
+];
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 7ad40514..4d875a44 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -9,6 +9,7 @@ import {
   getEnterpriseAccountToken,
   generateGitHubAPIUrl,
   isEnterpriseHost,
+  getDiscussionState,
 } from '../utils/helpers';
 import { removeNotification } from '../utils/remove-notification';
 import {
@@ -130,12 +131,6 @@ export const useNotifications = (colors: boolean): NotificationsState => {
                     notifications: await axios.all<Notification>(
                       accountNotifications.notifications.map(
                         async (notification: Notification) => {
-                          if (
-                            notification.subject.type !== 'PullRequest' &&
-                            notification.subject.type !== 'Issue'
-                          ) {
-                            return notification;
-                          }
                           const isEnterprise = isEnterpriseHost(
                             accountNotifications.hostname,
                           );
@@ -146,28 +141,47 @@ export const useNotifications = (colors: boolean): NotificationsState => {
                               )
                             : accounts.token;
 
-                          const cardinalData = (
-                            await apiRequestAuth(
-                              notification.subject.url,
-                              'GET',
-                              token,
-                            )
-                          ).data;
-
-                          const state =
-                            cardinalData.state === 'closed'
-                              ? cardinalData.state_reason ||
-                                (cardinalData.merged && 'merged') ||
-                                'closed'
-                              : (cardinalData.draft && 'draft') || 'open';
-
-                          return {
-                            ...notification,
-                            subject: {
-                              ...notification.subject,
-                              state,
-                            },
-                          };
+                          switch (notification.subject.type) {
+                            case 'Discussion':
+                              const discussionState = await getDiscussionState(
+                                notification,
+                                token,
+                              );
+
+                              return {
+                                ...notification,
+                                subject: {
+                                  ...notification.subject,
+                                  state: discussionState,
+                                },
+                              };
+                            case 'Issue':
+                            case 'PullRequest':
+                              const cardinalData = (
+                                await apiRequestAuth(
+                                  notification.subject.url,
+                                  'GET',
+                                  token,
+                                )
+                              ).data;
+
+                              const state =
+                                cardinalData.state === 'closed'
+                                  ? cardinalData.state_reason ||
+                                    (cardinalData.merged && 'merged') ||
+                                    'closed'
+                                  : (cardinalData.draft && 'draft') || 'open';
+
+                              return {
+                                ...notification,
+                                subject: {
+                                  ...notification.subject,
+                                  state,
+                                },
+                              };
+                            default:
+                              return notification;
+                          }
                         },
                       ),
                     ),
diff --git a/src/typesGithub.ts b/src/typesGithub.ts
index 61bda5b3..d7b18de7 100644
--- a/src/typesGithub.ts
+++ b/src/typesGithub.ts
@@ -15,6 +15,15 @@ export type Reason =
   | 'subscribed'
   | 'team_mention';
 
+// Note: ANSWERED and OPEN are not an official discussion state type in the GitHub API
+export type DiscussionStateType =
+  | 'ANSWERED'
+  | 'DUPLICATE'
+  | 'OPEN'
+  | 'OUTDATED'
+  | 'REOPENED'
+  | 'RESOLVED';
+
 export type SubjectType =
   | 'CheckSuite'
   | 'Commit'
@@ -35,7 +44,10 @@ export type IssueStateType =
 
 export type PullRequestStateType = 'closed' | 'draft' | 'merged' | 'open';
 
-export type StateType = IssueStateType | PullRequestStateType;
+export type StateType =
+  | DiscussionStateType
+  | IssueStateType
+  | PullRequestStateType;
 
 export type ViewerSubscription = 'IGNORED' | 'SUBSCRIBED' | 'UNSUBSCRIBED';
 
@@ -145,22 +157,31 @@ export interface Owner {
 export interface Subject {
   title: string;
   url: string | null;
-  state?: StateType;
+  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;
 }
 
-export interface GraphQLSearch {
+export interface GraphQLSearch<T> {
   data: {
     data: {
       search: {
-        edges: DiscussionEdge[];
+        edges: T[];
       };
     };
   };
 }
 
-export interface DiscussionEdge {
+export interface DiscussionStateSearchResultEdge {
+  node: {
+    viewerSubscription: ViewerSubscription;
+    title: string;
+    stateReason: DiscussionStateType;
+    isAnswered: boolean;
+  };
+}
+
+export interface DiscussionSearchResultEdge {
   node: {
     viewerSubscription: ViewerSubscription;
     title: string;
diff --git a/src/utils/github-api.ts b/src/utils/github-api.ts
index 0e378990..eb2a6444 100644
--- a/src/utils/github-api.ts
+++ b/src/utils/github-api.ts
@@ -2,6 +2,9 @@ import {
   AlertIcon,
   CheckIcon,
   CommentDiscussionIcon,
+  DiscussionClosedIcon,
+  DiscussionDuplicateIcon,
+  DiscussionOutdatedIcon,
   GitCommitIcon,
   GitMergeIcon,
   GitPullRequestClosedIcon,
@@ -106,7 +109,16 @@ export function getNotificationTypeIcon(
     case 'Commit':
       return GitCommitIcon;
     case 'Discussion':
-      return CommentDiscussionIcon;
+      switch (subject.state) {
+        case 'DUPLICATE':
+          return DiscussionDuplicateIcon;
+        case 'OUTDATED':
+          return DiscussionOutdatedIcon;
+        case 'RESOLVED':
+          return DiscussionClosedIcon;
+        default:
+          return CommentDiscussionIcon;
+      }
     case 'Issue':
       switch (subject.state) {
         case 'draft':
@@ -164,6 +176,8 @@ export function getNotificationTypeIconColor(subject: Subject): string {
   }
 
   switch (subject.state) {
+    case 'ANSWERED':
+      return 'text-green-500';
     case 'closed':
       return 'text-red-500';
     case 'completed':
@@ -178,6 +192,8 @@ export function getNotificationTypeIconColor(subject: Subject): string {
       return 'text-green-500';
     case 'reopened':
       return 'text-green-500';
+    case 'RESOLVED':
+      return 'text-purple-500';
     default:
       return 'text-gray-300';
   }
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 48cd1347..d343f7b1 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -3,6 +3,9 @@ import {
   Notification,
   GraphQLSearch,
   DiscussionCommentEdge,
+  DiscussionStateType,
+  DiscussionStateSearchResultEdge,
+  DiscussionSearchResultEdge,
 } from '../typesGithub';
 import { apiRequestAuth } from '../utils/api-requests';
 import { openExternalLink } from '../utils/comms';
@@ -75,11 +78,8 @@ async function getDiscussionUrl(
 ): Promise<string> {
   let url = `${notification.repository.html_url}/discussions`;
 
-  const response: GraphQLSearch = await apiRequestAuth(
-    `https://api.github.com/graphql`,
-    'POST',
-    token,
-    {
+  const response: GraphQLSearch<DiscussionSearchResultEdge> =
+    await apiRequestAuth(`https://api.github.com/graphql`, 'POST', token, {
       query: `{
       search(query:"${formatSearchQueryString(
         notification.repository.full_name,
@@ -92,6 +92,7 @@ async function getDiscussionUrl(
                       viewerSubscription
                       title
                       url
+                      stateReason
                       comments(last: 100) {
                         edges {
                           node {
@@ -113,8 +114,7 @@ async function getDiscussionUrl(
           }
       }
     }`,
-    },
-  );
+    });
   let edges =
     response?.data?.data?.search?.edges?.filter(
       (edge) => edge.node.title === notification.subject.title,
@@ -139,6 +139,53 @@ async function getDiscussionUrl(
   return url;
 }
 
+export async function getDiscussionState(
+  notification: Notification,
+  token: string,
+): Promise<DiscussionStateType> {
+  const response: GraphQLSearch<DiscussionStateSearchResultEdge> =
+    await apiRequestAuth(`https://api.github.com/graphql`, 'POST', token, {
+      query: `{
+        search(query:"${formatSearchQueryString(
+          notification.repository.full_name,
+          notification.subject.title,
+          notification.updated_at,
+        )}", type: DISCUSSION, first: 10) {
+          edges {
+            node {
+              ... on Discussion {
+                viewerSubscription
+                title
+                stateReason  
+                isAnswered
+              }
+            }
+          }
+        }
+      }`,
+    });
+  let edges =
+    response?.data?.data?.search?.edges?.filter(
+      (edge) => edge.node.title === notification.subject.title,
+    ) || [];
+  if (edges.length > 1)
+    edges = edges.filter(
+      (edge) => edge.node.viewerSubscription === 'SUBSCRIBED',
+    );
+
+  if (edges[0]) {
+    if (edges[0].node.isAnswered) {
+      return 'ANSWERED';
+    }
+
+    if (edges[0].node.stateReason) {
+      return edges[0].node.stateReason;
+    }
+  }
+
+  return 'OPEN';
+}
+
 export const getLatestDiscussionCommentId = (
   comments: DiscussionCommentEdge[],
 ) =>


Test patch:
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 2d133363..4285d006 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -3,7 +3,10 @@ import axios from 'axios';
 import nock from 'nock';
 
 import { mockAccounts, mockSettings } from '../__mocks__/mock-state';
-import { mockedUser } from '../__mocks__/mockedData';
+import {
+  mockedDiscussionNotifications,
+  mockedUser,
+} from '../__mocks__/mockedData';
 import { AuthState } from '../types';
 import { useNotifications } from './useNotifications';
 
@@ -191,28 +194,43 @@ describe('hooks/useNotifications.ts', () => {
         const notifications = [
           {
             id: 1,
-            title: 'This is a notification.',
-            subject: { type: 'Issue', url: 'https://api.github.com/1' },
+            subject: {
+              title: 'This is a notification.',
+              type: 'Issue',
+              url: 'https://api.github.com/1',
+            },
           },
           {
             id: 2,
-            title: 'A merged PR.',
-            subject: { type: 'PullRequest', url: 'https://api.github.com/2' },
+            subject: {
+              title: 'A merged PR.',
+              type: 'PullRequest',
+              url: 'https://api.github.com/2',
+            },
           },
           {
             id: 3,
-            title: 'A closed PR.',
-            subject: { type: 'PullRequest', url: 'https://api.github.com/3' },
+            subject: {
+              title: 'A closed PR.',
+              type: 'PullRequest',
+              url: 'https://api.github.com/3',
+            },
           },
           {
             id: 4,
-            title: 'A draft PR.',
-            subject: { type: 'PullRequest', url: 'https://api.github.com/4' },
+            subject: {
+              title: 'A draft PR.',
+              type: 'PullRequest',
+              url: 'https://api.github.com/4',
+            },
           },
           {
             id: 5,
-            title: 'A draft PR.',
-            subject: { type: 'PullRequest', url: 'https://api.github.com/5' },
+            subject: {
+              title: 'A draft PR.',
+              type: 'PullRequest',
+              url: 'https://api.github.com/5',
+            },
           },
         ];
 
@@ -266,6 +284,183 @@ describe('hooks/useNotifications.ts', () => {
           result.current.notifications[0].notifications[4].subject.state,
         ).toBe('draft');
       });
+
+      it('should fetch discussion notifications with success - with colors', async () => {
+        const accounts: AuthState = {
+          ...mockAccounts,
+          enterpriseAccounts: [],
+          user: mockedUser,
+        };
+
+        nock('https://api.github.com')
+          .get('/notifications?participating=false')
+          .reply(200, mockedDiscussionNotifications);
+
+        nock('https://api.github.com')
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'This is an answered discussion',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: null,
+                      isAnswered: true,
+                    },
+                  },
+                ],
+              },
+            },
+          })
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'This is a duplicate discussion',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: 'DUPLICATE',
+                      isAnswered: false,
+                    },
+                  },
+                ],
+              },
+            },
+          })
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'This is an open discussion',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: null,
+                      isAnswered: false,
+                    },
+                  },
+                  {
+                    node: {
+                      title: 'This is an open discussion',
+                      viewerSubscription: 'IGNORED',
+                      stateReason: null,
+                      isAnswered: false,
+                    },
+                  },
+                ],
+              },
+            },
+          })
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'This is nm outdated discussion',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: 'OUTDATED',
+                      isAnswered: false,
+                    },
+                  },
+                ],
+              },
+            },
+          })
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'This is a reopened discussion',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: 'REOPENED',
+                      isAnswered: false,
+                    },
+                  },
+                ],
+              },
+            },
+          })
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'This is a resolved discussion',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: 'RESOLVED',
+                      isAnswered: false,
+                    },
+                  },
+                ],
+              },
+            },
+          })
+          .post('/graphql')
+          .reply(200, {
+            data: {
+              search: {
+                edges: [
+                  {
+                    node: {
+                      title: 'unknown search result',
+                      viewerSubscription: 'SUBSCRIBED',
+                      stateReason: null,
+                      isAnswered: false,
+                    },
+                  },
+                ],
+              },
+            },
+          });
+
+        const { result } = renderHook(() => useNotifications(true));
+
+        act(() => {
+          result.current.fetchNotifications(accounts, {
+            ...mockSettings,
+            colors: true,
+          });
+        });
+
+        expect(result.current.isFetching).toBe(true);
+
+        await waitFor(() => {
+          expect(result.current.notifications[0].hostname).toBe('github.com');
+        });
+
+        const resultNotifications = result.current.notifications[0];
+
+        expect(resultNotifications.notifications.length).toBe(7);
+        expect(resultNotifications.notifications[0].subject.state).toBe(
+          'ANSWERED',
+        );
+        expect(resultNotifications.notifications[1].subject.state).toBe(
+          'DUPLICATE',
+        );
+        expect(resultNotifications.notifications[2].subject.state).toBe('OPEN');
+        expect(resultNotifications.notifications[3].subject.state).toBe(
+          'OUTDATED',
+        );
+        expect(resultNotifications.notifications[4].subject.state).toBe(
+          'REOPENED',
+        );
+        expect(resultNotifications.notifications[5].subject.state).toBe(
+          'RESOLVED',
+        );
+        expect(resultNotifications.notifications[6].subject.state).toBe('OPEN');
+      });
     });
   });
 
diff --git a/src/utils/__snapshots__/github-api.test.ts.snap b/src/utils/__snapshots__/github-api.test.ts.snap
index e6b22630..0b472531 100644
--- a/src/utils/__snapshots__/github-api.test.ts.snap
+++ b/src/utils/__snapshots__/github-api.test.ts.snap
@@ -122,18 +122,22 @@ exports[`getNotificationTypeIconColor should format the notification color for c
 
 exports[`getNotificationTypeIconColor should format the notification color for check suite 5`] = `"text-gray-300"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 1`] = `"text-red-500"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 1`] = `"text-green-500"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 2`] = `"text-purple-500"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 2`] = `"text-red-500"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 3`] = `"text-gray-600"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 3`] = `"text-purple-500"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 4`] = `"text-purple-500"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 4`] = `"text-gray-600"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 5`] = `"text-gray-300"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 5`] = `"text-purple-500"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 6`] = `"text-green-500"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 6`] = `"text-gray-300"`;
 
 exports[`getNotificationTypeIconColor should format the notification color for state 7`] = `"text-green-500"`;
 
-exports[`getNotificationTypeIconColor should format the notification color for state 8`] = `"text-gray-300"`;
+exports[`getNotificationTypeIconColor should format the notification color for state 8`] = `"text-green-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 9`] = `"text-purple-500"`;
+
+exports[`getNotificationTypeIconColor should format the notification color for state 10`] = `"text-gray-300"`;
diff --git a/src/utils/github-api.test.ts b/src/utils/github-api.test.ts
index d3a335b2..6e000f22 100644
--- a/src/utils/github-api.test.ts
+++ b/src/utils/github-api.test.ts
@@ -72,6 +72,21 @@ describe('getNotificationTypeIcon', () => {
       getNotificationTypeIcon(createSubjectMock({ type: 'Discussion' }))
         .displayName,
     ).toBe('CommentDiscussionIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({ type: 'Discussion', state: 'DUPLICATE' }),
+      ).displayName,
+    ).toBe('DiscussionDuplicateIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({ type: 'Discussion', state: 'OUTDATED' }),
+      ).displayName,
+    ).toBe('DiscussionOutdatedIcon');
+    expect(
+      getNotificationTypeIcon(
+        createSubjectMock({ type: 'Discussion', state: 'RESOLVED' }),
+      ).displayName,
+    ).toBe('DiscussionClosedIcon');
     expect(
       getNotificationTypeIcon(createSubjectMock({ type: 'Issue' })).displayName,
     ).toBe('IssueOpenedIcon');
@@ -220,6 +235,9 @@ describe('getNotificationTypeIconColor', () => {
   });
 
   it('should format the notification color for state', () => {
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'ANSWERED' })),
+    ).toMatchSnapshot();
     expect(
       getNotificationTypeIconColor(createSubjectMock({ state: 'closed' })),
     ).toMatchSnapshot();
@@ -241,6 +259,9 @@ describe('getNotificationTypeIconColor', () => {
     expect(
       getNotificationTypeIconColor(createSubjectMock({ state: 'reopened' })),
     ).toMatchSnapshot();
+    expect(
+      getNotificationTypeIconColor(createSubjectMock({ state: 'RESOLVED' })),
+    ).toMatchSnapshot();
     expect(
       getNotificationTypeIconColor(
         createSubjectMock({