Repository: gitify-app/gitify
Pull request #706 (2024-01-18 08:14:41)

feat: add a "mark notification as done" button
## Related issues
#500 - feature request: mark notifications as done

## Context
Hi! This is my first PR on this repo; let me thank you for making this great app!

As a new user, I instantly found quite frustrating that I wasn't able to mark notifications as done. After doing some digging, I found that [GitHub does now provide an endpoint for that](https://docs.github.com/en/rest/activity/notifications?apiVersion=2022-11-28#mark-a-thread-as-done) 🎉 

## Discussion
This PR changes the existing "mark notification as read" icon, to better reflect what it does, and adds a new "mark notification as done" button right next to it: 
<img width="498" alt="image" src="https://github.com/gitify-app/gitify/assets/26418696/0c55156a-b297-468f-9d4f-f8cb37e32a2c">

Unfortunately, it looks like GitHub does not provide an endpoint for marking all notifications from a repository as done, so I couldn't add a new button to the Repository component 😞

Also, if that's OK with you I'd like to change the "Unsubscribe" button:
- use the [bell-slash](https://primer.style/foundations/icons/bell-slash-16) icon to match GitHub's icon
- change its positionning which feels odd in my opinion
<img width="447" alt="image" src="https://github.com/gitify-app/gitify/assets/26418696/6627568f-22d5-4a85-9ff0-e326e25c9892">

Let me know what you think about it; I can do that in another PR if you prefer so 🤷 



adufr: Also, on a side note, I have some other ideas for improvements that are now possible thanks to this new API endpoint:
- add a setting to "mark as done" when clicking a notification (while still keeping the existing setting to "mark as read")
- someway or another, add the posibility to not hide "read" notifications from Gitify (only hide "done" notifications)

LMK what you think about it, I'd gladly work on that too!

bmulholland: Your comments about read vs done are interesting. In my workflow, I generally treat "read" as "done." In your workflow, what's the difference between the two?
bmulholland: Re: the changes here -- it seems natural to mirror the Github behaviour and icons. Looks like those don't have a read icon, actually. Opening the entry marks it as read. And I guess Done probably marks it as read + done? Maybe we could match that, which means only two icons here?
bmulholland: BTW @adufr -- thanks for the contribution!
adufr: @bmulholland 

> Your comments about read vs done are interesting. In my workflow, I generally treat "read" as "done." In your workflow, what's the difference between the two?
> 
> Edit: Actually, could you please split that discussion to a new Issue? That will keep this PR focused on a single topic (which already expands into discussions about positioning of icons etc).

Yup I'll open 2 different issues, one for each matter
setchy: Great contribution @adufr.

@bmulholland @adufr your comment above got me thinking.... 
> _Opening the entry marks it as read._

Does this mean that the setting `Mark as read on click` is no longer applicable, perhaps due to a change in GitHub's default behavior when opening an inbox notification?
![Screenshot 2024-01-18 at 8 45 46 AM](https://github.com/gitify-app/gitify/assets/386277/940c4841-5331-4bba-a93b-692c8bae0220)

bmulholland: > Does this mean that the setting `Mark as read on click` is no longer applicable, perhaps due to a change in GitHub's default behavior when opening an inbox notification?

I've only recently picked up contributing here, so I'm not familiar with all the options. I wasn't considering that Setting. It does kinda feel like this read vs done workflow replaces the need for that. But I don't know what people actually use, and I suppose there's lots of possible workflows. We could take an opinionated approach to it but it would require deep consideration and that's beyond the level of effort I'm able to put in.

One easy take is this "mark as done" would replace the "mark as read," though. I honestly don't think most people know or care about the distinction, so having one button for mark as done, no button for mark as read, and keeping the setting seems like a reasonable middle ground.
adufr: > From my observations, changing `Mark as read on click` has no change in behavior within GitHub anymore. From what I can tell, regardless of setting, it is always marked as read.

Maybe `Mark as read on click` should be replaced with `Mark as done on click` then? 😅

But the `Mark as read` button could still be useful in the future; I'm going to open a feature request for a new setting that would allow to "Show read notifications" in Gitify (but hide "done" notifications)
bmulholland: Re: "Mark as read on click" -- I confirm this has no effect now, Github marks it as read when you open it anyway. We should remove the Setting.

Re: a "mark as done on click" -- When opening the notification, you get a blue bar at the top where you can click Done. That's two clicks, but it's also how Github does it, so maybe good enough?
adufr: > Re: a "mark as done on click" -- When opening the notification, you get a blue bar at the top where you can click Done. That's two clicks, but it's also how Github does it, so maybe good enough?

Why not add the setting anyway? For those who, like me, might be happy to have it?


bmulholland: > Why not add the setting anyway?

Sure, especially considering we just removed another setting :)
adufr: Me again -- do I need to change anything for this PR to be merged?

I'd like to work on other things (such as #714 or #708) that would require some shared functions, and I would prefer avoiding stacked PRs 😛 
afonsojramos: Well, first of all great contribution @adufr! It sparked quite a lot of discussions so it is not even just about the individual contribution, thank you!

Sorry for coming late to the party, but these past days have been a bit hectic.

> One easy take is this "mark as done" would replace the "mark as read," though. I honestly don't think most people know or care about the distinction, so having one button for mark as done, no button for mark as read, and keeping the setting seems like a reasonable middle ground.

Totally agree here! As now we've removed the "mark as read" I'd say that marking it as done on click would definitely be an option I would at least *consider* enabling to be honest. Everyone has a different workflow, so I'm up for it.

> Unfortunately, it looks like GitHub does not provide an endpoint for marking all notifications from a repository as done, so I couldn't add a new button to the Repository component 😞

GitHub doesn't, but we probably could I believe... Seems like an easy add to just traverse across all of the repo notifications and mark them as done.
adufr: Thanks!

> GitHub doesn't, but we probably could I believe... Seems like an easy add to just traverse across all of the repo notifications and mark them as done.

Yes I agree, I thought about it! but we should document ourselves about rate-limits just to be safe...

bmulholland: Okay, to keep us moving, how about we merge this PR, and agree on next steps (follow up PRs)? Are you cool with that @afonsojramos ? In that case, @adufr , only thing before merge is to get the branch green and conflict-free.

Those follow-on PRs are, I believe:
1. Make icons consistent with Github, as discussed, and fix their alignment (could be two PRs, for easier/faster review & merge)
1. Add option to "Mark as done on click"
2. Replace icon for "mark repo as read" with "mark repo as done." Optionally, deal with rate limits.
adufr: Hi! Sorry I've been sick as hell for the past week!
I'm finally starting to feel better and so I'm more ready than ever to resume my work on Gitify!

So to clarify, IMO this PR is still valid as-is (I've just updated it with `main`)
Once this get's merged:
1. I'll open a PR for the icons consistency
2. I'll open another PR for the "Mark as done on click" setting
afonsojramos: Tests and build fail though. I'd prefer to have that fixed before merging.
adufr: Sorry it should be good now!
bmulholland: @afonsojramos I missed that, and I agree. Thanks!

Patch:
diff --git a/package.json b/package.json
index ab2121ae..7a260b61 100644
--- a/package.json
+++ b/package.json
@@ -94,7 +94,7 @@
     "afterSign": "scripts/notarize.js"
   },
   "dependencies": {
-    "@electron/remote": "2.1.1",
+    "@electron/remote": "2.1.2",
     "@primer/octicons-react": "19.8.0",
     "axios": "1.6.5",
     "date-fns": "3.3.1",
@@ -112,10 +112,10 @@
     "typescript": "5.3.3"
   },
   "devDependencies": {
-    "@electron/notarize": "2.2.0",
+    "@electron/notarize": "2.2.1",
     "@testing-library/react": "14.1.2",
     "@types/jest": "29.5.11",
-    "@types/node": "20.11.5",
+    "@types/node": "20.11.14",
     "@types/nprogress": "0.2.3",
     "@types/react": "18.2.48",
     "@types/react-router-dom": "5.3.3",
@@ -128,7 +128,7 @@
     "husky": "8.0.3",
     "jest": "29.7.0",
     "jest-environment-jsdom": "29.7.0",
-    "nock": "13.5.0",
+    "nock": "13.5.1",
     "postcss": "8.4.33",
     "postcss-loader": "8.0.0",
     "prettier": "3.2.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6c66f222..19a4a619 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6,8 +6,8 @@ settings:
 
 dependencies:
   '@electron/remote':
-    specifier: 2.1.1
-    version: 2.1.1(electron@22.3.27)
+    specifier: 2.1.2
+    version: 2.1.2(electron@22.3.27)
   '@primer/octicons-react':
     specifier: 19.8.0
     version: 19.8.0(react@18.2.0)
@@ -56,8 +56,8 @@ dependencies:
 
 devDependencies:
   '@electron/notarize':
-    specifier: 2.2.0
-    version: 2.2.0
+    specifier: 2.2.1
+    version: 2.2.1
   '@testing-library/react':
     specifier: 14.1.2
     version: 14.1.2(react-dom@18.2.0)(react@18.2.0)
@@ -65,8 +65,8 @@ devDependencies:
     specifier: 29.5.11
     version: 29.5.11
   '@types/node':
-    specifier: 20.11.5
-    version: 20.11.5
+    specifier: 20.11.14
+    version: 20.11.14
   '@types/nprogress':
     specifier: 0.2.3
     version: 0.2.3
@@ -99,13 +99,13 @@ devDependencies:
     version: 8.0.3
   jest:
     specifier: 29.7.0
-    version: 29.7.0(@types/node@20.11.5)
+    version: 29.7.0(@types/node@20.11.14)
   jest-environment-jsdom:
     specifier: 29.7.0
     version: 29.7.0
   nock:
-    specifier: 13.5.0
-    version: 13.5.0
+    specifier: 13.5.1
+    version: 13.5.1
   postcss:
     specifier: 8.4.33
     version: 8.4.33
@@ -544,8 +544,8 @@ packages:
       - supports-color
     dev: true
 
-  /@electron/notarize@2.2.0:
-    resolution: {integrity: sha512-Sf7RG47rafeGuUm+kLEbTXMN8XZeYXN70dMBstrcgiykxCq3SLl1uqxFWndxSI1LfMqv4Eq9PTDHLPwiya31Kg==}
+  /@electron/notarize@2.2.1:
+    resolution: {integrity: sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg==}
     engines: {node: '>= 10.0.0'}
     dependencies:
       debug: 4.3.4
@@ -570,8 +570,8 @@ packages:
       - supports-color
     dev: true
 
-  /@electron/remote@2.1.1(electron@22.3.27):
-    resolution: {integrity: sha512-Lfxul2yBxL+FBVaKszNAkuUqSIDbUQ1I7BC394iRXyqA2XGz7im2bAxroNIM51jhySSPKUaOLHaFLxfV6pC9VQ==}
+  /@electron/remote@2.1.2(electron@22.3.27):
+    resolution: {integrity: sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA==}
     peerDependencies:
       electron: '>= 13.0.0'
     dependencies:
@@ -614,7 +614,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -635,14 +635,14 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.8.0
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.7.0
-      jest-config: 29.7.0(@types/node@20.11.5)
+      jest-config: 29.7.0(@types/node@20.11.14)
       jest-haste-map: 29.7.0
       jest-message-util: 29.7.0
       jest-regex-util: 29.6.3
@@ -670,7 +670,7 @@ packages:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       jest-mock: 29.7.0
     dev: true
 
@@ -697,7 +697,7 @@ packages:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -730,7 +730,7 @@ packages:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.18
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       chalk: 4.1.2
       collect-v8-coverage: 1.0.1
       exit: 0.1.2
@@ -818,7 +818,7 @@ packages:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       '@types/yargs': 17.0.25
       chalk: 4.1.2
     dev: true
@@ -1008,7 +1008,7 @@ packages:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
       '@types/keyv': 3.1.4
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       '@types/responselike': 1.0.3
 
   /@types/debug@4.1.7:
@@ -1035,13 +1035,13 @@ packages:
   /@types/fs-extra@9.0.13:
     resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
     dev: true
 
   /@types/graceful-fs@4.1.6:
     resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
     dev: true
 
   /@types/history@4.7.11:
@@ -1077,7 +1077,7 @@ packages:
   /@types/jsdom@20.0.1:
     resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       '@types/tough-cookie': 4.0.3
       parse5: 7.1.2
     dev: true
@@ -1088,7 +1088,7 @@ packages:
   /@types/keyv@3.1.4:
     resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
 
   /@types/ms@0.7.31:
     resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
@@ -1097,8 +1097,8 @@ packages:
   /@types/node@16.18.68:
     resolution: {integrity: sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==}
 
-  /@types/node@20.11.5:
-    resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==}
+  /@types/node@20.11.14:
+    resolution: {integrity: sha512-w3yWCcwULefjP9DmDDsgUskrMoOy5Z8MiwKHr1FvqGPtx7CvJzQvxD7eKpxNtklQxLruxSXWddyeRtyud0RcXQ==}
     dependencies:
       undici-types: 5.26.5
 
@@ -1110,7 +1110,7 @@ packages:
     resolution: {integrity: sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw==}
     requiresBuild: true
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       xmlbuilder: 15.1.1
     dev: true
     optional: true
@@ -1163,7 +1163,7 @@ packages:
   /@types/responselike@1.0.3:
     resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
 
   /@types/scheduler@0.16.4:
     resolution: {integrity: sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==}
@@ -1197,7 +1197,7 @@ packages:
     resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
     requiresBuild: true
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
     optional: true
 
   /@webassemblyjs/ast@1.11.6:
@@ -2006,7 +2006,7 @@ packages:
     dev: true
     optional: true
 
-  /create-jest@29.7.0(@types/node@20.11.5):
+  /create-jest@29.7.0(@types/node@20.11.14):
     resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -2015,7 +2015,7 @@ packages:
       chalk: 4.1.2
       exit: 0.1.2
       graceful-fs: 4.2.11
-      jest-config: 29.7.0(@types/node@20.11.5)
+      jest-config: 29.7.0(@types/node@20.11.14)
       jest-util: 29.7.0
       prompts: 2.4.2
     transitivePeerDependencies:
@@ -3274,7 +3274,7 @@ packages:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.5.1
@@ -3295,7 +3295,7 @@ packages:
       - supports-color
     dev: true
 
-  /jest-cli@29.7.0(@types/node@20.11.5):
+  /jest-cli@29.7.0(@types/node@20.11.14):
     resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -3309,10 +3309,10 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
       chalk: 4.1.2
-      create-jest: 29.7.0(@types/node@20.11.5)
+      create-jest: 29.7.0(@types/node@20.11.14)
       exit: 0.1.2
       import-local: 3.1.0
-      jest-config: 29.7.0(@types/node@20.11.5)
+      jest-config: 29.7.0(@types/node@20.11.14)
       jest-util: 29.7.0
       jest-validate: 29.7.0
       yargs: 17.7.2
@@ -3323,7 +3323,7 @@ packages:
       - ts-node
     dev: true
 
-  /jest-config@29.7.0(@types/node@20.11.5):
+  /jest-config@29.7.0(@types/node@20.11.14):
     resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     peerDependencies:
@@ -3338,7 +3338,7 @@ packages:
       '@babel/core': 7.21.8
       '@jest/test-sequencer': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       babel-jest: 29.7.0(@babel/core@7.21.8)
       chalk: 4.1.2
       ci-info: 3.8.0
@@ -3404,7 +3404,7 @@ packages:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
       '@types/jsdom': 20.0.1
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       jest-mock: 29.7.0
       jest-util: 29.7.0
       jsdom: 20.0.3
@@ -3421,7 +3421,7 @@ packages:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       jest-mock: 29.7.0
       jest-util: 29.7.0
     dev: true
@@ -3437,7 +3437,7 @@ packages:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.6
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -3488,7 +3488,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       jest-util: 29.7.0
     dev: true
 
@@ -3543,7 +3543,7 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -3574,7 +3574,7 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       chalk: 4.1.2
       cjs-module-lexer: 1.2.3
       collect-v8-coverage: 1.0.1
@@ -3626,7 +3626,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       chalk: 4.1.2
       ci-info: 3.8.0
       graceful-fs: 4.2.11
@@ -3651,7 +3651,7 @@ packages:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -3663,7 +3663,7 @@ packages:
     resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
     engines: {node: '>= 10.13.0'}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       merge-stream: 2.0.0
       supports-color: 8.1.1
 
@@ -3671,13 +3671,13 @@ packages:
     resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.14
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
     dev: true
 
-  /jest@29.7.0(@types/node@20.11.5):
+  /jest@29.7.0(@types/node@20.11.14):
     resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -3690,7 +3690,7 @@ packages:
       '@jest/core': 29.7.0
       '@jest/types': 29.6.3
       import-local: 3.1.0
-      jest-cli: 29.7.0(@types/node@20.11.5)
+      jest-cli: 29.7.0(@types/node@20.11.14)
     transitivePeerDependencies:
       - '@types/node'
       - babel-plugin-macros
@@ -4035,8 +4035,8 @@ packages:
   /neo-async@2.6.2:
     resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
 
-  /nock@13.5.0:
-    resolution: {integrity: sha512-9hc1eCS2HtOz+sE9W7JQw/tXJktg0zoPSu48s/pYe73e25JW9ywiowbqnUSd7iZPeVawLcVpPZeZS312fwSY+g==}
+  /nock@13.5.1:
+    resolution: {integrity: sha512-+s7b73fzj5KnxbKH4Oaqz07tQ8degcMilU4rrmnKvI//b0JMBU4wEXFQ8zqr+3+L4eWSfU3H/UoIVGUV0tue1Q==}
     engines: {node: '>= 10.13'}
     dependencies:
       debug: 4.3.4
@@ -5180,7 +5180,7 @@ packages:
       '@babel/core': 7.21.8
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
-      jest: 29.7.0(@types/node@20.11.5)
+      jest: 29.7.0(@types/node@20.11.14)
       jest-util: 29.7.0
       json5: 2.2.3
       lodash.memoize: 4.1.2
diff --git a/src/components/NotificationRow.tsx b/src/components/NotificationRow.tsx
index 612a3bbb..d8b58c72 100644
--- a/src/components/NotificationRow.tsx
+++ b/src/components/NotificationRow.tsx
@@ -1,6 +1,6 @@
 import React, { useCallback, useContext } from 'react';
 import { formatDistanceToNow, parseISO } from 'date-fns';
-import { CheckIcon, MuteIcon } from '@primer/octicons-react';
+import { CheckIcon, MuteIcon, ReadIcon } from '@primer/octicons-react';
 
 import {
   formatReason,
@@ -20,8 +20,13 @@ export const NotificationRow: React.FC<IProps> = ({
   notification,
   hostname,
 }) => {
-  const { settings, accounts, markNotification, unsubscribeNotification } =
-    useContext(AppContext);
+  const {
+    settings,
+    accounts,
+    markNotification,
+    markNotificationDone,
+    unsubscribeNotification,
+  } = useContext(AppContext);
 
   const pressTitle = useCallback(() => {
     openBrowser();
@@ -81,18 +86,30 @@ export const NotificationRow: React.FC<IProps> = ({
         </div>
       </div>
 
-      <div className="flex justify-center items-center w-8">
+      <div className="flex justify-center items-center gap-2">
         <button
           className="focus:outline-none"
           title="Mark as Read"
           onClick={() => markNotification(notification.id, hostname)}
         >
-          <CheckIcon
+          <ReadIcon
             className="hover:text-green-500"
-            size={20}
+            size={14}
             aria-label="Mark as Read"
           />
         </button>
+
+        <button
+          className="focus:outline-none"
+          title="Mark as Done"
+          onClick={() => markNotificationDone(notification.id, hostname)}
+        >
+          <CheckIcon
+            className="hover:text-green-500"
+            size={16}
+            aria-label="Mark as Done"
+          />
+        </button>
       </div>
     </div>
   );
diff --git a/src/components/Repository.tsx b/src/components/Repository.tsx
index 14ee53c2..ddf1ef4a 100644
--- a/src/components/Repository.tsx
+++ b/src/components/Repository.tsx
@@ -1,7 +1,7 @@
 const { shell } = require('electron');
 
 import React, { useCallback, useContext } from 'react';
-import { CheckIcon } from '@primer/octicons-react';
+import { ReadIcon } from '@primer/octicons-react';
 import { CSSTransition, TransitionGroup } from 'react-transition-group';
 
 import { AppContext } from '../context/App';
@@ -43,9 +43,9 @@ export const RepositoryNotifications: React.FC<IProps> = ({
 
         <div className="flex w-8 justify-center items-center">
           <button className="focus:outline-none" onClick={markRepoAsRead}>
-            <CheckIcon
+            <ReadIcon
               className="hover:text-green-500"
-              size={20}
+              size={16}
               aria-label="Mark Repository as Read"
             />
           </button>
diff --git a/src/context/App.tsx b/src/context/App.tsx
index 60197bca..dbc788cd 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -52,6 +52,7 @@ interface AppContextState {
   requestFailed: boolean;
   fetchNotifications: () => Promise<void>;
   markNotification: (id: string, hostname: string) => Promise<void>;
+  markNotificationDone: (id: string, hostname: string) => Promise<void>;
   unsubscribeNotification: (id: string, hostname: string) => Promise<void>;
   markRepoNotifications: (id: string, hostname: string) => Promise<void>;
 
@@ -70,6 +71,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
     requestFailed,
     isFetching,
     markNotification,
+    markNotificationDone,
     unsubscribeNotification,
     markRepoNotifications,
   } = useNotifications(settings.colors);
@@ -176,6 +178,12 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
     [accounts, notifications],
   );
 
+  const markNotificationDoneWithAccounts = useCallback(
+    async (id: string, hostname: string) =>
+      await markNotificationDone(accounts, id, hostname),
+    [accounts, notifications],
+  );
+
   const unsubscribeNotificationWithAccounts = useCallback(
     async (id: string, hostname: string) =>
       await unsubscribeNotification(accounts, id, hostname),
@@ -203,6 +211,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
         requestFailed,
         fetchNotifications: fetchNotificationsWithAccounts,
         markNotification: markNotificationWithAccounts,
+        markNotificationDone: markNotificationDoneWithAccounts,
         unsubscribeNotification: unsubscribeNotificationWithAccounts,
         markRepoNotifications: markRepoNotificationsWithAccounts,
 
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index 50cf161b..41875fb8 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -28,6 +28,11 @@ interface NotificationsState {
     id: string,
     hostname: string,
   ) => Promise<void>;
+  markNotificationDone: (
+    accounts: AuthState,
+    id: string,
+    hostname: string,
+  ) => Promise<void>;
   unsubscribeNotification: (
     accounts: AuthState,
     id: string,
@@ -215,6 +220,39 @@ export const useNotifications = (colors: boolean): NotificationsState => {
     [notifications],
   );
 
+  const markNotificationDone = useCallback(
+    async (accounts, id, hostname) => {
+      setIsFetching(true);
+
+      const isEnterprise = hostname !== Constants.DEFAULT_AUTH_OPTIONS.hostname;
+      const token = isEnterprise
+        ? getEnterpriseAccountToken(hostname, accounts.enterpriseAccounts)
+        : accounts.token;
+
+      try {
+        await apiRequestAuth(
+          `${generateGitHubAPIUrl(hostname)}notifications/threads/${id}`,
+          'DELETE',
+          token,
+          {},
+        );
+
+        const updatedNotifications = removeNotification(
+          id,
+          notifications,
+          hostname,
+        );
+
+        setNotifications(updatedNotifications);
+        setTrayIconColor(updatedNotifications);
+        setIsFetching(false);
+      } catch (err) {
+        setIsFetching(false);
+      }
+    },
+    [notifications],
+  );
+
   const unsubscribeNotification = useCallback(
     async (accounts, id, hostname) => {
       setIsFetching(true);
@@ -281,6 +319,7 @@ export const useNotifications = (colors: boolean): NotificationsState => {
 
     fetchNotifications,
     markNotification,
+    markNotificationDone,
     unsubscribeNotification,
     markRepoNotifications,
   };


Test patch:
diff --git a/src/components/NotificationRow.test.tsx b/src/components/NotificationRow.test.tsx
index cb66ae0c..0359f0ee 100644
--- a/src/components/NotificationRow.test.tsx
+++ b/src/components/NotificationRow.test.tsx
@@ -79,6 +79,31 @@ describe('components/Notification.js', () => {
     expect(markNotification).toHaveBeenCalledTimes(1);
   });
 
+  it('should mark a notification as done', () => {
+    const markNotificationDone = jest.fn();
+
+    const props = {
+      notification: mockedSingleNotification,
+      hostname: 'github.com',
+    };
+
+    const { getByTitle } = render(
+      <AppContext.Provider
+        value={{
+          settings: { ...mockSettings },
+          accounts: mockAccounts,
+        }}
+      >
+        <AppContext.Provider value={{ markNotificationDone }}>
+          <NotificationRow {...props} />
+        </AppContext.Provider>
+      </AppContext.Provider>,
+    );
+
+    fireEvent.click(getByTitle('Mark as Done'));
+    expect(markNotificationDone).toHaveBeenCalledTimes(1);
+  });
+
   it('should unsubscribe from a notification thread', () => {
     const unsubscribeNotification = jest.fn();
 
diff --git a/src/components/__snapshots__/NotificationRow.test.tsx.snap b/src/components/__snapshots__/NotificationRow.test.tsx.snap
index 8a922c50..9d9ed504 100644
--- a/src/components/__snapshots__/NotificationRow.test.tsx.snap
+++ b/src/components/__snapshots__/NotificationRow.test.tsx.snap
@@ -87,7 +87,7 @@ exports[`components/Notification.js should render itself & its children 1`] = `
     </div>
   </div>
   <div
-    className="flex justify-center items-center w-8"
+    className="flex justify-center items-center gap-2"
   >
     <button
       className="focus:outline-none"
@@ -100,7 +100,7 @@ exports[`components/Notification.js should render itself & its children 1`] = `
         className="hover:text-green-500"
         fill="currentColor"
         focusable="false"
-        height={20}
+        height={14}
         role="img"
         style={
           {
@@ -111,7 +111,36 @@ exports[`components/Notification.js should render itself & its children 1`] = `
           }
         }
         viewBox="0 0 16 16"
-        width={20}
+        width={14}
+      >
+        <path
+          d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
+        />
+      </svg>
+    </button>
+    <button
+      className="focus:outline-none"
+      onClick={[Function]}
+      title="Mark as Done"
+    >
+      <svg
+        aria-hidden="false"
+        aria-label="Mark as Done"
+        className="hover:text-green-500"
+        fill="currentColor"
+        focusable="false"
+        height={16}
+        role="img"
+        style={
+          {
+            "display": "inline-block",
+            "overflow": "visible",
+            "userSelect": "none",
+            "verticalAlign": "text-bottom",
+          }
+        }
+        viewBox="0 0 16 16"
+        width={16}
       >
         <path
           d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
diff --git a/src/components/__snapshots__/Repository.test.tsx.snap b/src/components/__snapshots__/Repository.test.tsx.snap
index cf5d4539..f62796ce 100644
--- a/src/components/__snapshots__/Repository.test.tsx.snap
+++ b/src/components/__snapshots__/Repository.test.tsx.snap
@@ -31,7 +31,7 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
           className="hover:text-green-500"
           fill="currentColor"
           focusable="false"
-          height={20}
+          height={16}
           role="img"
           style={
             {
@@ -42,10 +42,10 @@ exports[`components/Repository.tsx should render itself & its children 1`] = `
             }
           }
           viewBox="0 0 16 16"
-          width={20}
+          width={16}
         >
           <path
-            d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"
+            d="M7.115.65a1.752 1.752 0 0 1 1.77 0l6.25 3.663c.536.314.865.889.865 1.51v6.427A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25V5.823c0-.621.33-1.196.865-1.51Zm1.011 1.293a.252.252 0 0 0-.252 0l-5.72 3.353L6.468 7.76a2.748 2.748 0 0 1 3.066 0l4.312-2.464-5.719-3.353ZM13.15 12.5 8.772 9.06a1.25 1.25 0 0 0-1.544 0L2.85 12.5Zm1.35-5.85-3.687 2.106 3.687 2.897ZM5.187 8.756 1.5 6.65v5.003Z"
           />
         </svg>
       </button>
diff --git a/src/context/App.test.tsx b/src/context/App.test.tsx
index d0b45e63..aea2d742 100644
--- a/src/context/App.test.tsx
+++ b/src/context/App.test.tsx
@@ -38,6 +38,7 @@ describe('context/App.tsx', () => {
 
     const fetchNotificationsMock = jest.fn();
     const markNotificationMock = jest.fn();
+    const markNotificationDoneMock = jest.fn();
     const unsubscribeNotificationMock = jest.fn();
     const markRepoNotificationsMock = jest.fn();
 
@@ -45,6 +46,7 @@ describe('context/App.tsx', () => {
       (useNotifications as jest.Mock).mockReturnValue({
         fetchNotifications: fetchNotificationsMock,
         markNotification: markNotificationMock,
+        markNotificationDone: markNotificationDoneMock,
         unsubscribeNotification: unsubscribeNotificationMock,
         markRepoNotifications: markRepoNotificationsMock,
       });
@@ -121,6 +123,31 @@ describe('context/App.tsx', () => {
       );
     });
 
+    it('should call markNotificationDone', async () => {
+      const TestComponent = () => {
+        const { markNotificationDone } = useContext(AppContext);
+
+        return (
+          <button onClick={() => markNotificationDone('123-456', 'github.com')}>
+            Test Case
+          </button>
+        );
+      };
+
+      const { getByText } = customRender(<TestComponent />);
+
+      markNotificationDoneMock.mockReset();
+
+      fireEvent.click(getByText('Test Case'));
+
+      expect(markNotificationDoneMock).toHaveBeenCalledTimes(1);
+      expect(markNotificationDoneMock).toHaveBeenCalledWith(
+        { enterpriseAccounts: [], token: null, user: null },
+        '123-456',
+        'github.com',
+      );
+    });
+
     it('should call unsubscribeNotification', async () => {
       const TestComponent = () => {
         const { unsubscribeNotification } = useContext(AppContext);
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 66e40f26..c6f2b753 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -355,6 +355,92 @@ describe('hooks/useNotifications.ts', () => {
     });
   });
 
+  describe('markNotificationDone', () => {
+    const id = 'notification-123';
+
+    describe('github.com', () => {
+      const accounts = { ...mockAccounts, enterpriseAccounts: [] };
+      const hostname = 'github.com';
+
+      it('should mark a notification as done with success - github.com', async () => {
+        nock('https://api.github.com/')
+          .delete(`/notifications/threads/${id}`)
+          .reply(200);
+
+        const { result } = renderHook(() => useNotifications(false));
+
+        act(() => {
+          result.current.markNotificationDone(accounts, id, hostname);
+        });
+
+        await waitFor(() => {
+          expect(result.current.isFetching).toBe(false);
+        });
+
+        expect(result.current.notifications.length).toBe(0);
+      });
+
+      it('should mark a notification as done with failure - github.com', async () => {
+        nock('https://api.github.com/')
+          .delete(`/notifications/threads/${id}`)
+          .reply(400);
+
+        const { result } = renderHook(() => useNotifications(false));
+
+        act(() => {
+          result.current.markNotificationDone(accounts, id, hostname);
+        });
+
+        await waitFor(() => {
+          expect(result.current.isFetching).toBe(false);
+        });
+
+        expect(result.current.notifications.length).toBe(0);
+      });
+    });
+
+    describe('enterprise', () => {
+      const accounts = { ...mockAccounts, token: null };
+      const hostname = 'github.gitify.io';
+
+      it('should mark a notification as done with success - enterprise', async () => {
+        nock('https://github.gitify.io/')
+          .delete(`/notifications/threads/${id}`)
+          .reply(200);
+
+        const { result } = renderHook(() => useNotifications(false));
+
+        act(() => {
+          result.current.markNotificationDone(accounts, id, hostname);
+        });
+
+        await waitFor(() => {
+          expect(result.current.isFetching).toBe(false);
+        });
+
+        expect(result.current.notifications.length).toBe(0);
+      });
+
+      it('should mark a notification as done with failure - enterprise', async () => {
+        nock('https://github.gitify.io/')
+          .delete(`/notifications/threads/${id}`)
+          .reply(400);
+
+        const { result } = renderHook(() => useNotifications(false));
+
+        act(() => {
+          result.current.markNotificationDone(accounts, id, hostname);
+        });
+
+        await waitFor(() => {
+          expect(result.current.isFetching).toBe(false);
+        });
+
+        expect(result.current.notifications.length).toBe(0);
+      });
+    });
+  });
+
   describe('unsubscribeNotification', () => {
     const id = 'notification-123';