Add notification panel
This commit is contained in:
Generated
+14
-14
@@ -25,9 +25,9 @@
|
||||
"jquery": "^3.7.1",
|
||||
"marked": "^15.0.7",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"pankow": "^2.10.2",
|
||||
"pankow": "^2.10.3",
|
||||
"pankow-viewers": "^1.0.11",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.0",
|
||||
"vite": "^6.2.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.2",
|
||||
@@ -2351,9 +2351,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pankow": {
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/pankow/-/pankow-2.10.2.tgz",
|
||||
"integrity": "sha512-J0BkBajRhDjElVCQ+1AYxX9n07+ofwmmlPcCmkRkqd0N5xUmNFUnD74WM/6qXN625XU9rORCbCeuPX/IsB2uCA==",
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pankow/-/pankow-2.10.3.tgz",
|
||||
"integrity": "sha512-z7ptMlRGACpL3T6bNml5HlI+qgrLeb7+ImKfTE+UDqu/IBiyOrU9eabzhFlg0GUBbISZd0DPHhInWUdIywDLPQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
@@ -2528,9 +2528,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.85.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
|
||||
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
|
||||
"version": "1.86.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.0.tgz",
|
||||
"integrity": "sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
@@ -4155,9 +4155,9 @@
|
||||
}
|
||||
},
|
||||
"pankow": {
|
||||
"version": "2.10.2",
|
||||
"resolved": "https://registry.npmjs.org/pankow/-/pankow-2.10.2.tgz",
|
||||
"integrity": "sha512-J0BkBajRhDjElVCQ+1AYxX9n07+ofwmmlPcCmkRkqd0N5xUmNFUnD74WM/6qXN625XU9rORCbCeuPX/IsB2uCA==",
|
||||
"version": "2.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pankow/-/pankow-2.10.3.tgz",
|
||||
"integrity": "sha512-z7ptMlRGACpL3T6bNml5HlI+qgrLeb7+ImKfTE+UDqu/IBiyOrU9eabzhFlg0GUBbISZd0DPHhInWUdIywDLPQ==",
|
||||
"requires": {
|
||||
"@fontsource/inter": "^5.2.5",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
@@ -4266,9 +4266,9 @@
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.85.1",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
|
||||
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
|
||||
"version": "1.86.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.0.tgz",
|
||||
"integrity": "sha512-zV8vGUld/+mP4KbMLJMX7TyGCuUp7hnkOScgCMsWuHtns8CWBoz+vmEhoGMXsaJrbUP8gj+F1dLvVe79sK8UdA==",
|
||||
"requires": {
|
||||
"@parcel/watcher": "^2.4.1",
|
||||
"chokidar": "^4.0.0",
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
"jquery": "^3.7.1",
|
||||
"marked": "^15.0.7",
|
||||
"moment-timezone": "^0.5.47",
|
||||
"pankow": "^2.10.2",
|
||||
"pankow": "^2.10.3",
|
||||
"pankow-viewers": "^1.0.11",
|
||||
"sass": "^1.85.1",
|
||||
"sass": "^1.86.0",
|
||||
"vite": "^6.2.2",
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^11.1.2",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
import { fetcher } from 'pankow';
|
||||
import { API_ORIGIN } from '../constants.js';
|
||||
|
||||
function create() {
|
||||
const accessToken = localStorage.token;
|
||||
|
||||
return {
|
||||
async list(acknowledged = false) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/notifications`, { acknowledged, access_token: accessToken });
|
||||
} catch (e) {
|
||||
return [e];
|
||||
}
|
||||
|
||||
if (result.status !== 200) return [result];
|
||||
return [null, result.body.notifications];
|
||||
},
|
||||
async update(id, acknowledged) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/notifications/${id}`, { acknowledged }, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
return [e];
|
||||
}
|
||||
|
||||
if (result.status !== 204) return [result];
|
||||
return [null];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
create,
|
||||
};
|
||||
Reference in New Issue
Block a user