mirror of
https://github.com/binwiederhier/ntfy.git
synced 2025-10-29 12:02:09 +01:00
Merge pull request #1432 from binwiederhier/http-clipboard
Fix copy to clipboard on HTTP-only hosted sites
This commit is contained in:
commit
18d08298cc
5 changed files with 44 additions and 18 deletions
|
|
@ -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 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
|
* 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)
|
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)
|
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,10 @@ export const maybeWithBearerAuth = (headers, token) => {
|
||||||
return headers;
|
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) => {
|
export const maybeWithAuth = (headers, user) => {
|
||||||
if (user?.password) {
|
if (user?.password) {
|
||||||
|
|
@ -139,7 +142,7 @@ export const getKebabCaseLangStr = (language) => language.replace(/_/g, "-");
|
||||||
export const formatShortDateTime = (timestamp, language) =>
|
export const formatShortDateTime = (timestamp, language) =>
|
||||||
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
|
new Intl.DateTimeFormat(getKebabCaseLangStr(language), {
|
||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
timeStyle: "short",
|
timeStyle: "short"
|
||||||
}).format(new Date(timestamp * 1000));
|
}).format(new Date(timestamp * 1000));
|
||||||
|
|
||||||
export const formatShortDate = (timestamp, language) =>
|
export const formatShortDate = (timestamp, language) =>
|
||||||
|
|
@ -178,32 +181,32 @@ export const openUrl = (url) => {
|
||||||
export const sounds = {
|
export const sounds = {
|
||||||
ding: {
|
ding: {
|
||||||
file: ding,
|
file: ding,
|
||||||
label: "Ding",
|
label: "Ding"
|
||||||
},
|
},
|
||||||
juntos: {
|
juntos: {
|
||||||
file: juntos,
|
file: juntos,
|
||||||
label: "Juntos",
|
label: "Juntos"
|
||||||
},
|
},
|
||||||
pristine: {
|
pristine: {
|
||||||
file: pristine,
|
file: pristine,
|
||||||
label: "Pristine",
|
label: "Pristine"
|
||||||
},
|
},
|
||||||
dadum: {
|
dadum: {
|
||||||
file: dadum,
|
file: dadum,
|
||||||
label: "Dadum",
|
label: "Dadum"
|
||||||
},
|
},
|
||||||
pop: {
|
pop: {
|
||||||
file: pop,
|
file: pop,
|
||||||
label: "Pop",
|
label: "Pop"
|
||||||
},
|
},
|
||||||
"pop-swoosh": {
|
"pop-swoosh": {
|
||||||
file: popSwoosh,
|
file: popSwoosh,
|
||||||
label: "Pop swoosh",
|
label: "Pop swoosh"
|
||||||
},
|
},
|
||||||
beep: {
|
beep: {
|
||||||
file: beep,
|
file: beep,
|
||||||
label: "Beep",
|
label: "Beep"
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const playSound = async (id) => {
|
export const playSound = async (id) => {
|
||||||
|
|
@ -216,7 +219,7 @@ export const playSound = async (id) => {
|
||||||
export async function* fetchLinesIterator(fileURL, headers) {
|
export async function* fetchLinesIterator(fileURL, headers) {
|
||||||
const utf8Decoder = new TextDecoder("utf-8");
|
const utf8Decoder = new TextDecoder("utf-8");
|
||||||
const response = await fetch(fileURL, {
|
const response = await fetch(fileURL, {
|
||||||
headers,
|
headers
|
||||||
});
|
});
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
let { value: chunk, done: readerDone } = await reader.read();
|
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;
|
const re = /\n|\r|\r\n/gm;
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
|
|
||||||
for (;;) {
|
for (; ;) {
|
||||||
const result = re.exec(chunk);
|
const result = re.exec(chunk);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
if (readerDone) {
|
if (readerDone) {
|
||||||
|
|
@ -270,3 +273,21 @@ export const urlB64ToUint8Array = (base64String) => {
|
||||||
}
|
}
|
||||||
return outputArray;
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { ContentCopy, Public } from "@mui/icons-material";
|
import { ContentCopy, Public } from "@mui/icons-material";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import routes from "./routes";
|
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 accountApi, { LimitBasis, Role, SubscriptionInterval, SubscriptionStatus } from "../app/AccountApi";
|
||||||
import { Pref, PrefGroup } from "./Pref";
|
import { Pref, PrefGroup } from "./Pref";
|
||||||
import db from "../app/db";
|
import db from "../app/db";
|
||||||
|
|
@ -370,7 +370,7 @@ const PhoneNumbers = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = (phoneNumber) => {
|
const handleCopy = (phoneNumber) => {
|
||||||
navigator.clipboard.writeText(phoneNumber);
|
copyToClipboard(phoneNumber);
|
||||||
setSnackOpen(true);
|
setSnackOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -841,7 +841,7 @@ const TokensTable = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = async (token) => {
|
const handleCopy = async (token) => {
|
||||||
await navigator.clipboard.writeText(token);
|
copyToClipboard(token);
|
||||||
setSnackOpen(true);
|
setSnackOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import * as React from "react";
|
||||||
import StackTrace from "stacktrace-js";
|
import StackTrace from "stacktrace-js";
|
||||||
import { CircularProgress, Link, Button } from "@mui/material";
|
import { CircularProgress, Link, Button } from "@mui/material";
|
||||||
import { Trans, withTranslation } from "react-i18next";
|
import { Trans, withTranslation } from "react-i18next";
|
||||||
|
import { copyToClipboard } from "../app/utils";
|
||||||
|
|
||||||
class ErrorBoundaryImpl extends React.Component {
|
class ErrorBoundaryImpl extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
|
@ -64,7 +65,7 @@ class ErrorBoundaryImpl extends React.Component {
|
||||||
stack += `${this.state.niceStack}\n\n`;
|
stack += `${this.state.niceStack}\n\n`;
|
||||||
}
|
}
|
||||||
stack += `${this.state.originalStack}\n`;
|
stack += `${this.state.originalStack}\n`;
|
||||||
navigator.clipboard.writeText(stack);
|
copyToClipboard(stack);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUnsupportedIndexedDB() {
|
renderUnsupportedIndexedDB() {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ import { Trans, useTranslation } from "react-i18next";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
import { useRemark } from "react-remark";
|
import { useRemark } from "react-remark";
|
||||||
import styled from "@emotion/styled";
|
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 { formatMessage, formatTitle, isImage } from "../app/notificationUtils";
|
||||||
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
import { LightboxBackdrop, Paragraph, VerticallyCenteredContainer } from "./styles";
|
||||||
import subscriptionManager from "../app/SubscriptionManager";
|
import subscriptionManager from "../app/SubscriptionManager";
|
||||||
|
|
@ -239,7 +242,7 @@ const NotificationItem = (props) => {
|
||||||
await subscriptionManager.markNotificationRead(notification.id);
|
await subscriptionManager.markNotificationRead(notification.id);
|
||||||
};
|
};
|
||||||
const handleCopy = (s) => {
|
const handleCopy = (s) => {
|
||||||
navigator.clipboard.writeText(s);
|
copyToClipboard(s);
|
||||||
props.onShowSnack();
|
props.onShowSnack();
|
||||||
};
|
};
|
||||||
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
const expired = attachment && attachment.expires && attachment.expires < Date.now() / 1000;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue