Skip to content
Open
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
13 changes: 9 additions & 4 deletions desktop/src/features/notifications/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as React from "react";

import { isTauri } from "@tauri-apps/api/core";
import { useHomeFeedQuery } from "@/features/home/hooks";
import { useUsersBatchQuery } from "@/features/profile/hooks";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import type { Channel, FeedItem, HomeFeedResponse } from "@/shared/api/types";
import { isWindowsPlatform } from "@/shared/lib/platform";
import {
getDesktopNotificationPermissionState,
requestDesktopNotificationAccess,
type DesktopNotificationPermissionState,
} from "./lib/desktop";
import { ensureDesktopNotificationPermission } from "./lib/permission";
import {
COMING_SOON_SLOTS,
DEFAULT_SLOT_ALERTS_ENABLED,
Expand Down Expand Up @@ -248,10 +251,12 @@ export function useNotificationSettings(pubkey?: string) {

try {
let nextPermission = await refreshPermission();
if (nextPermission === "default") {
nextPermission = await requestDesktopNotificationAccess();
setPermission(nextPermission);
}
nextPermission = await ensureDesktopNotificationPermission({
currentPermission: nextPermission,
isWindowsTauri: isWindowsPlatform() && isTauri(),
requestAccess: requestDesktopNotificationAccess,
});
setPermission(nextPermission);

if (nextPermission !== "granted") {
setSettings((current) => ({
Expand Down
60 changes: 60 additions & 0 deletions desktop/src/features/notifications/lib/permission.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import test from "node:test";

import { ensureDesktopNotificationPermission } from "./permission.ts";

test("Windows Tauri retries a false denied permission and accepts the granted result", async () => {
let requestCount = 0;

const permission = await ensureDesktopNotificationPermission({
currentPermission: "denied",
isWindowsTauri: true,
requestAccess: async () => {
requestCount += 1;
return "granted";
},
});

assert.equal(permission, "granted");
assert.equal(requestCount, 1);
});

test("non-Windows-Tauri environments keep denied permission without requesting again", async () => {
for (const environment of [
"Windows web",
"non-Windows Tauri",
"non-Windows web",
]) {
let requestCount = 0;

const permission = await ensureDesktopNotificationPermission({
currentPermission: "denied",
isWindowsTauri: false,
requestAccess: async () => {
requestCount += 1;
return "granted";
},
});

assert.equal(permission, "denied", environment);
assert.equal(requestCount, 0, environment);
}
});

test("default permission still requests access on every platform", async () => {
for (const isWindowsTauri of [false, true]) {
let requestCount = 0;

const permission = await ensureDesktopNotificationPermission({
currentPermission: "default",
isWindowsTauri,
requestAccess: async () => {
requestCount += 1;
return "granted";
},
});

assert.equal(permission, "granted");
assert.equal(requestCount, 1);
}
});
26 changes: 26 additions & 0 deletions desktop/src/features/notifications/lib/permission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { DesktopNotificationPermissionState } from "./desktop";

type EnsureDesktopNotificationPermissionOptions = {
currentPermission: DesktopNotificationPermissionState;
isWindowsTauri: boolean;
requestAccess: () => Promise<DesktopNotificationPermissionState>;
};

/**
* Requests access for the normal default state and retries the Windows Tauri
* notification shim's known false-denied state.
*/
export async function ensureDesktopNotificationPermission({
currentPermission,
isWindowsTauri,
requestAccess,
}: EnsureDesktopNotificationPermissionOptions): Promise<DesktopNotificationPermissionState> {
if (
currentPermission === "default" ||
(currentPermission === "denied" && isWindowsTauri)
) {
return requestAccess();
}

return currentPermission;
}
34 changes: 34 additions & 0 deletions desktop/src/shared/lib/platform.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import test from "node:test";

import { isWindowsPlatform } from "./platform.ts";

function withNavigatorPlatform(platform, callback) {
const originalNavigator = Object.getOwnPropertyDescriptor(
globalThis,
"navigator",
);
Object.defineProperty(globalThis, "navigator", {
configurable: true,
value: { platform, userAgent: "" },
});

try {
callback();
} finally {
if (originalNavigator) {
Object.defineProperty(globalThis, "navigator", originalNavigator);
} else {
delete globalThis.navigator;
}
}
}

test("Windows platform detection accepts Win32 without matching Darwin", () => {
withNavigatorPlatform("Win32", () => {
assert.equal(isWindowsPlatform(), true);
});
withNavigatorPlatform("Darwin", () => {
assert.equal(isWindowsPlatform(), false);
});
});
9 changes: 9 additions & 0 deletions desktop/src/shared/lib/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export function isLinuxPlatform(): boolean {
);
}

/** Returns true on Windows desktops. */
export function isWindowsPlatform(): boolean {
if (typeof navigator === "undefined") {
return false;
}

return /^win/i.test(navigator.platform);
}

/**
* The platform's normal application-shortcut modifier:
* - macOS: Command (Meta)
Expand Down
44 changes: 44 additions & 0 deletions desktop/tests/e2e/profile.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1313,6 +1313,50 @@ test("notification settings drive the Inbox badge and desktop alerts", async ({
await expect.poll(getAppBadgeCount).toBe(baseline);
});

test("Windows retries a false denied notification permission from settings", async ({
page,
}) => {
await page.addInitScript(() => {
Object.defineProperty(navigator, "platform", {
configurable: true,
value: "Win32",
});
(window as Window & { isTauri?: boolean }).isTauri = true;
});
await page.goto("/");

await page.evaluate(() => {
(
window as Window & {
__BUZZ_E2E_SET_NOTIFICATION_PERMISSION__?: (
permission: NotificationPermission,
requestResult?: NotificationPermission,
) => void;
}
).__BUZZ_E2E_SET_NOTIFICATION_PERMISSION__?.("denied", "granted");
});

await openSettings(page, "notifications");
const desktopToggle = page.getByTestId("notifications-desktop-toggle");
const desktopState = page.getByTestId("notifications-desktop-state");

await desktopToggle.click();
await expect(desktopToggle).toBeChecked();
await expect(desktopState).toContainText("On");
await expect
.poll(() =>
page.evaluate(
() =>
(
window as Window & {
__BUZZ_E2E_GET_NOTIFICATION_PERMISSION_REQUEST_COUNT__?: () => number;
}
).__BUZZ_E2E_GET_NOTIFICATION_PERMISSION_REQUEST_COUNT__?.() ?? 0,
),
)
.toBe(1);
});

test("desktop notification clicks open the matching forum thread", async ({
page,
}) => {
Expand Down
22 changes: 22 additions & 0 deletions desktop/tests/helpers/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,11 +826,18 @@ export async function installBridge(page: Page, options: BridgeOptions) {
title: string;
}> = [];
const notificationInstances: MockNotification[] = [];
let notificationPermissionRequestCount = 0;
let notificationPermissionRequestResult: NotificationPermission | null =
null;

class MockNotification extends EventTarget {
static permission: NotificationPermission = "granted";

static async requestPermission(): Promise<NotificationPermission> {
notificationPermissionRequestCount += 1;
if (notificationPermissionRequestResult) {
MockNotification.permission = notificationPermissionRequestResult;
}
return MockNotification.permission;
}

Expand Down Expand Up @@ -863,10 +870,15 @@ export async function installBridge(page: Page, options: BridgeOptions) {
__BUZZ_E2E_APP_BADGE_COUNT__?: number;
__BUZZ_E2E_APP_BADGE_STATE__?: string;
__BUZZ_E2E_CLICK_NOTIFICATION__?: (index: number) => boolean;
__BUZZ_E2E_GET_NOTIFICATION_PERMISSION_REQUEST_COUNT__?: () => number;
__BUZZ_E2E_NOTIFICATIONS__?: Array<{
body: string | null;
title: string;
}>;
__BUZZ_E2E_SET_NOTIFICATION_PERMISSION__?: (
permission: NotificationPermission,
requestResult?: NotificationPermission,
) => void;
};
const currentConfig = testWindow.__BUZZ_E2E__ ?? {};

Expand All @@ -893,7 +905,17 @@ export async function installBridge(page: Page, options: BridgeOptions) {
notification.onclick?.(event);
return true;
};
testWindow.__BUZZ_E2E_GET_NOTIFICATION_PERMISSION_REQUEST_COUNT__ = () =>
notificationPermissionRequestCount;
testWindow.__BUZZ_E2E_NOTIFICATIONS__ = notificationLog;
testWindow.__BUZZ_E2E_SET_NOTIFICATION_PERMISSION__ = (
permission,
requestResult,
) => {
MockNotification.permission = permission;
notificationPermissionRequestCount = 0;
notificationPermissionRequestResult = requestResult ?? null;
};
},
{
identity,
Expand Down