From b415d2968d835b5c04e3ef1e6e5baa7684013fc1 Mon Sep 17 00:00:00 2001 From: akastijn Date: Sun, 19 Jul 2026 18:39:24 +0200 Subject: [PATCH] Add notification support with enable/disable functionality and sound effects for new messages. --- frontend/public/sounds/notification.mp3 | Bin 0 -> 34272 bytes .../app/pages/altitude/chat/chat.component.ts | 1 - .../chat-info/chat-info.component.html | 13 ++- .../chat-info/chat-info.component.scss | 8 ++ .../chat-info/chat-info.component.ts | 26 +++++- .../chat/service/chat-notification.service.ts | 80 ++++++++++++++++++ .../altitude/chat/service/chat.service.ts | 3 + .../chat/svg/notification-disabled.svg.ts | 16 ++++ .../chat/svg/notification-enabled.svg.ts | 16 ++++ 9 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 frontend/public/sounds/notification.mp3 create mode 100644 frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts create mode 100644 frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts create mode 100644 frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts diff --git a/frontend/public/sounds/notification.mp3 b/frontend/public/sounds/notification.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8bfa874763a3e68af6425390d15e565a61969fed GIT binary patch literal 34272 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 z - - {{ selectedServer() }} - + + {{ selectedServer() }} + +
+ @if (notificationEnabled()) { + + } @else { + + } +
diff --git a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss index c98a9b9..ecaf1f3 100644 --- a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss +++ b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss @@ -14,3 +14,11 @@ font-weight: bold; padding-left: 10px; } + +.notification-icon { + cursor: pointer; + width: 20px; + height: 20px; + margin-left: auto; + margin-right: 10px; +} diff --git a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts index c1f08db..86eca5e 100644 --- a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts +++ b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts @@ -1,14 +1,36 @@ -import {Component, input} from '@angular/core'; +import {Component, inject, input} from '@angular/core'; import {getServerColor, getServerTextColor} from '@pages/altitude/chat/util/server-color'; +import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service'; +import {NotificationEnabledSvgComponent} from '@pages/altitude/chat/svg/notification-enabled.svg'; +import {NotificationDisabledSvgComponent} from '@pages/altitude/chat/svg/notification-disabled.svg'; @Component({ selector: 'app-chat-info', - imports: [], + imports: [ + NotificationEnabledSvgComponent, + NotificationDisabledSvgComponent + ], templateUrl: './chat-info.component.html', styleUrl: './chat-info.component.scss' }) export class ChatInfoComponent { + private readonly notificationService: NotificationService = inject(NotificationService); + public readonly selectedServer = input.required(); + protected readonly notificationEnabled = this.notificationService.notificationEnabled; protected readonly getServerColor = getServerColor; protected readonly getServerTextColor = getServerTextColor; + + public enableNotifications() { + this.notificationService.requestPermission().then( + () => { + this.notificationService.enableNotifications(); + } + ); + } + + public disableNotifications() { + this.notificationService.disableNotifications(); + } + } diff --git a/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts b/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts new file mode 100644 index 0000000..0039ba3 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts @@ -0,0 +1,80 @@ +import {Injectable, signal} from '@angular/core'; + +@Injectable({providedIn: 'root'}) +export class NotificationService { + + private readonly notificationSound = new Audio('/public/sounds/notification.mp3'); + private readonly notificationStorageKey = 'chat-notifications-enabled'; + private readonly _notificationEnabled = signal( + localStorage.getItem(this.notificationStorageKey) === 'true' + ); + public readonly notificationEnabled = this._notificationEnabled.asReadonly(); + + async requestPermission(): Promise { + if (!('Notification' in window)) { + console.log('This browser does not support notifications'); + return false; + } + + if (Notification.permission === 'granted') { + return true; + } + + if (Notification.permission === 'denied') { + console.log('Notification permission was denied'); + return false; + } + + const permission = await Notification.requestPermission(); + if (permission === 'granted') { + this.enableSound() + return true + } + return false; + } + + notify(title: string, body: string | null) { + if (!this.notificationEnabled()) { + return; + } + if (Notification.permission === 'granted') { + if (body === null) { + new Notification(title); + } else { + new Notification(title, {body}); + } + this.playSound(); + } + } + + private enableSound() { + this.notificationSound.muted = true; + this.notificationSound.play() + .then(() => { + this.notificationSound.pause(); + this.notificationSound.currentTime = 0; + this.notificationSound.muted = false; + }) + .catch((error) => { + console.log('Could not unlock notification sound:', error); + this.notificationSound.muted = false; + }); + } + + private playSound() { + this.notificationSound.currentTime = 0; + this.notificationSound.play().catch((error) => { + console.log('Could not play notification sound:', error); + }); + } + + enableNotifications() { + this._notificationEnabled.set(true); + localStorage.setItem(this.notificationStorageKey, 'true'); + } + + disableNotifications() { + this._notificationEnabled.set(false); + localStorage.setItem(this.notificationStorageKey, 'false'); + } +} diff --git a/frontend/src/app/pages/altitude/chat/service/chat.service.ts b/frontend/src/app/pages/altitude/chat/service/chat.service.ts index c14ae6f..3148d6b 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -5,6 +5,7 @@ import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object'; import {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.object'; import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util'; import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object'; +import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service'; export interface ChatEvent { type: string; @@ -18,6 +19,7 @@ interface SsePayloadEvent { @Injectable({providedIn: 'root'}) export class ChatService implements OnDestroy { private eventSource?: EventSourcePolyfill; + private readonly notificationService: NotificationService = inject(NotificationService); private readonly authService: AuthService = inject(AuthService) private readonly _messages = signal([]) public readonly messages = computed(() => this._messages().filter((message) => message.server === this._selectedServer()).sort((a, b) => b.timestamp - a.timestamp)); @@ -58,6 +60,7 @@ export class ChatService implements OnDestroy { find.unreadMessages++; } }) + this.notificationService.notify(`(${chatMessages.length}) new messages`, null); }); source.onerror = (err) => { diff --git a/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts b/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts new file mode 100644 index 0000000..984aaa0 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts @@ -0,0 +1,16 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'svg-notification-disabled', + imports: [], + template: ` + + + + ` +}) +export class NotificationDisabledSvgComponent { + +} diff --git a/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts b/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts new file mode 100644 index 0000000..e6a45f0 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts @@ -0,0 +1,16 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'svg-notification-enabled', + imports: [], + template: ` + + + + ` +}) +export class NotificationEnabledSvgComponent { + +}