diff --git a/frontend/public/sounds/notification.mp3 b/frontend/public/sounds/notification.mp3
new file mode 100644
index 0000000..8bfa874
Binary files /dev/null and b/frontend/public/sounds/notification.mp3 differ
diff --git a/frontend/src/app/pages/altitude/chat/chat.component.ts b/frontend/src/app/pages/altitude/chat/chat.component.ts
index 5171aa9..15cf10b 100644
--- a/frontend/src/app/pages/altitude/chat/chat.component.ts
+++ b/frontend/src/app/pages/altitude/chat/chat.component.ts
@@ -42,7 +42,6 @@ export class ChatComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.chatService.connect();
-
}
ngOnDestroy(): void {
diff --git a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html
index 03ec28a..060c7ec 100644
--- a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html
+++ b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html
@@ -1,6 +1,13 @@
-
- {{ 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 {
+
+}