diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8fb4eae..fc573b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,18 +69,15 @@ 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.9.2 — Relay Status, Toasts & Debug Tools - Better visibility into what's happening under the hood. - - **Relay status badge** — compact "8/12 relays" indicator in feed header with color coding (green/yellow/red); hover for per-relay connection details - - **Toast notifications** — transient messages for connection events: "Connection lost", "Back online", "Resetting relay connections", "Relays reconnected" - - **Per-tab "last updated" timestamp** — shows how fresh each feed tab is; Global/Following/Trending tracked independently - - **Subscription debug panel** — Ctrl+Shift+D toggles a hidden panel showing NDK uptime, live subscription status, per-relay state, feed timestamps, and recent diagnostics log - - **Consolidated relay management** — all relay controls (add, remove, health check, publish list, recommendations) now in one Relays view; removed duplicate relay section from Settings - - **Per-relay remove button** — hover any relay card to remove it individually - - **Full NIP badge display** — relay cards now show all supported NIPs, not just a filtered subset - - **Status dot tooltips** — hover relay status dots for explanations (Online, Slow, Offline, Awaiting check) - - **Wider scrollbar** — 12px for easier mouse dragging on long threads - - **Acknowledgements section** — added to README + ### New in v0.9.3 — Themes, Font Size & Settings Polish + Make Wrystr yours. + - **7 color themes** — Midnight (default), Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman; instant switching from Settings + - **Font size presets** — Small / Normal / Large / Extra Large; scales the entire UI uniformly + - **Collapsible muted accounts** — muted user list collapsed by default in Settings; click to expand + - **Removed old connection indicator** — sidebar offline/online dot removed in favor of the relay status badge in feed header + + ### Previous: v0.9.2 — Relay Status, Toasts & Debug Tools + - Relay status badge, toast notifications, per-tab timestamps, debug panel (Ctrl+Shift+D), consolidated relay management, wider scrollbar ### Previous: v0.9.1 — Live Feed & Relay Reliability - Live streaming feed, timeouts on all relay fetches, fixed relay death spiral, NDK subscription hygiene, feed diagnostics, background relay recovery diff --git a/CLAUDE.md b/CLAUDE.md index 2112564..1221319 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR - `src/App.tsx` — root component; shows `OnboardingFlow` for new users, then view routing via UI store - `src/stores/` — Zustand stores per domain: `feed.ts`, `user.ts`, `ui.ts`, `lightning.ts`, `drafts.ts`, `relayHealth.ts`, `bookmark.ts`, `toast.ts` - `src/lib/nostr/` — NDK wrapper split into domain modules (`core.ts`, `notes.ts`, `social.ts`, `articles.ts`, `engagement.ts`, `dms.ts`, `bookmarks.ts`, `muting.ts`, `search.ts`, `relays.ts`, `trending.ts`); barrel `index.ts` re-exports all; all Nostr calls go through here +- `src/lib/themes.ts` — Color theme definitions (7 themes) and `applyTheme()` utility - `src/lib/lightning/` — NWC client (`nwc.ts`); Lightning payment logic - `src/hooks/` — `useProfile.ts`, `useReactionCount.ts` - `src/components/feed/` — Feed, NoteCard, NoteContent, NoteActions, InlineReplyBox, TextSegments, MediaCards, ComposeBox @@ -60,7 +61,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR - `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), EmojiPicker (categorized emoji insertion) +- `src/components/shared/` — RelaysView (relay health dashboard + recommendations), SettingsView (themes + font size + NWC + identity + data export), EmojiPicker (categorized emoji insertion) - `src/components/sidebar/` — Sidebar navigation **Backend** (`src-tauri/`): Rust + Tauri 2.0 @@ -105,7 +106,7 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR - Zaps: NWC wallet connect (NIP-47) + NIP-57 via NDKZapper - **Advanced search** — query parser with modifiers: `by:author`, `mentions:npub`, `kind:N`, `is:article`, `has:image`, `since:date`, `until:date`, `#hashtag`, `"phrase"`, boolean `OR`; NIP-05 resolution; client-side content filters; search help panel - Search: NIP-50 full-text, hashtag (#t filter), people, articles -- Settings: NWC wallet, notifications, data export, identity, mute lists +- Settings: color themes (7 presets), font size presets, NWC wallet, notifications, data export, identity, mute lists - **Relay management** — consolidated Relays view with add/remove individual relays, health checker (NIP-11 info, WebSocket latency, online/slow/offline status), expandable cards with all supported NIPs, per-relay remove button, "Remove dead" workflow, publish relay list (NIP-65) - **Relay recommendations** — suggest relays based on follows' NIP-65 relay lists; "Discover relays" button with follow count, one-click "Add" - **Relay status badge** — compact "N/M relays" indicator in feed header with color coding; hover tooltip shows per-relay connection state @@ -141,6 +142,8 @@ CI triggers on the tag and builds all three platforms (Ubuntu, Windows, macOS AR - **Profile media gallery** — "Media" tab on profiles with grid layout; images open lightbox, videos/audio navigate to thread - **Emoji picker** — shared categorized emoji picker (Frequent/Faces/Gestures/Objects/Symbols) in compose box, inline reply, thread reply; emoji reaction picker on note cards via visible + button - **External link opener** — global click handler intercepts http(s) links and opens in system browser via `@tauri-apps/plugin-opener` +- **Color themes** — 7 built-in themes (Midnight, Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman); CSS custom properties swapped at runtime; persisted to localStorage +- **Font size presets** — Small/Normal/Large/Extra Large; CSS zoom scaling on document root; persisted to localStorage **Not yet implemented:** - Web of Trust scoring diff --git a/PKGBUILD b/PKGBUILD index f1e3c95..f232217 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: hoornet pkgname=wrystr -pkgver=0.9.2 +pkgver=0.9.3 pkgrel=1 pkgdesc="Cross-platform Nostr desktop client with Lightning integration" arch=('x86_64') diff --git a/README.md b/README.md index 7bf8aab..718c7a0 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,10 @@ sudo dnf install gstreamer1-plugins-base gstreamer1-plugins-good gstreamer1-liba - Search: NIP-50 full-text, `#hashtag`, people search with inline follow, **article search** (kind 30023) - **NIP-05 verification badges** — cached verification with green checkmark on note cards +**Personalization** +- **Color themes** — 7 built-in themes: Midnight (default dark), Light, Catppuccin Mocha, Tokyo Night, Gruvbox, Ethereal, Hackerman; instant switching from Settings +- **Font size** — Small / Normal / Large / Extra Large presets; scales the entire UI uniformly + **Performance & UX** - **Resilient relay connectivity** — all relay queries have timeouts (no more infinite loading); automatic reconnection with NDK instance reset as last resort; toast notifications for connection events; feed diagnostics for debugging - **Per-tab "last updated" timestamp** — relative time indicator in feed header shows how fresh each tab's data is @@ -157,7 +161,6 @@ npm run tauri build # production binary See [ROADMAP.md](./ROADMAP.md) for the full prioritised next steps. Up next: -- Color themes / light mode - UI polish and visual makeover - Nostr NIP research sprint — expanding protocol support - Web of Trust scoring diff --git a/package.json b/package.json index 30dc8b7..d13f78d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wrystr", "private": true, - "version": "0.9.2", + "version": "0.9.3", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 68167bc..65d0c36 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -6157,7 +6157,7 @@ dependencies = [ [[package]] name = "wrystr" -version = "0.9.1" +version = "0.9.3" dependencies = [ "keyring", "rusqlite", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8497242..d253d1a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wrystr" -version = "0.9.2" +version = "0.9.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 5ee6c72..655b73f 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.9.2", + "version": "0.9.3", "identifier": "com.hoornet.wrystr", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/App.tsx b/src/App.tsx index 2535ff1..543ace4 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,7 @@ import { ToastContainer } from "./components/shared/ToastContainer"; import { DebugPanel } from "./components/shared/DebugPanel"; import { HelpModal } from "./components/shared/HelpModal"; import { useUIStore } from "./stores/ui"; +import { getTheme, applyTheme } from "./lib/themes"; import { useUpdater } from "./hooks/useUpdater"; import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts"; @@ -56,12 +57,25 @@ function App() { const toggleHelp = useUIStore((s) => s.toggleHelp); const showDebugPanel = useUIStore((s) => s.showDebugPanel); const toggleDebugPanel = useUIStore((s) => s.toggleDebugPanel); + const fontSize = useUIStore((s) => s.fontSize); + const themeId = useUIStore((s) => s.themeId); const [onboardingDone, setOnboardingDone] = useState( () => !!localStorage.getItem("wrystr_pubkey") ); useKeyboardShortcuts(); + // Apply zoom level based on font size setting + useEffect(() => { + document.documentElement.style.zoom = `${fontSize / 14}`; + }, [fontSize]); + + // Apply color theme + useEffect(() => { + const theme = getTheme(themeId); + if (theme) applyTheme(theme); + }, [themeId]); + // Intercept external link clicks and open in system browser via Tauri opener useEffect(() => { const handler = (e: MouseEvent) => { diff --git a/src/components/shared/SettingsView.tsx b/src/components/shared/SettingsView.tsx index d921a09..ba606ac 100644 --- a/src/components/shared/SettingsView.tsx +++ b/src/components/shared/SettingsView.tsx @@ -2,6 +2,8 @@ import { useState } from "react"; import { save } from "@tauri-apps/plugin-dialog"; import { writeTextFile } from "@tauri-apps/plugin-fs"; import { useUserStore } from "../../stores/user"; +import { useUIStore } from "../../stores/ui"; +import { themes } from "../../lib/themes"; import { useMuteStore } from "../../stores/mute"; import { useBookmarkStore } from "../../stores/bookmark"; import { getStoredRelayUrls } from "../../lib/nostr"; @@ -30,17 +32,28 @@ function MutedRow({ pubkey, onUnmute }: { pubkey: string; onUnmute: () => void } function MuteSection() { const { mutedPubkeys, unmute } = useMuteStore(); + const [expanded, setExpanded] = useState(false); if (mutedPubkeys.length === 0) return null; return (
-

- Muted accounts ({mutedPubkeys.length}) -

-
- {mutedPubkeys.map((pk) => ( - unmute(pk)} /> - ))} -
+ + {expanded && ( +
+ {mutedPubkeys.map((pk) => ( + unmute(pk)} /> + ))} +
+ )}
); } @@ -270,6 +283,78 @@ function NotificationSection() { ); } +function ThemeSection() { + const { themeId, setTheme } = useUIStore(); + + return ( +
+

+ Theme +

+
+ {themes.map((theme) => ( + + ))} +
+
+ ); +} + +const FONT_PRESETS = [ + { label: "Small", size: 12 }, + { label: "Normal", size: 14 }, + { label: "Large", size: 16 }, + { label: "Extra Large", size: 18 }, +]; + +function FontSizeSection() { + const { fontSize, setFontSize } = useUIStore(); + + return ( +
+

+ Font Size +

+
+ {FONT_PRESETS.map(({ label, size }) => ( + + ))} +
+

+ Adjusts the base text size across the app. Articles use their own reading font. +

+
+ ); +} + export function SettingsView() { return (
@@ -278,6 +363,8 @@ export function SettingsView() {
+ + diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 8093c76..680abbb 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -1,5 +1,4 @@ import { useUIStore } from "../../stores/ui"; -import { useFeedStore } from "../../stores/feed"; import { useUserStore } from "../../stores/user"; import { useNotificationsStore } from "../../stores/notifications"; import { useDraftStore } from "../../stores/drafts"; @@ -25,7 +24,6 @@ const NAV_ITEMS = [ export function Sidebar() { const { currentView, setView, sidebarCollapsed, toggleSidebar } = useUIStore(); - const { connected } = useFeedStore(); const { loggedIn } = useUserStore(); const { unreadCount: notifUnread, dmUnreadCount } = useNotificationsStore(); const draftCount = useDraftStore((s) => s.drafts.length); @@ -125,22 +123,6 @@ export function Sidebar() { {/* Account switcher (full) — expanded only */} {!c && } - {/* Footer — connection status */} -
- {c ? ( - /* Collapsed: single dot */ - - ) : ( - /* Expanded: dot + label */ -
- - {connected ? "online" : "offline"} -
- )} -
); } diff --git a/src/lib/themes.ts b/src/lib/themes.ts new file mode 100644 index 0000000..10707f2 --- /dev/null +++ b/src/lib/themes.ts @@ -0,0 +1,178 @@ +interface ThemeColors { + bg: string; + "bg-raised": string; + "bg-hover": string; + border: string; + "border-subtle": string; + text: string; + "text-muted": string; + "text-dim": string; + accent: string; + "accent-hover": string; + zap: string; + danger: string; + warning: string; + success: string; +} + +export interface Theme { + id: string; + name: string; + colors: ThemeColors; +} + +export const themes: Theme[] = [ + { + id: "midnight", + name: "Midnight", + colors: { + bg: "#0a0a0a", + "bg-raised": "#111111", + "bg-hover": "#1a1a1a", + border: "#222222", + "border-subtle": "#1a1a1a", + text: "#e0e0e0", + "text-muted": "#777777", + "text-dim": "#555555", + accent: "#8b5cf6", + "accent-hover": "#7c3aed", + zap: "#f59e0b", + danger: "#ef4444", + warning: "#f59e0b", + success: "#22c55e", + }, + }, + { + id: "light", + name: "Light", + colors: { + bg: "#f5f5f5", + "bg-raised": "#ffffff", + "bg-hover": "#e8e8e8", + border: "#d4d4d4", + "border-subtle": "#e5e5e5", + text: "#1a1a1a", + "text-muted": "#6b7280", + "text-dim": "#9ca3af", + accent: "#7c3aed", + "accent-hover": "#6d28d9", + zap: "#d97706", + danger: "#dc2626", + warning: "#d97706", + success: "#16a34a", + }, + }, + { + id: "catppuccin", + name: "Catppuccin Mocha", + colors: { + bg: "#1e1e2e", + "bg-raised": "#313244", + "bg-hover": "#45475a", + border: "#45475a", + "border-subtle": "#313244", + text: "#cdd6f4", + "text-muted": "#a6adc8", + "text-dim": "#6c7086", + accent: "#cba6f7", + "accent-hover": "#b4befe", + zap: "#f9e2af", + danger: "#f38ba8", + warning: "#f9e2af", + success: "#a6e3a1", + }, + }, + { + id: "tokyo-night", + name: "Tokyo Night", + colors: { + bg: "#1a1b26", + "bg-raised": "#24283b", + "bg-hover": "#292e42", + border: "#3b4261", + "border-subtle": "#292e42", + text: "#a9b1d6", + "text-muted": "#565f89", + "text-dim": "#3b4261", + accent: "#7aa2f7", + "accent-hover": "#89b4fa", + zap: "#e0af68", + danger: "#f7768e", + warning: "#e0af68", + success: "#9ece6a", + }, + }, + { + id: "gruvbox", + name: "Gruvbox", + colors: { + bg: "#282828", + "bg-raised": "#3c3836", + "bg-hover": "#504945", + border: "#504945", + "border-subtle": "#3c3836", + text: "#ebdbb2", + "text-muted": "#a89984", + "text-dim": "#665c54", + accent: "#fe8019", + "accent-hover": "#d65d0e", + zap: "#fabd2f", + danger: "#fb4934", + warning: "#fabd2f", + success: "#b8bb26", + }, + }, + { + id: "ethereal", + name: "Ethereal", + colors: { + bg: "#1a1a2e", + "bg-raised": "#16213e", + "bg-hover": "#1f2f50", + border: "#2a3a5c", + "border-subtle": "#1f2f50", + text: "#dfe6e9", + "text-muted": "#a0aec0", + "text-dim": "#5a6a8a", + accent: "#a29bfe", + "accent-hover": "#6c5ce7", + zap: "#ffeaa7", + danger: "#ff7675", + warning: "#ffeaa7", + success: "#55efc4", + }, + }, + { + id: "hackerman", + name: "Hackerman", + colors: { + bg: "#0a0a0a", + "bg-raised": "#0d1117", + "bg-hover": "#161b22", + border: "#1a2332", + "border-subtle": "#131a24", + text: "#00ff41", + "text-muted": "#00bb2d", + "text-dim": "#006b1a", + accent: "#00ff41", + "accent-hover": "#33ff66", + zap: "#ffff00", + danger: "#ff0000", + warning: "#ffff00", + success: "#00ff41", + }, + }, +]; + +export const DEFAULT_THEME_ID = "midnight"; + +export function getTheme(id: string): Theme | undefined { + return themes.find((t) => t.id === id); +} + +export function applyTheme(theme: Theme): void { + const root = document.documentElement; + for (const [key, value] of Object.entries(theme.colors)) { + root.style.setProperty(`--color-${key}`, value); + } +} diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 302b737..dc024e9 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -29,6 +29,8 @@ interface UIState { showHelp: boolean; showDebugPanel: boolean; feedLanguageFilter: string | null; + fontSize: number; + themeId: string; setView: (view: View) => void; setFeedTab: (tab: FeedTab) => void; openProfile: (pubkey: string) => void; @@ -39,12 +41,16 @@ interface UIState { openArticle: (naddr: string, event?: NDKEvent) => void; goBack: () => void; setFeedLanguageFilter: (filter: string | null) => void; + setFontSize: (size: number) => void; + setTheme: (id: string) => void; toggleSidebar: () => void; toggleHelp: () => void; toggleDebugPanel: () => void; } const SIDEBAR_KEY = "wrystr_sidebar_collapsed"; +const FONT_SIZE_KEY = "wrystr_font_size"; +const THEME_KEY = "wrystr_theme"; export const useUIStore = create((set, _get) => ({ currentView: "feed", @@ -62,6 +68,8 @@ export const useUIStore = create((set, _get) => ({ showHelp: false, showDebugPanel: false, feedLanguageFilter: null, + fontSize: parseInt(localStorage.getItem(FONT_SIZE_KEY) || "14", 10), + themeId: localStorage.getItem(THEME_KEY) || "midnight", setView: (currentView) => set({ currentView }), setFeedTab: (feedTab) => set({ feedTab }), openProfile: (pubkey) => set((s) => { @@ -91,6 +99,14 @@ export const useUIStore = create((set, _get) => ({ return { showHelp: false, currentView: "feed", selectedNote: null, viewStack: [] }; }), setFeedLanguageFilter: (feedLanguageFilter) => set({ feedLanguageFilter }), + setFontSize: (fontSize) => { + localStorage.setItem(FONT_SIZE_KEY, String(fontSize)); + set({ fontSize }); + }, + setTheme: (themeId) => { + localStorage.setItem(THEME_KEY, themeId); + set({ themeId }); + }, toggleSidebar: () => set((s) => { const next = !s.sidebarCollapsed; localStorage.setItem(SIDEBAR_KEY, String(next));