summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs113
1 files changed, 108 insertions, 5 deletions
diff --git a/src/lib.rs b/src/lib.rs
index 26ea17b..772ea60 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,7 +14,7 @@ use std::{
use base64::{Engine, prelude::BASE64_STANDARD};
use chrono::{Local, NaiveDateTime, TimeDelta, Utc};
use filamento::{
- chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{CommandError, ConnectionError, DatabaseError, SubscribeError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Offline, Online, Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage
+ chat::{Body, Chat, ChatStoreFields, Delivery, Message, MessageStoreFields}, db::Db, error::{AvatarPublishError, CommandError, ConnectionError, DatabaseError, SubscribeError}, files::{opfs::OPFSError, FileStore, FilesMem, FilesOPFS}, presence::{Offline, Online, Presence, PresenceType, Show}, roster::{Contact, ContactStoreFields}, user::{User, UserStoreFields}, UpdateMessage
};
use futures::stream::StreamExt;
use indexmap::IndexMap;
@@ -23,7 +23,7 @@ use leptos::{
ev::{Event, KeyboardEvent, MouseEvent, SubmitEvent},
html::{self, Div, Input, Pre, Textarea},
prelude::*,
- tachys::{dom::document, reactive_graph::bind::GetValue},
+ tachys::{dom::document, reactive_graph::bind::GetValue, renderer::dom::Element},
task::{spawn, spawn_local},
};
use reactive_stores::{ArcStore, Store, StoreField};
@@ -35,6 +35,7 @@ use tokio::sync::{
};
use tracing::{debug, error};
use uuid::Uuid;
+use web_sys::{js_sys::Uint8Array, wasm_bindgen::{prelude::Closure, JsCast, UnwrapThrowExt}, FileReader, HtmlInputElement, ProgressEvent};
const NO_AVATAR: &str = "/assets/no-avatar.png";
@@ -51,7 +52,7 @@ pub struct Client {
file_store: Files,
}
-#[derive(Clone)]
+#[derive(Clone, Debug)]
pub enum Files {
Mem(FilesMem),
Opfs(FilesOPFS),
@@ -1133,11 +1134,113 @@ pub fn Settings() -> impl IntoView {
}
}
+#[derive(Debug, Clone, Error)]
+pub enum ProfileSaveError {
+ #[error("avatar publish: {0}")]
+ Avatar(#[from] CommandError<AvatarPublishError<Files>>),
+}
+
#[component]
pub fn ProfileSettings() -> 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 || {
+ error.with(|error| {
+ if let Some(error) = error {
+ view! { <div class="error">{error.to_string()}</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 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 reader = FileReader::new().unwrap_throw();
+ // let name = file.name();
+
+ // This closure only needs to be called a single time as we will just
+ // remake it on each loop
+ // * web_sys drops it for us when using this specific constructor
+ let read_file = {
+ // FileReader is cloned prior to moving into the closure
+ let reader = reader.to_owned();
+ Closure::once_into_js(move |_: ProgressEvent| {
+ // `.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();
+ // Do whatever you want with the Vec<u8>
+ profile_upload_data.set(Some(data));
+ })
+ };
+ reader.set_onloadend(Some(read_file.as_ref().unchecked_ref()));
+
+ // read_as_array_buffer takes a &Blob
+ //
+ // Per https://w3c.github.io/FileAPI/#file-section
+ // > A File object is a Blob object with a name attribute..
+ //
+ // File is a subclass (inherits) from the Blob interface, so a File
+ // can be used anywhere a Blob is required.
+ reader.read_as_array_buffer(&file).unwrap_throw();
+
+ // You can also use `.read_as_text(&file)` instead if you just want a string.
+ // This example shows how to extract an array buffer as it is more flexible
+ //
+ // If you use `.read_as_text` change the closure from ...
+ //
+ // let result = reader.result().unwrap_throw();
+ // let vec_of_u8_bytes = Uint8Array::new(&result).to_vec();
+ // let content = String::from_utf8(vec_of_u8_bytes).unwrap_throw();
+ //
+ // to ...
+ //
+ // let result = reader.result().unwrap_throw();
+ // let content = result.as_string().unwrap_throw();
+ } else {
+ profile_upload_data.set(None);
+ }
+ };
+
+ let save_profile = Action::new_local(move |_| {
+ let client = client.clone();
+ async move {}
+ });
+
+ let new_nick= RwSignal::new("".to_string());
+
view! {
- <form>
- </form>
+ <div class="profile-settings">
+ <form on:submit=move |ev| {
+ ev.prevent_default();
+ save_profile.dispatch(());
+ }>
+ {error_message}
+ <div class="change-avatar">
+ <input type="file" id="client-user-avatar" on:change=from_input />
+ </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" />
+ </form>
+ </div>
+ <div class="profile-preview">
+ <h2>Profile Preview</h2>
+ <div class="preview">
+ <img />
+ <div>nick</div>
+ </div>
+ </div>
}
}