Repository: gitify-app/gitify
Pull request #1967: feat: make the volume of notifications adjustable
Author: login: RuizhePang
Labels: name: enhancement
description: New feature or enhancement to existing functionality
Created: 2025-04-22T14:49:45Z | Merged: 2025-04-23T19:22:05Z

Closes #1929 

Add a spinner in setting page, which can control the volume of notifications.

| `Play Sound` enabled |  `Play Sound` disabled |
| --- | --- |
| ![2025-04-23T15-35-26 277Z-Gitify-screenshot](https://github.com/user-attachments/assets/05939472-8809-41aa-b44d-7e1406caa8c0) | ![2025-04-22T17-02-43 862Z-Gitify-screenshot](https://github.com/user-attachments/assets/8a046fca-5568-43b6-abe8-26ba73cdec91) |



Linked issue #1929: Support alternative or custom notification sounds
### 📝 Provide a description of the new feature

The default notification sound (https://github.com/gitify-app/gitify/blob/main/assets/sounds/clearly.mp3) is quite meek and I barely notice it's even happened half the time. When coupled with the icon change being fairly small AND no native toast notifications on windows showing (at-least for me, maybe I'm setup wrong) I miss stuff all the time.

It would be nice to either have a selection of notification sounds (some more alarming than others) OR even allow a custom notification sound to be provided.

If allowing custom sounds or even multiple is more complex than desired then could we at-least expose the volume the sound is played at so it can be increased by the user from the default `0.2`

https://github.com/gitify-app/gitify/blob/main/src/renderer/utils/notifications/native.ts#L99

![Image](https://github.com/user-attachments/assets/1fc8ad92-e370-4be2-8e1f-db994c096236)
Linked issue comments: user: login: RuizhePang
created_at: 2025-04-22T03:56:31Z
body: @setchy Can I work on this? I wanna have a try.
author_association: CONTRIBUTOR
user: login: afonsojramos
created_at: 2025-04-22T10:14:40Z
body: Go ahead @RuizhePang! Let us know if you need any help!
author_association: MEMBER

Diff:
diff --git a/src/renderer/__mocks__/state-mocks.ts b/src/renderer/__mocks__/state-mocks.ts
index a12d6ce96..63ea4921e 100644
--- a/src/renderer/__mocks__/state-mocks.ts
+++ b/src/renderer/__mocks__/state-mocks.ts
@@ -101,6 +101,7 @@ const mockSystemSettings: SystemSettingsState = {
   showNotificationsCountInTray: true,
   showNotifications: true,
   playSound: true,
+  notificationVolume: 20,
   useAlternateIdleIcon: false,
   openAtStartup: false,
 };
diff --git a/src/renderer/components/icons/VolumeDownIcon.tsx b/src/renderer/components/icons/VolumeDownIcon.tsx
new file mode 100644
index 000000000..93b2910a4
--- /dev/null
+++ b/src/renderer/components/icons/VolumeDownIcon.tsx
@@ -0,0 +1,18 @@
+import type { FC } from 'react';
+
+export const VolumeDownIcon: FC = () => (
+  <svg
+    className="size-4"
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 24 24"
+    width="24"
+    height="24"
+    role="img"
+    aria-label="Volume Down Icon"
+  >
+    <g transform="translate(2, 0)">
+      <path d="M11.553 3.064A.75.75 0 0 1 12 3.75v16.5a.75.75 0 0 1-1.255.555L5.46 16H2.75A1.75 1.75 0 0 1 1 14.25v-4.5C1 8.784 1.784 8 2.75 8h2.71l5.285-4.805a.752.752 0 0 1 .808-.13ZM10.5 5.445l-4.245 3.86a.748.748 0 0 1-.505.195h-3a.25.25 0 0 0-.25.25v4.5c0 .138.112.25.25.25h3c.187 0 .367.069.505.195l4.245 3.86z" />
+      <path d="M16.243 7.757a.75.75 0 1 0-1.061 1.061 4.5 4.5 0 0 1 0 6.364.75.75 0 0 0 1.06 1.06 6 6 0 0 0 0-8.485Z" />
+    </g>
+  </svg>
+);
diff --git a/src/renderer/components/icons/VolumeUpIcon.tsx b/src/renderer/components/icons/VolumeUpIcon.tsx
new file mode 100644
index 000000000..84aac91a4
--- /dev/null
+++ b/src/renderer/components/icons/VolumeUpIcon.tsx
@@ -0,0 +1,16 @@
+import type { FC } from 'react';
+
+export const VolumeUpIcon: FC = () => (
+  <svg
+    className="size-4"
+    xmlns="http://www.w3.org/2000/svg"
+    viewBox="0 0 24 24"
+    width="24"
+    height="24"
+    role="img"
+    aria-label="Volume Up Icon"
+  >
+    <path d="M11.553 3.064A.75.75 0 0 1 12 3.75v16.5a.75.75 0 0 1-1.255.555L5.46 16H2.75A1.75 1.75 0 0 1 1 14.25v-4.5C1 8.784 1.784 8 2.75 8h2.71l5.285-4.805a.752.752 0 0 1 .808-.13ZM10.5 5.445l-4.245 3.86a.748.748 0 0 1-.505.195h-3a.25.25 0 0 0-.25.25v4.5c0 .138.112.25.25.25h3c.187 0 .367.069.505.195l4.245 3.86Zm8.218-1.223a.75.75 0 0 1 1.06 0c4.296 4.296 4.296 11.26 0 15.556a.75.75 0 0 1-1.06-1.06 9.5 9.5 0 0 0 0-13.436.75.75 0 0 1 0-1.06Z" />
+    <path d="M16.243 7.757a.75.75 0 1 0-1.061 1.061 4.5 4.5 0 0 1 0 6.364.75.75 0 0 0 1.06 1.06 6 6 0 0 0 0-8.485Z" />
+  </svg>
+);
diff --git a/src/renderer/components/settings/SystemSettings.tsx b/src/renderer/components/settings/SystemSettings.tsx
index 58745f203..ab224cba4 100644
--- a/src/renderer/components/settings/SystemSettings.tsx
+++ b/src/renderer/components/settings/SystemSettings.tsx
@@ -1,15 +1,24 @@
 import { type FC, useContext } from 'react';
 
-import { DeviceDesktopIcon } from '@primer/octicons-react';
-import { Box, Stack, Text } from '@primer/react';
+import { DeviceDesktopIcon, SyncIcon } from '@primer/octicons-react';
+import {
+  Box,
+  Button,
+  ButtonGroup,
+  IconButton,
+  Stack,
+  Text,
+} from '@primer/react';
 
 import { APPLICATION } from '../../../shared/constants';
 import { isLinux, isMacOS } from '../../../shared/platform';
-import { AppContext } from '../../context/App';
+import { AppContext, defaultSettings } from '../../context/App';
 import { OpenPreference } from '../../types';
 import { Constants } from '../../utils/constants';
 import { Checkbox } from '../fields/Checkbox';
 import { RadioGroup } from '../fields/RadioGroup';
+import { VolumeDownIcon } from '../icons/VolumeDownIcon';
+import { VolumeUpIcon } from '../icons/VolumeUpIcon';
 import { Title } from '../primitives/Title';
 
 export const SystemSettings: FC = () => {
@@ -70,12 +79,72 @@ export const SystemSettings: FC = () => {
           }
         />
 
-        <Checkbox
-          name="playSound"
-          label="Play sound"
-          checked={settings.playSound}
-          onChange={(evt) => updateSetting('playSound', evt.target.checked)}
-        />
+        <Box>
+          <Stack
+            direction="horizontal"
+            gap="condensed"
+            align="center"
+            className="text-sm"
+          >
+            <Checkbox
+              name="playSound"
+              label="Play sound"
+              checked={settings.playSound}
+              onChange={(evt) => updateSetting('playSound', evt.target.checked)}
+            />
+
+            <ButtonGroup className="ml-2" hidden={!settings.playSound}>
+              <IconButton
+                aria-label="Volume down"
+                size="small"
+                icon={VolumeDownIcon}
+                unsafeDisableTooltip={true}
+                onClick={() => {
+                  const newVolume = Math.max(
+                    settings.notificationVolume - 10,
+                    0,
+                  );
+                  updateSetting('notificationVolume', newVolume);
+                }}
+                data-testid="settings-volume-down"
+              />
+
+              <Button aria-label="Volume percentage" size="small" disabled>
+                {settings.notificationVolume.toFixed(0)}%
+              </Button>
+
+              <IconButton
+                aria-label="Volume up"
+                size="small"
+                icon={VolumeUpIcon}
+                unsafeDisableTooltip={true}
+                onClick={() => {
+                  const newVolume = Math.min(
+                    settings.notificationVolume + 10,
+                    100,
+                  );
+                  updateSetting('notificationVolume', newVolume);
+                }}
+                data-testid="settings-volume-up"
+              />
+
+              <IconButton
+                aria-label="Reset volume"
+                size="small"
+                variant="danger"
+                icon={SyncIcon}
+                unsafeDisableTooltip={true}
+                onClick={() => {
+                  updateSetting(
+                    'notificationVolume',
+                    defaultSettings.notificationVolume,
+                  );
+                }}
+                data-testid="settings-volume-reset"
+              />
+            </ButtonGroup>
+          </Stack>
+        </Box>
 
         <Checkbox
           name="useAlternateIdleIcon"
diff --git a/src/renderer/context/App.tsx b/src/renderer/context/App.tsx
index d3421e6c2..ae9e3dc9e 100644
--- a/src/renderer/context/App.tsx
+++ b/src/renderer/context/App.tsx
@@ -93,6 +93,7 @@ const defaultSystemSettings: SystemSettingsState = {
   showNotificationsCountInTray: true,
   showNotifications: true,
   playSound: true,
+  notificationVolume: 20,
   useAlternateIdleIcon: false,
   openAtStartup: false,
 };
diff --git a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap
index 1ad989a05..84662142f 100644
--- a/src/renderer/routes/__snapshots__/Settings.test.tsx.snap
+++ b/src/renderer/routes/__snapshots__/Settings.test.tsx.snap
@@ -1128,27 +1128,160 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] =
             </label>
           </div>
           <div
-            class="Stack__StyledStack-sc-x3xa2i-0 cRiRbv text-sm"
-            data-align="center"
-            data-direction="horizontal"
-            data-gap="condensed"
-            data-justify="start"
-            data-padding="none"
-            data-wrap="nowrap"
+            class="Box-sc-g0xbh4-0"
           >
-            <input
-              checked=""
-              class="size-4 rounded-sm cursor-pointer"
-              data-testid="checkbox-playSound"
-              id="playSound"
-              type="checkbox"
-            />
-            <label
-              class="font-medium text-gitify-font cursor-pointer"
-              for="playSound"
+            <div
+              class="Stack__StyledStack-sc-x3xa2i-0 cRiRbv text-sm"
+              data-align="center"
+              data-direction="horizontal"
+              data-gap="condensed"
+              data-justify="start"
+              data-padding="none"
+              data-wrap="nowrap"
             >
-              Play sound
-            </label>
+              <div
+                class="Stack__StyledStack-sc-x3xa2i-0 cRiRbv text-sm"
+                data-align="center"
+                data-direction="horizontal"
+                data-gap="condensed"
+                data-justify="start"
+                data-padding="none"
+                data-wrap="nowrap"
+              >
+                <input
+                  checked=""
+                  class="size-4 rounded-sm cursor-pointer"
+                  data-testid="checkbox-playSound"
+                  id="playSound"
+                  type="checkbox"
+                />
+                <label
+                  class="font-medium text-gitify-font cursor-pointer"
+                  for="playSound"
+                >
+                  Play sound
+                </label>
+              </div>
+              <div
+                class="ButtonGroup-sc-1gxhls1-0 ePLgSl ml-2"
+              >
+                <button
+                  aria-describedby="«r5»-loading-announcement"
+                  aria-label="Volume down"
+                  class="types__StyledButton-sc-ws60qy-0 QOGvM"
+                  data-component="IconButton"
+                  data-loading="false"
+                  data-no-visuals="true"
+                  data-size="small"
+                  data-testid="settings-volume-down"
+                  sx="[object Object]"
+                  type="button"
+                >
+                  <svg
+                    aria-label="Volume Down Icon"
+                    class="size-4"
+                    height="24"
+                    role="img"
+                    viewBox="0 0 24 24"
+                    width="24"
+                    xmlns="http://www.w3.org/2000/svg"
+                  >
+                    <g
+                      transform="translate(2, 0)"
+                    >
+                      <path
+                        d="M11.553 3.064A.75.75 0 0 1 12 3.75v16.5a.75.75 0 0 1-1.255.555L5.46 16H2.75A1.75 1.75 0 0 1 1 14.25v-4.5C1 8.784 1.784 8 2.75 8h2.71l5.285-4.805a.752.752 0 0 1 .808-.13ZM10.5 5.445l-4.245 3.86a.748.748 0 0 1-.505.195h-3a.25.25 0 0 0-.25.25v4.5c0 .138.112.25.25.25h3c.187 0 .367.069.505.195l4.245 3.86z"
+                      />
+                      <path
+                        d="M16.243 7.757a.75.75 0 1 0-1.061 1.061 4.5 4.5 0 0 1 0 6.364.75.75 0 0 0 1.06 1.06 6 6 0 0 0 0-8.485Z"
+                      />
+                    </g>
+                  </svg>
+                </button>
+                <button
+                  aria-describedby="«r6»-loading-announcement"
+                  aria-label="Volume percentage"
+                  class="types__StyledButton-sc-ws60qy-0 QOGvM"
+                  data-loading="false"
+                  data-no-visuals="true"
+                  data-size="small"
+                  disabled=""
+                  sx="[object Object]"
+                  type="button"
+                >
+                  <span
+                    class="Box-sc-g0xbh4-0 cNXQpH"
+                    data-component="buttonContent"
+                    sx="[object Object]"
+                  >
+                    <span
+                      data-component="text"
+                    >
+                      20
+                      %
+                    </span>
+                  </span>
+                </button>
+                <button
+                  aria-describedby="«r7»-loading-announcement"
+                  aria-label="Volume up"
+                  class="types__StyledButton-sc-ws60qy-0 QOGvM"
+                  data-component="IconButton"
+                  data-loading="false"
+                  data-no-visuals="true"
+                  data-size="small"
+                  data-testid="settings-volume-up"
+                  sx="[object Object]"
+                  type="button"
+                >
+                  <svg
+                    aria-label="Volume Up Icon"
+                    class="size-4"
+                    height="24"
+                    role="img"
+                    viewBox="0 0 24 24"
+                    width="24"
+                    xmlns="http://www.w3.org/2000/svg"
+                  >
+                    <path
+                      d="M11.553 3.064A.75.75 0 0 1 12 3.75v16.5a.75.75 0 0 1-1.255.555L5.46 16H2.75A1.75 1.75 0 0 1 1 14.25v-4.5C1 8.784 1.784 8 2.75 8h2.71l5.285-4.805a.752.752 0 0 1 .808-.13ZM10.5 5.445l-4.245 3.86a.748.748 0 0 1-.505.195h-3a.25.25 0 0 0-.25.25v4.5c0 .138.112.25.25.25h3c.187 0 .367.069.505.195l4.245 3.86Zm8.218-1.223a.75.75 0 0 1 1.06 0c4.296 4.296 4.296 11.26 0 15.556a.75.75 0 0 1-1.06-1.06 9.5 9.5 0 0 0 0-13.436.75.75 0 0 1 0-1.06Z"
+                    />
+                    <path
+                      d="M16.243 7.757a.75.75 0 1 0-1.061 1.061 4.5 4.5 0 0 1 0 6.364.75.75 0 0 0 1.06 1.06 6 6 0 0 0 0-8.485Z"
+                    />
+                  </svg>
+                </button>
+                <button
+                  aria-describedby="«r8»-loading-announcement"
+                  aria-label="Reset volume"
+                  class="types__StyledButton-sc-ws60qy-0 gjscLt"
+                  data-component="IconButton"
+                  data-loading="false"
+                  data-no-visuals="true"
+                  data-size="small"
+                  data-testid="settings-volume-reset"
+                  sx="[object Object]"
+                  type="button"
+                >
+                  <svg
+                    aria-hidden="true"
+                    class="octicon octicon-sync"
+                    display="inline-block"
+                    fill="currentColor"
+                    focusable="false"
+                    height="16"
+                    overflow="visible"
+                    style="vertical-align: text-bottom;"
+                    viewBox="0 0 16 16"
+                    width="16"
+                  >
+                    <path
+                      d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"
+                    />
+                  </svg>
+                </button>
+              </div>
+            </div>
           </div>
           <div
             class="Stack__StyledStack-sc-x3xa2i-0 cRiRbv text-sm"
@@ -1228,7 +1361,7 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] =
         data-wrap="nowrap"
       >
         <button
-          aria-describedby="«r5»-loading-announcement"
+          aria-describedby="«r9»-loading-announcement"
           class="types__StyledButton-sc-ws60qy-0 fZJSho"
           data-loading="false"
           data-no-visuals="true"
@@ -1273,11 +1406,11 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] =
         <span
           aria-label="View release notes"
           class="Tooltip__TooltipBase-sc-17tf59c-0 iTeBSL tooltipped-n"
-          id="«r6»"
+          id="«ra»"
           role="tooltip"
         >
           <button
-            aria-describedby="«r7»-loading-announcement"
+            aria-describedby="«rb»-loading-announcement"
             class="types__StyledButton-sc-ws60qy-0 QOGvM"
             data-loading="false"
             data-no-visuals="true"
@@ -1313,11 +1446,11 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] =
         <span
           aria-label="Accounts"
           class="Tooltip__TooltipBase-sc-17tf59c-0 iTeBSL tooltipped-n"
