mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
feat(notifications): apply inbox-only push filter immediately on toggle ✨
Flipping the setting only wrote to the store; the server-side JMAP push filter stayed stale until the next app launch ran resyncWebPush. When push is already enabled on the device, re-run enableWebPush on change so the toggle takes effect without a restart.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { NotificationSettings } from '../notification-settings';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import * as webPush from '@/lib/web-push';
|
||||
|
||||
// next-intl's useTranslations -> identity so labels are their keys.
|
||||
vi.mock('next-intl', () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
// Keep the section wrapper out of the way.
|
||||
vi.mock('../settings-section', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('../settings-section')>()),
|
||||
SettingsSection: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/web-push', () => ({
|
||||
WebPushUnsupportedError: class WebPushUnsupportedError extends Error {},
|
||||
isWebPushSupported: () => true,
|
||||
isWebPushEnabled: vi.fn(async () => true),
|
||||
enableWebPush: vi.fn(async () => ({ subscriptionId: 'sub-1' })),
|
||||
disableWebPush: vi.fn(async () => undefined),
|
||||
listPushDevices: vi.fn(async () => []),
|
||||
revokePushDevice: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
const fakeClient = { getAccountId: () => 'acct-1' } as unknown as ReturnType<
|
||||
typeof useAuthStore.getState
|
||||
>['client'];
|
||||
|
||||
describe('NotificationSettings - pushNotifyInboxOnly', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
useSettingsStore.setState({ emailNotificationsEnabled: true, pushNotifyInboxOnly: false });
|
||||
useAuthStore.setState({ client: fakeClient, username: 'me@example.com' });
|
||||
usePolicyStore.setState({
|
||||
...usePolicyStore.getState(),
|
||||
policy: {},
|
||||
isSettingLocked: () => false,
|
||||
isSettingHidden: () => false,
|
||||
} as never);
|
||||
});
|
||||
|
||||
it('does not re-sync push on mount', async () => {
|
||||
render(<NotificationSettings />);
|
||||
// Let the "is push enabled?" effect settle.
|
||||
await waitFor(() => expect(webPush.isWebPushEnabled).toHaveBeenCalled());
|
||||
expect(webPush.enableWebPush).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('re-runs enableWebPush with the new flag when the toggle flips while push is on', async () => {
|
||||
render(<NotificationSettings />);
|
||||
await waitFor(() => expect(webPush.isWebPushEnabled).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
useSettingsStore.getState().updateSetting('pushNotifyInboxOnly', true);
|
||||
});
|
||||
|
||||
await waitFor(() => expect(webPush.enableWebPush).toHaveBeenCalledTimes(1));
|
||||
expect(webPush.enableWebPush).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ client: fakeClient, inboxOnly: true }),
|
||||
);
|
||||
// forceRecreate stays off - a filter PATCH, not a destroy/recreate.
|
||||
expect(
|
||||
(webPush.enableWebPush as ReturnType<typeof vi.fn>).mock.calls[0][0],
|
||||
).not.toHaveProperty('forceRecreate', true);
|
||||
});
|
||||
|
||||
it('does not re-sync when push is disabled on this device', async () => {
|
||||
(webPush.isWebPushEnabled as ReturnType<typeof vi.fn>).mockResolvedValueOnce(false);
|
||||
render(<NotificationSettings />);
|
||||
await waitFor(() => expect(webPush.isWebPushEnabled).toHaveBeenCalled());
|
||||
|
||||
act(() => {
|
||||
useSettingsStore.getState().updateSetting('pushNotifyInboxOnly', true);
|
||||
});
|
||||
|
||||
// Give any effect a chance to fire.
|
||||
await Promise.resolve();
|
||||
expect(webPush.enableWebPush).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { SettingsSection, SettingItem, ToggleSwitch, Select } from './settings-section';
|
||||
@@ -148,6 +148,43 @@ export function NotificationSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
// Flipping "Inbox only" only writes the setting; the server-side JMAP push
|
||||
// filter stays stale until the next app launch runs resyncWebPush. When push
|
||||
// is already on for this device, re-run the enable flow now so the toggle
|
||||
// takes effect immediately instead of "on next restart". The ref seeds to the
|
||||
// mounted value so this never fires on mount or on an unrelated re-render.
|
||||
const lastSyncedInboxOnly = useRef(pushNotifyInboxOnly);
|
||||
useEffect(() => {
|
||||
if (lastSyncedInboxOnly.current === pushNotifyInboxOnly) return;
|
||||
lastSyncedInboxOnly.current = pushNotifyInboxOnly;
|
||||
// Push off on this device: enabling it later builds the filter from the
|
||||
// current setting, so there's nothing to re-sync now.
|
||||
if (!client || pushStatus.kind !== 'enabled') return;
|
||||
|
||||
let cancelled = false;
|
||||
setPushStatus({ kind: 'busy' });
|
||||
void enableWebPush({
|
||||
client,
|
||||
relayBaseUrl: activeRelayUrl,
|
||||
accountLabel: username ?? undefined,
|
||||
inboxOnly: pushNotifyInboxOnly,
|
||||
})
|
||||
.then(() => { if (!cancelled) setPushStatus({ kind: 'enabled' }); })
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
if (err instanceof WebPushUnsupportedError) {
|
||||
setPushStatus({ kind: 'unsupported' });
|
||||
return;
|
||||
}
|
||||
setPushStatus({
|
||||
kind: 'error',
|
||||
message: err instanceof Error ? err.message : 'Failed to update push filter',
|
||||
});
|
||||
})
|
||||
.finally(() => { if (!cancelled) void refreshDevices(); });
|
||||
return () => { cancelled = true; };
|
||||
}, [pushNotifyInboxOnly, client, pushStatus.kind, activeRelayUrl, username, refreshDevices]);
|
||||
|
||||
const handleDisablePush = async () => {
|
||||
if (!client) return;
|
||||
const confirmed = await confirmDialog({
|
||||
|
||||
Reference in New Issue
Block a user