mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 02:39:40 +02:00
✨ feat(matrix-web): add browser notifications for new messages
- Create notification service with permission handling and settings - Integrate notifications into Matrix store timeline event handler - Add notification settings panel with toggles for: - Enable/disable notifications - Sound on/off - Message preview visibility - Show notifications only when page is not focused - Handle permission states: granted, denied, default Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
60b7cad508
commit
4492273942
3 changed files with 317 additions and 3 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { browser } from '$app/environment';
|
||||
import type { MatrixClient, Room, MatrixEvent, RoomMember as SDKRoomMember } from 'matrix-js-sdk';
|
||||
import { showMessageNotification, canShowNotifications, isDocumentFocused } from '$lib/notifications';
|
||||
import type {
|
||||
SyncState,
|
||||
MatrixCredentials,
|
||||
|
|
@ -248,6 +249,27 @@ class MatrixStore {
|
|||
if (room?.roomId === this._currentRoomId) {
|
||||
this._timeline = [...(room.getLiveTimeline().getEvents() || [])];
|
||||
}
|
||||
|
||||
// Show browser notification for new messages from others
|
||||
if (
|
||||
browser &&
|
||||
event.getType() === 'm.room.message' &&
|
||||
event.getSender() !== this._client!.getUserId() &&
|
||||
!isDocumentFocused()
|
||||
) {
|
||||
const content = event.getContent();
|
||||
const body = content?.body || '';
|
||||
const senderName = this.getSenderName(event);
|
||||
const roomName = room?.name || 'Unbekannt';
|
||||
|
||||
showMessageNotification(senderName, body, roomName, {
|
||||
onClick: () => {
|
||||
if (room) {
|
||||
this.selectRoom(room.roomId);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Typing indicators
|
||||
|
|
|
|||
171
apps/matrix/apps/web/src/lib/notifications/index.ts
Normal file
171
apps/matrix/apps/web/src/lib/notifications/index.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { browser } from '$app/environment';
|
||||
|
||||
/**
|
||||
* Browser Notification Service for Matrix Chat
|
||||
*/
|
||||
|
||||
// Notification settings stored in localStorage
|
||||
const SETTINGS_KEY = 'matrix_notification_settings';
|
||||
|
||||
interface NotificationSettings {
|
||||
enabled: boolean;
|
||||
sound: boolean;
|
||||
showPreview: boolean;
|
||||
}
|
||||
|
||||
const defaultSettings: NotificationSettings = {
|
||||
enabled: true,
|
||||
sound: true,
|
||||
showPreview: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get notification settings from localStorage
|
||||
*/
|
||||
export function getNotificationSettings(): NotificationSettings {
|
||||
if (!browser) return defaultSettings;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(SETTINGS_KEY);
|
||||
if (stored) {
|
||||
return { ...defaultSettings, ...JSON.parse(stored) };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse notification settings:', e);
|
||||
}
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save notification settings to localStorage
|
||||
*/
|
||||
export function saveNotificationSettings(settings: Partial<NotificationSettings>): void {
|
||||
if (!browser) return;
|
||||
|
||||
const current = getNotificationSettings();
|
||||
const updated = { ...current, ...settings };
|
||||
localStorage.setItem(SETTINGS_KEY, JSON.stringify(updated));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser notifications are supported
|
||||
*/
|
||||
export function isNotificationSupported(): boolean {
|
||||
return browser && 'Notification' in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current notification permission status
|
||||
*/
|
||||
export function getNotificationPermission(): NotificationPermission | 'unsupported' {
|
||||
if (!isNotificationSupported()) return 'unsupported';
|
||||
return Notification.permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request notification permission
|
||||
*/
|
||||
export async function requestNotificationPermission(): Promise<NotificationPermission | 'unsupported'> {
|
||||
if (!isNotificationSupported()) return 'unsupported';
|
||||
|
||||
try {
|
||||
const permission = await Notification.requestPermission();
|
||||
return permission;
|
||||
} catch (e) {
|
||||
console.error('Failed to request notification permission:', e);
|
||||
return 'denied';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if notifications are enabled and permitted
|
||||
*/
|
||||
export function canShowNotifications(): boolean {
|
||||
if (!isNotificationSupported()) return false;
|
||||
if (Notification.permission !== 'granted') return false;
|
||||
|
||||
const settings = getNotificationSettings();
|
||||
return settings.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the document is currently focused
|
||||
*/
|
||||
export function isDocumentFocused(): boolean {
|
||||
if (!browser) return true;
|
||||
return document.hasFocus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a browser notification for a new message
|
||||
*/
|
||||
export function showMessageNotification(
|
||||
senderName: string,
|
||||
messageBody: string,
|
||||
roomName: string,
|
||||
options?: {
|
||||
onClick?: () => void;
|
||||
icon?: string;
|
||||
}
|
||||
): void {
|
||||
if (!canShowNotifications()) return;
|
||||
if (isDocumentFocused()) return;
|
||||
|
||||
const settings = getNotificationSettings();
|
||||
|
||||
const title = roomName ? `${senderName} in ${roomName}` : senderName;
|
||||
const body = settings.showPreview ? messageBody : 'Neue Nachricht';
|
||||
|
||||
try {
|
||||
const notification = new Notification(title, {
|
||||
body: body.slice(0, 200), // Limit body length
|
||||
icon: options?.icon || '/favicon.png',
|
||||
tag: 'matrix-message', // Group notifications
|
||||
silent: !settings.sound,
|
||||
} as NotificationOptions);
|
||||
|
||||
notification.onclick = () => {
|
||||
window.focus();
|
||||
notification.close();
|
||||
options?.onClick?.();
|
||||
};
|
||||
|
||||
// Auto-close after 5 seconds
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
}, 5000);
|
||||
} catch (e) {
|
||||
console.error('Failed to show notification:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play notification sound
|
||||
*/
|
||||
export function playNotificationSound(): void {
|
||||
if (!browser) return;
|
||||
|
||||
const settings = getNotificationSettings();
|
||||
if (!settings.sound) return;
|
||||
|
||||
try {
|
||||
// Create a simple beep using Web Audio API
|
||||
const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.3);
|
||||
} catch (e) {
|
||||
// Ignore audio errors (common when user hasn't interacted yet)
|
||||
}
|
||||
}
|
||||
|
|
@ -14,13 +14,41 @@
|
|||
Key,
|
||||
DeviceMobile,
|
||||
CircleNotch,
|
||||
BellRinging,
|
||||
SpeakerHigh,
|
||||
Eye,
|
||||
} from '@manacore/shared-icons';
|
||||
import { VerificationDialog, RecoveryKeyDialog } from '$lib/components/crypto';
|
||||
import {
|
||||
getNotificationSettings,
|
||||
saveNotificationSettings,
|
||||
getNotificationPermission,
|
||||
requestNotificationPermission,
|
||||
isNotificationSupported,
|
||||
} from '$lib/notifications';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
let verificationDialogOpen = $state(false);
|
||||
let recoveryDialogOpen = $state(false);
|
||||
let recoveryDialogMode = $state<'setup' | 'restore'>('setup');
|
||||
|
||||
// Notification settings
|
||||
let notificationSettings = $state(getNotificationSettings());
|
||||
let notificationPermission = $state<NotificationPermission | 'unsupported'>(
|
||||
browser ? getNotificationPermission() : 'default'
|
||||
);
|
||||
let notificationsSupported = browser && isNotificationSupported();
|
||||
|
||||
async function handleRequestPermission() {
|
||||
const permission = await requestNotificationPermission();
|
||||
notificationPermission = permission;
|
||||
}
|
||||
|
||||
function updateNotificationSetting(key: keyof typeof notificationSettings, value: boolean) {
|
||||
notificationSettings = { ...notificationSettings, [key]: value };
|
||||
saveNotificationSettings({ [key]: value });
|
||||
}
|
||||
|
||||
// Crypto status derived
|
||||
let cryptoReady = $derived(matrixStore.cryptoReady);
|
||||
let verificationStatus = $derived(matrixStore.verificationStatus);
|
||||
|
|
@ -191,14 +219,107 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Notifications Section (Placeholder) -->
|
||||
<!-- Notifications Section -->
|
||||
<section class="card">
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-4">
|
||||
<h2 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<Bell class="h-5 w-5" />
|
||||
Benachrichtigungen
|
||||
</h2>
|
||||
<p class="text-sm text-muted-foreground">Benachrichtigungseinstellungen folgen bald...</p>
|
||||
|
||||
{#if !notificationsSupported}
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Dein Browser unterstützt keine Benachrichtigungen.
|
||||
</p>
|
||||
{:else if notificationPermission === 'denied'}
|
||||
<div class="rounded-lg bg-error/10 p-4 text-sm">
|
||||
<p class="font-medium text-error">Benachrichtigungen blockiert</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
Du hast Benachrichtigungen für diese Seite blockiert. Bitte ändere die Einstellung
|
||||
in deinem Browser.
|
||||
</p>
|
||||
</div>
|
||||
{:else if notificationPermission === 'default'}
|
||||
<div class="flex items-center justify-between rounded-lg bg-muted p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<BellRinging class="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
<p class="font-medium">Benachrichtigungen aktivieren</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Erhalte Benachrichtigungen für neue Nachrichten
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-primary text-sm" onclick={handleRequestPermission}>
|
||||
Erlauben
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-3">
|
||||
<!-- Enable/Disable Toggle -->
|
||||
<label class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer">
|
||||
<div class="flex items-center gap-3">
|
||||
<BellRinging class="h-6 w-6" />
|
||||
<div>
|
||||
<p class="font-medium">Benachrichtigungen</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Desktop-Benachrichtigungen für neue Nachrichten
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
checked={notificationSettings.enabled}
|
||||
onchange={() =>
|
||||
updateNotificationSetting('enabled', !notificationSettings.enabled)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Sound Toggle -->
|
||||
<label
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer {!notificationSettings.enabled ? 'opacity-50' : ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<SpeakerHigh class="h-6 w-6" />
|
||||
<div>
|
||||
<p class="font-medium">Ton</p>
|
||||
<p class="text-sm text-muted-foreground">Ton bei neuen Nachrichten abspielen</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
checked={notificationSettings.sound}
|
||||
disabled={!notificationSettings.enabled}
|
||||
onchange={() => updateNotificationSetting('sound', !notificationSettings.sound)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<!-- Preview Toggle -->
|
||||
<label
|
||||
class="flex items-center justify-between rounded-lg bg-muted p-4 cursor-pointer {!notificationSettings.enabled ? 'opacity-50' : ''}"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<Eye class="h-6 w-6" />
|
||||
<div>
|
||||
<p class="font-medium">Vorschau anzeigen</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Nachrichteninhalt in Benachrichtigung anzeigen
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle toggle-primary"
|
||||
checked={notificationSettings.showPreview}
|
||||
disabled={!notificationSettings.enabled}
|
||||
onchange={() =>
|
||||
updateNotificationSetting('showPreview', !notificationSettings.showPreview)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue