Add notification support with enable/disable functionality and sound effects for new messages.

This commit is contained in:
2026-07-19 18:39:24 +02:00
parent a3cb1247af
commit b415d2968d
9 changed files with 157 additions and 6 deletions
@@ -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<boolean>(
localStorage.getItem(this.notificationStorageKey) === 'true'
);
public readonly notificationEnabled = this._notificationEnabled.asReadonly();
async requestPermission(): Promise<boolean> {
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');
}
}
@@ -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<ChatMessage[]>([])
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) => {