diff --git a/docs/releases.md b/docs/releases.md index facc9968..48c4ec16 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -1475,6 +1475,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release * Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673)) * Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu) +* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting) ### ntfy Android app v1.16.1 (UNRELEASED) diff --git a/web/src/app/utils.js b/web/src/app/utils.js index 08710c1f..b798589c 100644 --- a/web/src/app/utils.js +++ b/web/src/app/utils.js @@ -77,7 +77,10 @@ export const maybeWithBearerAuth = (headers, token) => { return headers; }; -export const withBasicAuth = (headers, username, password) => ({ ...headers, Authorization: basicAuth(username, password) }); +export const withBasicAuth = (headers, username, password) => ({ + ...headers, + Authorization: basicAuth(username, password) +}); export const maybeWithAuth = (headers, user) => { if (user?.password) { @@ -139,7 +142,7 @@ export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-"); export const formatShortDateTime = (timestamp, language) => new Intl.DateTimeFormat(getKebabCaseLangStr(language), { dateStyle: "short", - timeStyle: "short", + timeStyle: "short" }).format(new Date(timestamp * 1000)); export const formatShortDate = (timestamp, language) => @@ -178,32 +181,32 @@ export const openUrl = (url) => { export const sounds = { ding: { file: ding, - label: "Ding", + label: "Ding" }, juntos: { file: juntos, - label: "Juntos", + label: "Juntos" }, pristine: { file: pristine, - label: "Pristine", + label: "Pristine" }, dadum: { file: dadum, - label: "Dadum", + label: "Dadum" }, pop: { file: pop, - label: "Pop", + label: "Pop" }, "pop-swoosh": { file: popSwoosh, - label: "Pop swoosh", + label: "Pop swoosh" }, beep: { file: beep, - label: "Beep", - }, + label: "Beep" + } }; export const playSound = async (id) => { @@ -216,7 +219,7 @@ export const playSound = async (id) => { export async function* fetchLinesIterator(fileURL, headers) { const utf8Decoder = new TextDecoder("utf-8"); const response = await fetch(fileURL, { - headers, + headers }); const reader = response.body.getReader(); let { value: chunk, done: readerDone } = await reader.read(); @@ -225,7 +228,7 @@ export async function* fetchLinesIterator(fileURL, headers) { const re = /\n|\r|\r\n/gm; let startIndex = 0; - for (;;) { + for (; ;) { const result = re.exec(chunk); if (!result) { if (readerDone) { @@ -270,3 +273,21 @@ export const urlB64ToUint8Array = (base64String) => { } return outputArray; }; + +export const copyToClipboard = (text) => { + if (navigator.clipboard && window.isSecureContext) { + return navigator.clipboard.writeText(text); + } else { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); // Avoid mobile keyboards from popping up + textarea.style.position = "fixed"; // Avoid scroll jump + textarea.style.left = "-9999px"; + document.body.appendChild(textarea); + textarea.focus(); + textarea.select(); + document.execCommand("copy"); + document.body.removeChild(textarea); + return Promise.resolve(); + } +}; diff --git a/web/src/components/Account.jsx b/web/src/components/Account.jsx index 65aa38e8..bc5e3000 100644 --- a/web/src/components/Account.jsx +++ b/web/src/components/Account.jsx @@ -45,7 +45,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { ContentCopy, Public } from "@mui/icons-material"; import AddIcon from "@mui/icons-material/Add"; import routes from "./routes"; -import { formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; +import { copyToClipboard, formatBytes, formatShortDate, formatShortDateTime, openUrl } from "../app/utils"; import accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi"; import { Pref, PrefGroup } from "./Pref"; import db from "../app/db"; @@ -370,7 +370,7 @@ const PhoneNumbers = () => { }; const handleCopy = (phoneNumber) => { - navigator.clipboard.writeText(phoneNumber); + copyToClipboard(phoneNumber); setSnackOpen(true); }; @@ -841,7 +841,7 @@ const TokensTable = (props) => { }; const handleCopy = async (token) => { - await navigator.clipboard.writeText(token); + copyToClipboard(token); setSnackOpen(true); }; diff --git a/web/src/components/ErrorBoundary.jsx b/web/src/components/ErrorBoundary.jsx index adb177c6..92e2f83b 100644 --- a/web/src/components/ErrorBoundary.jsx +++ b/web/src/components/ErrorBoundary.jsx @@ -2,6 +2,7 @@ import * as React from "react"; import StackTrace from "stacktrace-js"; import { CircularProgress, Link, Button } from "@mui/material"; import { Trans, withTranslation } from "react-i18next"; +import { copyToClipboard } from "../app/utils"; class ErrorBoundaryImpl extends React.Component { constructor(props) { @@ -64,7 +65,7 @@ class ErrorBoundaryImpl extends React.Component { stack += `${this.state.niceStack}\n\n`; } stack += `${this.state.originalStack}\n`; - navigator.clipboard.writeText(stack); + copyToClipboard(stack); } renderUnsupportedIndexedDB() { diff --git a/web/src/components/Notifications.jsx b/web/src/components/Notifications.jsx index dceb5b91..9f984431 100644 --- a/web/src/components/Notifications.jsx +++ b/web/src/components/Notifications.jsx @@ -26,7 +26,10 @@ import { Trans, useTranslation } from "react-i18next"; import { useOutletContext } from "react-router-dom"; import { useRemark } from "react-remark"; import styled from "@emotion/styled"; -import { formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags } from "../app/utils"; +import { + copyToClipboard, + formatBytes, formatShortDateTime, maybeActionErrors, openUrl, shortUrl, topicShortUrl, unmatchedTags +} from "../app/utils"; import { formatMessage, formatTitle, isImage } from "../app/notificationUtils"; import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles"; import subscriptionManager from "../app/SubscriptionManager"; @@ -239,7 +242,7 @@ const NotificationItem = (props) => { await subscriptionManager.markNotificationRead(notification.id); }; const handleCopy = (s) => { - navigator.clipboard.writeText(s); + copyToClipboard(s); props.onShowSnack(); }; const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;