diff --git a/package-lock.json b/package-lock.json
index 11ffee4..e6f00dd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "wrystr",
- "version": "0.6.0",
+ "version": "0.8.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wrystr",
- "version": "0.6.0",
+ "version": "0.8.2",
"dependencies": {
"@nostr-dev-kit/ndk": "^3.0.3",
"@tailwindcss/vite": "^4.2.1",
@@ -14,12 +14,15 @@
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-http": "^2.5.7",
+ "@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.0",
"@types/dompurify": "^3.0.5",
"dompurify": "^3.3.2",
+ "highlight.js": "^11.11.1",
"marked": "^17.0.4",
+ "marked-highlight": "^2.2.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-qr-code": "^2.0.18",
@@ -2152,6 +2155,15 @@
"@tauri-apps/api": "^2.10.1"
}
},
+ "node_modules/@tauri-apps/plugin-notification": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
+ "integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==",
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@tauri-apps/api": "^2.8.0"
+ }
+ },
"node_modules/@tauri-apps/plugin-opener": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz",
@@ -3085,6 +3097,15 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/highlight.js": {
+ "version": "11.11.1",
+ "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
+ "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/html-encoding-sniffer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
@@ -3561,6 +3582,15 @@
"node": ">= 20"
}
},
+ "node_modules/marked-highlight": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.3.tgz",
+ "integrity": "sha512-FCfZRxW/msZAiasCML4isYpxyQWKEEx44vOgdn5Kloae+Qc3q4XR7WjpKKf8oMLk7JP9ZCRd2vhtclJFdwxlWQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "marked": ">=4 <18"
+ }
+ },
"node_modules/mdast-util-to-hast": {
"version": "13.2.1",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz",
diff --git a/package.json b/package.json
index 2bb7244..04387a1 100644
--- a/package.json
+++ b/package.json
@@ -18,12 +18,15 @@
"@tauri-apps/plugin-dialog": "^2.6.0",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-http": "^2.5.7",
+ "@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.0",
"@types/dompurify": "^3.0.5",
"dompurify": "^3.3.2",
+ "highlight.js": "^11.11.1",
"marked": "^17.0.4",
+ "marked-highlight": "^2.2.3",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-qr-code": "^2.0.18",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index b619151..c0171d6 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -29,4 +29,5 @@ rusqlite = { version = "0.32", features = ["bundled"] }
tauri-plugin-http = "2.5.7"
tauri-plugin-dialog = "2.6.0"
tauri-plugin-fs = "2.4.5"
+tauri-plugin-notification = "2.3.3"
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index db07f97..10694a8 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -20,6 +20,7 @@
"allow": [
{ "path": "**" }
]
- }
+ },
+ "notification:default"
]
}
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 6207123..121ca77 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -144,6 +144,7 @@ pub fn run() {
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
+ .plugin(tauri_plugin_notification::init())
.setup(|app| {
// ── SQLite ───────────────────────────────────────────────────────
let data_dir = app.path().app_data_dir()?;
diff --git a/src/components/article/ArticleEditor.tsx b/src/components/article/ArticleEditor.tsx
index 0b492bb..4068a3c 100644
--- a/src/components/article/ArticleEditor.tsx
+++ b/src/components/article/ArticleEditor.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, useCallback } from "react";
-import { marked } from "marked";
+import { renderMarkdown } from "../../lib/markdown";
import { publishArticle } from "../../lib/nostr";
import { useUIStore } from "../../stores/ui";
import { MarkdownToolbar, handleEditorKeyDown } from "./MarkdownToolbar";
@@ -100,7 +100,7 @@ export function ArticleEditor() {
return () => clearInterval(iv);
}, [lastSaved]);
- const renderedHtml = marked(content || "*Nothing to preview yet.*") as string;
+ const renderedHtml = renderMarkdown(content || "*Nothing to preview yet.*");
const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0;
const canPublish = title.trim().length > 0 && content.trim().length > 0;
diff --git a/src/components/article/ArticleView.tsx b/src/components/article/ArticleView.tsx
index 5ce661a..ed2ebd2 100644
--- a/src/components/article/ArticleView.tsx
+++ b/src/components/article/ArticleView.tsx
@@ -1,7 +1,6 @@
import { useEffect, useState, useRef, useCallback } from "react";
import { NDKEvent } from "@nostr-dev-kit/ndk";
-import { marked } from "marked";
-import DOMPurify from "dompurify";
+import { renderMarkdown } from "../../lib/markdown";
import { useUIStore } from "../../stores/ui";
import { useUserStore } from "../../stores/user";
import { useBookmarkStore } from "../../stores/bookmark";
@@ -27,11 +26,6 @@ function getTags(event: NDKEvent, name: string): string[] {
return event.tags.filter((t) => t[0] === name).map((t) => t[1]).filter(Boolean);
}
-function renderMarkdown(md: string): string {
- const html = marked(md, { breaks: true }) as string;
- return DOMPurify.sanitize(html, { ADD_ATTR: ["id"] });
-}
-
// ── Author row ────────────────────────────────────────────────────────────────
function AuthorRow({ pubkey, publishedAt, readingTime }: { pubkey: string; publishedAt: number | null; readingTime?: number }) {
diff --git a/src/components/shared/SettingsView.tsx b/src/components/shared/SettingsView.tsx
index 7422839..da67c2f 100644
--- a/src/components/shared/SettingsView.tsx
+++ b/src/components/shared/SettingsView.tsx
@@ -7,6 +7,7 @@ import { useBookmarkStore } from "../../stores/bookmark";
import { getNDK, getStoredRelayUrls, addRelay, removeRelay, publishRelayList } from "../../lib/nostr";
import { useProfile } from "../../hooks/useProfile";
import { NWCWizard } from "./NWCWizard";
+import { getNotificationSettings, saveNotificationSettings, ensurePermission } from "../../lib/notifications";
function MutedRow({ pubkey, onUnmute }: { pubkey: string; onUnmute: () => void }) {
const profile = useProfile(pubkey);
@@ -263,6 +264,54 @@ function ExportSection() {
);
}
+function NotificationSection() {
+ const [settings, setSettings] = useState(getNotificationSettings);
+
+ const toggle = (key: "mentions" | "dms" | "zaps") => {
+ const next = { ...settings, [key]: !settings[key] };
+ setSettings(next);
+ saveNotificationSettings(next);
+ // Request permission on first enable
+ if (next[key]) ensurePermission().catch(() => {});
+ };
+
+ const items: Array<{ key: "mentions" | "dms" | "zaps"; label: string }> = [
+ { key: "mentions", label: "Mentions" },
+ { key: "dms", label: "Direct messages" },
+ { key: "zaps", label: "Zaps received" },
+ ];
+
+ return (
+
+ OS-level push notifications. Requires system permission.
+
+ Notifications
+
+