summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@bunny.garden>2025-06-11 01:55:09 +0100
committerLibravatar cel 🌸 <cel@bunny.garden>2025-06-11 01:55:09 +0100
commit8bd186fe47eda25b36f945f926ce19093d16fe39 (patch)
treeb787e6fc48e3e1430deb520417eeb1653ca122c4 /src
parent089005c8d2f94d5ca42366fdd82cb4429fcc2dfa (diff)
downloadmacaw-web-8bd186fe47eda25b36f945f926ce19093d16fe39.tar.gz
macaw-web-8bd186fe47eda25b36f945f926ce19093d16fe39.tar.bz2
macaw-web-8bd186fe47eda25b36f945f926ce19093d16fe39.zip
feat: profile settings panel
Diffstat (limited to 'src')
-rw-r--r--src/views/macaw/settings.rs173
1 files changed, 152 insertions, 21 deletions
diff --git a/src/views/macaw/settings.rs b/src/views/macaw/settings.rs
index 11a3fc3..c4cc99b 100644
--- a/src/views/macaw/settings.rs
+++ b/src/views/macaw/settings.rs
@@ -4,23 +4,53 @@ use profile_settings::ProfileSettings;
use crate::{components::{icon::IconComponent, modal::Modal}, icon::Icon};
mod profile_settings {
- use filamento::error::{AvatarPublishError, CommandError};
+ use filamento::{error::{AvatarPublishError, CommandError, NickError}, user::User};
use thiserror::Error;
use leptos::prelude::*;
- use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent};
+ use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, Event, FileReader, HtmlInputElement, ProgressEvent, Url};
- use crate::{client::Client, files::Files};
+ use crate::{client::Client, files::Files, user::{fetch_avatar, NO_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,9 +62,24 @@ 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>();
@@ -43,6 +88,9 @@ mod profile_settings {
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();
@@ -92,32 +140,115 @@ 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_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| {
- ev.prevent_default();
- save_profile.dispatch(());
+ <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>
+ <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>
- <input disabled=profile_save_pending placeholder="Nickname" type="text" id="client-user-nick" bind:value=new_nick name="client-user-nick" />
+ <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>
}
}
}
@@ -153,10 +284,10 @@ pub fn Settings() -> impl IntoView {
<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::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>"privacy"</div> }.into_any(),
+ SettingsPage::Privacy => view! { <div style="padding: 16px">"Privacy settings coming soon!"</div> }.into_any(),
}
} else {
view! {}.into_any()