Bump to v0.9.3 — color themes, font size, settings polish

7 color themes (Midnight, Light, Catppuccin Mocha, Tokyo Night, Gruvbox,
Ethereal, Hackerman), font size presets (Small/Normal/Large/Extra Large),
collapsible muted accounts list, removed old sidebar connection indicator.
This commit is contained in:
Jure
2026-03-24 12:16:08 +01:00
parent 97fd6c55bf
commit ad028c7406
13 changed files with 326 additions and 46 deletions
+9 -12
View File
@@ -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.2Relay 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.3Themes, 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
+5 -2
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: hoornet <hoornet@users.noreply.github.com>
pkgname=wrystr
pkgver=0.9.2
pkgver=0.9.3
pkgrel=1
pkgdesc="Cross-platform Nostr desktop client with Lightning integration"
arch=('x86_64')
+4 -1
View File
@@ -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
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wrystr",
"private": true,
"version": "0.9.2",
"version": "0.9.3",
"type": "module",
"scripts": {
"dev": "vite",
+1 -1
View File
@@ -6157,7 +6157,7 @@ dependencies = [
[[package]]
name = "wrystr"
version = "0.9.1"
version = "0.9.3"
dependencies = [
"keyring",
"rusqlite",
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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",
+14
View File
@@ -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) => {
+95 -8
View File
@@ -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 (
<section>
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
Muted accounts ({mutedPubkeys.length})
</h2>
<div className="space-y-1">
{mutedPubkeys.map((pk) => (
<MutedRow key={pk} pubkey={pk} onUnmute={() => unmute(pk)} />
))}
</div>
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full text-left group"
>
<span className="text-text-dim text-[10px] transition-transform" style={{ transform: expanded ? "rotate(90deg)" : "rotate(0deg)" }}>
</span>
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest text-text-dim group-hover:text-text transition-colors">
Muted accounts ({mutedPubkeys.length})
</h2>
</button>
{expanded && (
<div className="space-y-1 mt-2">
{mutedPubkeys.map((pk) => (
<MutedRow key={pk} pubkey={pk} onUnmute={() => unmute(pk)} />
))}
</div>
)}
</section>
);
}
@@ -270,6 +283,78 @@ function NotificationSection() {
);
}
function ThemeSection() {
const { themeId, setTheme } = useUIStore();
return (
<section>
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-3 text-text-dim">
Theme
</h2>
<div className="grid grid-cols-4 gap-2">
{themes.map((theme) => (
<button
key={theme.id}
onClick={() => setTheme(theme.id)}
className={`flex flex-col items-center gap-1.5 p-2 border transition-colors rounded-sm ${
themeId === theme.id
? "border-accent bg-bg-hover"
: "border-border hover:border-accent/40"
}`}
>
<div className="flex gap-0.5 w-full h-5 rounded-sm overflow-hidden">
<div className="flex-1" style={{ background: theme.colors.bg }} />
<div className="flex-1" style={{ background: theme.colors["bg-raised"] }} />
<div className="flex-1" style={{ background: theme.colors.accent }} />
<div className="flex-1" style={{ background: theme.colors.text }} />
</div>
<span className="text-[10px] text-text-muted truncate w-full text-center">
{theme.name}
</span>
</button>
))}
</div>
</section>
);
}
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 (
<section>
<h2 className="text-text text-[11px] font-medium uppercase tracking-widest mb-2 text-text-dim">
Font Size
</h2>
<div className="flex items-center gap-2">
{FONT_PRESETS.map(({ label, size }) => (
<button
key={size}
onClick={() => setFontSize(size)}
className={`px-3 py-1.5 text-[11px] border transition-colors ${
fontSize === size
? "border-accent text-accent"
: "border-border text-text-muted hover:text-text hover:border-accent/40"
}`}
>
{label}
</button>
))}
</div>
<p className="text-text-dim text-[10px] mt-2 px-1">
Adjusts the base text size across the app. Articles use their own reading font.
</p>
</section>
);
}
export function SettingsView() {
return (
<div className="h-full flex flex-col">
@@ -278,6 +363,8 @@ export function SettingsView() {
</header>
<div className="flex-1 overflow-y-auto p-4 space-y-8">
<ThemeSection />
<FontSizeSection />
<WalletSection />
<NotificationSection />
<ExportSection />
-18
View File
@@ -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 && <AccountSwitcher />}
{/* Footer — connection status */}
<div className={`border-t border-border shrink-0 ${c ? "py-2 flex justify-center" : "px-3 py-2"}`}>
{c ? (
/* Collapsed: single dot */
<span
title={connected ? "Online" : "Offline"}
className={`w-2 h-2 rounded-full inline-block ${connected ? "bg-success" : "bg-danger"}`}
/>
) : (
/* Expanded: dot + label */
<div className="flex items-center gap-1.5 text-[10px] text-text-dim">
<span className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-success" : "bg-danger"}`} />
<span>{connected ? "online" : "offline"}</span>
</div>
)}
</div>
</aside>
);
}
+178
View File
@@ -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);
}
}
+16
View File
@@ -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<UIState>((set, _get) => ({
currentView: "feed",
@@ -62,6 +68,8 @@ export const useUIStore = create<UIState>((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<UIState>((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));