-          id="«r8»"
+          id="«rc»"
           role="tooltip"
         >
           <button
-            aria-describedby="«r9»-loading-announcement"
+            aria-describedby="«rd»-loading-announcement"
             aria-label="Accounts"
             class="types__StyledButton-sc-ws60qy-0 QOGvM"
             data-component="IconButton"
@@ -1348,11 +1481,11 @@ exports[`renderer/routes/Settings.tsx should render itself & its children 1`] =
         <span
           aria-label="Quit Gitify"
           class="Tooltip__TooltipBase-sc-17tf59c-0 iTeBSL tooltipped-nw"
-          id="«ra»"
+          id="«re»"
           role="tooltip"
         >
           <button
-            aria-describedby="«rb»-loading-announcement"
+            aria-describedby="«rf»-loading-announcement"
             aria-label="Quit Gitify"
             class="types__StyledButton-sc-ws60qy-0 gjscLt"
             data-component="IconButton"
diff --git a/src/renderer/types.ts b/src/renderer/types.ts
index 3c631bc53..e3edef714 100644
--- a/src/renderer/types.ts
+++ b/src/renderer/types.ts
@@ -90,6 +90,7 @@ export interface SystemSettingsState {
   showNotifications: boolean;
   useAlternateIdleIcon: boolean;
   playSound: boolean;
+  notificationVolume: number;
   openAtStartup: boolean;
 }
 
