mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Improve coverage around desktop notifications. Closes #115.
This commit is contained in:
@@ -9,6 +9,17 @@ const NOTIFICATION_ICON_PATH = '/favicon-256x256.png';
|
||||
type NotificationPermissionState = NotificationPermission | 'unsupported';
|
||||
type ConversationNotificationMap = Record<string, boolean>;
|
||||
|
||||
interface NotificationEnableToastInfo {
|
||||
level: 'success' | 'warning';
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface NotificationEnvironment {
|
||||
protocol: string;
|
||||
isSecureContext: boolean;
|
||||
}
|
||||
|
||||
function getConversationNotificationKey(type: 'channel' | 'contact', id: string): string {
|
||||
return getStateKey(type, id);
|
||||
}
|
||||
@@ -92,6 +103,40 @@ function buildMessageNotificationHash(message: Message): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getNotificationEnableToastInfo(
|
||||
environment?: Partial<NotificationEnvironment>
|
||||
): NotificationEnableToastInfo {
|
||||
if (typeof window === 'undefined') {
|
||||
return { level: 'success', title: 'Notifications enabled' };
|
||||
}
|
||||
|
||||
const protocol = environment?.protocol ?? window.location.protocol;
|
||||
const isSecureContext = environment?.isSecureContext ?? window.isSecureContext;
|
||||
|
||||
if (protocol === 'http:') {
|
||||
return {
|
||||
level: 'warning',
|
||||
title: 'Notifications enabled with warning',
|
||||
description:
|
||||
'Desktop notifications are on for this conversation, but you are using HTTP instead of HTTPS. Notifications will likely not work reliably.',
|
||||
};
|
||||
}
|
||||
|
||||
// Best-effort heuristic only. Browsers do not expose certificate trust details
|
||||
// directly to page JS, so an HTTPS page that is not a secure context is the
|
||||
// closest signal we have for an untrusted/self-signed setup.
|
||||
if (protocol === 'https:' && !isSecureContext) {
|
||||
return {
|
||||
level: 'warning',
|
||||
title: 'Notifications enabled with warning',
|
||||
description:
|
||||
'Desktop notifications are on for this conversation, but your HTTPS connection is untrusted, such as a self-signed certificate. Notification delivery may be inconsistent depending on your browser.',
|
||||
};
|
||||
}
|
||||
|
||||
return { level: 'success', title: 'Notifications enabled' };
|
||||
}
|
||||
|
||||
export function useBrowserNotifications() {
|
||||
const [permission, setPermission] = useState<NotificationPermissionState>(getInitialPermission);
|
||||
const [enabledByConversation, setEnabledByConversation] =
|
||||
@@ -110,8 +155,6 @@ export function useBrowserNotifications() {
|
||||
|
||||
const toggleConversationNotifications = useCallback(
|
||||
async (type: 'channel' | 'contact', id: string, label: string) => {
|
||||
const blockedDescription =
|
||||
'Allow notifications in your browser settings, then try again. Some browsers may refuse notifications on non-HTTPS or self-signed HTTPS origins. Check your browser documentation for how to trust an insecure origin and the associated risks before doing so.';
|
||||
const conversationKey = getConversationNotificationKey(type, id);
|
||||
if (enabledByConversation[conversationKey]) {
|
||||
setEnabledByConversation((prev) => {
|
||||
@@ -120,20 +163,23 @@ export function useBrowserNotifications() {
|
||||
writeStoredEnabledMap(next);
|
||||
return next;
|
||||
});
|
||||
toast.success(`${label} notifications disabled`);
|
||||
toast.success('Notifications disabled', {
|
||||
description: `Desktop notifications are off for ${label}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (permission === 'unsupported') {
|
||||
toast.error('Browser notifications unavailable', {
|
||||
toast.error('Notifications unavailable', {
|
||||
description: 'This browser does not support desktop notifications.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (permission === 'denied') {
|
||||
toast.error('Browser notifications blocked', {
|
||||
description: blockedDescription,
|
||||
toast.error('Notifications blocked', {
|
||||
description:
|
||||
'Desktop notifications are blocked by your browser. Allow notifications in browser settings, then try again. Non-HTTPS or untrusted HTTPS origins may also prevent notifications from working reliably.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -155,13 +201,24 @@ export function useBrowserNotifications() {
|
||||
icon: NOTIFICATION_ICON_PATH,
|
||||
tag: `meshcore-notification-preview-${conversationKey}`,
|
||||
});
|
||||
toast.success(`${label} notifications enabled`);
|
||||
const toastInfo = getNotificationEnableToastInfo();
|
||||
if (toastInfo.level === 'warning') {
|
||||
toast.warning(toastInfo.title, {
|
||||
description: toastInfo.description,
|
||||
});
|
||||
} else {
|
||||
toast.success(toastInfo.title, {
|
||||
description: `Desktop notifications are on for ${label}.`,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
toast.error('Browser notifications not enabled', {
|
||||
toast.error('Notifications not enabled', {
|
||||
description:
|
||||
nextPermission === 'denied' ? blockedDescription : 'Permission request was dismissed.',
|
||||
nextPermission === 'denied'
|
||||
? 'Desktop notifications were denied by your browser. Allow notifications in browser settings, then try again.'
|
||||
: 'The browser permission request was dismissed.',
|
||||
});
|
||||
},
|
||||
[enabledByConversation, permission]
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useBrowserNotifications } from '../hooks/useBrowserNotifications';
|
||||
import {
|
||||
getNotificationEnableToastInfo,
|
||||
useBrowserNotifications,
|
||||
} from '../hooks/useBrowserNotifications';
|
||||
import type { Message } from '../types';
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
toast: {
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -57,6 +61,10 @@ describe('useBrowserNotifications', () => {
|
||||
configurable: true,
|
||||
value: NotificationMock,
|
||||
});
|
||||
Object.defineProperty(window, 'isSecureContext', {
|
||||
configurable: true,
|
||||
value: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('stores notification opt-in per conversation', async () => {
|
||||
@@ -84,6 +92,10 @@ describe('useBrowserNotifications', () => {
|
||||
icon: '/favicon-256x256.png',
|
||||
tag: `meshcore-notification-preview-channel-${incomingChannelMessage.conversation_key}`,
|
||||
});
|
||||
expect(mocks.toast.warning).toHaveBeenCalledWith('Notifications enabled with warning', {
|
||||
description:
|
||||
'Desktop notifications are on for this conversation, but you are using HTTP instead of HTTPS. Notifications will likely not work reliably.',
|
||||
});
|
||||
});
|
||||
|
||||
it('only sends desktop notifications for opted-in conversations', async () => {
|
||||
@@ -164,9 +176,65 @@ describe('useBrowserNotifications', () => {
|
||||
);
|
||||
});
|
||||
|
||||
expect(mocks.toast.error).toHaveBeenCalledWith('Browser notifications blocked', {
|
||||
expect(mocks.toast.error).toHaveBeenCalledWith('Notifications blocked', {
|
||||
description:
|
||||
'Allow notifications in your browser settings, then try again. Some browsers may refuse notifications on non-HTTPS or self-signed HTTPS origins. Check your browser documentation for how to trust an insecure origin and the associated risks before doing so.',
|
||||
'Desktop notifications are blocked by your browser. Allow notifications in browser settings, then try again. Non-HTTPS or untrusted HTTPS origins may also prevent notifications from working reliably.',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a warning toast when notifications are enabled on HTTP', async () => {
|
||||
const { result } = renderHook(() => useBrowserNotifications());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.toggleConversationNotifications(
|
||||
'channel',
|
||||
incomingChannelMessage.conversation_key,
|
||||
'#flightless'
|
||||
);
|
||||
});
|
||||
|
||||
expect(mocks.toast.warning).toHaveBeenCalledWith('Notifications enabled with warning', {
|
||||
description:
|
||||
'Desktop notifications are on for this conversation, but you are using HTTP instead of HTTPS. Notifications will likely not work reliably.',
|
||||
});
|
||||
expect(mocks.toast.success).not.toHaveBeenCalledWith('Notifications enabled');
|
||||
});
|
||||
|
||||
it('best-effort detects insecure HTTPS for the enable-warning copy', () => {
|
||||
expect(
|
||||
getNotificationEnableToastInfo({
|
||||
protocol: 'https:',
|
||||
isSecureContext: false,
|
||||
})
|
||||
).toEqual({
|
||||
level: 'warning',
|
||||
title: 'Notifications enabled with warning',
|
||||
description:
|
||||
'Desktop notifications are on for this conversation, but your HTTPS connection is untrusted, such as a self-signed certificate. Notification delivery may be inconsistent depending on your browser.',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows a descriptive success toast when notifications are disabled', async () => {
|
||||
const { result } = renderHook(() => useBrowserNotifications());
|
||||
|
||||
await act(async () => {
|
||||
await result.current.toggleConversationNotifications(
|
||||
'channel',
|
||||
incomingChannelMessage.conversation_key,
|
||||
'#flightless'
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current.toggleConversationNotifications(
|
||||
'channel',
|
||||
incomingChannelMessage.conversation_key,
|
||||
'#flightless'
|
||||
);
|
||||
});
|
||||
|
||||
expect(mocks.toast.success).toHaveBeenCalledWith('Notifications disabled', {
|
||||
description: 'Desktop notifications are off for #flightless.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user