Skip to content

Commit

Permalink
Merge pull request mrlvsb#556 from patrick11514/master
Browse files Browse the repository at this point in the history
Notifications rewrite + small fixes
  • Loading branch information
Kobzol authored Nov 19, 2024
2 parents 7c2474d + 54e09d9 commit 3e990d2
Show file tree
Hide file tree
Showing 14 changed files with 482 additions and 318 deletions.
137 changes: 0 additions & 137 deletions frontend/src/Notifications.svelte

This file was deleted.

2 changes: 1 addition & 1 deletion frontend/src/TaskDetail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import SummaryComments from './SummaryComments.svelte';
import SubmitsDiff from './SubmitsDiff.svelte';
import { fetch } from './api.js';
import { user } from './global';
import { notifications } from './notifications.js';
import { notifications } from './utilities/notifications';
import { hideComments, HideCommentsState } from './stores.js';
export let url;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Teacher/AllTasks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DataTablesCore from 'datatables.net-bs5';
import DataTable from 'datatables.net-vue3';
import { format } from 'date-fns';
import { onMounted, ref } from 'vue';
import { getFromAPI } from '../utilities';
import { getFromAPI } from '../utilities/api';
DataTable.use(DataTablesCore);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/Teacher/InbusImport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/
import { computed, ref } from 'vue';
import { csrfToken } from '../api.js';
import { csrfToken } from '../utilities/api';
import { ConcreteActivity, InbusSubjectVersion } from './inbusdto';
interface KelvinSubject {
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/components/Loader.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
<script setup lang="ts">
import { generateRange } from '../utilities';
/**
* Generate array with range of numbers from start
*
* @param size size of the array
* @param start starting number
*
* @returns array of numbers from start to start + size
*/
const generateRange = (size: number, start = 0) => {
return Array.from({ length: size }).map((_, i) => i + start);
};
const size = 60;
const color = '#FF3E00';
Expand All @@ -23,6 +33,7 @@ const unit = 'px';
align-items: center;
justify-content: center;
}
.dot {
height: var(--dotSize);
width: var(--dotSize);
Expand All @@ -32,29 +43,35 @@ const unit = 'px';
border-radius: 100%;
animation: sync 0.6s ease-in-out infinite alternate both running;
}
@-webkit-keyframes sync {
33% {
-webkit-transform: translateY(10px);
transform: translateY(10px);
}
66% {
-webkit-transform: translateY(-10px);
transform: translateY(-10px);
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
}
}
@keyframes sync {
33% {
-webkit-transform: translateY(10px);
transform: translateY(10px);
}
66% {
-webkit-transform: translateY(-10px);
transform: translateY(-10px);
}
100% {
-webkit-transform: translateY(0);
transform: translateY(0);
Expand Down
155 changes: 155 additions & 0 deletions frontend/src/components/Notifications.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<script setup lang="ts">
/**
* This component displays bell with number of notifications next to it.
* After opening, it shows list of all notifications, which user got.
* It is available to both teachers and students, and it is accesible from
* main layout on each page, next to name.
*/
import {
notifications,
pushNotifications,
importantNotificationsCount,
notificationsCount,
type Notification
} from '../utilities/notifications';
import TimeAgo from './TimeAgo.vue';
async function enablePushNotifications() {
if (!(await pushNotifications.subscribePushNotifications())) {
alert(
'Notifications are denied, click on the icon before the URL address and enable them manually.\n\nAlso check if option "Use Google services for push messaging" is enabled in your browser privacy settings.'
);
}
}
async function openNotification(notification: Notification) {
if (notification.public) {
await notifications.markRead(notification.id);
}
document.location.href = notification.action_object_url;
}
const getFilteredNotifications = (notifications: Readonly<Notification[]>) => {
const ret = notifications.slice();
ret.sort((a, b) => {
const sortByImportantOrUnread =
Number((b.important || 0) && b.unread) - Number((a.important || 0) && a.unread);
const sortByDate = new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime();
return sortByImportantOrUnread || sortByDate;
});
return ret;
};
</script>

<template>
<li v-if="notifications" class="nav-item dropdown">
<button
class="btn nav-link dropdown-toggle"
href="#"
type="button"
data-bs-toggle="dropdown"
data-bs-auto-close="outside"
aria-expanded="false"
title="Notifications"
>
<span class="iconify" data-icon="bi:bell"></span>
<span class="d-md-none ms-1">Notifications</span>
<span
v-if="notificationsCount > 0"
:class="`badge ${
importantNotificationsCount >= 1 ? 'text-bg-danger' : 'text-bg-warning'
} border border-light rounded-pill`"
>
{{ notificationsCount }}
<span class="visually-hidden">New alerts</span>
</span>
</button>

<div class="dropdown-menu dropdown-menu-end shadow p-0 rounded dropdown-menu-custom">
<ul class="list-group list-group-flush" style="max-height: 50vh; overflow-y: auto">
<li class="list-group-item">
<div class="d-flex align-items-center">
<div>
Notifications<template v-if="notificationsCount > 0">
&nbsp;({{ notificationsCount }})
</template>
</div>

<div class="ms-auto">
<button
v-if="pushNotifications.ref.value.supported && !pushNotifications.ref.value.enabled"
class="btn text-body"
title="Enable desktop notifications"
@click="enablePushNotifications"
>
<span class="iconify" data-icon="ic:outline-notifications-active"></span>
</button>
<button
class="btn text-body"
:class="{ 'text-muted': notificationsCount <= 0 }"
title="Clear all notifications"
@click="notifications.markAllRead"
>
<span class="iconify" data-icon="mdi:notification-clear-all"></span>
</button>
</div>
</div>
</li>
<template v-if="notifications.notificationsRef.value.length > 0">
<li
v-for="item in getFilteredNotifications(notifications.notificationsRef.value)"
:key="item.id"
class="list-group-item p-1 d-flex align-items-center justify-content-between"
:class="{ 'text-body-secondary': !item.unread || !item.important }"
>
<div>
<strong>{{ item.actor }}&nbsp;</strong>
<div v-if="item.custom_text" v-html="item.custom_text" />

Check warning on line 111 in frontend/src/components/Notifications.vue

View workflow job for this annotation

GitHub Actions / test_frontend

'v-html' directive can lead to XSS attack

<template v-else>
{{ item.verb }}

<a
v-if="item.action_object_url"
:href="item.action_object_url"
@click.prevent="openNotification(item)"
@auxclick="notifications.markRead(item.id)"
>
{{ item.action_object }}
</a>
<template v-else>{item.action_object}</template>

<template v-if="item.target"> on {{ item.target }}</template>
</template>
<span>&nbsp;(<TimeAgo :datetime="item.timestamp" />) </span>
</div>

<div>
<button
type="button"
:hidden="!item.unread"
class="btn-close"
aria-label="Close"
@click="notifications.markRead(item.id)"
></button>
</div>
</li>
</template>
<span v-else class="list-group-item p-1 text-center">There are no notifications!</span>
</ul>
</div>
</li>
</template>

<style>
/* 768px - md (medium) bootstrap breakpoint; when the navbar collapses, this gets disabled */
@media (min-width: 768px) {
.dropdown-menu-custom {
min-width: 26rem;
}
}
</style>
2 changes: 1 addition & 1 deletion frontend/src/components/SuspensionWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defineProps<{

<template>
<Suspense>
<component :is="childComponent" />
<component :is="childComponent" v-bind="$attrs" />
<template #fallback>
<div class="d-flex justify-content-center loading-animation">
<Loader />
Expand Down
Loading

0 comments on commit 3e990d2

Please sign in to comment.