Server/Web: Support "copy" action button to copy a value to the clipboard

This commit is contained in:
binwiederhier
2026-02-08 14:20:03 -05:00
parent 65050ef4dc
commit 3f0a7b65ee
10 changed files with 384 additions and 22 deletions

7
web/src/app/actions.js Normal file
View File

@@ -0,0 +1,7 @@
// Action types for ntfy messages
// These correspond to the server action types in server/actions.go
export const ACTION_VIEW = "view";
export const ACTION_BROADCAST = "broadcast";
export const ACTION_HTTP = "http";
export const ACTION_COPY = "copy";

View File

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

View File

@@ -36,6 +36,7 @@ import {
topicUrl,
unmatchedTags,
} from "../app/utils";
import { ACTION_BROADCAST, ACTION_COPY, ACTION_HTTP, ACTION_VIEW } from "../app/actions";
import { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
import subscriptionManager from "../app/SubscriptionManager";
@@ -345,7 +346,7 @@ const NotificationItem = (props) => {
</Tooltip>
</>
)}
{hasUserActions && <UserActions notification={notification} />}
{hasUserActions && <UserActions notification={notification} onShowSnack={props.onShowSnack} />}
</CardActions>
)}
</Card>
@@ -487,7 +488,7 @@ const Image = (props) => {
const UserActions = (props) => (
<>
{props.notification.actions.map((action) => (
<UserAction key={action.id} notification={props.notification} action={action} />
<UserAction key={action.id} notification={props.notification} action={action} onShowSnack={props.onShowSnack} />
))}
</>
);
@@ -549,7 +550,7 @@ const UserAction = (props) => {
const { t } = useTranslation();
const { notification } = props;
const { action } = props;
if (action.action === "broadcast") {
if (action.action === ACTION_BROADCAST) {
return (
<Tooltip title={t("notifications_actions_not_supported")}>
<span>
@@ -560,7 +561,7 @@ const UserAction = (props) => {
</Tooltip>
);
}
if (action.action === "view") {
if (action.action === ACTION_VIEW) {
const handleClick = () => {
openUrl(action.url);
if (action.clear) {
@@ -580,7 +581,7 @@ const UserAction = (props) => {
</Tooltip>
);
}
if (action.action === "http") {
if (action.action === ACTION_HTTP) {
const method = action.method ?? "POST";
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
return (
@@ -602,6 +603,22 @@ const UserAction = (props) => {
</Tooltip>
);
}
if (action.action === ACTION_COPY) {
const handleClick = async () => {
await copyToClipboard(action.value);
props.onShowSnack();
if (action.clear) {
await clearNotification(notification);
}
};
return (
<Tooltip title={t("common_copy_to_clipboard")}>
<Button onClick={handleClick} aria-label={t("common_copy_to_clipboard")}>
{action.label}
</Button>
</Tooltip>
);
}
return null; // Others
};

View File

@@ -12,6 +12,15 @@ const registerSW = () => {
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({
onRegisteredSW(swUrl, registration) {
console.log("[ServiceWorker] Registered:", { swUrl, registration });