diff --git a/package.json b/package.json
index dc8794f0..87afad84 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "impro",
- "version": "0.18.197",
+ "version": "0.18.198",
"type": "module",
"scripts": {
"start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve",
diff --git a/src/css/style.css b/src/css/style.css
index d0915c59..42247399 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -7440,7 +7440,7 @@ context-menu-item-group > context-menu-item > button {
flex-direction: column-reverse;
}
-.modal-dialog-button.confirm-button.is-pending {
+.modal-dialog-button.is-pending {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -7449,7 +7449,7 @@ context-menu-item-group > context-menu-item > button {
opacity: 1;
}
-.modal-dialog-button.confirm-button .loading-spinner {
+.modal-dialog-button .loading-spinner {
width: 14px;
height: 14px;
border-width: 2px;
diff --git a/src/js/modals/choice.modal.js b/src/js/modals/choice.modal.js
index 77d974af..67f18492 100644
--- a/src/js/modals/choice.modal.js
+++ b/src/js/modals/choice.modal.js
@@ -2,6 +2,8 @@ import { html } from "/js/lib/lit-html.js";
import { Modal } from "/js/modals/modal.js";
class ChoiceModal extends Modal {
+ #pendingValue = null;
+
get className() {
return "bottom-sheet text-modal confirm-modal choice-modal compact";
}
@@ -10,9 +12,30 @@ class ChoiceModal extends Modal {
return { "data-testid": "choice-modal" };
}
- render({ dismiss, props: { message, title, choices } }) {
+ canDismiss() {
+ return this.#pendingValue === null;
+ }
+
+ render({ dismiss, update, props: { message, title, choices, onChoose } }) {
+ const pendingValue = this.#pendingValue;
+ const isPending = pendingValue !== null;
+ const handleChoice = async (choice) => {
+ if (!onChoose) {
+ dismiss(choice.value);
+ return;
+ }
+ this.#pendingValue = choice.value;
+ update();
+ try {
+ await onChoose(choice.value);
+ dismiss(choice.value);
+ } catch {
+ this.#pendingValue = null;
+ update();
+ }
+ };
return html`
-
+
${title
? html`
${title}
@@ -22,17 +45,24 @@ class ChoiceModal extends Modal {
${message}
`;
@@ -40,7 +70,9 @@ class ChoiceModal extends Modal {
}
// Presents a stacked list of choices; resolves with the chosen value, or null
-// when dismissed without choosing a value
+// when dismissed without choosing a value. An optional `onChoose(value)` is
+// awaited before the modal dismisses (the chosen button shows a spinner);
+// throw from it to keep the modal open.
export async function choiceModal(message, options = {}) {
return (await ChoiceModal.open({ message, ...options })) ?? null;
}
diff --git a/src/js/views/settings/notifications.view.js b/src/js/views/settings/notifications.view.js
index 3ad59249..2ebad4e0 100644
--- a/src/js/views/settings/notifications.view.js
+++ b/src/js/views/settings/notifications.view.js
@@ -3,7 +3,7 @@ import { pageEffect, bindPageTitle } from "/js/router.js";
import { headerTemplate } from "/js/templates/header.template.js";
import { auth } from "/js/auth.js";
import { classnames } from "/js/utils.js";
-import { confirmModal } from "/js/modals/confirm.modal.js";
+import { choiceModal } from "/js/modals/choice.modal.js";
import { showToast } from "/js/toasts.js";
import { Signal, ReactiveStore } from "/js/signals.js";
import "/js/components/toggle-switch.js";
@@ -14,7 +14,6 @@ function consumeCourierCallbackParams() {
const result = {
error: params.get("error"),
errorDescription: params.get("error_description"),
- chatPreviews: params.get("chat_previews") === "1",
};
history.replaceState(null, "", window.location.pathname);
return result;
@@ -33,7 +32,6 @@ export default async function settingsNotificationsView({
systemNotificationService?.isEnabled ?? false,
);
state.$pushBusy = new Signal.State(false);
- state.$chatPreviews = new Signal.State(false);
async function handleToggle(checked) {
if (!systemNotificationService) return;
@@ -68,18 +66,31 @@ export default async function settingsNotificationsView({
return;
}
let permission = null;
- const confirmed = await confirmModal(
- "You'll be sent to the notification service to authorize a separate, read-only grant for delivering push notifications. You can turn this off again at any time.",
+ const choice = await choiceModal(
+ "You'll be sent to the notification service to authorize push notifications. Message previews require additional read-only access to chat messages.",
{
title: "Enable push notifications?",
- confirmButtonText: "Continue",
- onConfirm: () => {
+ choices: [
+ {
+ value: "with-previews",
+ label: "Enable",
+ style: "primary",
+ },
+ {
+ value: "without-previews",
+ label: "Enable without message previews",
+ style: "primary",
+ },
+ { value: "cancel", label: "Cancel", style: "cancel" },
+ ],
+ onChoose: (value) => {
+ if (value === "cancel") return null;
permission = Notification.requestPermission();
return permission;
},
},
);
- if (!confirmed) return;
+ if (choice === null || choice === "cancel") return;
if ((await permission) !== "granted") {
showToast(
"Notifications are blocked for this site. Re-enable them in your browser's site settings.",
@@ -89,7 +100,7 @@ export default async function settingsNotificationsView({
}
try {
await courierPushService.startEnableFlow({
- chatPreviews: state.$chatPreviews.get(),
+ chatPreviews: choice === "with-previews",
});
} catch (error) {
console.error(error);
@@ -99,28 +110,6 @@ export default async function settingsNotificationsView({
}
}
- async function handlePreviewsToggle(checked) {
- if (!courierPushService) return;
- if (checked) {
- const confirmed = await confirmModal(
- "Message previews let the notification service read the content of your messages, so it can show who sent a message and what it says. Without this, chat notifications only tell you that you have unread messages.",
- {
- title: "Show message previews?",
- confirmButtonText: "Continue",
- },
- );
- if (!confirmed) return;
- }
- try {
- await courierPushService.startEnableFlow({ chatPreviews: checked });
- } catch (error) {
- console.error(error);
- showToast("Couldn't reach the notification service.", {
- style: "error",
- });
- }
- }
-
(async () => {
const callback = consumeCourierCallbackParams();
if (!callback || !courierPushService) return;
@@ -134,7 +123,6 @@ export default async function settingsNotificationsView({
state.$pushBusy.set(true);
try {
await courierPushService.completeEnableFlow();
- state.$chatPreviews.set(callback.chatPreviews);
showToast("Push notifications enabled.");
} catch (error) {
console.error(error);
@@ -168,7 +156,6 @@ export default async function settingsNotificationsView({
const pushEnabled = courierPushService?.isEnabled ?? false;
const pushBusy = state.$pushBusy.get();
- const chatPreviews = state.$chatPreviews.get();
const pushSupported = courierPushService?.isSupported ?? false;
const pushRequiresInstall = courierPushService?.requiresInstall ?? false;
const serviceDid = courierPushService?.serviceDid ?? null;
@@ -176,7 +163,6 @@ export default async function settingsNotificationsView({
const systemRowDisabled = !isSupported || isDenied;
const pushRowDisabled = !pushSupported || !hasService || pushBusy;
- const previewsRowDisabled = pushBusy;
render(
html`
@@ -240,34 +226,6 @@ export default async function settingsNotificationsView({
>
- ${pushEnabled
- ? html`
-
-
Show message previews
-
- Include the sender and message text in chat notifications.
- This lets your notification service read your messages. With
- this off, chat notifications only say that you have unread
- messages.
-
-
-
-
- handlePreviewsToggle(event.detail.checked)}
- >
-
- `
- : null}
`,
root,
diff --git a/tests/e2e/specs/views/settingsNotifications.view.test.js b/tests/e2e/specs/views/settingsNotifications.view.test.js
index 8f9b6809..76c5a322 100644
--- a/tests/e2e/specs/views/settingsNotifications.view.test.js
+++ b/tests/e2e/specs/views/settingsNotifications.view.test.js
@@ -280,7 +280,7 @@ test.describe("Settings > Notifications view", () => {
// handoff return has none — so it must be asked for on this press.
await page
.locator(
- '[data-testid="confirm-modal"] [data-testid="modal-confirm-button"]',
+ '[data-testid="choice-modal"] [data-testid="modal-choice-without-previews"]',
)
.click();
@@ -292,6 +292,60 @@ test.describe("Settings > Notifications view", () => {
notificationService.authUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
),
);
+ expect(new URL(page.url()).searchParams.get("chat_previews")).toBe("0");
+ });
+
+ test("message previews are chosen from the same prompt", async ({
+ page,
+ }) => {
+ const mockServer = new MockServer();
+ mockServer.setNotificationServiceDid(notificationService.did);
+ await mockServer.setup(page);
+ await stubNotificationPermission(page, { initial: "default" });
+ await login(page);
+ await page.goto("/settings/notifications");
+
+ const toggle = page.locator('[data-testid="push-notifications-toggle"]');
+ await expect(toggle).not.toHaveAttribute("disabled", "", {
+ timeout: 10000,
+ });
+ await toggle.click();
+ await page
+ .locator(
+ '[data-testid="choice-modal"] [data-testid="modal-choice-with-previews"]',
+ )
+ .click();
+
+ await expect(page).toHaveURL(
+ new RegExp(
+ notificationService.authUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"),
+ ),
+ );
+ expect(new URL(page.url()).searchParams.get("chat_previews")).toBe("1");
+ });
+
+ test("cancelling the prompt asks for no permission", async ({ page }) => {
+ const mockServer = new MockServer();
+ mockServer.setNotificationServiceDid(notificationService.did);
+ await mockServer.setup(page);
+ await stubNotificationPermission(page, { initial: "default" });
+ await login(page);
+ await page.goto("/settings/notifications");
+
+ const toggle = page.locator('[data-testid="push-notifications-toggle"]');
+ await expect(toggle).not.toHaveAttribute("disabled", "", {
+ timeout: 10000,
+ });
+ await toggle.click();
+ await page
+ .locator(
+ '[data-testid="choice-modal"] [data-testid="modal-choice-cancel"]',
+ )
+ .click();
+
+ await expect(page.locator('[data-testid="choice-modal"]')).toHaveCount(0);
+ expect(await page.evaluate(() => window.__permissionRequests)).toBe(0);
+ await expect(page).toHaveURL(/\/settings\/notifications$/);
});
test("a denied prompt stops before the handoff", async ({ page }) => {
@@ -312,7 +366,7 @@ test.describe("Settings > Notifications view", () => {
await toggle.click();
await page
.locator(
- '[data-testid="confirm-modal"] [data-testid="modal-confirm-button"]',
+ '[data-testid="choice-modal"] [data-testid="modal-choice-without-previews"]',
)
.click();