Implement native sharing
parent
51fc5952bc
commit
ed4cbd5ae9
|
@ -12,6 +12,12 @@ functions:
|
||||||
events:
|
events:
|
||||||
- share:options
|
- share:options
|
||||||
|
|
||||||
|
handleShareTarget:
|
||||||
|
path: share.ts:handleShareTarget
|
||||||
|
env: client
|
||||||
|
events:
|
||||||
|
- http:request:/share_target
|
||||||
|
|
||||||
clipboardMarkdownShare:
|
clipboardMarkdownShare:
|
||||||
path: share.ts:clipboardMarkdownShare
|
path: share.ts:clipboardMarkdownShare
|
||||||
events:
|
events:
|
||||||
|
@ -31,3 +37,10 @@ functions:
|
||||||
path: publish.ts:publishShare
|
path: publish.ts:publishShare
|
||||||
events:
|
events:
|
||||||
- share:publish
|
- share:publish
|
||||||
|
|
||||||
|
config:
|
||||||
|
# Built-in configuration schemas
|
||||||
|
schema.config.properties:
|
||||||
|
shareTargetPage:
|
||||||
|
type: string
|
||||||
|
format: page-ref
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
editor,
|
editor,
|
||||||
events,
|
events,
|
||||||
markdown,
|
markdown,
|
||||||
|
space,
|
||||||
system,
|
system,
|
||||||
} from "@silverbulletmd/silverbullet/syscalls";
|
} from "@silverbulletmd/silverbullet/syscalls";
|
||||||
import { findNodeOfType, renderToText } from "../../plug-api/lib/tree.ts";
|
import { findNodeOfType, renderToText } from "../../plug-api/lib/tree.ts";
|
||||||
|
@ -11,6 +12,11 @@ import {
|
||||||
encodePageURI,
|
encodePageURI,
|
||||||
parsePageRef,
|
parsePageRef,
|
||||||
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||||
|
import type { EndpointRequest } from "@silverbulletmd/silverbullet/types";
|
||||||
|
import { localDateString } from "$lib/dates.ts";
|
||||||
|
import { cleanPageRef } from "@silverbulletmd/silverbullet/lib/resolve";
|
||||||
|
import { builtinFunctions } from "$lib/builtin_query_functions.ts";
|
||||||
|
import { renderTheTemplate } from "$common/syscalls/template.ts";
|
||||||
|
|
||||||
type ShareOption = {
|
type ShareOption = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -134,3 +140,97 @@ export async function clipboardRichTextShare(text: string) {
|
||||||
await editor.copyToClipboard(new Blob([html], { type: "text/html" }));
|
await editor.copyToClipboard(new Blob([html], { type: "text/html" }));
|
||||||
await editor.flashNotification("Copied to rich text to clipboard!");
|
await editor.flashNotification("Copied to rich text to clipboard!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseMultipartFormData(body: string, boundary: string) {
|
||||||
|
const parts = body.split(`--${boundary}`);
|
||||||
|
return parts.slice(1, -1).map((part) => {
|
||||||
|
const [headers, content] = part.split("\r\n\r\n");
|
||||||
|
const nameMatch = headers.match(/name="([^"]+)"/);
|
||||||
|
if (!nameMatch) {
|
||||||
|
throw new Error("Could not parse form field name");
|
||||||
|
}
|
||||||
|
const name = nameMatch[1];
|
||||||
|
const value = content.trim();
|
||||||
|
return { name, value };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
export async function handleShareTarget(request: EndpointRequest) {
|
||||||
|
console.log("Share target received:", {
|
||||||
|
method: request.method,
|
||||||
|
headers: request.headers,
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse multipart form data
|
||||||
|
const contentType = request.headers["content-type"];
|
||||||
|
if (!contentType) {
|
||||||
|
throw new Error(
|
||||||
|
`No content type found in ${JSON.stringify(request.headers)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const boundary = contentType.split("boundary=")[1];
|
||||||
|
if (!boundary) {
|
||||||
|
throw new Error(`No multipart boundary found in ${contentType}`);
|
||||||
|
}
|
||||||
|
const formData = parseMultipartFormData(request.body, boundary);
|
||||||
|
const { title = "", text = "", url = "" } = formData.reduce(
|
||||||
|
(acc: Record<string, string>, curr: { name: string; value: string }) => {
|
||||||
|
acc[curr.name] = curr.value;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Format the shared content
|
||||||
|
const timestamp = localDateString(new Date());
|
||||||
|
const sharedContent = `\n\n## ${title}
|
||||||
|
${text}
|
||||||
|
${url ? `URL: ${url}` : ""}\nAdded at ${timestamp}`;
|
||||||
|
|
||||||
|
// Get the target page from space config, with fallback
|
||||||
|
let targetPage = "Inbox";
|
||||||
|
try {
|
||||||
|
targetPage = cleanPageRef(
|
||||||
|
await renderTheTemplate(
|
||||||
|
await system.getSpaceConfig("shareTargetPage", "Inbox"),
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
builtinFunctions,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error parsing share target page from config", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read existing page content
|
||||||
|
let currentContent = "";
|
||||||
|
try {
|
||||||
|
currentContent = await space.readPage(targetPage);
|
||||||
|
} catch (_e) {
|
||||||
|
// If page doesn't exist, create it with a header
|
||||||
|
currentContent = `# ${targetPage}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the new content
|
||||||
|
const newContent = currentContent + sharedContent;
|
||||||
|
|
||||||
|
// Write the updated content back to the page
|
||||||
|
await space.writePage(targetPage, newContent);
|
||||||
|
|
||||||
|
// Return a redirect response to the target page
|
||||||
|
return {
|
||||||
|
status: 303, // "See Other" redirect
|
||||||
|
headers: {
|
||||||
|
"Location": `/${targetPage}`,
|
||||||
|
},
|
||||||
|
body: "Content shared successfully",
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error("Error handling share:", e);
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: "Error processing share: " + e.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,5 +14,15 @@
|
||||||
"display_override": ["window-controls-overlay"],
|
"display_override": ["window-controls-overlay"],
|
||||||
"scope": "/",
|
"scope": "/",
|
||||||
"theme_color": "#e1e1e1",
|
"theme_color": "#e1e1e1",
|
||||||
"description": "Markdown as a platform"
|
"description": "Markdown as a platform",
|
||||||
|
"share_target": {
|
||||||
|
"action": "/_/share_target",
|
||||||
|
"method": "POST",
|
||||||
|
"enctype": "multipart/form-data",
|
||||||
|
"params": {
|
||||||
|
"title": "title",
|
||||||
|
"text": "text",
|
||||||
|
"url": "url"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ An attempt at documenting the changes/new features introduced in each release.
|
||||||
|
|
||||||
## Edge
|
## Edge
|
||||||
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
|
_These features are not yet properly released, you need to use [the edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27) to try them._
|
||||||
|
* Native [[Share]] functionality, allowing you to use your OS'es native share functionality to share data with SilverBullet.
|
||||||
|
Target page can be configured as `shareTargetPage` in [[^SETTINGS]].
|
||||||
|
|
||||||
* (Security) Implemented a lockout mechanism after a number of failed login attempts for [[Authentication]] (configured via [[Install/Configuration#Authentication]]) (by [Peter Weston](https://github.com/silverbulletmd/silverbullet/pull/1152))
|
* (Security) Implemented a lockout mechanism after a number of failed login attempts for [[Authentication]] (configured via [[Install/Configuration#Authentication]]) (by [Peter Weston](https://github.com/silverbulletmd/silverbullet/pull/1152))
|
||||||
|
|
||||||
|
|
|
@ -82,4 +82,8 @@ emoji:
|
||||||
aliases:
|
aliases:
|
||||||
smile: 😀
|
smile: 😀
|
||||||
sweat_smile: 😅
|
sweat_smile: 😅
|
||||||
|
|
||||||
|
# Share Configuration
|
||||||
|
# Page where shared content will be stored (defaults to "Shared Items")
|
||||||
|
shareTargetPage: "Inbox"
|
||||||
```
|
```
|
||||||
|
|
Loading…
Reference in New Issue