Add notification support with enable/disable functionality and sound effects for new messages.
This commit is contained in:
parent
a3cb1247af
commit
b415d2968d
BIN
frontend/public/sounds/notification.mp3
Normal file
BIN
frontend/public/sounds/notification.mp3
Normal file
Binary file not shown.
|
|
@ -42,7 +42,6 @@ export class ChatComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.chatService.connect();
|
this.chatService.connect();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
<div class="full-width"
|
<div class="full-width"
|
||||||
[style.background-color]="getServerColor(selectedServer())">
|
[style.background-color]="getServerColor(selectedServer())">
|
||||||
<span class="server" [style.color]="getServerTextColor(selectedServer())">
|
<span class="server" [style.color]="getServerTextColor(selectedServer())">
|
||||||
{{ selectedServer() }}
|
{{ selectedServer() }}
|
||||||
</span>
|
</span>
|
||||||
|
<div class="notification-icon">
|
||||||
|
@if (notificationEnabled()) {
|
||||||
|
<svg-notification-enabled (click)="disableNotifications()"/>
|
||||||
|
} @else {
|
||||||
|
<svg-notification-disabled (click)="enableNotifications()"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,3 +14,11 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {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({
|
@Component({
|
||||||
selector: 'app-chat-info',
|
selector: 'app-chat-info',
|
||||||
imports: [],
|
imports: [
|
||||||
|
NotificationEnabledSvgComponent,
|
||||||
|
NotificationDisabledSvgComponent
|
||||||
|
],
|
||||||
templateUrl: './chat-info.component.html',
|
templateUrl: './chat-info.component.html',
|
||||||
styleUrl: './chat-info.component.scss'
|
styleUrl: './chat-info.component.scss'
|
||||||
})
|
})
|
||||||
export class ChatInfoComponent {
|
export class ChatInfoComponent {
|
||||||
|
private readonly notificationService: NotificationService = inject(NotificationService);
|
||||||
|
|
||||||
public readonly selectedServer = input.required<string>();
|
public readonly selectedServer = input.required<string>();
|
||||||
|
protected readonly notificationEnabled = this.notificationService.notificationEnabled;
|
||||||
protected readonly getServerColor = getServerColor;
|
protected readonly getServerColor = getServerColor;
|
||||||
protected readonly getServerTextColor = getServerTextColor;
|
protected readonly getServerTextColor = getServerTextColor;
|
||||||
|
|
||||||
|
public enableNotifications() {
|
||||||
|
this.notificationService.requestPermission().then(
|
||||||
|
() => {
|
||||||
|
this.notificationService.enableNotifications();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public disableNotifications() {
|
||||||
|
this.notificationService.disableNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.object';
|
||||||
import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util';
|
import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util';
|
||||||
import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object';
|
import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object';
|
||||||
|
import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service';
|
||||||
|
|
||||||
export interface ChatEvent {
|
export interface ChatEvent {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -18,6 +19,7 @@ interface SsePayloadEvent {
|
||||||
@Injectable({providedIn: 'root'})
|
@Injectable({providedIn: 'root'})
|
||||||
export class ChatService implements OnDestroy {
|
export class ChatService implements OnDestroy {
|
||||||
private eventSource?: EventSourcePolyfill;
|
private eventSource?: EventSourcePolyfill;
|
||||||
|
private readonly notificationService: NotificationService = inject(NotificationService);
|
||||||
private readonly authService: AuthService = inject(AuthService)
|
private readonly authService: AuthService = inject(AuthService)
|
||||||
private readonly _messages = signal<ChatMessage[]>([])
|
private readonly _messages = signal<ChatMessage[]>([])
|
||||||
public readonly messages = computed(() => this._messages().filter((message) => message.server === this._selectedServer()).sort((a, b) => b.timestamp - a.timestamp));
|
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++;
|
find.unreadMessages++;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.notificationService.notify(`(${chatMessages.length}) new messages`, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
source.onerror = (err) => {
|
source.onerror = (err) => {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'svg-notification-disabled',
|
||||||
|
imports: [],
|
||||||
|
template: `
|
||||||
|
<svg width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M3 3L21 21M9.37747 3.56325C10.1871 3.19604 11.0827 3 12 3C13.5913 3 15.1174 3.59 16.2426 4.6402C17.3679 5.69041 18 7.11479 18 8.6C18 10.3566 18.2892 11.7759 18.712 12.9122M17 17H15M6.45339 6.46451C6.15686 7.13542 6 7.86016 6 8.6C6 11.2862 5.3238 13.1835 4.52745 14.4866C3.75616 15.7486 3.37051 16.3797 3.38485 16.5436C3.40095 16.7277 3.43729 16.7925 3.58603 16.9023C3.71841 17 4.34762 17 5.60605 17H9M9 17V18C9 19.6569 10.3431 21 12 21C13.6569 21 15 19.6569 15 18V17M9 17H15"
|
||||||
|
stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class NotificationDisabledSvgComponent {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import {Component} from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'svg-notification-enabled',
|
||||||
|
imports: [],
|
||||||
|
template: `
|
||||||
|
<svg width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
d="M9.00195 17H5.60636C4.34793 17 3.71872 17 3.58633 16.9023C3.4376 16.7925 3.40126 16.7277 3.38515 16.5436C3.37082 16.3797 3.75646 15.7486 4.52776 14.4866C5.32411 13.1835 6.00031 11.2862 6.00031 8.6C6.00031 7.11479 6.63245 5.69041 7.75766 4.6402C8.88288 3.59 10.409 3 12.0003 3C13.5916 3 15.1177 3.59 16.2429 4.6402C17.3682 5.69041 18.0003 7.11479 18.0003 8.6C18.0003 11.2862 18.6765 13.1835 19.4729 14.4866C20.2441 15.7486 20.6298 16.3797 20.6155 16.5436C20.5994 16.7277 20.563 16.7925 20.4143 16.9023C20.2819 17 19.6527 17 18.3943 17H15.0003M9.00195 17L9.00031 18C9.00031 19.6569 10.3435 21 12.0003 21C13.6572 21 15.0003 19.6569 15.0003 18V17M9.00195 17H15.0003"
|
||||||
|
stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
export class NotificationEnabledSvgComponent {
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user