Skip to content

Commit ff8291f

Browse files
authored
Add unsubscribe option (#26)
1 parent 591acf1 commit ff8291f

File tree

3 files changed

+110
-34
lines changed

3 files changed

+110
-34
lines changed

src/api/notifications.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,12 @@ export function markNotificationAsRead(id: Thread['id'], accessToken: NonNullabl
169169
headers: createBaseGithubApiHeaders(accessToken),
170170
})
171171
}
172+
173+
export function unsubscribeNotification(id: Thread['id'], accessToken: NonNullable<AppStorageContext['accessToken']>) {
174+
return redaxios.put(`https://api.github.com/notifications/threads/${id}/subscription`, null, {
175+
headers: createBaseGithubApiHeaders(accessToken),
176+
body: {
177+
ignored: true,
178+
},
179+
})
180+
}

src/pages/HomePage.vue

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
<script lang="ts" setup>
22
import { open } from '@tauri-apps/api/shell'
3-
import { computed, onScopeDispose, ref } from 'vue'
3+
import { computed, onScopeDispose, ref, watch } from 'vue'
44
import { type ReferenceElement } from 'wowerlay'
5+
import { whenever } from '@vueuse/core'
56
import { useStore } from '../stores/store'
67
import NotificationItem from '../components/NotificationItem.vue'
7-
import { type MinimalRepository, type Thread, markNotificationAsRead } from '../api/notifications'
8+
import { type MinimalRepository, type Thread, markNotificationAsRead, unsubscribeNotification } from '../api/notifications'
89
import { toGithubWebURL } from '../utils/github'
910
import { AppStorage } from '../storage'
1011
import NotificationSkeleton from '../components/NotificationSkeleton.vue'
@@ -110,33 +111,17 @@ const popoverTarget = ref<ReferenceElement | null>(null)
110111
const popoverRef = ref<InstanceType<typeof Popover> | null>(null)
111112
112113
async function handleSelectMarkAsRead(triggeredByKeyboard = false) {
113-
if (triggeredByKeyboard) {
114-
if (store.checkedItems.length > 0) {
115-
store.markCheckedNotificationsAsRead(AppStorage.get('accessToken')!)
116-
store.checkedItems = []
117-
return
118-
}
119-
120-
if (contextMenuThread.value) {
121-
const thread = contextMenuThread.value
122-
markNotificationAsRead(contextMenuThread.value.id, AppStorage.get('accessToken')!)
123-
.then(() => {
124-
store.removeNotificationById(thread.id)
125-
})
126-
127-
return
128-
}
129-
}
130-
131-
if (!contextMenuThread.value)
132-
return
133-
134-
if (isChecked(contextMenuThread.value)) {
114+
if (
115+
(triggeredByKeyboard && store.checkedItems.length > 0)
116+
|| (contextMenuThread.value && isChecked(contextMenuThread.value))) {
135117
store.markCheckedNotificationsAsRead(AppStorage.get('accessToken')!)
136118
store.checkedItems = []
137119
return
138120
}
139121
122+
if (!contextMenuThread.value)
123+
return
124+
140125
const thread = contextMenuThread.value
141126
markNotificationAsRead(thread.id, AppStorage.get('accessToken')!)
142127
.then(() => {
@@ -179,27 +164,61 @@ useKey('o', () => {
179164
popoverRef.value?.hide()
180165
})
181166
167+
async function handleSelectUnsubscribe(triggeredByKeyboard = false) {
168+
if (
169+
(triggeredByKeyboard && store.checkedItems.length > 0)
170+
|| (contextMenuThread.value && isChecked(contextMenuThread.value))) {
171+
store.unsubscribeCheckedNotifications(AppStorage.get('accessToken')!)
172+
store.checkedItems = []
173+
return
174+
}
175+
176+
if (!contextMenuThread.value)
177+
return
178+
179+
const thread = contextMenuThread.value
180+
unsubscribeNotification(thread.id, AppStorage.get('accessToken')!)
181+
.then(() => {
182+
store.removeNotificationById(thread.id)
183+
})
184+
}
185+
186+
useKey('u', () => {
187+
handleSelectUnsubscribe(true)
188+
popoverRef.value?.hide()
189+
})
190+
182191
const contextMenuItems = computed(() => [
183192
menuItem({
184193
key: 'read',
185194
meta: { text: 'Mark as read', icon: Icons.Check16, key: 'M' },
186-
onSelect: () => handleSelectMarkAsRead(),
195+
onSelect() {
196+
handleSelectMarkAsRead()
197+
contextMenuThread.value = null
198+
},
187199
}),
188200
menuItem({
189201
key: 'open',
190202
meta: { text: 'Open', icon: Icons.LinkExternal16, key: 'O' },
191-
onSelect: () => handleSelectOpen(),
203+
onSelect() {
204+
handleSelectOpen()
205+
contextMenuThread.value = null
206+
},
207+
}),
208+
menuItem({
209+
key: 'unsubscribe',
210+
meta: { text: 'Unsubscribe', icon: Icons.BellSlash16, key: 'U' },
211+
onSelect() {
212+
handleSelectUnsubscribe()
213+
contextMenuThread.value = null
214+
},
192215
}),
193-
// menuItem({
194-
// key: 'unsubscribe',
195-
// meta: { text: 'Unsubscribe', icon: Icons.BellSlash16, key: 'U' },
196-
// disabled: true,
197-
// }),
198216
isChecked(contextMenuThread.value!) && menuItem({
199217
key: 'clear',
200218
meta: { text: 'Clear selections', icon: Icons.Circle, key: 'ESC' },
201219
onSelect: () => {
202220
store.checkedItems = []
221+
contextMenuThread.value = null
203222
},
204223
}),
205224
])
@@ -223,6 +242,27 @@ function handleThreadContextmenu(thread: Thread, event: MouseEvent) {
223242
}
224243
popoverRef.value?.show()
225244
}
245+
246+
// Edge-Case
247+
// If notifications are reloaded and the context menu target thread is deleted, close the context menu
248+
watch(() => store.notifications, (notifications) => {
249+
if (!contextMenuThread.value)
250+
return
251+
252+
const exists = notifications.some(notification => notification.id === contextMenuThread.value!.id)
253+
if (!exists) {
254+
contextMenuThread.value = null
255+
popoverRef.value?.hide()
256+
}
257+
})
258+
259+
// Edge-Case
260+
// If user reloaded in the middle of selecting notifications, clear the selection
261+
whenever(() => store.skeletonVisible, () => {
262+
popoverRef.value?.hide()
263+
store.checkedItems = []
264+
popoverTarget.value = null
265+
})
226266
</script>
227267

228268
<template>

src/stores/store.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import { invoke } from '@tauri-apps/api/tauri'
33
import { defineStore } from 'pinia'
44
import { readonly, ref, shallowRef, triggerRef, watch } from 'vue'
55
import pAll from 'p-all'
6-
import type { Thread } from '../api/notifications'
7-
import { getNotifications, markNotificationAsRead } from '../api/notifications'
6+
import { type Thread, getNotifications, markNotificationAsRead, unsubscribeNotification } from '../api/notifications'
87
import type { Release } from '../api/releases'
98
import { InvokeCommand, Page } from '../constants'
109
import { AppStorage } from '../storage'
@@ -149,17 +148,45 @@ export const useStore = defineStore('store', () => {
149148
}
150149
}
151150

151+
async function unsubscribeCheckedNotifications(accessToken: NonNullable<AppStorageContext['accessToken']>) {
152+
const deletedThreads: Thread['id'][] = []
153+
154+
try {
155+
await pAll(
156+
checkedItems.value.map(thread => async () => {
157+
try {
158+
await unsubscribeNotification(thread.id, accessToken)
159+
deletedThreads.push(thread.id)
160+
}
161+
catch (error) {
162+
console.error(error)
163+
}
164+
}),
165+
{
166+
stopOnError: false,
167+
concurrency: 7,
168+
},
169+
)
170+
}
171+
finally {
172+
deletedThreads.forEach(id => removeNotificationById(id))
173+
checkedItems.value = []
174+
triggerRef(notifications)
175+
}
176+
}
177+
152178
return {
153179
newRelease,
154180
notifications,
155181
currentPage: readonly(currentPage),
156-
removeNotificationById,
157182
loadingNotifications,
158183
skeletonVisible,
159184
pageFrom,
160185
failedLoadingNotifications,
161186
currentPageState,
162187
checkedItems,
188+
unsubscribeCheckedNotifications,
189+
removeNotificationById,
163190
findThreadIndex,
164191
markCheckedNotificationsAsRead,
165192
setPage,

0 commit comments

Comments
 (0)