Repository: gitify-app/gitify
Pull request #1260 (2024-06-17 22:04:34)

feat(settings): keyboard shortcut to open app
revive #1032

closes https://github.com/gitify-app/gitify/issues/995

Changed the shortcut key to be `CommandOrControl+Shift+G` since `CmdOrCtrl+Alt+G` is taken by Google Drive on macOS (this explains the previous flickering I reported on #1032).

![Screenshot 2024-06-17 at 6 07 12 PM](https://github.com/gitify-app/gitify/assets/386277/85a68f41-e37f-4b3c-bc7a-69912b9ee056)


afonsojramos: What if we give the ability to set the shortcut they want?
setchy: > What if we give the ability to set the shortcut they want?

Could that be a separate enhancement?

Patch:
diff --git a/main.js b/main.js
index 62c53099..d1fc79ae 100644
--- a/main.js
+++ b/main.js
@@ -1,4 +1,10 @@
-const { ipcMain: ipc, app, nativeTheme, Menu } = require('electron/main');
+const {
+  ipcMain: ipc,
+  app,
+  nativeTheme,
+  globalShortcut,
+  Menu,
+} = require('electron/main');
 const { menubar } = require('menubar');
 const { autoUpdater } = require('electron-updater');
 const { onFirstRunMaybe } = require('./first-run');
@@ -122,6 +128,24 @@ app.whenReady().then(async () => {
     }
   });
 
+  ipc.on(
+    'gitify:update-keyboard-shortcut',
+    (_, { enabled, keyboardShortcut }) => {
+      if (!enabled) {
+        globalShortcut.unregister(keyboardShortcut);
+        return;
+      }
+
+      globalShortcut.register(keyboardShortcut, () => {
+        if (mb.window.isVisible()) {
+          mb.hideWindow();
+        } else {
+          mb.showWindow();
+        }
+      });
+    },
+  );
+
   ipc.on('gitify:update-auto-launch', (_, settings) => {
     app.setLoginItemSettings(settings);
   });
diff --git a/src/__mocks__/state-mocks.ts b/src/__mocks__/state-mocks.ts
index cc2848e6..b6881017 100644
--- a/src/__mocks__/state-mocks.ts
+++ b/src/__mocks__/state-mocks.ts
@@ -83,6 +83,7 @@ export const mockSettings: SettingsState = {
   showAccountHostname: false,
   delayNotificationState: false,
   showPills: true,
+  keyboardShortcut: true,
 };
 
 export const mockState: GitifyState = {
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index a10b36f5..6e64fea0 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -1,6 +1,8 @@
 import {
   BellIcon,
   GearIcon,
+  GitPullRequestIcon,
+  IssueOpenedIcon,
   SyncIcon,
   XCircleIcon,
 } from '@primer/octicons-react';
@@ -9,7 +11,12 @@ import { useLocation, useNavigate } from 'react-router-dom';
 import { Logo } from '../components/Logo';
 import { AppContext } from '../context/App';
 import { quitApp } from '../utils/comms';
-import { openGitHubNotifications, openGitifyRepository } from '../utils/links';
+import {
+  openGitHubIssues,
+  openGitHubNotifications,
+  openGitHubPulls,
+  openGitifyRepository,
+} from '../utils/links';
 import { getNotificationCount } from '../utils/notifications';
 import { SidebarButton } from './buttons/SidebarButton';
 
@@ -56,6 +63,18 @@ export const Sidebar: FC = () => {
           icon={BellIcon}
           onClick={() => openGitHubNotifications()}
         />
+
+        <SidebarButton
+          title="My Issues"
+          icon={IssueOpenedIcon}
+          onClick={() => openGitHubIssues()}
+        />
+
+        <SidebarButton
+          title="My Pull Requests"
+          icon={GitPullRequestIcon}
+          onClick={() => openGitHubPulls()}
+        />
       </div>
 
       <div className="px-3 py-4">
diff --git a/src/context/App.tsx b/src/context/App.tsx
index 6114d349..a9b6c33b 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -31,7 +31,11 @@ import {
   getUserData,
   removeAccount,
 } from '../utils/auth/utils';
-import { setAutoLaunch, updateTrayTitle } from '../utils/comms';
+import {
+  setAutoLaunch,
+  setKeyboardShortcut,
+  updateTrayTitle,
+} from '../utils/comms';
 import Constants from '../utils/constants';
 import { getNotificationCount } from '../utils/notifications';
 import { clearState, loadState, saveState } from '../utils/storage';
@@ -57,6 +61,7 @@ export const defaultSettings: SettingsState = {
   showAccountHostname: false,
   delayNotificationState: false,
   showPills: true,
+  keyboardShortcut: true,
 };
 
 interface AppContextState {
@@ -140,6 +145,10 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     }
   }, [settings.showNotificationsCountInTray, notifications]);
 
+  useEffect(() => {
+    setKeyboardShortcut(settings.keyboardShortcut);
+  }, [settings.keyboardShortcut]);
+
   const updateSetting = useCallback(
     (name: keyof SettingsState, value: boolean | Theme) => {
       if (name === 'openAtStartup') {
@@ -220,6 +229,7 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
     }
 
     if (existing.settings) {
+      setKeyboardShortcut(existing.settings.keyboardShortcut);
       setSettings({ ...defaultSettings, ...existing.settings });
       return existing.settings;
     }
diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx
index cae398c5..2ae06c27 100644
--- a/src/routes/Settings.tsx
+++ b/src/routes/Settings.tsx
@@ -23,6 +23,7 @@ import { AppContext } from '../context/App';
 import { BUTTON_CLASS_NAME } from '../styles/gitify';
 import { Theme } from '../types';
 import { getAppVersion, quitApp } from '../utils/comms';
+import Constants from '../utils/constants';
 import {
   openGitHubParticipatingDocs,
   openGitifyReleaseNotes,
@@ -211,7 +212,7 @@ export const SettingsRoute: FC = () => {
               <div>
                 Keep the notification within Gitify window upon interaction
                 (click, mark as read, mark as done, etc) until the next refresh
-                window (scheduled or user initiated)
+                window (scheduled or user initiated).
               </div>
             }
           />
@@ -221,6 +222,23 @@ export const SettingsRoute: FC = () => {
           <legend id="system" className="mb-1 mt-2 font-semibold">
             System
           </legend>
+          <Checkbox
+            name="kbdShortcutEnabled"
+            label="Enable keyboard shortcut"
+            checked={!!settings.keyboardShortcut}
+            onChange={(evt) =>
+              updateSetting('keyboardShortcut', evt.target.checked)
+            }
+            tooltip={
+              <div>
+                When enabled you can choose to use the hotkeys{' '}
+                <span className="text-orange-600">
+                  {Constants.DEFAULT_KEYBOARD_SHORTCUT}
+                </span>{' '}
+                to show or hide Gitify.
+              </div>
+            }
+          />
           {isMacOS() && (
             <Checkbox
               name="showNotificationsCountInTray"
diff --git a/src/types.ts b/src/types.ts
index 410c5c95..66b78daf 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -74,6 +74,7 @@ interface SystemSettingsState {
   playSound: boolean;
   openAtStartup: boolean;
   showNotificationsCountInTray: boolean;
+  keyboardShortcut: boolean;
 }
 
 export interface GitifyState {
diff --git a/src/utils/comms.ts b/src/utils/comms.ts
index 815992ed..e19bef00 100644
--- a/src/utils/comms.ts
+++ b/src/utils/comms.ts
@@ -1,5 +1,6 @@
 import { ipcRenderer, shell } from 'electron';
 import type { Link } from '../types';
+import Constants from './constants';
 
 export function openExternalLink(url: Link): void {
   if (!url.toLowerCase().startsWith('file:///')) {
@@ -30,6 +31,13 @@ export function setAutoLaunch(value: boolean): void {
   });
 }
 
+export function setKeyboardShortcut(keyboardShortcut: boolean): void {
+  ipcRenderer.send('gitify:update-keyboard-shortcut', {
+    enabled: keyboardShortcut,
+    keyboardShortcut: Constants.DEFAULT_KEYBOARD_SHORTCUT,
+  });
+}
+
 export function updateTrayIcon(notificationsLength = 0): void {
   if (notificationsLength > 0) {
     ipcRenderer.send('gitify:icon-active');
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 229c3708..a199fac9 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -23,6 +23,8 @@ export const Constants = {
 
   FETCH_INTERVAL: 60000,
 
+  DEFAULT_KEYBOARD_SHORTCUT: 'CommandOrControl+Shift+G',
+
   // GitHub Docs
   GITHUB_DOCS: {
     OAUTH_URL:
diff --git a/src/utils/links.ts b/src/utils/links.ts
index 3b6f84e6..6edd4477 100644
--- a/src/utils/links.ts
+++ b/src/utils/links.ts
@@ -19,6 +19,14 @@ export function openGitHubNotifications() {
   openExternalLink('https://github.com/notifications' as Link);
 }
 
+export function openGitHubIssues() {
+  openExternalLink('https://github.com/issues' as Link);
+}
+
+export function openGitHubPulls() {
+  openExternalLink('https://github.com/pulls' as Link);
+}
+
 export function openAccountProfile(account: Account) {
   const url = new URL(`https://${account.hostname}`);
   url.pathname = account.user.login;


Test patch:
diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx
index 8c16f7ad..93b6b1d5 100644
--- a/src/components/Sidebar.test.tsx
+++ b/src/components/Sidebar.test.tsx
@@ -146,6 +146,50 @@ describe('components/Sidebar.tsx', () => {
     );
   });
 
+  it('opens my github issues page', () => {
+    const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink');
+
+    render(
+      <AppContext.Provider
+        value={{
+          isLoggedIn: true,
+          notifications: mockAccountNotifications,
+        }}
+      >
+        <MemoryRouter>
+          <Sidebar />
+        </MemoryRouter>
+      </AppContext.Provider>,
+    );
+    fireEvent.click(screen.getByLabelText('My Issues'));
+    expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
+    expect(openExternalLinkMock).toHaveBeenCalledWith(
+      'https://github.com/issues',
+    );
+  });
+
+  it('opens my github pull requests page', () => {
+    const openExternalLinkMock = jest.spyOn(comms, 'openExternalLink');
+
+    render(
+      <AppContext.Provider
+        value={{
+          isLoggedIn: true,
+          notifications: mockAccountNotifications,
+        }}
+      >
+        <MemoryRouter>
+          <Sidebar />
+        </MemoryRouter>
+      </AppContext.Provider>,
+    );
+    fireEvent.click(screen.getByLabelText('My Pull Requests'));
+    expect(openExternalLinkMock).toHaveBeenCalledTimes(1);
+    expect(openExternalLinkMock).toHaveBeenCalledWith(
+      'https://github.com/pulls',
+    );
+  });
+
   it('should quit the app', () => {
     const quitAppMock = jest.spyOn(comms, 'quitApp');
 
diff --git a/src/components/__snapshots__/Sidebar.test.tsx.snap b/src/components/__snapshots__/Sidebar.test.tsx.snap
index 4a2dbcf0..a4e4df9b 100644
--- a/src/components/__snapshots__/Sidebar.test.tsx.snap
+++ b/src/components/__snapshots__/Sidebar.test.tsx.snap
@@ -84,6 +84,51 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
             </svg>
             4
           </button>
+          <button
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+            title="My Issues"
+            type="button"
+          >
+            <svg
+              aria-label="My Issues"
+              class="octicon octicon-issue-opened"
+              fill="currentColor"
+              focusable="false"
+              height="12"
+              role="img"
+              style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+              viewBox="0 0 16 16"
+              width="12"
+            >
+              <path
+                d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+              />
+              <path
+                d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+              />
+            </svg>
+          </button>
+          <button
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+            title="My Pull Requests"
+            type="button"
+          >
+            <svg
+              aria-label="My Pull Requests"
+              class="octicon octicon-git-pull-request"
+              fill="currentColor"
+              focusable="false"
+              height="12"
+              role="img"
+              style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+              viewBox="0 0 16 16"
+              width="12"
+            >
+              <path
+                d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"
+              />
+            </svg>
+          </button>
         </div>
         <div
           class="px-3 py-4"
@@ -193,6 +238,51 @@ exports[`components/Sidebar.tsx should render itself & its children (logged in)
           </svg>
           4
         </button>
+        <button
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+          title="My Issues"
+          type="button"
+        >
+          <svg
+            aria-label="My Issues"
+            class="octicon octicon-issue-opened"
+            fill="currentColor"
+            focusable="false"
+            height="12"
+            role="img"
+            style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+            viewBox="0 0 16 16"
+            width="12"
+          >
+            <path
+              d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+            />
+            <path
+              d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+            />
+          </svg>
+        </button>
+        <button
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+          title="My Pull Requests"
+          type="button"
+        >
+          <svg
+            aria-label="My Pull Requests"
+            class="octicon octicon-git-pull-request"
+            fill="currentColor"
+            focusable="false"
+            height="12"
+            role="img"
+            style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+            viewBox="0 0 16 16"
+            width="12"
+          >
+            <path
+              d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"
+            />
+          </svg>
+        </button>
       </div>
       <div
         class="px-3 py-4"
@@ -359,6 +449,51 @@ exports[`components/Sidebar.tsx should render itself & its children (logged out)
             </svg>
             4
           </button>
+          <button
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+            title="My Issues"
+            type="button"
+          >
+            <svg
+              aria-label="My Issues"
+              class="octicon octicon-issue-opened"
+              fill="currentColor"
+              focusable="false"
+              height="12"
+              role="img"
+              style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+              viewBox="0 0 16 16"
+              width="12"
+            >
+              <path
+                d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+              />
+              <path
+                d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+              />
+            </svg>
+          </button>
+          <button
+            class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+            title="My Pull Requests"
+            type="button"
+          >
+            <svg
+              aria-label="My Pull Requests"
+              class="octicon octicon-git-pull-request"
+              fill="currentColor"
+              focusable="false"
+              height="12"
+              role="img"
+              style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+              viewBox="0 0 16 16"
+              width="12"
+            >
+              <path
+                d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"
+              />
+            </svg>
+          </button>
         </div>
         <div
           class="px-3 py-4"
@@ -468,6 +603,51 @@ exports[`components/Sidebar.tsx should render itself & its children (logged out)
           </svg>
           4
         </button>
+        <button
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+          title="My Issues"
+          type="button"
+        >
+          <svg
+            aria-label="My Issues"
+            class="octicon octicon-issue-opened"
+            fill="currentColor"
+            focusable="false"
+            height="12"
+            role="img"
+            style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+            viewBox="0 0 16 16"
+            width="12"
+          >
+            <path
+              d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z"
+            />
+            <path
+              d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z"
+            />
+          </svg>
+        </button>
+        <button
+          class="flex justify-evenly items-center w-full my-1 cursor-pointer text-xs font-extrabold focus:outline-none disabled:text-gray-500  disabled:cursor-default text-white hover:text-gray-500 py-1"
+          title="My Pull Requests"
+          type="button"
+        >
+          <svg
+            aria-label="My Pull Requests"
+            class="octicon octicon-git-pull-request"
+            fill="currentColor"
+            focusable="false"
+            height="12"
+            role="img"
+            style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+            viewBox="0 0 16 16"
+            width="12"
+          >
+            <path
+              d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z"
+            />
+          </svg>
+        </button>
       </div>
       <div
         class="px-3 py-4"
diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx
index f7cc6191..0f6ec61b 100644
--- a/src/context/App.test.tsx
+++ b/src/context/App.test.tsx
@@ -379,6 +379,7 @@ describe('context/App.tsx', () => {
           showAccountHostname: false,
           delayNotificationState: false,
           showPills: true,
+          keyboardShortcut: true,
         } as SettingsState,
       });
     });
@@ -430,6 +431,7 @@ describe('context/App.tsx', () => {
           showAccountHostname: false,
           delayNotificationState: false,
           showPills: true,
+          keyboardShortcut: true,
         } as SettingsState,
       });
     });
diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx
index 399aaefe..b316bb09 100644
--- a/src/routes/Settings.test.tsx
+++ b/src/routes/Settings.test.tsx
@@ -378,6 +378,31 @@ describe('routes/Settings.tsx', () => {
   });
 
   describe('System section', () => {
+    it('should toggle the keyboardShortcut checkbox', async () => {
+      await act(async () => {
+        render(
+          <AppContext.Provider
+            value={{
+              auth: mockAuth,
+              settings: mockSettings,
+              updateSetting,
+            }}
+          >
+            <MemoryRouter>
+              <SettingsRoute />
+            </MemoryRouter>
+          </AppContext.Provider>,
+        );
+      });
+
+      fireEvent.click(screen.getByLabelText('Enable keyboard shortcut'), {
+        target: { checked: true },
+      });
+
+      expect(updateSetting).toHaveBeenCalledTimes(1);
+      expect(updateSetting).toHaveBeenCalledWith('keyboardShortcut', false);
+    });
+
     it('should toggle the showNotificationsCountInTray checkbox', async () => {
       await act(async () => {
         render(
diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap
index 709e935c..35f74840 100644
--- a/src/routes/__snapshots__/Settings.test.tsx.snap
+++ b/src/routes/__snapshots__/Settings.test.tsx.snap
@@ -453,6 +453,54 @@ exports[`routes/Settings.tsx General should render itself & its children 1`] = `
       >
         System
       </legend>
+      <div
+        class="mb-3 mt-1 text-sm"
+      >
+        <div
+          class="flex items-start"
+        >
+          <div
+            class="flex h-5 items-center"
+          >
+            <input
+              checked=""
+              class="size-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
+              id="kbdShortcutEnabled"
+              type="checkbox"
+            />
+          </div>
+          <div
+            class="ml-3"
+          >
+            <label
+              class="font-medium text-gray-700 dark:text-gray-200"
+              for="kbdShortcutEnabled"
+            >
+              Enable keyboard shortcut
+              <span
+                aria-label="tooltip-kbdShortcutEnabled"
+                class="relative"
+                id="tooltip-kbdShortcutEnabled"
+              >
+                <svg
+                  aria-hidden="true"
+                  class="ml-1 text-blue-500"
+                  fill="currentColor"
+                  focusable="false"
+                  height="16"
+                  style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"
+                  viewBox="0 0 16 16"
+                  width="16"
+                >
+                  <path
+                    d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.92 6.085h.001a.749.749 0 1 1-1.342-.67c.169-.339.436-.701.849-.977C6.845 4.16 7.369 4 8 4a2.756 2.756 0 0 1 1.637.525c.503.377.863.965.863 1.725 0 .448-.115.83-.329 1.15-.205.307-.47.513-.692.662-.109.072-.22.138-.313.195l-.006.004a6.24 6.24 0 0 0-.26.16.952.952 0 0 0-.276.245.75.75 0 0 1-1.248-.832c.184-.264.42-.489.692-.661.103-.067.207-.132.313-.195l.007-.004c.1-.061.182-.11.258-.161a.969.969 0 0 0 .277-.245C8.96 6.514 9 6.427 9 6.25a.612.612 0 0 0-.262-.525A1.27 1.27 0 0 0 8 5.5c-.369 0-.595.09-.74.187a1.01 1.01 0 0 0-.34.398ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
+                  />
+                </svg>
+              </span>
+            </label>
+          </div>
+        </div>
+      </div>
       <div
         class="mb-3 mt-1 text-sm"
       >
diff --git a/src/utils/links.test.ts b/src/utils/links.test.ts
index d01bb5e2..316fff36 100644
--- a/src/utils/links.test.ts
+++ b/src/utils/links.test.ts
@@ -10,8 +10,10 @@ import * as helpers from './helpers';
 import {
   openAccountProfile,
   openDeveloperSettings,
+  openGitHubIssues,
   openGitHubNotifications,
   openGitHubParticipatingDocs,
+  openGitHubPulls,
   openGitifyReleaseNotes,
   openGitifyRepository,
   openHost,
@@ -50,6 +52,20 @@ describe('utils/links.ts', () => {
     );
   });
 
+  it('openGitHubIssues', () => {
+    openGitHubIssues();
+    expect(comms.openExternalLink).toHaveBeenCalledWith(
+      'https://github.com/issues',
+    );
+  });
+
+  it('openGitHubPulls', () => {
+    openGitHubPulls();
+    expect(comms.openExternalLink).toHaveBeenCalledWith(
+      'https://github.com/pulls',
+    );
+  });
+
   it('openAccountProfile', () => {
     openAccountProfile(mockGitHubCloudAccount);
     expect(comms.openExternalLink).toHaveBeenCalledWith(