Merge branch 'main' into release-2.17.x

This commit is contained in:
binwiederhier
2026-02-08 22:31:35 -05:00
4 changed files with 8 additions and 32 deletions

View File

@@ -2304,6 +2304,11 @@ _Supported on:_ :material-android: :material-firefox:
The `copy` action **copies a given value to the clipboard when the action button is tapped**. This is useful for The `copy` action **copies a given value to the clipboard when the action button is tapped**. This is useful for
one-time passcodes, tokens, or any other value you want to quickly copy without opening the full notification. one-time passcodes, tokens, or any other value you want to quickly copy without opening the full notification.
!!! info
The copy action button is only shown in the web app and Android app notification list, **not** in browser desktop
notifications. This is because browsers do not allow clipboard access from notification actions without direct
user interaction with the page.
Here's an example using the [`X-Actions` header](#using-a-header): Here's an example using the [`X-Actions` header](#using-a-header):
=== "Command line (curl)" === "Command line (curl)"

View File

@@ -4,7 +4,7 @@ import { NavigationRoute, registerRoute } from "workbox-routing";
import { NetworkFirst } from "workbox-strategies"; import { NetworkFirst } from "workbox-strategies";
import { clientsClaim } from "workbox-core"; import { clientsClaim } from "workbox-core";
import { dbAsync } from "../src/app/db"; import { dbAsync } from "../src/app/db";
import { ACTION_COPY, ACTION_HTTP, ACTION_VIEW } from "../src/app/actions"; import { ACTION_HTTP, ACTION_VIEW } from "../src/app/actions";
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils"; import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
import initI18n from "../src/app/i18n"; import initI18n from "../src/app/i18n";
import { import {
@@ -256,26 +256,6 @@ const handleClick = async (event) => {
if (action.clear) { if (action.clear) {
await clearNotification(); await clearNotification();
} }
} else if (action.action === ACTION_COPY) {
try {
// Service worker can't access the clipboard API directly, so we try to
// open a focused client and use it, or fall back to opening a window
const allClients = await self.clients.matchAll({ type: "window" });
const focusedClient = allClients.find((c) => c.focused) || allClients[0];
if (focusedClient) {
focusedClient.postMessage({ type: "copy", value: action.value });
}
if (action.clear) {
await clearNotification();
}
} catch (e) {
console.error("[ServiceWorker] Error performing copy action", e);
self.registration.showNotification(`${t("notifications_actions_failed_notification")}: ${action.label} (${action.action})`, {
body: e.message,
icon,
badge,
});
}
} else if (action.action === ACTION_HTTP) { } else if (action.action === ACTION_HTTP) {
try { try {
const response = await fetch(action.url, { const response = await fetch(action.url, {

View File

@@ -2,7 +2,7 @@
// and cannot be used in the service worker // and cannot be used in the service worker
import emojisMapped from "./emojisMapped"; import emojisMapped from "./emojisMapped";
import { ACTION_COPY, ACTION_HTTP, ACTION_VIEW } from "./actions"; import { ACTION_HTTP, ACTION_VIEW } from "./actions";
const toEmojis = (tags) => { const toEmojis = (tags) => {
if (!tags) return []; if (!tags) return [];
@@ -82,7 +82,7 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUr
topicRoute, topicRoute,
}, },
actions: message.actions actions: message.actions
?.filter(({ action }) => action === ACTION_VIEW || action === ACTION_HTTP || action === ACTION_COPY) ?.filter(({ action }) => action === ACTION_VIEW || action === ACTION_HTTP)
.map(({ label }) => ({ .map(({ label }) => ({
action: label, action: label,
title: label, title: label,

View File

@@ -12,15 +12,6 @@ const registerSW = () => {
return; return;
} }
// Listen for messages from the service worker (e.g., "copy" action)
navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data?.type === "copy" && event.data?.value) {
navigator.clipboard?.writeText(event.data.value).catch((e) => {
console.error("[ServiceWorker] Failed to copy to clipboard", e);
});
}
});
viteRegisterSW({ viteRegisterSW({
onRegisteredSW(swUrl, registration) { onRegisteredSW(swUrl, registration) {
console.log("[ServiceWorker] Registered:", { swUrl, registration }); console.log("[ServiceWorker] Registered:", { swUrl, registration });