From 1df46a0063ea87df4b82b41363826e429803a194 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Tue, 12 May 2026 19:59:12 -0400 Subject: [PATCH] feat(desktop): customize home experience Co-authored-by: Cursor --- desktop/src-tauri/Cargo.lock | 7 + desktop/src-tauri/Cargo.toml | 2 +- .../src-tauri/src/commands/home_background.rs | 111 +++++ desktop/src-tauri/src/commands/mod.rs | 2 + desktop/src-tauri/src/lib.rs | 1 + desktop/src-tauri/tauri.conf.json | 6 +- desktop/src/app/routes/index.tsx | 4 + desktop/src/features/chat/ui/ChatHeader.tsx | 44 +- desktop/src/features/home/ui/FeedSection.tsx | 4 +- .../features/home/ui/HomeBackgroundLayer.tsx | 117 +++++ desktop/src/features/home/ui/HomeScreen.tsx | 8 +- .../src/features/home/ui/HomeSearchPanel.tsx | 250 ++++++++++ desktop/src/features/home/ui/HomeView.tsx | 444 ++++++++++++------ .../features/home/ui/RecentNotesSection.tsx | 10 +- .../home/useHomeBackgroundSettings.ts | 144 ++++++ .../ui/HomeBackgroundSettingsCard.tsx | 273 +++++++++++ .../features/settings/ui/SettingsPanels.tsx | 153 +++--- desktop/src/shared/api/tauri.ts | 52 +- 18 files changed, 1382 insertions(+), 250 deletions(-) create mode 100644 desktop/src-tauri/src/commands/home_background.rs create mode 100644 desktop/src/features/home/ui/HomeBackgroundLayer.tsx create mode 100644 desktop/src/features/home/ui/HomeSearchPanel.tsx create mode 100644 desktop/src/features/home/useHomeBackgroundSettings.ts create mode 100644 desktop/src/features/settings/ui/HomeBackgroundSettingsCard.tsx diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index b6b24ae411..c2bc4c82e4 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -2280,6 +2280,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + [[package]] name = "httparse" version = "1.10.1" @@ -5818,6 +5824,7 @@ dependencies = [ "gtk", "heck 0.5.0", "http", + "http-range", "jni", "libc", "log", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 8720843b05..c8a50f425a 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -26,7 +26,7 @@ windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem"] } [dependencies] atomic-write-file = "0.3" dirs = "6" -tauri = { version = "2", features = [] } +tauri = { version = "2", features = ["protocol-asset"] } tauri-plugin-deep-link = "2" tauri-plugin-opener = "2" tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } diff --git a/desktop/src-tauri/src/commands/home_background.rs b/desktop/src-tauri/src/commands/home_background.rs new file mode 100644 index 0000000000..2d8e1acb7b --- /dev/null +++ b/desktop/src-tauri/src/commands/home_background.rs @@ -0,0 +1,111 @@ +use serde::Serialize; +use tauri::Manager; +use tauri_plugin_dialog::DialogExt; +use uuid::Uuid; + +const SCRIPT_MAX_BYTES: u64 = 256 * 1024; + +#[derive(Serialize)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PickHomeBackgroundResult { + Asset { + path: String, + file_name: Option, + }, + Script { + content: String, + file_name: Option, + }, +} + +fn dialog_filter(kind: &str) -> Result<(&'static str, &'static [&'static str]), String> { + match kind { + "image" => Ok(( + "Images", + &["jpg", "jpeg", "png", "gif", "webp", "avif", "svg"], + )), + "video" => Ok(("Videos", &["mp4", "mov", "m4v", "webm", "mkv"])), + "script" => Ok(("Animations", &["html", "htm", "js", "css", "txt"])), + _ => Err("unsupported background file type".to_string()), + } +} + +#[tauri::command] +pub async fn pick_home_background_file( + app: tauri::AppHandle, + kind: String, +) -> Result, String> { + let (filter_name, filter_exts) = dialog_filter(&kind)?; + let (tx, rx) = tokio::sync::oneshot::channel(); + + app.dialog() + .file() + .add_filter(filter_name, filter_exts) + .pick_file(move |path| { + let _ = tx.send(path); + }); + + let selected = rx.await.map_err(|_| "dialog cancelled".to_string())?; + let file_path = match selected { + Some(path) => path, + None => return Ok(None), + }; + + let path = file_path.as_path().ok_or("invalid path")?.to_path_buf(); + let file_name = path + .file_name() + .and_then(|name| name.to_str()) + .map(ToOwned::to_owned); + + if kind == "script" { + let metadata = + std::fs::metadata(&path).map_err(|error| format!("failed to inspect file: {error}"))?; + if metadata.len() > SCRIPT_MAX_BYTES { + return Err("script file is too large; choose a file under 256 KB".to_string()); + } + + let content = std::fs::read_to_string(&path) + .map_err(|error| format!("failed to read file: {error}"))?; + return Ok(Some(PickHomeBackgroundResult::Script { + content, + file_name, + })); + } + + let background_dir = app + .path() + .app_data_dir() + .map_err(|error| format!("failed to resolve app data directory: {error}"))? + .join("home-backgrounds"); + std::fs::create_dir_all(&background_dir) + .map_err(|error| format!("failed to prepare background directory: {error}"))?; + + let extension = path + .extension() + .and_then(|extension| extension.to_str()) + .map(|extension| { + extension + .chars() + .filter(|c| c.is_ascii_alphanumeric()) + .take(8) + .collect::() + .to_ascii_lowercase() + }) + .filter(|extension| !extension.is_empty()) + .unwrap_or_else(|| { + if kind == "video" { + "mp4".to_string() + } else { + "png".to_string() + } + }); + let persisted_path = + background_dir.join(format!("home-background-{}.{}", Uuid::new_v4(), extension)); + std::fs::copy(&path, &persisted_path) + .map_err(|error| format!("failed to copy background file: {error}"))?; + + Ok(Some(PickHomeBackgroundResult::Asset { + path: persisted_path.to_string_lossy().into_owned(), + file_name, + })) +} diff --git a/desktop/src-tauri/src/commands/mod.rs b/desktop/src-tauri/src/commands/mod.rs index 2bc5062f5e..a201220eeb 100644 --- a/desktop/src-tauri/src/commands/mod.rs +++ b/desktop/src-tauri/src/commands/mod.rs @@ -7,6 +7,7 @@ mod channel_templates; mod channels; mod dms; mod export_util; +mod home_background; mod identity; mod media; mod media_download; @@ -29,6 +30,7 @@ pub use canvas::*; pub use channel_templates::*; pub use channels::*; pub use dms::*; +pub use home_background::*; pub use identity::*; pub use media::*; pub use media_download::*; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 5e71c2579c..678dac8fc7 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -491,6 +491,7 @@ pub fn run() { add_reaction, remove_reaction, get_event, + pick_home_background_file, upload_media, pick_and_upload_media, upload_media_bytes, diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index c47e8f107c..2fea0748e7 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -30,7 +30,11 @@ } ], "security": { - "csp": null + "csp": null, + "assetProtocol": { + "enable": true, + "scope": ["$APPDATA/home-backgrounds/**"] + } } }, "plugins": { diff --git a/desktop/src/app/routes/index.tsx b/desktop/src/app/routes/index.tsx index 2a111bc887..c6d02ec32b 100644 --- a/desktop/src/app/routes/index.tsx +++ b/desktop/src/app/routes/index.tsx @@ -60,6 +60,7 @@ function HomeRouteComponent() { return ( { if (!item.channelId) { @@ -76,6 +77,9 @@ function HomeRouteComponent() { onOpenPulse={() => { void goPulse(); }} + onOpenSearchResult={(hit) => { + void openSearchHit(hit); + }} /> ); } diff --git a/desktop/src/features/chat/ui/ChatHeader.tsx b/desktop/src/features/chat/ui/ChatHeader.tsx index 8f93be920a..42784ea555 100644 --- a/desktop/src/features/chat/ui/ChatHeader.tsx +++ b/desktop/src/features/chat/ui/ChatHeader.tsx @@ -84,35 +84,37 @@ export function ChatHeader({ return (
-
-
- -

- {title} -

- {statusBadge ? ( -
- {statusBadge} -
- ) : null} -
+
+ +

+ {title} +

+ {statusBadge ? ( +
+ {statusBadge} +
+ ) : null}
+
{actions ?
{actions}
: null}
); diff --git a/desktop/src/features/home/ui/FeedSection.tsx b/desktop/src/features/home/ui/FeedSection.tsx index f926b348c9..38ce0d4dac 100644 --- a/desktop/src/features/home/ui/FeedSection.tsx +++ b/desktop/src/features/home/ui/FeedSection.tsx @@ -151,7 +151,7 @@ export function FeedSection({ {items.length === 0 ? ( -
+

{emptyTitle}

@@ -160,7 +160,7 @@ export function FeedSection({

) : ( -
+
{items.map((item) => { const channelId = item.channelId; const canOpenChannel = diff --git a/desktop/src/features/home/ui/HomeBackgroundLayer.tsx b/desktop/src/features/home/ui/HomeBackgroundLayer.tsx new file mode 100644 index 0000000000..c642ba96a0 --- /dev/null +++ b/desktop/src/features/home/ui/HomeBackgroundLayer.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; + +import type { HomeBackgroundSettings } from "@/features/home/useHomeBackgroundSettings"; +import { cn } from "@/shared/lib/cn"; + +type HomeBackgroundLayerProps = { + className?: string; + settings: HomeBackgroundSettings; +}; + +function buildAnimationSrcDoc(code: string) { + return ` + + + + + + + + +${code} + +`; +} + +function hasRenderableBackground(settings: HomeBackgroundSettings) { + if (settings.mode === "none") { + return false; + } + + if (settings.mode === "animation") { + return settings.animationCode.trim().length > 0; + } + + return settings.sourceUrl.trim().length > 0; +} + +export function HomeBackgroundLayer({ + className, + settings, +}: HomeBackgroundLayerProps) { + const mediaSource = + settings.mode === "image" || settings.mode === "video" + ? settings.sourceUrl.trim() + : ""; + const [failedMediaSource, setFailedMediaSource] = React.useState< + string | null + >(null); + + if (!hasRenderableBackground(settings)) { + return null; + } + + if (mediaSource && failedMediaSource === mediaSource) { + return null; + } + + return ( +