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 --- src/lib.rs | 220 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 195 insertions(+), 25 deletions(-) (limited to 'src') 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! { -
-
-
- +