Add notification panel

This commit is contained in:
Johannes Zellner
2025-03-18 16:53:15 +01:00
parent e665857aa8
commit 0fe5d9d628
4 changed files with 156 additions and 37 deletions

View File

@@ -1,11 +1,85 @@
<script setup>
import { onMounted, ref, useTemplateRef } from 'vue';
import { marked } from 'marked';
import { eachLimit } from 'async';
import { Button, Popover } from 'pankow';
import NotificationsModel from '../models/NotificationsModel.js';
const props = defineProps(['profile', 'subscription']);
const notificationModel = NotificationsModel.create();
const notificationButton = useTemplateRef('notificationButton');
const notificationPopover = useTemplateRef('notificationPopover');
const notifications = ref([]);
const notificationsAllBusy = ref(false);
function onOpenNotifications(popover, event, elem) {
popover.open(event, elem);
}
async function onMarkNotificationRead(notification) {
notification.busy = true;
const [error] = await notificationModel.update(notification.id, true);
if (error) return console.error(error);
await refresh();
}
async function onMarkAllNotificationRead() {
notificationsAllBusy.value = true;
await eachLimit(notifications.value.slice(4), 2, async (notification) => {
notification.busy = true;
const [error] = await notificationModel.update(notification.id, true);
if (error) return console.error(error);
});
await refresh();
notificationsAllBusy.value = false;
}
async function refresh() {
const [error, result] = await notificationModel.list();
if (error) return console.error(error);
result.forEach(n => {
n.isCollapsed = true;
n.busy = false;
});
notifications.value = result;
}
onMounted(async () => {
await refresh();
});
</script>
<template>
<div class="headerbar">
<Popover ref="notificationPopover" :width="'min(80%, 400px)'" :height="'min(80%, 600px)'">
<div style="padding: 10px; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
<div style="overflow: auto; margin-bottom: 10px">
<div class="notification-item" v-for="notification in notifications" :key="notification.id">
<div class="notification-item-title" @click="notification.isCollapsed = !notification.isCollapsed">
{{ notification.title }}
<!-- TODO translate -->
<Button plain small tool :loading="notification.busy" :disabled="notification.busy" class="notification-read-button" @click.stop="onMarkNotificationRead(notification)">Read</Button>
</div>
<div class="notification-item-body" v-if="!notification.isCollapsed">
<pre v-if="notification.messageJson" style="cursor: auto">{{ JSON.stringify(notification.messageJson, null, 4) }}</pre>
<div v-else style="cursor: auto; overflow: auto;" v-html="marked.parse(notification.message)"></div>
</div>
</div>
</div>
<!-- TODO translate -->
<Button @click="onMarkAllNotificationRead()" :loading="notificationsAllBusy" :disabled="notificationsAllBusy">Mark all as read</Button>
</div>
</Popover>
<div style="flex-grow: 1;"></div>
<div v-show="profile.isAtLeastOwner && (subscription.plan.id === 'free' || subscription.plan.id === 'expired')" ng-click="openSubscriptionSetup()" style="cursor: pointer">
<span class="badge" ng-class="{'badge-danger': subscription.plan.id !== 'free', 'badge-success': subscription.plan.id === 'free' }">
@@ -15,17 +89,9 @@ const props = defineProps(['profile', 'subscription']);
<div v-show="!profile.isAtLeastOwner && subscription.plan.id === 'expired'">
<span class="badge badge-danger">Subscription Expired</span>
</div>
<a v-show="profile.isAtLeastAdmin" href="#/notifications">
<i class="fas fa-bell" v-if="notificationCount"></i>
<i class="far fa-bell" v-else></i>
<span v-show="notificationCount">{{ notificationCount === 100 ? '100+' : notificationCount }}</span>
</a>
<a v-show="profile.isAtLeastAdmin" href="#/support">
<i class="fa fa-question fa-fw"></i>
</a>
<a href="#/profile">
<img :src="profile.avatarUrl" class="headerbar-avatar"/> {{ profile.username }}
</a>
<Button plain secondary tool v-if="profile.isAtLeastAdmin" ref="notificationButton" @click="onOpenNotifications(notificationPopover, $event, notificationButton.$el)" :icon="notifications.length ? 'fas fa-bell' : 'far fa-bell'">{{ notifications.length > 99 ? '99+' : notifications.length }}</Button>
<Button plain secondary tool v-if="profile.isAtLeastAdmin" href="#/support" icon="fa fa-question fa-fw"/>
<Button plain secondary v-if="profile.isAtLeastAdmin" href="#/profile"><img :src="profile.avatarUrl" class="headerbar-avatar"/> {{ profile.username }}</Button>
</div>
</template>
@@ -41,16 +107,6 @@ const props = defineProps(['profile', 'subscription']);
.headerbar a {
display: flex;
align-items: center;
padding: 6px 15px;
border-radius: var(--pankow-border-radius);
color: var(--pankow-text-color);
}
.headerbar a:focus,
.headerbar a:active,
.headerbar a:hover {
text-decoration: none;
background-color: var(--pankow-color-background-hover);
}
.headerbar-avatar {
@@ -60,4 +116,31 @@ const props = defineProps(['profile', 'subscription']);
border-radius: var(--pankow-border-radius);
}
.notification-item {
margin-bottom: 10px;
padding-bottom: 10px;
cursor: pointer;
border-bottom: 1px solid var(--pankow-input-border-color);
position: relative;
}
.notification-item-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.notification-item:hover .notification-item-title {
font-weight: bold;
}
.notification-read-button {
visibility: hidden;
margin-right: 10px;
}
.notification-item:hover .notification-read-button {
visibility: visible;
}
</style>