wip: feat: abstract NotificationItem in dedicated component #2681

Closed
dpschen wants to merge 6 commits from dpschen/frontend:feature/abstract-NotificationItem-component into main
4 changed files with 99 additions and 64 deletions
Showing only changes of commit 0f940e9183 - Show all commits

View File

@ -1,27 +1,25 @@
<template>
<div class="notifications">
<!-- FIXME: add label -->
<slot :togglePopup="togglePopup" :hasUnreadNotifications="hasUnreadNotifications">
<NavbarTriggerButton
:pressed="showNotifications"
ref="toggleButton"
@click="togglePopup"
>
<span v-if="hasUnreadNotifications" class="unread-indicator" />
<icon icon="bell"/>
</NavbarTriggerButton>
</slot>
<NavbarTriggerButton
:pressed="modalIsOpen"
ref="toggleButton"
:aria-label="modalIsOpen ? $t('notification.hideNotifications') : $t('notification.showNotifications')"
@click="togglePopup"
>
<span v-if="hasUnreadNotifications" class="unread-indicator" />
<icon icon="bell"/>
</NavbarTriggerButton>
<!-- FIXME: create dedicated dropdown menu -->
<CustomTransition name="fade">
<div class="notifications-list" v-if="showNotifications" ref="popup">
<div class="notifications-list" v-if="modalIsOpen" ref="popup">
<h3 class="head">{{ $t('notification.title') }}</h3>
<NotificationItem
v-for="(n, index) in notifications"
v-for="n in notifications"
:key="n.id"
class="notification-item"
:notification="n"
@markNotificationAsRead="markNotificationAsRead(index, n)"
@markNotificationAsRead="markNotificationAsRead(n)"
/>
<p class="nothing" v-if="notifications.length === 0">
{{ $t('notification.none') }}<br/>
@ -35,76 +33,43 @@
</template>
<script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref} from 'vue'
import {onClickOutside} from '@vueuse/core'
import {ref} from 'vue'
import {onClickOutside, tryOnUnmounted} from '@vueuse/core'
import NotificationService from '@/services/notification'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import type {INotification} from '@/modelTypes/INotification'
import NavbarTriggerButton from '@/components/home/NavbarTriggerButton.vue'
import NotificationItem from '@/components/notifications/NotificationItem.vue'
import {useNotificationStore} from '@/stores/notifications'
import {findIndexById} from '@/helpers/utils'
const NOTIFICATIONS_PULL_INTERVAL = 10000
const allNotifications = ref<INotification[]>([])
const showNotifications = ref(false)
const modalIsOpen = ref(false)
const popup = ref(null)
const toggleButton = ref(null)
function togglePopup() {
dpschen marked this conversation as resolved Outdated

If vueuse has a helper like ours, do we even need ours?

If vueuse has a helper like ours, do we even need ours?

No, I'm replacing ours piece by piece.
Not all vueuse composables are perfect though.
E.g. there is one useTextareaAutosize, but it e.g. doesn't cover the case where the textarea scales with the width of the window. So we should be careful. The onClickoutside seems well written though!

No, I'm replacing ours piece by piece. Not all vueuse composables are perfect though. E.g. there is one `useTextareaAutosize`, but it e.g. doesn't cover the case where the textarea scales with the width of the window. So we should be careful. The `onClickoutside` seems well written though!
showNotifications.value = !showNotifications.value
modalIsOpen.value = !modalIsOpen.value
}
onClickOutside(
popup,
() => {
if (!showNotifications.value) {
if (!modalIsOpen.value) {
return
}
showNotifications.value = false
modalIsOpen.value = false
},
{ ignore: [toggleButton]},
)
const notifications = computed(() => {
return allNotifications.value ? allNotifications.value.filter(n => n.name !== '') : []
})
const unreadNotifications = computed(() => {
return notifications.value.filter(n => n.readAt === null).length
})
const {
notifications,
hasUnreadNotifications,
const hasUnreadNotifications = computed(() => unreadNotifications.value > 0)
startNotificationPulling,
markNotificationAsRead,
} = useNotificationStore()
let interval: ReturnType<typeof setInterval>
onMounted(() => {
interval = setInterval(loadNotifications, NOTIFICATIONS_PULL_INTERVAL)
})
onUnmounted(() => clearInterval(interval))
const notificationService = new NotificationService()
loadNotifications()
async function loadNotifications() {
allNotifications.value = await notificationService.getAll()
}
async function markNotificationAsRead(notification: INotification) {
const index = findIndexById(allNotifications.value, notification.id)
if (index === -1) {
return
}
allNotifications.value[index] = {
...notification,
read: true,
}
await notificationService.update(allNotifications.value[index])
}
const stopNotificationPulling = startNotificationPulling()
tryOnUnmounted(stopNotificationPulling)
</script>
<style lang="scss" scoped>

View File

@ -943,6 +943,8 @@
"contact": "contact us"
},
"notification": {
"showNotifications": "show Notifications",
"hideNotifications": "hide Notifications",
"title": "Notifications",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen.",

View File

@ -1,9 +1,11 @@
import type {IUser} from '@/modelTypes/IUser'
import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification'
import AbstractService from '@/services/abstractService'
import NotificationModel from '@/models/notification'
import {NOTIFICATION_NAMES, type INotification} from '@/modelTypes/INotification'
import type {IUser} from '@/modelTypes/IUser'
import {getTextIdentifier} from '@/models/task'
import {getDisplayName} from '@/models/user'
import {i18n} from '@/i18n'
export function getNotificationTitle(notificationItem: INotification, user: IUser | null) {

View File

@ -0,0 +1,66 @@
import {computed, ref} from 'vue'
import type {INotification} from '@/modelTypes/INotification'
import NotificationService from '@/services/notification'
import {acceptHMRUpdate, defineStore} from 'pinia'
import {findIndexById} from '@/helpers/utils'
const NOTIFICATIONS_PULL_INTERVAL = 10000
export const useNotificationStore = defineStore('notification', () => {
const allNotifications = ref<INotification[]>([])
const notifications = computed(() => {
return allNotifications.value ? allNotifications.value.filter(n => n.name !== '') : []
})
const unreadNotifications = computed(() => {
return notifications.value.filter(n => n.readAt === null).length
})
const hasUnreadNotifications = computed(() => unreadNotifications.value > 0)
let interval: ReturnType<typeof setInterval>
const notificationService = new NotificationService()
async function loadNotifications() {
allNotifications.value = await notificationService.getAll()
}
function startNotificationPulling() {
loadNotifications()
interval = setInterval(loadNotifications, NOTIFICATIONS_PULL_INTERVAL)
return stopNotificationPulling
}
function stopNotificationPulling() {
clearInterval(interval)
}
async function markNotificationAsRead(notificationItem: INotification) {
const index = findIndexById(allNotifications.value, notificationItem.id)
if (index === -1) {
return
}
allNotifications.value[index] = {
...notificationItem,
read: true,
}
await notificationService.update(allNotifications.value[index])
}
return {
notifications,
unreadNotifications,
hasUnreadNotifications,
startNotificationPulling,
stopNotificationPulling,
markNotificationAsRead,
}
})
// support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useNotificationStore, import.meta.hot))
}