From e8aa5b71df378669ff732444a5f8c57098052e5e Mon Sep 17 00:00:00 2001 From: cel 🌸 Date: Thu, 8 May 2025 10:17:25 +0100 Subject: feat: sidebar, files in opfs --- .helix/languages.toml | 2 +- Cargo.lock | 1 + Cargo.toml | 2 +- Trunk.toml | 2 + assets/style.scss | 68 +++++++++++++--- index.html | 2 +- src/lib.rs | 220 ++++++++++++++++++++++++++++++++++++++++++++------ 7 files changed, 258 insertions(+), 39 deletions(-) diff --git a/.helix/languages.toml b/.helix/languages.toml index 75f513c..80cd6e7 100644 --- a/.helix/languages.toml +++ b/.helix/languages.toml @@ -1,5 +1,5 @@ [language-server.rust-analyzer] command = "rust-analyzer" -config = { rustfmt.overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"], cargo.features = ["filamento/reactive_stores"] } +config = { rustfmt.overrideCommand = ["leptosfmt", "--stdin", "--rustfmt"], cargo.features = ["filamento/reactive_stores"], cargo.target = "wasm32-unknown-unknown" } # environment = { "DATABASE_URL" = "sqlite://filamento.db" } # config = { cargo.features = ["stanza/rfc_6121", "stanza/xep_0203", "stanza/xep_0030", "stanza/xep_0060", "stanza/xep_0172", "stanza/xep_0390", "stanza/xep_0128", "stanza/xep_0115", "stanza/xep_0084", "sqlx/sqlite", "sqlx/runtime-tokio", "sqlx/uuid", "sqlx/chrono", "jid/sqlx", "uuid/v4", "tokio/full", "rsasl/provider_base64", "rsasl/plain", "rsasl/config_builder", "rsasl/scram-sha-1"] } diff --git a/Cargo.lock b/Cargo.lock index a47f772..2c12742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -834,6 +834,7 @@ dependencies = [ "uuid", "wasm-bindgen", "wasm-bindgen-futures", + "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b535a9a..53db452 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ base64 = "0.22.1" chrono = "0.4.41" chrono-humanize = "0.2.3" console_error_panic_hook = "0.1.7" -filamento = { path = "../luz/filamento", features = ["serde", "reactive_stores"] } +filamento = { path = "../luz/filamento", features = ["serde", "reactive_stores", "opfs"] } futures = "0.3.31" indexmap = "2.9.0" jid = { path = "../luz/jid" } diff --git a/Trunk.toml b/Trunk.toml index a669c48..6e16d80 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -43,6 +43,8 @@ port = 3000 # disable_address_lookup = false # Open a browser tab once the initial build is complete. open = false +headers = { "Cross-Origin-Embedder-Policy" = "require-corp", "Cross-Origin-Opener-Policy" = "same-origin" } + # Whether to disable fallback to index.html for missing files. # no_spa = false # Disable auto-reload of the web app. diff --git a/assets/style.scss b/assets/style.scss index 8020885..14909c5 100644 --- a/assets/style.scss +++ b/assets/style.scss @@ -139,7 +139,7 @@ p { margin: 0; } -.chats-list { +.chats-list, .roster-list { border-width: 0 2px 0 0; border-color: black; border-style: solid; @@ -147,18 +147,21 @@ p { width: 400px; flex: 0 0 auto; align-self: stretch; + display: flex; + flex-direction: column; } -.chats-list>* { +.chats-list>*, .roster-list>* { padding: 8px 16px; } -.chats-list h2 { +.chats-list h2, .roster-list h2 { margin-top: 0.4rem; border-bottom: 2px solid black; + flex: 0 0 auto; } -.chats-list-item { +.chats-list-item, .roster-list-item, { display: flex; gap: 8px; border-radius: 1em; @@ -168,7 +171,9 @@ p { } .chats-list-item:hover, -.chats-list-item.open { +.chats-list-item.open, +.roster-list-item:hover, +.roster-list-item.open, { background: #00000060; /* color: black; */ /* border: 2px solid black; */ @@ -183,13 +188,14 @@ p { height: 48px; } -.chats-list-item .avatar { +.chats-list-item .avatar, .roster-list-item .avatar { width: 48px; height: 48px; } -.chats-list-chats { - height: auto; +.chats-list-chats, .roster-list-roster { + flex-grow: 1; + overflow: scroll; } .open-chat-views { @@ -392,22 +398,62 @@ p { opacity: 0; } +.sidebar { + display: flex; +} + .dock { display: flex; flex-direction: column; border-width: 0 2px 0 0; } -.dock-icon { - margin: 8px; +.shortcuts { + padding: 8px 0; +} + +.dock-item { + display: flex; + justify-content: center; + position: relative; +} + +.dock-pill { + position: absolute; + border-radius: 50%; + left: -4px; + top: calc(50% - 4px); + width: 8px; + height: 8px; + opacity: 0; + background-color: #dcdcdc; + transition-duration: 250ms; +} + +.dock-item.open .dock-pill { + opacity: 100%; +} + +.dock-item:hover .dock-pill, .dock-item.hovering .dock-pill { + top: calc(50% - 14px); + height: 28px; + opacity: 100%; +} + +.dock-item.focused .dock-pill { + top: calc(50% - 24px); + height: 48px; + opacity: 100%; } .shortcuts { border-bottom: 2px solid black; } -.dock-icon img { +.dock-item img { width: 64px; + height: 64px; + padding: 4px 8px; } /* font-families */ diff --git a/index.html b/index.html index 7255857..7f241d5 100644 --- a/index.html +++ b/index.html @@ -8,7 +8,7 @@ - + diff --git a/src/lib.rs b/src/lib.rs index a6dd648..2aa32ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,7 +14,7 @@ use std::{ use base64::{prelude::BASE64_STANDARD, Engine}; use chrono::{NaiveDateTime, TimeDelta}; use filamento::{ - chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FilesMem, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage + chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError}, files::FileStore, files::opfs::OPFSError, files::FilesMem, files::FilesOPFS, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage }; use futures::stream::StreamExt; use indexmap::IndexMap; @@ -28,7 +28,7 @@ use reactive_stores::{ArcStore, Store, StoreField}; use stylance::import_style; use thiserror::Error; use tokio::sync::{mpsc::{self, Receiver}, Mutex}; -use tracing::debug; +use tracing::{debug, error}; use uuid::Uuid; const NO_AVATAR: &str = "/assets/no-avatar.png"; @@ -40,13 +40,64 @@ pub enum AppState { #[derive(Clone)] pub struct Client { - client: filamento::Client, + client: filamento::Client, jid: Arc, - file_store: FilesMem, + file_store: Files, +} + +#[derive(Clone)] +pub enum Files { + Mem(FilesMem), + Opfs(FilesOPFS), +} + +impl FileStore for Files { + type Err = OPFSError; + + async fn is_stored(&self, name: &str) -> Result { + match self { + Files::Mem(files_mem) => Ok(files_mem.is_stored(name).await.unwrap()), + Files::Opfs(files_opfs) => Ok(files_opfs.is_stored(name).await?), + } + } + + async fn store( + &self, + name: &str, + data: &[u8], + ) -> Result<(), Self::Err> { + match self { + Files::Mem(files_mem) => Ok(files_mem.store(name, data).await.unwrap()), + Files::Opfs(files_opfs) => Ok(files_opfs.store(name, data).await?), + } + } + + async fn delete(&self, name: &str) -> Result<(), Self::Err> { + match self { + Files::Mem(files_mem) => Ok(files_mem.delete(name).await.unwrap()), + Files::Opfs(files_opfs) => Ok(files_opfs.delete(name).await?), + } + } +} + +impl Files { + pub async fn get_src(&self, file_name: &str) -> Option { + match self { + Files::Mem(files_mem) => { + if let Some(data) = files_mem.get_file(file_name).await { + let data = BASE64_STANDARD.encode(data); + Some(format!("data:image/jpg;base64, {}", data)) + } else { + None + } + }, + Files::Opfs(files_opfs) => files_opfs.get_src(file_name).await.ok(), + } + } } impl Deref for Client { - type Target = filamento::Client; + type Target = filamento::Client; fn deref(&self) -> &Self::Target { &self.client @@ -89,6 +140,8 @@ pub enum LoginError { InvalidJID(#[from] jid::ParseError), #[error("Connection Error: {0}")] ConnectionError(#[from] CommandError), + #[error("OPFS: {0}")] + OPFS(#[from] OPFSError), } #[component] @@ -138,14 +191,26 @@ fn LoginPage(set_app: WriteSignal, set_client: RwSignal Files::Opfs(f), + Err(e) => { + set_error.set(Some(e.into())); + set_login_pending.set(false); + return + }, + } + } else { + Files::Mem(FilesMem::new()) + }; let (client, updates) = - filamento::Client::new(jid.clone(), password.read_untracked().clone(), db, files_mem.clone()); + filamento::Client::new(jid.clone(), password.read_untracked().clone(), db, files.clone()); // TODO: remember_me let client = Client { client, jid: Arc::new(jid), - file_store: files_mem, + file_store: files, }; if *connect_on_login.read_untracked() { @@ -312,7 +377,7 @@ impl MessageSubscriptions { } } -#[derive(Store)] +#[derive(Store, Clone)] pub struct Roster { #[store(key: JID = |(jid, _)| jid.clone())] contacts: HashMap, @@ -464,28 +529,70 @@ fn Macaw( }); view! { - - + + // } } +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum SidebarOpen { + Roster, + Chats, +} + +pub fn toggle_open(state: &mut Option, open: SidebarOpen) { + match state { + Some(opened) => if *opened == open { + *state = None + } else { + *state = Some(open) + }, + None => *state = Some(open), + } +} + #[component] -pub fn Dock() -> impl IntoView { +pub fn Sidebar() -> impl IntoView { + // for what has been clicked open (in the background) + let (open, set_open) = signal(None::); + // for what is just in the hovered state (not clicked to be pinned open yet necessarily) + let (hovered, set_hovered) = signal(None::); + view! { -
-
-
- +