diff options
Diffstat (limited to '')
-rw-r--r-- | src/views/login_page.rs | 23 | ||||
-rw-r--r-- | src/views/macaw.rs | 79 | ||||
-rw-r--r-- | src/views/macaw/open_chats_panel.rs | 13 | ||||
-rw-r--r-- | src/views/macaw/settings.rs | 291 | ||||
-rw-r--r-- | src/views/mod.rs | 1 |
5 files changed, 334 insertions, 73 deletions
diff --git a/src/views/login_page.rs b/src/views/login_page.rs index 2edd4b5..3506aee 100644 --- a/src/views/login_page.rs +++ b/src/views/login_page.rs @@ -1,9 +1,11 @@ use std::{str::FromStr, sync::Arc}; -use filamento::{db::Db, error::{CommandError, ConnectionError}, files::{opfs::OPFSError, FilesMem, FilesOPFS}, UpdateMessage}; +use filamento::{ + db::Db, error::{CommandError, ConnectionError, DatabaseOpenError}, files::{opfs::OPFSError, FilesMem, FilesOPFS}, UpdateMessage +}; use jid::JID; -use thiserror::Error; use leptos::prelude::*; +use thiserror::Error; use tokio::sync::mpsc::Receiver; use tracing::debug; @@ -21,6 +23,8 @@ pub enum LoginError { InvalidJID(#[from] jid::ParseError), #[error("Connection Error: {0}")] ConnectionError(#[from] CommandError<ConnectionError>), + #[error("Failed to open database: {0}")] + DatabaseOpen(#[from] DatabaseOpenError), #[error("OPFS: {0}")] OPFS(#[from] OPFSError), } @@ -79,10 +83,17 @@ pub fn LoginPage( debug!("creating db in opfs"); Db::create_connect_and_migrate(jid.as_bare().to_string()) .await - .unwrap() } else { debug!("creating db in memory"); - Db::create_connect_and_migrate_memory().await.unwrap() + Db::create_connect_and_migrate_memory().await + }; + let db = match db { + Ok(db) => db, + Err(e) => { + set_error.set(Some(e.into())); + set_login_pending.set(false); + return; + } }; let files = if remember_me { let opfs = FilesOPFS::new(jid.as_bare().to_string()).await; @@ -113,9 +124,7 @@ pub fn LoginPage( if *connect_on_login.read_untracked() { match client.connect().await { - Ok(r) => { - resource.set(Some(r)) - } + Ok(r) => resource.set(Some(r)), Err(e) => { set_error.set(Some(e.into())); set_login_pending.set(false); diff --git a/src/views/macaw.rs b/src/views/macaw.rs index 4a5b794..0ef8255 100644 --- a/src/views/macaw.rs +++ b/src/views/macaw.rs @@ -1,6 +1,10 @@ use std::collections::{HashMap, HashSet}; -use filamento::{chat::{Chat, Message, MessageStoreFields}, user::User, UpdateMessage}; +use filamento::{ + UpdateMessage, + chat::{Chat, Message, MessageStoreFields}, + user::User, +}; use jid::BareJID; use leptos::{prelude::*, task::spawn_local}; use open_chats_panel::OpenChatsPanelView; @@ -10,12 +14,23 @@ use tokio::sync::mpsc::Receiver; use tracing::debug; use uuid::Uuid; -use crate::{client::Client, components::sidebar::Sidebar, contact::{ArcMacawContact, MacawContact}, message::{ArcMacawMessage, MacawMessage}, message_subscriptions::MessageSubscriptions, open_chats::OpenChatsPanel, roster::{Roster, RosterStoreFields}, state_store::StateStore, user::{fetch_avatar, ArcMacawUser, MacawUser}, user_presences::{Presences, UserPresences}}; +use crate::{ + client::Client, + components::sidebar::Sidebar, + contact::{ArcMacawContact, MacawContact}, + message::{ArcMacawMessage, MacawMessage}, + message_subscriptions::MessageSubscriptions, + open_chats::OpenChatsPanel, + roster::{Roster, RosterStoreFields}, + state_store::StateStore, + user::{ArcMacawUser, MacawUser, fetch_avatar}, + user_presences::{Presences, UserPresences}, +}; use super::AppState; -pub mod settings; mod open_chats_panel; +pub mod settings; #[component] pub fn Macaw( @@ -44,23 +59,21 @@ pub fn Macaw( let open_chats = Store::new(OpenChatsPanel::default()); provide_context(open_chats); - let show_settings = RwSignal::new(None::<SettingsPage>); + let show_settings = RwSignal::new(None::<SettingsPage>); provide_context(show_settings); let user_presences = Store::new(UserPresences::new()); provide_context(user_presences); - let client_user: LocalResource<MacawUser> = LocalResource::new(move || { - async move { - let client = use_context::<Client>().expect("client not in context"); - let user = client.get_user((*client.jid).clone()).await.unwrap(); - ArcMacawUser::got_user(user).await.into() - } + let client_user: LocalResource<MacawUser> = LocalResource::new(move || async move { + let client = use_context::<Client>().expect("client not in context"); + let user = client.get_user((*client.jid).clone()).await.unwrap(); + ArcMacawUser::got_user(user).await.into() }); provide_context(client_user); // TODO: timestamp incoming/outgoing subscription requests - let (subscription_requests, set_subscription_requests)= signal(HashSet::<BareJID>::new()); + let (subscription_requests, set_subscription_requests) = signal(HashSet::<BareJID>::new()); provide_context(subscription_requests); provide_context(set_subscription_requests); @@ -73,7 +86,12 @@ pub fn Macaw( UpdateMessage::Online(online, items) => { let mut contacts = HashMap::new(); for (contact, user) in items { - contacts.insert(contact.user_jid.clone(), ArcMacawContact::got_contact_and_user(contact, user).await.into()); + contacts.insert( + contact.user_jid.clone(), + ArcMacawContact::got_contact_and_user(contact, user) + .await + .into(), + ); } roster.contacts().set(contacts); } @@ -82,7 +100,9 @@ pub fn Macaw( user_presences.write().clear(); } UpdateMessage::RosterUpdate(contact, user) => { - let new_contact = ArcMacawContact::got_contact_and_user(contact.clone(), user).await.into(); + let new_contact = ArcMacawContact::got_contact_and_user(contact.clone(), user) + .await + .into(); roster.contacts().update(|roster| { if let Some(macaw_contact) = roster.get_mut(&contact.user_jid) { macaw_contact.set(contact); @@ -99,15 +119,24 @@ pub fn Macaw( } UpdateMessage::Presence { from, presence } => { let bare_jid = from.to_bare(); - if let Some(presences) = user_presences.read_untracked().user_presences.get(&bare_jid) { + if let Some(presences) = user_presences + .read_untracked() + .user_presences + .get(&bare_jid) + { if let Some(resource) = from.resourcepart() { - presences.write().update_presence(resource.clone(), presence); + presences + .write() + .update_presence(resource.clone(), presence); } } else { if let Some(resource) = from.resourcepart() { let mut presences = Presences::new(); presences.update_presence(resource.clone(), presence); - user_presences.write().user_presences.insert(bare_jid, ArcRwSignal::new(presences)); + user_presences + .write() + .user_presences + .insert(bare_jid, ArcRwSignal::new(presences)); } } } @@ -125,12 +154,15 @@ pub fn Macaw( } UpdateMessage::MessageDelivery { id, chat, delivery } => { messages_store.modify(&id, |message| { - <ArcStore<filamento::chat::Message> as Clone>::clone(&message).delivery() + <ArcStore<filamento::chat::Message> as Clone>::clone(&message) + .delivery() .set(Some(delivery)) }); } UpdateMessage::SubscriptionRequest(jid) => { - set_subscription_requests.update(|req| { req.insert(jid); }); + set_subscription_requests.update(|req| { + req.insert(jid); + }); } UpdateMessage::NickChanged { jid, nick } => { users_store.modify(&jid, |(user, _avatar)| { @@ -152,11 +184,12 @@ pub fn Macaw( <Sidebar /> // <ChatsList /> <OpenChatsPanelView /> - {move || if let Some(_) = *show_settings.read() { - view! { <Settings /> }.into_any() - } else { - view! {}.into_any() + {move || { + if let Some(_) = *show_settings.read() { + view! { <Settings /> }.into_any() + } else { + view! {}.into_any() + } }} } } - diff --git a/src/views/macaw/open_chats_panel.rs b/src/views/macaw/open_chats_panel.rs index bdb0084..43ce59e 100644 --- a/src/views/macaw/open_chats_panel.rs +++ b/src/views/macaw/open_chats_panel.rs @@ -53,7 +53,13 @@ mod open_chat { use leptos::prelude::*; use reactive_stores::{ArcStore, Store}; - use crate::{chat::MacawChat, components::{chat_header::ChatViewHeader, message_composer::ChatViewMessageComposer, message_history_buffer::MessageHistoryBuffer}}; + use crate::{ + chat::MacawChat, + components::{ + chat_header::ChatViewHeader, message_composer::ChatViewMessageComposer, + message_history_buffer::MessageHistoryBuffer, + }, + }; #[component] pub fn OpenChatView(chat: MacawChat) -> impl IntoView { @@ -63,12 +69,9 @@ mod open_chat { <MessageHistoryBuffer chat=chat.clone() /> {move || { let chat_jid = chat.get().correspondent().get(); - view! { - <ChatViewMessageComposer chat=chat_jid /> - } + view! { <ChatViewMessageComposer chat=chat_jid /> } }} </div> } } } - diff --git a/src/views/macaw/settings.rs b/src/views/macaw/settings.rs index 11a3fc3..1a23b82 100644 --- a/src/views/macaw/settings.rs +++ b/src/views/macaw/settings.rs @@ -1,26 +1,67 @@ use leptos::prelude::*; use profile_settings::ProfileSettings; -use crate::{components::{icon::IconComponent, modal::Modal}, icon::Icon}; +use crate::{ + components::{icon::IconComponent, modal::Modal}, + icon::Icon, +}; mod profile_settings { - use filamento::error::{AvatarPublishError, CommandError}; - use thiserror::Error; + use filamento::{ + error::{AvatarPublishError, CommandError, NickError}, + user::User, + }; use leptos::prelude::*; - use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent}; + use thiserror::Error; + use web_sys::{ + Event, FileReader, HtmlInputElement, ProgressEvent, Url, + js_sys::Uint8Array, + wasm_bindgen::{JsCast, UnwrapThrowExt, prelude::Closure}, + }; - use crate::{client::Client, files::Files}; + use crate::{ + client::Client, + files::Files, + user::{NO_AVATAR, fetch_avatar}, + }; #[derive(Debug, Clone, Error)] pub enum ProfileSaveError { #[error("avatar publish: {0}")] Avatar(#[from] CommandError<AvatarPublishError<Files>>), + #[error("nick publish: {0}")] + Nick(#[from] CommandError<NickError>), } #[component] pub fn ProfileSettings() -> impl IntoView { let client: Client = use_context().expect("no client in context"); + let old_profile = LocalResource::new(move || { + let value = client.clone(); + async move { + // TODO: error + let jid = &*value.jid; + let old_profile = value.get_user(jid.clone()).await.unwrap(); + old_profile + } + }); + + view! { + {move || { + if let Some(old_profile) = old_profile.get() { + view! { <ProfileForm old_profile /> }.into_any() + } else { + view! {}.into_any() + } + }} + } + } + + #[component] + pub fn ProfileForm(old_profile: User) -> impl IntoView { + let client: Client = use_context().expect("no client in context"); + // TODO: compartmentalise into error component, form component... let (error, set_error) = signal(None::<ProfileSaveError>); let error_message = move || { @@ -32,17 +73,35 @@ mod profile_settings { } }) }; + + let (success_message, set_success_message) = signal(None::<String>); + let success_message = move || { + if let Some(message) = success_message.get() { + view! { <div class="success">{message}</div> }.into_any() + } else { + view! {}.into_any() + } + }; + let (profile_save_pending, set_profile_save_pending) = signal(false); let profile_upload_data = RwSignal::new(None::<Vec<u8>>); + let new_nick = RwSignal::new(old_profile.nick.clone().unwrap_or_default().to_string()); + let has_avatar = RwSignal::new(old_profile.avatar.is_some()); + let new_avatar_preview_url = RwSignal::new(None::<String>); + let remove_avatar = RwSignal::new(false); + let from_input = move |ev: Event| { let elem = ev.target().unwrap().unchecked_into::<HtmlInputElement>(); - + // let UploadSignal(file_signal) = expect_context(); // file_signal.update(Vec::clear); // Clear list from previous change let files = elem.files().unwrap_throw(); if let Some(file) = files.get(0) { + let url = Url::create_object_url_with_blob(&file).unwrap_throw(); + + new_avatar_preview_url.set(Some(url)); let reader = FileReader::new().unwrap_throw(); // let name = file.name(); @@ -56,7 +115,7 @@ mod profile_settings { // `.result` valid after the `read_*` completes on FileReader // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/result let result = reader.result().unwrap_throw(); - let data= Uint8Array::new(&result).to_vec(); + let data = Uint8Array::new(&result).to_vec(); // Do whatever you want with the Vec<u8> profile_upload_data.set(Some(data)); }) @@ -92,32 +151,141 @@ mod profile_settings { let save_profile = Action::new_local(move |_| { let client = client.clone(); - async move {} + let old_nick = old_profile.nick.clone(); + async move { + set_profile_save_pending.set(true); + + let new_nick = new_nick.get(); + let new_nick = if new_nick.is_empty() { + None + } else { + Some(new_nick) + }; + if new_nick != old_nick { + match client.change_nick(new_nick).await { + Ok(_) => {} + Err(e) => { + set_error.set(Some(ProfileSaveError::Nick(e))); + set_profile_save_pending.set(false); + return; + } + } + } + + if let Some(profile_data) = profile_upload_data.get() { + match client.change_avatar(Some(profile_data)).await { + Ok(_) => {} + Err(e) => { + set_error.set(Some(ProfileSaveError::Avatar(e))); + set_profile_save_pending.set(false); + return; + } + } + } else if remove_avatar.get() { + match client.change_avatar(None).await { + Ok(_) => {} + Err(e) => { + set_error.set(Some(ProfileSaveError::Avatar(e))); + set_profile_save_pending.set(false); + return; + } + } + } + + set_profile_save_pending.set(false); + set_error.set(None); + set_success_message.set(Some("Profile Updated!".to_string())); + } }); - let new_nick= RwSignal::new("".to_string()); + let _old_account_avatar = LocalResource::new(move || { + let avatar = old_profile.avatar.clone(); + async move { + let url = fetch_avatar(avatar.as_deref()).await; + new_avatar_preview_url.set(Some(url)); + } + }); view! { <div class="profile-settings"> - <form on:submit=move |ev| { + <div class="profile-preview"> + <h2>Profile Preview</h2> + <div class="preview"> + <img class="avatar" src=new_avatar_preview_url /> + <div class="nick"> + {move || { + let nick = new_nick.get(); + if nick.is_empty() { old_profile.jid.to_string() } else { nick } + }} + </div> + </div> + </div> + <form + class="profile-form" + on:submit=move |ev| { ev.prevent_default(); save_profile.dispatch(()); - }> + } + > + {success_message} {error_message} - <div class="change-avatar"> - <input type="file" id="client-user-avatar" on:change=from_input /> + <div> + <h3>Nick</h3> + <input + disabled=profile_save_pending + placeholder="Nick" + type="text" + id="client-user-nick" + bind:value=new_nick + name="client-user-nick" + /> </div> - <input disabled=profile_save_pending placeholder="Nickname" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" /> - <input disabled=profile_save_pending class="button" type="submit" value="Save Changes" /> + <div> + <h3>Avatar</h3> + <div class="change-avatar"> + <label for="client-user-avatar"> + <div class="button">Change Avatar</div> + </label> + <input + type="file" + id="client-user-avatar" + on:change=move |e| { + has_avatar.set(true); + remove_avatar.set(false); + from_input(e); + } + /> + {move || { + if has_avatar.get() { + view! { + <a + on:click=move |_| { + profile_upload_data.set(None); + remove_avatar.set(true); + has_avatar.set(false); + new_avatar_preview_url.set(Some(NO_AVATAR.to_string())); + } + style="cursor: pointer" + > + Remove Avatar + </a> + } + .into_any() + } else { + view! {}.into_any() + } + }} + </div> + </div> + <hr /> + <input + disabled=profile_save_pending + class="button" + type="submit" + value="Save Changes" + /> </form> </div> - <div class="profile-preview"> - <h2>Profile Preview</h2> - <div class="preview"> - <img /> - <div>nick</div> - </div> - </div> } } } @@ -135,31 +303,81 @@ pub fn Settings() -> impl IntoView { let show_settings: RwSignal<Option<SettingsPage>> = use_context().unwrap(); view! { - <Modal on_background_click=move |_| { show_settings.set(None); }> + <Modal on_background_click=move |_| { + show_settings.set(None); + }> <div class="settings panel"> <div class="header"> <h2>Settings</h2> <div class="header-icon close"> - <IconComponent icon=Icon::Close24 on:click=move |_| show_settings.set(None)/> + <IconComponent + icon=Icon::Close24 + on:click=move |_| show_settings.set(None) + /> </div> </div> <div class="settings-main"> <div class="settings-sidebar"> - <div class:open=move || *show_settings.read() == Some(SettingsPage::Account) on:click=move |_| show_settings.set(Some(SettingsPage::Account))>Account</div> - <div class:open=move || *show_settings.read() == Some(SettingsPage::Chat) on:click=move |_| show_settings.set(Some(SettingsPage::Chat))>Chat</div> - <div class:open=move || *show_settings.read() == Some(SettingsPage::Privacy) on:click=move |_| show_settings.set(Some(SettingsPage::Privacy))>Privacy</div> - <div class:open=move || *show_settings.read() == Some(SettingsPage::Profile) on:click=move |_| show_settings.set(Some(SettingsPage::Profile))>Profile</div> + <div + class:open=move || *show_settings.read() == Some(SettingsPage::Account) + on:click=move |_| show_settings.set(Some(SettingsPage::Account)) + > + Account + </div> + <div + class:open=move || *show_settings.read() == Some(SettingsPage::Chat) + on:click=move |_| show_settings.set(Some(SettingsPage::Chat)) + > + Chat + </div> + <div + class:open=move || *show_settings.read() == Some(SettingsPage::Privacy) + on:click=move |_| show_settings.set(Some(SettingsPage::Privacy)) + > + Privacy + </div> + <div + class:open=move || *show_settings.read() == Some(SettingsPage::Profile) + on:click=move |_| show_settings.set(Some(SettingsPage::Profile)) + > + Profile + </div> </div> <div class="settings-page"> - {move || if let Some(page) = show_settings.get() { - match page { - SettingsPage::Account => view! { <div>"account"</div> }.into_any(), - SettingsPage::Chat => view! { <div>"chat"</div> }.into_any(), - SettingsPage::Profile => view! { <ProfileSettings /> }.into_any(), - SettingsPage::Privacy => view! { <div>"privacy"</div> }.into_any(), + {move || { + if let Some(page) = show_settings.get() { + match page { + SettingsPage::Account => { + view! { + <div style="padding: 16px"> + "Account settings coming soon!" + </div> + } + .into_any() + } + SettingsPage::Chat => { + view! { + <div style="padding: 16px"> + "Chat settings coming soon!" + </div> + } + .into_any() + } + SettingsPage::Profile => { + view! { <ProfileSettings /> }.into_any() + } + SettingsPage::Privacy => { + view! { + <div style="padding: 16px"> + "Privacy settings coming soon!" + </div> + } + .into_any() + } + } + } else { + view! {}.into_any() } - } else { - view! {}.into_any() }} </div> </div> @@ -167,4 +385,3 @@ pub fn Settings() -> impl IntoView { </Modal> } } - diff --git a/src/views/mod.rs b/src/views/mod.rs index 112f930..fa988cd 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -33,4 +33,3 @@ pub fn App() -> impl IntoView { }} } } - |