1
0
Fork 0
mirror of https://github.com/binwiederhier/ntfy.git synced 2025-11-30 12:20:15 +01:00
This commit is contained in:
Simon Ramsay 2025-11-22 04:29:15 +00:00 committed by GitHub
commit 5dc35c53cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 106 additions and 4 deletions

View file

@ -1,7 +1,7 @@
MAKEFLAGS := --jobs=1
PYTHON := python3
PIP := pip3
VERSION := $(shell git describe --tag)
VERSION := 1111
COMMIT := $(shell git rev-parse --short HEAD)
.PHONY:

View file

@ -636,6 +636,16 @@ func (s *Server) handleWebManifest(w http.ResponseWriter, _ *http.Request, _ *vi
{SRC: "/static/images/pwa-192x192.png", Sizes: "192x192", Type: "image/png"},
{SRC: "/static/images/pwa-512x512.png", Sizes: "512x512", Type: "image/png"},
},
ShareTarget: &webManifestShareTarget{
Action: "/share-target",
Method: "POST",
Enctype: "multipart/form-data",
Params: &webManifestShareParams{
Title: "title",
Text: "text",
Url: "url",
},
},
}
return s.writeJSONWithContentType(w, response, "application/manifest+json")
}

View file

@ -587,6 +587,8 @@ type webManifestResponse struct {
BackgroundColor string `json:"background_color"`
ThemeColor string `json:"theme_color"`
Icons []*webManifestIcon `json:"icons"`
// Optional PWA share_target support
ShareTarget *webManifestShareTarget `json:"share_target,omitempty"`
}
type webManifestIcon struct {
@ -594,3 +596,16 @@ type webManifestIcon struct {
Sizes string `json:"sizes"`
Type string `json:"type"`
}
type webManifestShareTarget struct {
Action string `json:"action"`
Method string `json:"method"`
Enctype string `json:"enctype"`
Params *webManifestShareParams `json:"params"`
}
type webManifestShareParams struct {
Title string `json:"title,omitempty"`
Text string `json:"text,omitempty"`
Url string `json:"url,omitempty"`
}

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"scripts": {
"start": "NODE_OPTIONS=\"--enable-source-maps\" vite",
"start": "NODE_OPTIONS=\"--enable-source-maps\" vite --host",
"build": "vite build",
"serve": "vite preview",
"format": "prettier . --write",

View file

@ -221,6 +221,40 @@ self.addEventListener("notificationclick", (event) => {
event.waitUntil(handleClick(event));
});
// Handle incoming Share Target POSTs from installed PWA share action.
// The share_target in the manifest posts to `/share-target` as a `multipart/form-data` POST.
// We parse the form and redirect to the app with query params so the client can pick them up.
self.addEventListener("fetch", (event) => {
try {
const reqUrl = new URL(event.request.url);
if (reqUrl.pathname === "/share-target" && event.request.method === "POST") {
event.respondWith(
(async () => {
try {
const formData = await event.request.formData();
const title = formData.get("title") || "";
const text = formData.get("text") || "";
const sharedUrl = formData.get("url") || "";
const params = new URLSearchParams();
if (title) params.set("share_title", title);
if (text) params.set("share_text", text);
if (sharedUrl) params.set("share_url", sharedUrl);
// todo support files?
// redirect to app.html which is the SPA entry used by the service worker
const redirectUrl = `/app.html?${params.toString()}`;
return Response.redirect(redirectUrl, 303);
} catch (e) {
return new Response("", { status: 500 });
}
})()
);
}
// other fetches are not handled here
} catch (e) {
// ignore malformed urls
}
});
// See https://vite-pwa-org.netlify.app/guide/inject-manifest.html#service-worker-code
// self.__WB_MANIFEST is the workbox injection point that injects the manifest of the
// vite dist files and their revision ids, for example:

View file

@ -104,6 +104,7 @@ const Layout = () => {
const { account, setAccount } = useContext(AccountContext);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
const [sharePayload, setSharePayload] = useState(null);
const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all());
const webPushTopics = useWebPushTopics();
@ -120,6 +121,29 @@ const Layout = () => {
useBackgroundProcesses();
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
// Check URL for share-target params inserted by the service worker redirect
useEffect(() => {
try {
const params = new URLSearchParams(window.location.search);
const title = params.get("share_title");
const text = params.get("share_text");
const url = params.get(" share_url");
if (title || text || url) {
const payload = {
title: title || undefined,
message: text || undefined,
url: url || undefined,
};
setSharePayload(payload);
setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT);
// remove params to avoid repeated triggers
window.history.replaceState({}, document.title, window.location.pathname + window.location.hash);
}
} catch (e) {
// ignore
}
}, []);
return (
<Box sx={{ display: "flex" }}>
<ActionBar selected={selected} onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)} />
@ -139,7 +163,12 @@ const Layout = () => {
}}
/>
</Main>
<Messaging selected={selected} dialogOpenMode={sendDialogOpenMode} onDialogOpenModeChange={setSendDialogOpenMode} />
<Messaging
selected={selected}
dialogOpenMode={sendDialogOpenMode}
onDialogOpenModeChange={setSendDialogOpenMode}
sharePayload={sharePayload}
/>
</Box>
);
};

View file

@ -53,7 +53,9 @@ const Messaging = (props) => {
openMode={dialogOpenMode}
baseUrl={subscription?.baseUrl ?? config.base_url}
topic={subscription?.topic ?? ""}
message={message}
message={props.sharePayload?.message ?? message}
title={props.sharePayload?.title}
clickUrl={props.sharePayload?.url}
attachFile={attachFile}
getPastedImage={getPastedImage}
onClose={handleDialogClose}

View file

@ -109,6 +109,18 @@ const PublishDialog = (props) => {
setMessage(props.message);
}, [props.message]);
useEffect(() => {
if (typeof props.title !== "undefined") {
setTitle(props.title || "");
}
}, [props.title]);
useEffect(() => {
if (typeof props.clickUrl !== "undefined") {
setClickUrl(props.clickUrl || "");
}
}, [props.clickUrl]);
const updateBaseUrl = (newVal) => {
if (validUrl(newVal)) {
setBaseUrl(newVal.replace(/\/$/, "")); // strip traililng slash after https?://