Repository: gitify-app/gitify
Pull request #1098 (2024-05-08 20:17:14)

refactor: main script
Update `main.js` to 
* use `mb` instead of menubarApp so that it matches the examples from https://github.com/max-mapper/menubar
* group ready event code by
  * mb config
  * mb events
  * auto updater
  * ipc events

setchy: Will leave in draft until #1097 is merged
setchy: Actually, since #1097 is still addressing testing feedback, let's move this one through first...

Patch:
diff --git a/main.js b/main.js
index 1cb8e2cf..99bb3600 100644
--- a/main.js
+++ b/main.js
@@ -1,4 +1,4 @@
-const { ipcMain, app, nativeTheme } = require('electron');
+const { ipcMain: ipc, app, nativeTheme } = require('electron');
 const { menubar } = require('menubar');
 const { autoUpdater } = require('electron-updater');
 const { onFirstRunMaybe } = require('./first-run');
@@ -6,8 +6,6 @@ const path = require('node:path');
 
 require('@electron/remote/main').initialize();
 
-app.setAppUserModelId('com.electron.gitify');
-
 const iconIdle = path.join(
   __dirname,
   'assets',
@@ -34,7 +32,7 @@ app.on('ready', async () => {
   await onFirstRunMaybe();
 });
 
-const menubarApp = menubar({
+const mb = menubar({
   icon: iconIdle,
   index: `file://${__dirname}/index.html`,
   browserWindow: browserWindowOpts,
@@ -42,69 +40,69 @@ const menubarApp = menubar({
   showDockIcon: false,
 });
 
-menubarApp.on('ready', () => {
+mb.on('ready', () => {
+  mb.app.setAppUserModelId('com.electron.gitify');
+  mb.tray.setIgnoreDoubleClickEvents(true);
   // Force the window to retrieve its previous zoom factor
-  menubarApp.window.webContents.setZoomFactor(
-    menubarApp.window.webContents.getZoomFactor(),
-  );
+  mb.window.webContents.setZoomFactor(mb.window.webContents.getZoomFactor());
+
+  mb.window.webContents.on('devtools-opened', () => {
+    mb.window.setSize(800, 600);
+    mb.window.center();
+    mb.window.resizable = true;
+  });
 
-  menubarApp.tray.setIgnoreDoubleClickEvents(true);
+  mb.window.webContents.on('devtools-closed', () => {
+    const trayBounds = mb.tray.getBounds();
+    mb.window.setSize(browserWindowOpts.width, browserWindowOpts.height);
+    mb.positioner.move('trayCenter', trayBounds);
+    mb.window.resizable = false;
+  });
+
+  mb.window.webContents.on('before-input-event', (event, input) => {
+    if (input.key === 'Escape') {
+      mb.window.hide();
+      event.preventDefault();
+    }
+  });
 
   autoUpdater.checkForUpdatesAndNotify();
 
   nativeTheme.on('updated', () => {
     if (nativeTheme.shouldUseDarkColors) {
-      menubarApp.window.webContents.send('update-native-theme', 'DARK');
+      mb.window.webContents.send('update-native-theme', 'DARK');
     } else {
-      menubarApp.window.webContents.send('update-native-theme', 'LIGHT');
+      mb.window.webContents.send('update-native-theme', 'LIGHT');
     }
   });
 
-  ipcMain.handle('get-platform', () => process.platform);
-  ipcMain.handle('get-app-version', () => app.getVersion());
+  ipc.handle('get-platform', () => process.platform);
+
+  ipc.handle('get-app-version', () => app.getVersion());
+
+  ipc.on('reopen-window', () => mb.showWindow());
+
+  ipc.on('hide-window', () => mb.hideWindow());
 
-  ipcMain.on('reopen-window', () => menubarApp.showWindow());
-  ipcMain.on('hide-window', () => menubarApp.hideWindow());
+  ipc.on('app-quit', () => mb.app.quit());
 
-  ipcMain.on('app-quit', () => menubarApp.app.quit());
-  ipcMain.on('update-icon', (_, arg) => {
-    if (!menubarApp.tray.isDestroyed()) {
+  ipc.on('update-icon', (_, arg) => {
+    if (!mb.tray.isDestroyed()) {
       if (arg === 'TrayActive') {
-        menubarApp.tray.setImage(iconActive);
+        mb.tray.setImage(iconActive);
       } else {
-        menubarApp.tray.setImage(iconIdle);
+        mb.tray.setImage(iconIdle);
       }
     }
   });
-  ipcMain.on('update-title', (_, title) => {
-    if (!menubarApp.tray.isDestroyed()) {
-      menubarApp.tray.setTitle(title);
-    }
-  });
-  ipcMain.on('set-login-item-settings', (event, settings) => {
-    app.setLoginItemSettings(settings);
-  });
-
-  menubarApp.window.webContents.on('devtools-opened', () => {
-    menubarApp.window.setSize(800, 600);
-    menubarApp.window.center();
-    menubarApp.window.resizable = true;
-  });
 
-  menubarApp.window.webContents.on('devtools-closed', () => {
-    const trayBounds = menubarApp.tray.getBounds();
-    menubarApp.window.setSize(
-      browserWindowOpts.width,
-      browserWindowOpts.height,
-    );
-    menubarApp.positioner.move('trayCenter', trayBounds);
-    menubarApp.window.resizable = false;
+  ipc.on('update-title', (_, title) => {
+    if (!mb.tray.isDestroyed()) {
+      mb.tray.setTitle(title);
+    }
   });
 
-  menubarApp.window.webContents.on('before-input-event', (event, input) => {
-    if (input.key === 'Escape') {
-      menubarApp.window.hide();
-      event.preventDefault();
-    }
+  ipc.on('set-login-item-settings', (_, settings) => {
+    app.setLoginItemSettings(settings);
   });
 });
diff --git a/src/__mocks__/mockedData.ts b/src/__mocks__/mockedData.ts
index 776a0787..085fa8e1 100644
--- a/src/__mocks__/mockedData.ts
+++ b/src/__mocks__/mockedData.ts
@@ -427,7 +427,6 @@ export const mockedGraphQLResponse: GraphQLSearch<Discussion> = {
     search: {
       nodes: [
         {
-          viewerSubscription: 'SUBSCRIBED',
           title: '1.16.0',
           isAnswered: false,
           stateReason: 'OPEN',
@@ -440,20 +439,6 @@ export const mockedGraphQLResponse: GraphQLSearch<Discussion> = {
           },
           comments: mockDiscussionComments,
         },
-        {
-          viewerSubscription: 'IGNORED',
-          title: '1.16.0',
-          isAnswered: false,
-          stateReason: 'ANSWERED',
-          url: 'https://github.com/gitify-app/notifications-test/discussions/123',
-          author: {
-            login: 'discussion-creator',
-            url: 'https://github.com/discussion-creator',
-            avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4',
-            type: 'User',
-          },
-          comments: mockDiscussionComments,
-        },
       ],
     },
   },
diff --git a/src/typesGitHub.ts b/src/typesGitHub.ts
index cb099bff..b9889cef 100644
--- a/src/typesGitHub.ts
+++ b/src/typesGitHub.ts
@@ -59,8 +59,6 @@ export type StateType =
   | IssueStateReasonType
   | PullRequestStateType;
 
-export type ViewerSubscription = 'IGNORED' | 'SUBSCRIBED' | 'UNSUBSCRIBED';
-
 export type CheckSuiteStatus =
   | 'action_required'
   | 'cancelled'
@@ -451,7 +449,6 @@ export interface GraphQLSearch<T> {
 }
 
 export interface Discussion {
-  viewerSubscription: ViewerSubscription;
   title: string;
   stateReason: DiscussionStateType;
   isAnswered: boolean;
diff --git a/src/utils/api/client.ts b/src/utils/api/client.ts
index 3401851d..e1bd63db 100644
--- a/src/utils/api/client.ts
+++ b/src/utils/api/client.ts
@@ -20,7 +20,8 @@ import { apiRequestAuth } from './request';
 import { print } from 'graphql/language/printer';
 import Constants from '../constants';
 import { QUERY_SEARCH_DISCUSSIONS } from './graphql/discussions';
-import { formatSearchQueryString, getGitHubAPIBaseUrl } from './utils';
+import { formatAsGitHubSearchSyntax } from './graphql/utils';
+import { getGitHubAPIBaseUrl } from './utils';
 
 /**
  * Get Hypermedia links to resources accessible in GitHub's REST API
@@ -239,7 +240,7 @@ export async function getHtmlUrl(url: string, token: string): Promise<string> {
 /**
  * Search for Discussions that match notification title and repository.
  *
- * Returns first 10 matching discussions and their latest comments / replies
+ * Returns the latest discussion and their latest comments / replies
  *
  */
 export async function searchDiscussions(
@@ -249,14 +250,30 @@ export async function searchDiscussions(
   return apiRequestAuth(Constants.GITHUB_API_GRAPHQL_URL, 'POST', token, {
     query: print(QUERY_SEARCH_DISCUSSIONS),
     variables: {
-      queryStatement: formatSearchQueryString(
+      queryStatement: formatAsGitHubSearchSyntax(
         notification.repository.full_name,
         notification.subject.title,
-        notification.updated_at,
       ),
-      firstDiscussions: 10,
+      firstDiscussions: 1,
       lastComments: 1,
       lastReplies: 1,
     },
   });
 }
+
+/**
+ * Return the latest discussion that matches the notification title and repository.
+ */
+export async function getLatestDiscussion(
+  notification: Notification,
+  token: string,
+): Promise<Discussion | null> {
+  try {
+    const response = await searchDiscussions(notification, token);
+    return (
+      response.data?.data.search.nodes.filter(
+        (discussion) => discussion.title === notification.subject.title,
+      )[0] ?? null
+    );
+  } catch (err) {}
+}
diff --git a/src/utils/api/graphql/discussions.ts b/src/utils/api/graphql/discussions.ts
index 405337ae..7472f10a 100644
--- a/src/utils/api/graphql/discussions.ts
+++ b/src/utils/api/graphql/discussions.ts
@@ -31,7 +31,6 @@ export const QUERY_SEARCH_DISCUSSIONS = gql`
     search(query:$queryStatement, type: DISCUSSION, first: $firstDiscussions) {
       nodes {
         ... on Discussion {
-          viewerSubscription
           title
           stateReason
           isAnswered
diff --git a/src/utils/api/graphql/utils.ts b/src/utils/api/graphql/utils.ts
new file mode 100644
index 00000000..6cc36a0d
--- /dev/null
+++ b/src/utils/api/graphql/utils.ts
@@ -0,0 +1,6 @@
+export function formatAsGitHubSearchSyntax(
+  repo: string,
+  title: string,
+): string {
+  return `${title} in:title repo:${repo}`;
+}
diff --git a/src/utils/api/utils.ts b/src/utils/api/utils.ts
index 954704b0..3b35b300 100644
--- a/src/utils/api/utils.ts
+++ b/src/utils/api/utils.ts
@@ -10,15 +10,3 @@ export function getGitHubAPIBaseUrl(hostname: string): URL {
   }
   return url;
 }
-
-export function formatSearchQueryString(
-  repo: string,
-  title: string,
-  lastUpdated: string,
-): string {
-  return `${title} in:title repo:${repo} updated:>${addHours(lastUpdated, -2)}`;
-}
-
-export function addHours(date: string, hours: number): string {
-  return new Date(new Date(date).getTime() + hours * 36e5).toISOString();
-}
diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts
index 535f1772..7a8a1ee5 100644
--- a/src/utils/helpers.ts
+++ b/src/utils/helpers.ts
@@ -1,14 +1,14 @@
 import { formatDistanceToNow, parseISO } from 'date-fns';
 import type { AuthState } from '../types';
-import type {
-  Discussion,
-  DiscussionComment,
-  Notification,
-} from '../typesGitHub';
+import type { Notification } from '../typesGitHub';
 import { openExternalLink } from '../utils/comms';
-import { getHtmlUrl, searchDiscussions } from './api/client';
+import { getHtmlUrl, getLatestDiscussion } from './api/client';
 import { Constants } from './constants';
-import { getCheckSuiteAttributes, getWorkflowRunAttributes } from './subject';
+import {
+  getCheckSuiteAttributes,
+  getLatestDiscussionComment,
+  getWorkflowRunAttributes,
+} from './subject';
 
 export function isGitHubLoggedIn(accounts: AuthState): boolean {
   return accounts.token != null;
@@ -95,14 +95,12 @@ async function getDiscussionUrl(
   const url = new URL(notification.repository.html_url);
   url.pathname += '/discussions';
 
-  const discussion = await fetchDiscussion(notification, token);
+  const discussion = await getLatestDiscussion(notification, token);
 
   if (discussion) {
     url.href = discussion.url;
 
-    const comments = discussion.comments.nodes;
-
-    const latestComment = getLatestDiscussionComment(comments);
+    const latestComment = getLatestDiscussionComment(discussion.comments.nodes);
 
     if (latestComment) {
       url.hash = `#discussioncomment-${latestComment.databaseId}`;
@@ -112,44 +110,6 @@ async function getDiscussionUrl(
   return url.toString();
 }
 
-export async function fetchDiscussion(
-  notification: Notification,
-  token: string,
-): Promise<Discussion | null> {
-  try {
-    const response = await searchDiscussions(notification, token);
-
-    let discussions =
-      response.data?.data.search.nodes.filter(
-        (discussion) => discussion.title === notification.subject.title,
-      ) || [];
-
-    if (discussions.length > 1) {
-      discussions = discussions.filter(
-        (discussion) => discussion.viewerSubscription === 'SUBSCRIBED',
-      );
-    }
-
-    return discussions[0];
-  } catch (err) {}
-}
-
-export function getLatestDiscussionComment(
-  comments: DiscussionComment[],
-): DiscussionComment | null {
-  if (!comments || comments.length === 0) {
-    return null;
-  }
-
-  // Return latest reply if available
-  if (comments[0].replies.nodes.length === 1) {
-    return comments[0].replies.nodes[0];
-  }
-
-  // Return latest comment if no replies
-  return comments[0];
-}
-
 export async function generateGitHubWebUrl(
   notification: Notification,
   accounts: AuthState,
diff --git a/src/utils/subject.ts b/src/utils/subject.ts
index 391cfe2c..d899e7ea 100644
--- a/src/utils/subject.ts
+++ b/src/utils/subject.ts
@@ -1,6 +1,7 @@
 import type {
   CheckSuiteAttributes,
   CheckSuiteStatus,
+  DiscussionComment,
   DiscussionStateType,
   GitifyPullRequestReview,
   GitifySubject,
@@ -16,11 +17,11 @@ import {
   getCommitComment,
   getIssue,
   getIssueOrPullRequestComment,
+  getLatestDiscussion,
   getPullRequest,
   getPullRequestReviews,
   getRelease,
 } from './api/client';
-import { fetchDiscussion, getLatestDiscussionComment } from './helpers';
 
 export async function getGitifySubjectDetails(
   notification: Notification,
@@ -145,7 +146,7 @@ async function getGitifySubjectForDiscussion(
   notification: Notification,
   token: string,
 ): Promise<GitifySubject> {
-  const discussion = await fetchDiscussion(notification, token);
+  const discussion = await getLatestDiscussion(notification, token);
   let discussionState: DiscussionStateType = 'OPEN';
 
   if (discussion) {
@@ -184,6 +185,22 @@ async function getGitifySubjectForDiscussion(
   };
 }
 
+export function getLatestDiscussionComment(
+  comments: DiscussionComment[],
+): DiscussionComment | null {
+  if (!comments || comments.length === 0) {
+    return null;
+  }
+
+  // Return latest reply if available
+  if (comments[0].replies.nodes.length === 1) {
+    return comments[0].replies.nodes[0];
+  }
+
+  // Return latest comment if no replies
+  return comments[0];
+}
+
 async function getGitifySubjectForIssue(
   notification: Notification,
   token: string,


Test patch:
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 053cf0cd..11b96b29 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -319,7 +319,6 @@ describe('hooks/useNotifications.ts', () => {
                 nodes: [
                   {
                     title: 'This is a Discussion.',
-                    viewerSubscription: 'SUBSCRIBED',
                     stateReason: null,
                     isAnswered: true,
                     url: 'https://github.com/gitify-app/notifications-test/discussions/612',
diff --git a/src/utils/api/graphql/utils.test.ts b/src/utils/api/graphql/utils.test.ts
new file mode 100644
index 00000000..5d143988
--- /dev/null
+++ b/src/utils/api/graphql/utils.test.ts
@@ -0,0 +1,11 @@
+import { formatAsGitHubSearchSyntax } from './utils';
+
+describe('utils/api/graphql/utils.ts', () => {
+  describe('formatAsGitHubCodeSearchSyntax', () => {
+    test('formats search query string correctly', () => {
+      const result = formatAsGitHubSearchSyntax('exampleRepo', 'exampleTitle');
+
+      expect(result).toBe('exampleTitle in:title repo:exampleRepo');
+    });
+  });
+});
diff --git a/src/utils/api/utils.test.ts b/src/utils/api/utils.test.ts
index 54af2e1e..551b3caf 100644
--- a/src/utils/api/utils.test.ts
+++ b/src/utils/api/utils.test.ts
@@ -1,8 +1,4 @@
-import {
-  addHours,
-  formatSearchQueryString,
-  getGitHubAPIBaseUrl,
-} from './utils';
+import { getGitHubAPIBaseUrl } from './utils';
 
 describe('utils/api/utils.ts', () => {
   describe('generateGitHubAPIUrl', () => {
@@ -16,30 +12,4 @@ describe('utils/api/utils.ts', () => {
       expect(result.toString()).toBe('https://github.manos.im/api/v3/');
     });
   });
-
-  describe('formatSearchQueryString', () => {
-    test('formats search query string correctly', () => {
-      const result = formatSearchQueryString(
-        'exampleRepo',
-        'exampleTitle',
-        '2024-02-20T12:00:00.000Z',
-      );
-
-      expect(result).toBe(
-        'exampleTitle in:title repo:exampleRepo updated:>2024-02-20T10:00:00.000Z',
-      );
-    });
-  });
-
-  describe('addHours', () => {
-    test('adds hours correctly for positive values', () => {
-      const result = addHours('2024-02-20T12:00:00.000Z', 3);
-      expect(result).toBe('2024-02-20T15:00:00.000Z');
-    });
-
-    test('adds hours correctly for negative values', () => {
-      const result = addHours('2024-02-20T12:00:00.000Z', -2);
-      expect(result).toBe('2024-02-20T10:00:00.000Z');
-    });
-  });
 });
diff --git a/src/utils/subject.test.ts b/src/utils/subject.test.ts
index db1729ec..758f1391 100644
--- a/src/utils/subject.test.ts
+++ b/src/utils/subject.test.ts
@@ -12,7 +12,6 @@ import type {
   DiscussionStateType,
   Notification,
   Repository,
-  ViewerSubscription,
 } from '../typesGitHub';
 import {
   getCheckSuiteAttributes,
@@ -239,7 +238,7 @@ describe('utils/subject.ts', () => {
           .reply(200, {
             data: {
               search: {
-                nodes: [mockDiscussionNode('SUBSCRIBED', null, true)],
+                nodes: [mockDiscussionNode(null, true)],
               },
             },
           });
@@ -267,7 +266,7 @@ describe('utils/subject.ts', () => {
           .reply(200, {
             data: {
               search: {
-                nodes: [mockDiscussionNode('SUBSCRIBED', 'DUPLICATE', false)],
+                nodes: [mockDiscussionNode('DUPLICATE', false)],
               },
             },
           });
@@ -295,7 +294,7 @@ describe('utils/subject.ts', () => {
           .reply(200, {
             data: {
               search: {
-                nodes: [mockDiscussionNode('SUBSCRIBED', null, false)],
+                nodes: [mockDiscussionNode(null, false)],
               },
             },
           });
@@ -323,7 +322,7 @@ describe('utils/subject.ts', () => {
           .reply(200, {
             data: {
               search: {
-                nodes: [mockDiscussionNode('SUBSCRIBED', 'OUTDATED', false)],
+                nodes: [mockDiscussionNode('OUTDATED', false)],
               },
             },
           });
@@ -351,7 +350,7 @@ describe('utils/subject.ts', () => {
           .reply(200, {
             data: {
               search: {
-                nodes: [mockDiscussionNode('SUBSCRIBED', 'REOPENED', false)],
+                nodes: [mockDiscussionNode('REOPENED', false)],
               },
             },
           });
@@ -379,7 +378,7 @@ describe('utils/subject.ts', () => {
           .reply(200, {
             data: {
               search: {
-                nodes: [mockDiscussionNode('SUBSCRIBED', 'RESOLVED', true)],
+                nodes: [mockDiscussionNode('RESOLVED', true)],
               },
             },
           });
@@ -400,37 +399,6 @@ describe('utils/subject.ts', () => {
           comments: 0,
         });
       });
-
-      it('filtered response by subscribed', async () => {
-        nock('https://api.github.com')
-          .post('/graphql')
-          .reply(200, {
-            data: {
-              search: {
-                nodes: [
-                  mockDiscussionNode('SUBSCRIBED', null, false),
-                  mockDiscussionNode('IGNORED', null, true),
-                ],
-              },
-            },
-          });
-
-        const result = await getGitifySubjectDetails(
-          mockNotification,
-          mockAccounts.token,
-        );
-
-        expect(result).toEqual({
-          state: 'OPEN',
-          user: {
-            login: mockDiscussionAuthor.login,
-            html_url: mockDiscussionAuthor.url,
-            avatar_url: mockDiscussionAuthor.avatar_url,
-            type: mockDiscussionAuthor.type,
-          },
-          comments: 0,
-        });
-      });
     });
 
     describe('Issues', () => {
@@ -1183,14 +1151,12 @@ describe('utils/subject.ts', () => {
 });
 
 function mockDiscussionNode(
-  subscription: ViewerSubscription,
   state: DiscussionStateType,
   isAnswered: boolean,
 ): Discussion {
   return {
     title: 'This is a mocked discussion',
     url: 'https://github.com/gitify-app/notifications-test/discussions/1',
-    viewerSubscription: subscription,
     stateReason: state,
     isAnswered: isAnswered,
     author: mockDiscussionAuthor,