diff --git a/src/renderer/utils/notifications/native.test.ts b/src/renderer/utils/notifications/native.test.ts
index f30cc9c8f..6e33703aa 100644
--- a/src/renderer/utils/notifications/native.test.ts
+++ b/src/renderer/utils/notifications/native.test.ts
@@ -134,4 +134,33 @@ describe('renderer/utils/notifications/native.ts', () => {
       expect(window.Audio.prototype.play).toHaveBeenCalledTimes(1);
     });
   });
+
+  describe('triggerNativeNotifications', () => {
+    afterEach(() => {
+      jest.clearAllMocks();
+    });
+
+    it('should raise only sound notification with correct volume', () => {
+      const settings: SettingsState = {
+        ...defaultSettings,
+        playSound: true,
+        showNotifications: false,
+        notificationVolume: 80,
+      };
+
+      const raiseSoundNotificationMock = jest.spyOn(
+        native,
+        'raiseSoundNotification',
+      );
+      jest.spyOn(native, 'raiseNativeNotification');
+
+      native.triggerNativeNotifications([], mockAccountNotifications, {
+        auth: mockAuth,
+        settings,
+      });
+
+      expect(raiseSoundNotificationMock).toHaveBeenCalledWith(0.8);
+      expect(native.raiseNativeNotification).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/src/renderer/utils/notifications/native.ts b/src/renderer/utils/notifications/native.ts
index cda7f6367..cce0f4614 100644
--- a/src/renderer/utils/notifications/native.ts
+++ b/src/renderer/utils/notifications/native.ts
@@ -2,6 +2,7 @@ import path from 'node:path';
 
 import { APPLICATION } from '../../../shared/constants';
 import { isWindows } from '../../../shared/platform';
+import { defaultSettings } from '../../context/App';
 import type { AccountNotifications, GitifyState } from '../../types';
 import { Notification } from '../../typesGitHub';
 import { getAccountUUID } from '../auth/utils';
@@ -48,7 +49,7 @@ export const triggerNativeNotifications = (
   }
 
   if (state.settings.playSound) {
-    raiseSoundNotification();
+    raiseSoundNotification(state.settings.notificationVolume / 100);
   }
 
   if (state.settings.showNotifications) {
@@ -86,7 +87,9 @@ export const raiseNativeNotification = (notifications: Notification[]) => {
   return nativeNotification;
 };
 
-export const raiseSoundNotification = () => {
+export const raiseSoundNotification = (
+  volume = defaultSettings.notificationVolume / 100,
+) => {
   const audio = new Audio(
     path.join(
       __dirname,
@@ -96,6 +99,6 @@ export const raiseSoundNotification = () => {
       Constants.NOTIFICATION_SOUND,
     ),
   );
-  audio.volume = 0.2;
+  audio.volume = volume;
   audio.play();
 };


Reviews:
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T14:53:14Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T15:12:14Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T15:13:38Z
user: login: RuizhePang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:47:08Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T15:49:04Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T15:50:05Z
user: login: RuizhePang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T15:52:26Z
user: login: RuizhePang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:36:00Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T16:39:40Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T16:39:57Z
user: login: RuizhePang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:47:36Z
user: login: RuizhePang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:47:51Z
user: login: RuizhePang
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-22T16:54:31Z
user: login: setchy
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-22T17:01:57Z
user: login: setchy
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-23T19:21:56Z

Review comments:
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: setchy
body: I think this would be better suited to the SystemSettings section rather than NotificationSettings, perhaps underneath the `Play notification sound` settings
created_at: 2025-04-22T14:53:14Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
+          <Text as="label" htmlFor="notificationVolume">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: setchy
body: The rendering of this input looked a bit out of place compared to the other settings.

Can you try and copy the pattern used by controls such as `Zoom`, `Open Links`, `Theme`. 
created_at: 2025-04-22T15:12:14Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
+          <Text as="label" htmlFor="notificationVolume">
+            Notification Volume:
+          </Text>
+          <input
+            id="notificationVolume"
+            type="number"
+            min={0}
+            max={100}
+            step={1}
+            value={settings.notificationVolume}
+            onChange={(evt) => {
+              let val = Number(evt.target.value);
+              if (Number.isNaN(val)) val = 0;
+              if (val > 100) val = 100;
+              if (val < 0) val = 0;
+              updateSetting('notificationVolume', val);
+            }}
+            style={{
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: setchy
body: If possible, can we use tailwind classes, or preferably, a Primer component
created_at: 2025-04-22T15:13:38Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: RuizhePang
body: <img width="495" alt="image" src="https://github.com/user-attachments/assets/ed131954-3dad-4cb6-afa2-a1c99e538c10" />

Is this better?
created_at: 2025-04-22T15:47:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: setchy
body: Better location, yes.

Label styling still need some work :) 
created_at: 2025-04-22T15:49:04Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: setchy
body: Thinking out loud.  I wonder if the volume input could be to the right of `Play sound` and only show if `Play sound` is enabled.  

That might help with the consistent formatting since this currently doesn't have a checkbox so looks slightly out-of-place
created_at: 2025-04-22T15:50:05Z
author_association: MEMBER
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: RuizhePang
body: Great idea! I will have a try.
created_at: 2025-04-22T15:52:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: RuizhePang
body: @setchy 

Here are my new updates on UI.

<img width="498" alt="image" src="https://github.com/user-attachments/assets/d1305442-73cd-46fd-882a-4464afeae845" />

<img width="496" alt="image" src="https://github.com/user-attachments/assets/485d55e6-a33d-4350-a93b-d59ce51a37d0" />


created_at: 2025-04-22T16:36:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -85,6 +85,7 @@ const defaultNotificationSettings: NotificationSettingsState = {
   markAsDoneOnOpen: false,
   markAsDoneOnUnsubscribe: false,
   delayNotificationState: false,
+  notificationVolume: 20,
path: src/renderer/context/App.tsx
user: login: setchy
body: This now belongs in SystemSettingsState
created_at: 2025-04-22T16:39:40Z
author_association: MEMBER
diff_hunk: @@ -81,6 +81,7 @@ export interface NotificationSettingsState {
   markAsDoneOnOpen: boolean;
   markAsDoneOnUnsubscribe: boolean;
   delayNotificationState: boolean;
+  notificationVolume: number;
path: src/renderer/types.ts
user: login: setchy
body: This now belongs in SystemSettingsState
created_at: 2025-04-22T16:39:57Z
author_association: MEMBER
diff_hunk: @@ -85,6 +85,7 @@ const defaultNotificationSettings: NotificationSettingsState = {
   markAsDoneOnOpen: false,
   markAsDoneOnUnsubscribe: false,
   delayNotificationState: false,
+  notificationVolume: 20,
path: src/renderer/context/App.tsx
user: login: RuizhePang
body: Done
created_at: 2025-04-22T16:47:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,6 +81,7 @@ export interface NotificationSettingsState {
   markAsDoneOnOpen: boolean;
   markAsDoneOnUnsubscribe: boolean;
   delayNotificationState: boolean;
+  notificationVolume: number;
path: src/renderer/types.ts
user: login: RuizhePang
body: Done
created_at: 2025-04-22T16:47:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
+          <Text as="label" htmlFor="notificationVolume">
+            Notification Volume:
+          </Text>
+          <input
+            id="notificationVolume"
+            type="number"
+            min={0}
+            max={100}
+            step={1}
+            value={settings.notificationVolume}
+            onChange={(evt) => {
+              let val = Number(evt.target.value);
+              if (Number.isNaN(val)) val = 0;
+              if (val > 100) val = 100;
+              if (val < 0) val = 0;
+              updateSetting('notificationVolume', val);
+            }}
+            style={{
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: RuizhePang
body: This should have been completed.
created_at: 2025-04-22T16:54:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -246,6 +246,33 @@ export const NotificationSettings: FC = () => {
             </Text>
           }
         />
+
+        <Stack direction="horizontal" gap="condensed" alignItems="center">
path: src/renderer/components/settings/NotificationSettings.tsx
user: login: setchy
body: I've made a subtle change to indent it, but this looks great to me, thanks @RuizhePang 
created_at: 2025-04-22T17:01:57Z
author_association: MEMBER

PR comments:
user: login: setchy
created_at: 2025-04-22T19:00:46Z
body: A proposed further consolidation of the UI
![2025-04-22T19-00-16 845Z-Gitify-screenshot](https://github.com/user-attachments/assets/85d961cb-a43d-43de-b9e7-a03db87683a7)

LMK what you think @RuizhePang @afonsojramos - can push the changes if we think this is a better representation

author_association: MEMBER
user: login: RuizhePang
created_at: 2025-04-23T00:56:44Z
body: The new version looks great! But the volume down icon looks more like a mute symbol. Is there a more suitable option?
author_association: CONTRIBUTOR
user: login: setchy
created_at: 2025-04-23T15:36:42Z
body: > But the volume down icon looks more like a mute symbol. Is there a more suitable option?

Agree - there isn't a great native option within https://primer.style/octicons/ for Volume Down, however I have used https://primer.style/octicons/icon/unmute-24/ as a base to create a custom Icon 

Updated UI
![2025-04-23T15-35-26 277Z-Gitify-screenshot](https://github.com/user-attachments/assets/05939472-8809-41aa-b44d-7e1406caa8c0)


author_association: MEMBER
user: login: RuizhePang
created_at: 2025-04-23T17:00:12Z
body: > > But the volume down icon looks more like a mute symbol. Is there a more suitable option?
> 
> Agree - there isn't a great native option within https://primer.style/octicons/ for Volume Down, however I have used https://primer.style/octicons/icon/unmute-24/ as a base to create a custom Icon
> 
> Updated UI ![2025-04-23T15-35-26 277Z-Gitify-screenshot](https://private-user-images.githubusercontent.com/386277/436616398-05939472-8809-41aa-b44d-7e1406caa8c0.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDU0Mjc3NDksIm5iZiI6MTc0NTQyNzQ0OSwicGF0aCI6Ii8zODYyNzcvNDM2NjE2Mzk4LTA1OTM5NDcyLTg4MDktNDFhYS1iNDRkLTdlMTQwNmNhYThjMC5wbmc_WC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BS0lBVkNPRFlMU0E1M1BRSzRaQSUyRjIwMjUwNDIzJTJGdXMtZWFzdC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDI1MDQyM1QxNjU3MjlaJlgtQW16LUV4cGlyZXM9MzAwJlgtQW16LVNpZ25hdHVyZT0yYjBkMDA4MWU2ZjE0YWExMjIxNjUyYTI4ZTRhN2M4MzU2ODA5Y2ZhY2Y5MjA3YWMwNDZmY2EyYTU3NWRjNDM3JlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCJ9.kykNp7-riqXwbSQS2KZNL-8tuz7QMuPgp2HSruDgrmQ)

Excellent! 👍
author_association: CONTRIBUTOR