diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75052ae..b0c88df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,12 +69,21 @@ jobs: > **Windows note:** The installer is not yet code-signed. Windows SmartScreen will show an "Unknown publisher" warning — click "More info → Run anyway" to install. - ### New in v0.8.2 — Writing & Reading Polish - - **No more 280-char limit** — Nostr has no protocol limit; compose freely up to 4000 chars with soft warnings, never blocked - - **Serif reading font** — article reader uses Georgia/serif at 17px for comfortable long-form reading; code blocks stay monospace - - **Reading progress bar** — thin accent-colored bar at the top of articles tracks your scroll position - - **Article table of contents** — floating TOC in the right margin on wide screens; parses h2/h3 headings, highlights active section, click to scroll; hidden on narrow screens - - **Connection indicator fix** — data-aware connectivity checking with 25s grace period; no more false "offline" on startup + ### New in v0.8.3 — Trending, Remote Signer, Media + - **Trending feed polish** — 24h time window with engagement decay scoring; articles (kind 30023) appear alongside notes in trending; better empty state + - **NIP-46 remote signer** — connect via bunker:// URI (nsecBunker, Amber, etc.); session persistence across restarts; third login tab in onboarding and add-account modal + - **Media feed** — new "Media" view in sidebar; All/Videos/Images/Audio tabs filter notes by embedded media type + - **Profile media gallery** — new "Media" tab on profiles shows a grid of the user's images, videos, and audio; images open in lightbox, videos/audio navigate to thread + - **Syntax highlighting** — code blocks in notes and articles render with syntax highlighting + - **OS push notifications** — background poller (60s) for mentions, zaps, new followers; each type independently toggleable + - **Zen writing mode** — distraction-free article editing with auto-save indicator + - **NIP-05 verification badges** — cached verification with green checkmark on note cards + - **Dedicated hashtag pages** — clicking #tag opens a live feed, not generic search + - **Keyword muting** — word/phrase mute list, client-side filtering across all views + - **Follow suggestion dismissal** — persistent "don't suggest again" per person + + ### Previous: v0.8.2 — Writing & Reading Polish + - No more 280-char limit, serif reading font, reading progress bar, article TOC, connection indicator fix ### Previous: v0.8.0 — Polish, Portability & Discovery - Profile banner polish, data export, relay recommendations, reading list tracking, trending hashtags diff --git a/CLAUDE.md b/CLAUDE.md index 5e7ef03..8a5ec03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR - `src/lib/nostr/relayHealth.ts` — Relay health checker (NIP-11, latency probing, status classification) - `src/components/article/` — ArticleEditor, ArticleView, ArticleFeed, ArticleCard, MarkdownToolbar (NIP-23) - `src/components/bookmark/` — BookmarkView +- `src/components/media/` — MediaFeed (media discovery with tab filtering) - `src/components/zap/` — ZapModal - `src/components/onboarding/` — OnboardingFlow (welcome, create key, backup, login) - `src/components/shared/` — RelaysView (relay health dashboard + recommendations), SettingsView (NWC + identity + data export) @@ -125,9 +126,17 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR - Media players (video/audio inline, YouTube/Vimeo/Spotify cards) - Multi-account switcher with keychain-backed session restore - System tray, keyboard shortcuts, auto-updater +- **NIP-05 verification badges** — cached verification with green checkmark on note cards +- **Dedicated hashtag pages** — clicking #tag opens a live feed, not generic search +- **Keyword muting** — word/phrase mute list, client-side filtering across all views +- **Follow suggestion dismissal** — persistent "don't suggest again" per person +- **Background notification poller** — 60s polling for mentions, zaps, new followers; each type independently toggleable +- **Trending feed polish** — 24h time window, time decay scoring, articles mixed with notes +- **NIP-46 remote signer** — bunker:// URI login, session persistence via toPayload/fromPayload, account switching +- **Media feed** — dedicated "Media" view with All/Videos/Images/Audio tabs; filters notes by embedded media type +- **Profile media gallery** — "Media" tab on profiles with grid layout; images open lightbox, videos/audio navigate to thread **Not yet implemented:** - Web of Trust scoring -- NIP-46 remote signer - NIP-96 file storage - Custom feeds / lists diff --git a/PKGBUILD b/PKGBUILD index 6d6dfe4..2732868 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: hoornet pkgname=wrystr -pkgver=0.8.2 +pkgver=0.8.3 pkgrel=1 pkgdesc="Cross-platform Nostr desktop client with Lightning integration" arch=('x86_64') diff --git a/package.json b/package.json index 04387a1..aa192da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wrystr", "private": true, - "version": "0.8.2", + "version": "0.8.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 468cad8..c36feb1 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -234,28 +234,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "base64" version = "0.21.7" @@ -436,8 +414,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -492,15 +468,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - [[package]] name = "combine" version = "4.6.7" @@ -1116,12 +1083,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futf" version = "0.1.5" @@ -1996,16 +1957,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.91" @@ -2200,6 +2151,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2252,16 +2215,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minisign-verify" version = "0.2.5" @@ -2352,6 +2305,20 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2822,7 +2789,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -2978,6 +2945,15 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3013,7 +2989,6 @@ version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ - "aws-lc-rs", "bytes", "getrandom 0.3.4", "lru-slab", @@ -3319,10 +3294,8 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime_guess", "percent-encoding", "pin-project-lite", - "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -3428,7 +3401,6 @@ version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ - "aws-lc-rs", "once_cell", "ring", "rustls-pki-types", @@ -3492,7 +3464,6 @@ version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4325,6 +4296,25 @@ dependencies = [ "urlpattern", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.5.3" @@ -4490,6 +4480,18 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows", + "windows-version", +] + [[package]] name = "tempfile" version = "3.26.0" @@ -4932,12 +4934,6 @@ dependencies = [ "unic-common", ] -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -5936,11 +5932,9 @@ dependencies = [ [[package]] name = "wrystr" -version = "0.6.0" +version = "0.8.2" dependencies = [ "keyring", - "mime_guess", - "reqwest 0.13.2", "rusqlite", "serde", "serde_json", @@ -5949,6 +5943,7 @@ dependencies = [ "tauri-plugin-dialog", "tauri-plugin-fs", "tauri-plugin-http", + "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-updater", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c0171d6..0688ffa 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wrystr" -version = "0.8.2" +version = "0.8.3" description = "Cross-platform Nostr desktop client with Lightning integration" authors = ["hoornet"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9226669..e1626eb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Wrystr", - "version": "0.8.2", + "version": "0.8.3", "identifier": "com.hoornet.wrystr", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index bee548d..4e60b2a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { ThreadView } from "./components/thread/ThreadView"; import { ArticleEditor } from "./components/article/ArticleEditor"; import { ArticleView } from "./components/article/ArticleView"; import { ArticleFeed } from "./components/article/ArticleFeed"; +import { MediaFeed } from "./components/media/MediaFeed"; import { OnboardingFlow } from "./components/onboarding/OnboardingFlow"; import { AboutView } from "./components/shared/AboutView"; import { ZapHistoryView } from "./components/zap/ZapHistoryView"; @@ -73,6 +74,7 @@ function App() { {currentView === "profile" && } {currentView === "thread" && } {currentView === "articles" && } + {currentView === "media" && } {currentView === "article-editor" && } {currentView === "article" && } {currentView === "about" && } diff --git a/src/components/feed/Feed.tsx b/src/components/feed/Feed.tsx index b0bf743..22f451e 100644 --- a/src/components/feed/Feed.tsx +++ b/src/components/feed/Feed.tsx @@ -6,6 +6,7 @@ import { useUIStore } from "../../stores/ui"; import { fetchFollowFeed, getNDK } from "../../lib/nostr"; import { detectScript, getEventLanguageTag, FILTER_SCRIPTS } from "../../lib/language"; import { NoteCard } from "./NoteCard"; +import { ArticleCard } from "../article/ArticleCard"; import { ComposeBox } from "./ComposeBox"; import { SkeletonNoteList } from "../shared/Skeleton"; import { NDKEvent } from "@nostr-dev-kit/ndk"; @@ -168,25 +169,33 @@ export function Feed() { {!isLoading && filteredNotes.length === 0 && (

