Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-notification-preview-and-transport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Fix encrypted message content appearing in notifications when previews are disabled, and keep push working when switching notification transports fails.
5 changes: 5 additions & 0 deletions .changeset/fix-push-transport-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Switching to native push notifications now correctly uses the platform's native delivery instead of keeping the old push app.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ windows = { version = "0.61", features = [
tauri-plugin-single-instance = { version = "2.4.3", features = ["deep-link"] }

[target.'cfg(any(windows, target_os = "linux"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "cc3a468263db75e40a8a4751f3dae9bce5bc4234" }
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "87e452fab17711a82422a97ab9541ebf647fa28a" }

# default-features = false drops notify-rust so macOS uses the native
# UNUserNotificationCenter backend (needs a signed .app to deliver).
[target.'cfg(target_os = "macos")'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "cc3a468263db75e40a8a4751f3dae9bce5bc4234", default-features = false }
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "87e452fab17711a82422a97ab9541ebf647fa28a", default-features = false }

[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-updater = { version = "2", optional = true }
Expand All @@ -113,7 +113,7 @@ cef = { version = "=148.0.0", optional = true }
libloading = "0.8"

[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "cc3a468263db75e40a8a4751f3dae9bce5bc4234", features = [
tauri-plugin-notifications = { git = "https://github.com/SableClient/tauri-plugin-notifications.git", rev = "87e452fab17711a82422a97ab9541ebf647fa28a", features = [
"push-notifications",
] }
tauri-plugin-edge-to-edge = { git = "https://github.com/SableClient/tauri-plugin-edge-to-edge.git", rev = "33c6116c27be28c06df5a9d02231ecc5fdeb93c5" }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Box,
Expand All @@ -17,12 +17,16 @@ import {
import { menuIcon, X } from '$components/icons/phosphor';
import { useAtom } from 'jotai';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useClientConfig } from '../../../hooks/useClientConfig';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { pushSubscriptionAtom } from '../../../state/pushSubscription';
import { deRegisterAllPushers } from './PushNotifications';
import { disableNativePush } from './NotificationTransport';
import { disableUnifiedPush } from './UnifiedPushNotifications';
import { SettingTile } from '../../../components/setting-tile';
import { isTauri } from '@tauri-apps/api/core';

type ConfirmDeregisterDialogProps = {
onClose: () => void;
Expand Down Expand Up @@ -78,9 +82,21 @@ function ConfirmDeregisterDialog({ onClose, onConfirm, isLoading }: ConfirmDereg

export function DeregisterAllPushersSetting() {
const mx = useMatrixClient();
const [deregisterState] = useAsyncCallback(deRegisterAllPushers);
const clientConfig = useClientConfig();
const deregisterAll = useCallback(async () => {
await deRegisterAllPushers(mx);
if (isTauri()) {
await Promise.allSettled([disableNativePush(mx, clientConfig), disableUnifiedPush(mx)]);
}
}, [mx, clientConfig]);
const [deregisterState, runDeregisterAll] = useAsyncCallback(deregisterAll);
const [isConfirming, setIsConfirming] = useState(false);
const [, setPushNotifications] = useSetting(settingsAtom, 'usePushNotifications');
const [backgroundPushEnabled, setBackgroundPushEnabled] = useSetting(
settingsAtom,
'backgroundPushEnabled'
);
const [, setBackgroundPushProvider] = useSetting(settingsAtom, 'backgroundPushProvider');

const [, setPushSubscription] = useAtom(pushSubscriptionAtom);

Expand All @@ -94,9 +110,18 @@ export function DeregisterAllPushersSetting() {
};

const handleConfirmDeregister = async () => {
await deRegisterAllPushers(mx);
try {
await runDeregisterAll();
} catch {
return;
}
setPushNotifications(false);
setPushSubscription(null);

if (backgroundPushEnabled) {
setBackgroundPushEnabled(false);
}
setBackgroundPushProvider(null);
setIsConfirming(false);
};

Expand Down Expand Up @@ -132,7 +157,6 @@ export function DeregisterAllPushersSetting() {
</Button>
}
>
{/* FIXME: these two things below, even before my changes, don't really seem to ever appear? */}
{deregisterState.status === AsyncStatus.Error && (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
<br />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, expect, it, vi } from 'vitest';

const invoke = vi.hoisted(() =>
vi.fn<(cmd: string, args?: Record<string, unknown>) => Promise<unknown>>()
);

const isAndroidTauri = vi.hoisted(() => vi.fn<() => boolean>());

vi.mock('@tauri-apps/api/core', () => ({ invoke }));
vi.mock('./TauriNotificationsApiClient', () => ({ isAndroidTauri }));

import { getNativePushNotificationsApi } from './NativePushNotificationsApiClient';

describe('NativePushNotificationsApiClient', () => {
it('registers with provider: "fcm" on Android so the plugin bypasses a saved UnifiedPush distributor', async () => {
isAndroidTauri.mockReturnValue(true);
invoke.mockResolvedValueOnce({
deviceToken: 'fcm-token',
p256dh: 'p256dh',
auth: 'auth',
});

const api = await getNativePushNotificationsApi();
await api.registerForPushNotifications('vapid-pub');

expect(invoke).toHaveBeenCalledWith('plugin:notifications|register_for_push_notifications', {
vapid: 'vapid-pub',
provider: 'fcm',
});
});

it('omits the provider arg on iOS (APNs ignores it)', async () => {
isAndroidTauri.mockReturnValue(false);
invoke.mockResolvedValueOnce({
deviceToken: 'apns-token',
});

const api = await getNativePushNotificationsApi();
await api.registerForPushNotifications('vapid-pub');

expect(invoke).toHaveBeenCalledWith('plugin:notifications|register_for_push_notifications', {
vapid: 'vapid-pub',
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { invoke } from '@tauri-apps/api/core';
import { isAndroidTauri } from './TauriNotificationsApiClient';

export type NativePushRegistration = {
deviceToken: string;
Expand All @@ -24,6 +25,7 @@ export async function getNativePushNotificationsApi(): Promise<NativePushNotific
registerForPushNotifications: (vapid?: string) =>
invoke<NativePushRegistration>('plugin:notifications|register_for_push_notifications', {
vapid,
...(isAndroidTauri() ? { provider: 'fcm' } : {}),
}),
unregisterForPushNotifications: notificationsApi.unregisterForPushNotifications,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

const isTauri = vi.hoisted(() => vi.fn<() => boolean>());

vi.mock('@tauri-apps/api/core', () => ({ isTauri }));

import {
enablePushNotifications,
disablePushNotifications,
togglePusher,
} from './PushNotifications';

const matrixClient = vi.hoisted(() => ({
getAccessToken: vi.fn<() => string>(() => 'token'),
}));
const clientConfig = {} as never;

afterEach(() => {
vi.clearAllMocks();
});

describe('enablePushNotifications', () => {
it('no-ops in the Tauri runtime instead of throwing', async () => {
isTauri.mockReturnValue(true);

await expect(
enablePushNotifications(matrixClient as never, clientConfig, [
null,
vi.fn<(sub: unknown) => void>(),
])
).resolves.toBeUndefined();
});
});

describe('disablePushNotifications', () => {
it('no-ops in the Tauri runtime', async () => {
isTauri.mockReturnValue(true);

await expect(
disablePushNotifications(matrixClient as never, clientConfig, [
null,
vi.fn<(sub: unknown) => void>(),
])
).resolves.toBeUndefined();
});
});

describe('togglePusher', () => {
it('does not throw in the Tauri runtime when push is enabled', async () => {
isTauri.mockReturnValue(true);

await expect(
togglePusher(matrixClient as never, clientConfig, true, true, [
null,
vi.fn<(sub: unknown) => void>(),
])
).resolves.toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ export async function enablePushNotifications(
clientConfig: ClientConfig,
pushSubscriptionAtom: PushSubscriptionState
): Promise<void> {
if (isTauri()) {
throw new Error('Push notifications are disabled in Tauri runtime.');
}
if (isTauri()) return;
if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
debugLog.error(
'notification',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it, vi } from 'vitest';
import { switchBackgroundPushTransport } from './SystemNotification';
import type { NotificationTransportProvider } from './NotificationTransport';

describe('switchBackgroundPushTransport', () => {
it('deactivates the previous transport before activating the new one', async () => {
const order: string[] = [];
const deactivate = vi
.fn<(kind: NotificationTransportProvider | null) => Promise<void>>()
.mockImplementation(async (kind) => {
order.push(`deactivate:${kind}`);
});
const activate = vi
.fn<() => Promise<NotificationTransportProvider | null>>()
.mockImplementation(async () => {
order.push('activate');
return 'native';
});

const nextKind = await switchBackgroundPushTransport({
previousKind: 'unifiedpush',
activate,
deactivate,
});

expect(nextKind).toBe('native');
expect(order).toEqual(['deactivate:unifiedpush', 'activate']);
});

it('skips deactivate when there is no previous transport', async () => {
const deactivate = vi.fn<(kind: NotificationTransportProvider | null) => Promise<void>>();
const activate = vi
.fn<() => Promise<NotificationTransportProvider | null>>()
.mockResolvedValue('native');

const nextKind = await switchBackgroundPushTransport({
previousKind: null,
activate,
deactivate,
});

expect(nextKind).toBe('native');
expect(deactivate).not.toHaveBeenCalled();
expect(activate).toHaveBeenCalledOnce();
});

it('propagates a deactivate failure without activating', async () => {
const deactivate = vi
.fn<(kind: NotificationTransportProvider | null) => Promise<void>>()
.mockRejectedValue(new Error('unregister failed'));
const activate = vi.fn<() => Promise<NotificationTransportProvider | null>>();

await expect(
switchBackgroundPushTransport({
previousKind: 'unifiedpush',
activate,
deactivate,
})
).rejects.toThrow('unregister failed');

expect(activate).not.toHaveBeenCalled();
});

it('restores the previous transport when activation fails, then rethrows', async () => {
const order: string[] = [];
const deactivate = vi
.fn<(kind: NotificationTransportProvider | null) => Promise<void>>()
.mockImplementation(async (kind) => {
order.push(`deactivate:${kind}`);
});
const activate = vi
.fn<() => Promise<NotificationTransportProvider | null>>()
.mockRejectedValue(new Error('register failed'));
const reactivate = vi
.fn<(kind: NotificationTransportProvider) => Promise<void>>()
.mockImplementation(async (kind) => {
order.push(`reactivate:${kind}`);
});

await expect(
switchBackgroundPushTransport({
previousKind: 'unifiedpush',
activate,
deactivate,
reactivate,
})
).rejects.toThrow('register failed');

expect(order).toEqual(['deactivate:unifiedpush', 'reactivate:unifiedpush']);
});
});
Loading
Loading