1
0
Эх сурвалжийг харах

Fix copy to clipboard on HTTP-only hosted sites

binwiederhier 6 сар өмнө
parent
commit
b105ed6727

+ 1 - 0
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 ([#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)
 

+ 33 - 12
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();
+  }
+};

+ 3 - 3
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);
   };
 

+ 2 - 1
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() {

+ 5 - 2
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;