- {isFollowing && follows.length === 0 - ? "You're not following anyone yet." - : feedLanguageFilter - ? `No ${feedLanguageFilter} notes found.` - : "No notes to show."} + {isTrending + ? "No trending notes right now." + : isFollowing && follows.length === 0 + ? "You're not following anyone yet." + : feedLanguageFilter + ? `No ${feedLanguageFilter} notes found.` + : "No notes to show."}

- {isFollowing && follows.length === 0 - ? "Use search to find people to follow." - : feedLanguageFilter - ? "Try clearing the script filter or refreshing." - : "Try refreshing or switching tabs."} + {isTrending + ? "Check back in a bit." + : isFollowing && follows.length === 0 + ? "Use search to find people to follow." + : feedLanguageFilter + ? "Try clearing the script filter or refreshing." + : "Try refreshing or switching tabs."}

)} - {filteredNotes.map((event, index) => ( - - ))} + {filteredNotes.map((event, index) => + event.kind === 30023 ? ( + + ) : ( + + ) + )} ); diff --git a/src/components/media/MediaFeed.tsx b/src/components/media/MediaFeed.tsx new file mode 100644 index 0000000..1fd6b3b --- /dev/null +++ b/src/components/media/MediaFeed.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import { fetchGlobalFeed } from "../../lib/nostr"; +import { parseContent, ContentSegment } from "../../lib/parsing"; +import { NoteCard } from "../feed/NoteCard"; +import { SkeletonNoteList } from "../shared/Skeleton"; + +type MediaTab = "all" | "videos" | "images" | "audio"; + +const MEDIA_TYPES: Record = { + all: ["image", "video", "audio", "youtube", "vimeo", "spotify", "tidal"], + videos: ["video", "youtube", "vimeo"], + images: ["image"], + audio: ["audio", "spotify", "tidal"], +}; + +function hasMediaType(content: string, types: ContentSegment["type"][]): boolean { + const segments = parseContent(content); + return segments.some((s) => types.includes(s.type)); +} + +export function MediaFeed() { + const [allNotes, setAllNotes] = useState([]); + const [loading, setLoading] = useState(true); + const [tab, setTab] = useState("all"); + + useEffect(() => { + setLoading(true); + fetchGlobalFeed(300) + .then((notes) => { + const mediaNotes = notes.filter((n) => hasMediaType(n.content, MEDIA_TYPES.all)); + setAllNotes(mediaNotes); + }) + .catch(() => setAllNotes([])) + .finally(() => setLoading(false)); + }, []); + + const filtered = tab === "all" + ? allNotes + : allNotes.filter((n) => hasMediaType(n.content, MEDIA_TYPES[tab])); + + return ( +
+
+
+

Media

+ {(["all", "videos", "images", "audio"] as const).map((t) => ( + + ))} +
+
+ +
+ {loading && } + + {!loading && filtered.length === 0 && ( +
+

+ No {tab === "all" ? "media" : tab} found. +

+

+ Try switching tabs or check back later. +

+
+ )} + + {filtered.map((event) => ( + + ))} +
+
+ ); +} diff --git a/src/components/onboarding/OnboardingFlow.tsx b/src/components/onboarding/OnboardingFlow.tsx index cea5875..238985d 100644 --- a/src/components/onboarding/OnboardingFlow.tsx +++ b/src/components/onboarding/OnboardingFlow.tsx @@ -187,8 +187,8 @@ function BackupStep({ signer, onComplete }: { signer: NDKPrivateKeySigner; onCom // ─── Step: Login with existing key ─────────────────────────────────────────── function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () => void }) { - const { loginWithNsec, loginWithPubkey, loginError, loggedIn } = useUserStore(); - const [mode, setMode] = useState<"nsec" | "npub">("nsec"); + const { loginWithNsec, loginWithPubkey, loginWithRemoteSigner, loginError, loggedIn } = useUserStore(); + const [mode, setMode] = useState<"nsec" | "npub" | "bunker">("nsec"); const [value, setValue] = useState(""); const [loading, setLoading] = useState(false); @@ -201,6 +201,8 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () setLoading(true); if (mode === "nsec") { await loginWithNsec(value.trim()); + } else if (mode === "bunker") { + await loginWithRemoteSigner(value.trim()); } else { await loginWithPubkey(value.trim()); } @@ -211,12 +213,15 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () if (e.key === "Enter") handleLogin(); }; + const tabLabel = (m: "nsec" | "npub" | "bunker") => + m === "nsec" ? "Secret key" : m === "npub" ? "Public key" : "Remote signer"; + return ( Log in with your key.
- {(["nsec", "npub"] as const).map((m) => ( + {(["nsec", "npub", "bunker"] as const).map((m) => ( ))}
@@ -233,7 +238,7 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () value={value} onChange={(e) => setValue(e.target.value)} onKeyDown={handleKeyDown} - placeholder={mode === "nsec" ? "nsec1…" : "npub1…"} + placeholder={mode === "nsec" ? "nsec1…" : mode === "npub" ? "npub1…" : "bunker://…"} autoFocus className="w-full bg-bg border border-border px-3 py-2 text-text text-[12px] font-mono focus:outline-none focus:border-accent/50 placeholder:text-text-dim mb-2" style={{ WebkitUserSelect: "text", userSelect: "text" } as React.CSSProperties} @@ -242,6 +247,9 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () {mode === "npub" && (

Read-only mode — you can browse but not post, react, or zap.

)} + {mode === "bunker" && ( +

Connect to nsecBunker, Amber, or similar. Paste your bunker:// URI.

+ )} {loginError &&

{loginError}

} @@ -251,7 +259,7 @@ function LoginStep({ onBack, onComplete }: { onBack: () => void; onComplete: () disabled={!value.trim() || loading} className="w-full py-2.5 text-[13px] font-medium bg-accent hover:bg-accent-hover text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed" > - {loading ? "Logging in…" : "Log in"} + {loading ? (mode === "bunker" ? "Connecting…" : "Logging in…") : "Log in"} - - + {(["nsec", "pubkey", "bunker", "new"] as const).map((t) => ( + + ))} {/* Content */} @@ -165,14 +153,16 @@ export function LoginModal({ onClose }: LoginModalProps) { setInput(e.target.value)} onKeyDown={handleKeyDown} - placeholder={tab === "nsec" ? "nsec1…" : "npub1…"} + placeholder={tab === "nsec" ? "nsec1…" : tab === "pubkey" ? "npub1…" : "bunker://…"} autoFocus className="w-full bg-bg border border-border px-3 py-2 text-text text-[13px] font-mono placeholder:text-text-dim focus:outline-none focus:border-accent/50" /> @@ -189,16 +179,24 @@ export function LoginModal({ onClose }: LoginModalProps) {

)} + {tab === "bunker" && ( +

+ Connect to nsecBunker, Amber, or similar. Your keys never leave the signer. +

+ )} + {loginError && (

{loginError}

)} )} diff --git a/src/components/sidebar/AccountSwitcher.tsx b/src/components/sidebar/AccountSwitcher.tsx index 22effdb..9a5eca7 100644 --- a/src/components/sidebar/AccountSwitcher.tsx +++ b/src/components/sidebar/AccountSwitcher.tsx @@ -98,6 +98,9 @@ export function AccountSwitcher() { > {displayName(a)} + {a.loginType === "remote-signer" && ( + NIP-46 + )}