Skip to content

Commit 3b18282

Browse files
committed
Update notification renderer code
1 parent d4bd5f8 commit 3b18282

File tree

9 files changed

+101
-114
lines changed

9 files changed

+101
-114
lines changed
Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script lang="ts" setup>
22
import dayjs from 'dayjs'
3-
import type { Thread } from '../api/notifications'
4-
import type { NotificationListData } from '../types'
5-
import { formatReason, notificationSubjectIcon } from '../utils/notification'
3+
import { MinimalRepository, Thread } from '../api/notifications'
4+
import type { NotificationList } from '../types'
5+
import { formatReason, isRepository, notificationSubjectIcon } from '../utils/notification'
66
import Separator from './Separator.vue'
77
88
interface Emits {
@@ -11,74 +11,74 @@ interface Emits {
1111
}
1212
1313
interface Props {
14-
data: NotificationListData
14+
value: NotificationList[number]
1515
}
1616
1717
const props = defineProps<Props>()
1818
const emit = defineEmits<Emits>()
1919
20-
function handleNotificationClick(notification: Thread) {
21-
emit('click:notification', notification)
20+
function handleThreadClick(thread: Thread) {
21+
emit('click:notification', thread)
2222
}
2323
24-
function handleRepoClick() {
25-
emit('click:repo', props.data.repoFullName)
24+
function handleRepoClick(repo: MinimalRepository) {
25+
emit('click:repo', repo.full_name)
2626
}
2727
</script>
2828

2929
<template>
3030
<div
31-
draggable="false"
32-
class="notification"
31+
v-if="isRepository(value)"
32+
class="notification-title-wrapper"
3333
>
3434
<button
3535
class="notification-title"
36-
@click="handleRepoClick"
36+
@click="handleRepoClick(value as MinimalRepository)"
3737
>
3838
<img
3939
class="notification-title-icon"
40-
:src="props.data.repoAvatarURL"
40+
:src="(value as MinimalRepository).owner.avatar_url"
4141
alt="repo logo"
4242
>
4343
<span class="notification-title-text">
44-
{{ props.data.repoFullName }}
44+
{{ (value as MinimalRepository).full_name }}
4545
</span>
4646
</button>
4747

4848
<Separator />
49+
</div>
4950

50-
<button
51-
v-for="item of props.data.items"
52-
:key="item.id"
53-
class="notification-item"
54-
:class="{ 'notification-item-read': !item.unread }"
55-
@click="handleNotificationClick(item.raw)"
56-
>
57-
<Component
58-
:is="notificationSubjectIcon(item.type)"
59-
class="notification-item-icon"
60-
/>
61-
62-
<div class="notification-item-content">
63-
<div class="notification-item-content-title">
64-
{{ item.title }}
65-
</div>
66-
67-
<div class="notification-item-content-subtitle">
68-
{{ formatReason(item.reason) }}
69-
-
70-
{{ dayjs(item.updatedAt).fromNow() }}
71-
</div>
51+
<button
52+
v-else
53+
class="notification-item"
54+
:class="{ 'notification-item-read': !(value as Thread).unread }"
55+
@click="handleThreadClick(value as Thread)"
56+
>
57+
<Component
58+
:is="notificationSubjectIcon((value as Thread).subject.type)"
59+
class="notification-item-icon"
60+
/>
61+
62+
<div class="notification-item-content">
63+
<div class="notification-item-content-title">
64+
{{ (value as Thread).subject.title }}
7265
</div>
73-
</button>
74-
</div>
66+
67+
<div class="notification-item-content-subtitle">
68+
{{ formatReason((value as Thread).reason) }}
69+
-
70+
{{ dayjs((value as Thread).updated_at).fromNow() }}
71+
</div>
72+
</div>
73+
</button>
7574
</template>
7675

7776
<style lang="scss" scoped>
77+
* + .notification-title-wrapper {
78+
margin-top: 10px
79+
}
80+
7881
.notification {
79-
+ .notification {
80-
margin-top: 10px;
81-
}
8282
8383
&-title {
8484
@include focus-visible;
@@ -132,6 +132,7 @@ function handleRepoClick() {
132132
line-height: 20px;
133133
@include focus-visible;
134134
@include text-outline($size: 1px);
135+
margin-top: 5px;
135136
136137
&-read {
137138
color: var(--white-faded) !important;

src/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export enum NotificationSubject {
3030
Release = 'Release',
3131
}
3232

33+
export enum NotificationItemType {
34+
Repository = 'repository',
35+
Thread = 'thread',
36+
}
37+
3338
export enum NotificationReason {
3439
Assign = 'assign',
3540
Author = 'author',

src/pages/HomePage.vue

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import { open } from '@tauri-apps/api/shell'
66
import { ref } from 'vue'
77
import { useStore } from '../stores/store'
8-
import NotificationList from '../components/NotificationList.vue'
9-
import { useInterval } from '../composables/useInterval'
8+
import NotificationItem from '../components/NotificationItem.vue'
109
import type { Thread } from '../api/notifications'
1110
import { toGithubWebURL } from '../utils/github'
1211
import { AppStorage } from '../storage'
@@ -20,7 +19,7 @@ import AppButton from '../components/AppButton.vue'
2019
const store = useStore()
2120
2221
if (store.currentPageState.fetchOnEnter)
23-
store.fetchNotifications()
22+
store.fetchNotifications(true)
2423
2524
function handleNotificationClick(notification: Thread) {
2625
const url = toGithubWebURL({ notification, userId: AppStorage.get('user')!.id })
@@ -55,7 +54,7 @@ useElementNavigation({
5554
description="Oopsie! Couldn't load notifications."
5655
>
5756
<template #footer>
58-
<AppButton @click="store.fetchNotifications(true)">
57+
<AppButton @click="store.fetchNotifications()">
5958
Refresh
6059
</AppButton>
6160
</template>
@@ -68,15 +67,13 @@ useElementNavigation({
6867
description="It's all clear sir!"
6968
/>
7069

71-
<template v-else>
72-
<NotificationList
73-
v-for="notification of store.notifications"
74-
:key="notification.repoFullName"
75-
:data="notification"
76-
@click:notification="handleNotificationClick"
77-
@click:repo="handleRepoClick"
78-
/>
79-
</template>
70+
<NotificationItem
71+
v-for="item of store.notifications"
72+
:key="item.id"
73+
:value="item"
74+
@click:notification="handleNotificationClick"
75+
@click:repo="handleRepoClick"
76+
/>
8077
</div>
8178
</template>
8279

src/pages/SettingsPage.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ watchDebounced(openAtStartup, (enabled) => {
4141
}, { debounce: 350 })
4242
4343
useTauriEvent('window:hidden', () => {
44-
store.setPage(Page.Home)
44+
setTimeout(() => store.setPage(Page.Home), 50)
4545
})
4646
4747
function handleBack() {

src/stores/store.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { invoke } from '@tauri-apps/api/tauri'
22
import { defineStore } from 'pinia'
33
import { readonly, ref, shallowRef, watch } from 'vue'
4-
import type { Thread } from '../api/notifications'
4+
import type { MinimalRepository, Thread } from '../api/notifications'
55
import { getNotifications } from '../api/notifications'
66
import type { Release } from '../api/releases'
77
import { InvokeCommand, Page } from '../constants'
88
import { AppStorage } from '../storage'
9-
import type { NotificationListData, Option, PageState } from '../types'
10-
import { notificationListFromThreads } from '../utils/notification'
9+
import type { Option, PageState } from '../types'
10+
import { toNotificationList } from '../utils/notification'
1111

1212
function hasNewNotification(newThreads: Thread[], previousThreads: Thread[]) {
1313
const newThreadsFiltered = newThreads.filter(t => t.unread)
@@ -16,7 +16,7 @@ function hasNewNotification(newThreads: Thread[], previousThreads: Thread[]) {
1616
}
1717

1818
export const useStore = defineStore('store', () => {
19-
const notifications = ref<NotificationListData[]>([])
19+
const notifications = shallowRef<(Thread | MinimalRepository)[]>([])
2020
const loadingNotifications = ref(false)
2121
const failedLoadingNotifications = ref(false)
2222
const skeletonVisible = ref(false)
@@ -33,8 +33,10 @@ export const useStore = defineStore('store', () => {
3333
if (accessToken == null)
3434
return
3535

36-
if (withSkeletons)
36+
if (withSkeletons) {
3737
skeletonVisible.value = true
38+
notifications.value = []
39+
}
3840

3941
loadingNotifications.value = true
4042
failedLoadingNotifications.value = false
@@ -48,7 +50,7 @@ export const useStore = defineStore('store', () => {
4850

4951
notificationsRawPrevious = notificationsRaw
5052
notificationsRaw = data
51-
notifications.value = notificationListFromThreads(data)
53+
notifications.value = toNotificationList(data)
5254
}
5355
catch (error) {
5456
console.error('NotificationError: ', error)

src/types.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,11 @@
1-
import type { Raw, Ref } from 'vue'
2-
import type { Thread } from './api/notifications'
1+
import type { Ref } from 'vue'
2+
import type { MinimalRepository, Thread } from './api/notifications'
33
import type { User } from './api/user'
44

55
export type Option<T> = T | null
66
export type MaybeRef<T> = T | Ref<T>
77

8-
export interface NotificationListData {
9-
repoFullName: string
10-
repoAvatarURL: string
11-
items: NotificationListDataItem[]
12-
}
13-
14-
export interface NotificationListDataItem {
15-
id: Thread['id']
16-
unread: Thread['unread']
17-
title: Thread['subject']['title']
18-
reason: Thread['reason']
19-
type: Thread['subject']['type']
20-
updatedAt: Thread['updated_at']
21-
raw: Raw<Thread>
22-
}
8+
export type NotificationList = (Thread | MinimalRepository)[]
239

2410
export interface AppStorageContext {
2511
user: Option<User>

src/utils/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { AppStorageContext, Option } from '../types'
1+
import type { AppStorageContext } from '../types'
22

33
export function createBaseGithubApiHeaders(accessToken: AppStorageContext['accessToken']) {
44
const headers: Record<string, string> = {

src/utils/is.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const typeOf = (value: any) => Object.prototype.toString.call(value).slice(8, -1)
2+
3+
export function isObject<T = Record<PropertyKey, any>>(value: T): value is T {
4+
return typeOf(value) === 'Object'
5+
}

src/utils/notification.ts

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { markRaw } from 'vue'
2-
import type { Thread } from '../api/notifications'
1+
import type { MinimalRepository, Thread } from '../api/notifications'
32
import { Icons } from '../components/Icons'
43
import type { NotificationReason, NotificationSubject } from '../constants'
54
import { reasonFormatMap, subjectIconMap } from '../constants'
6-
import type { NotificationListData, NotificationListDataItem } from '../types'
5+
import type { NotificationList } from '../types'
6+
import { isObject } from './is'
77

88
export function notificationSubjectIcon(subject: NotificationSubject) {
99
return subjectIconMap[subject] || Icons.Question
@@ -13,43 +13,34 @@ export function formatReason(reason: NotificationReason) {
1313
return reasonFormatMap[reason] || 'Unknown reason'
1414
}
1515

16-
function notificationListItemFromThread(thread: Thread): NotificationListDataItem {
17-
return {
18-
id: thread.id,
19-
reason: thread.reason,
20-
title: thread.subject.title,
21-
type: thread.subject.type,
22-
unread: thread.unread,
23-
updatedAt: thread.updated_at,
24-
raw: markRaw(thread),
25-
}
26-
}
27-
28-
export function notificationListFromThreads(threads: Thread[]) {
29-
const repoIdIndexMap: Record<Thread['repository']['id'], number> = Object.create(null)
30-
const notifications: NotificationListData[] = []
16+
export function toNotificationList(threads: Thread[]) {
17+
const notifications: NotificationList = []
3118

32-
for (const thread of threads) {
33-
const { repository } = thread
19+
for (let index = 0; index < threads.length; index++) {
20+
const thread = threads[index]
3421

35-
if (repository.id in repoIdIndexMap) {
36-
const notification = notifications[repoIdIndexMap[repository.id]]
37-
notification.items.push(notificationListItemFromThread(thread))
38-
}
39-
else {
40-
const nextIndex = notifications.length
41-
const notification: NotificationListData = {
42-
repoAvatarURL: `${repository.owner.avatar_url}&s=40`,
43-
repoFullName: repository.full_name,
44-
items: [
45-
notificationListItemFromThread(thread),
46-
],
47-
}
48-
49-
repoIdIndexMap[repository.id] = nextIndex
50-
notifications.push(notification)
22+
if (index === 0) {
23+
notifications.push(
24+
thread.repository,
25+
thread,
26+
)
27+
continue
5128
}
29+
30+
const previousThread = threads[index - 1]
31+
32+
if (thread.repository.id === previousThread.repository.id)
33+
notifications.push(thread)
34+
else
35+
notifications.push(thread.repository, thread)
5236
}
5337

5438
return notifications
5539
}
40+
41+
export function isThread(value: any): value is Thread {
42+
return isObject<Thread>(value) && 'reason' in value
43+
}
44+
export function isRepository(value: any): value is MinimalRepository {
45+
return isObject<MinimalRepository>(value) && 'teams_url' in value
46+
}

0 commit comments

Comments
 (0)