diff --git a/Cargo.lock b/Cargo.lock index 783e1d6..51867e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -817,8 +817,10 @@ dependencies = [ "base64", "gloo-console 0.3.0", "gloo-net 0.5.0", + "gloo-timers 0.3.0", "js-sys", "shared_data", + "uuid", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -1511,12 +1513,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", - "futures-channel", "futures-util", "http 1.0.0", "http-body", @@ -1524,14 +1525,13 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1588,9 +1588,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown", @@ -1599,9 +1599,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1632,9 +1632,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libdeflate-sys" @@ -1824,6 +1824,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.45" @@ -2286,9 +2292,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -2785,12 +2791,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "00b24b79b7a07f10209f19e683ca1e289d80b1e76ffa8c2b779718566a083679" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2805,10 +2812,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -2829,9 +2837,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -3120,6 +3128,15 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -3226,9 +3243,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" @@ -3410,9 +3427,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "winnow" -version = "0.5.35" +version = "0.5.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d" +checksum = "a7cad8365489051ae9f054164e459304af2e7e9bb407c958076c8bf4aef52da5" dependencies = [ "memchr", ] diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index af70e2b..d79b5ba 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -12,7 +12,9 @@ wasm-bindgen = { version = "0.2.89", default-features = false, features = ["std" yew = { version = "0.21.0", default-features = false, features = ["csr"] } yew-router = { version = "0.18.0", default-features = false } shared_data = { path = "../shared_data" } -web-sys = { version = "0.3.66", default-features = false, features = ["Window", "DragEvent", "DataTransfer", "FileList", "HtmlDocument", "HtmlButtonElement", "Blob"] } +web-sys = { version = "0.3.66", default-features = false, features = ["Window", "DragEvent", "DataTransfer", "FileList", "HtmlDocument", "HtmlButtonElement", "Blob", "HtmlElement", "CssStyleDeclaration"] } gloo-console = { version = "0.3.0", default-features = false } js-sys = { version = "0.3.66", default-features = false } base64 = { version = "0.21.7", default-features = false, features = ["alloc"] } +gloo-timers = "0.3.0" +uuid = { version = "1.7.0", features = ["v4"] } diff --git a/frontend/src/edit_post.rs b/frontend/src/edit_post.rs index 6730d65..fdedf82 100644 --- a/frontend/src/edit_post.rs +++ b/frontend/src/edit_post.rs @@ -18,9 +18,11 @@ use super::{ }; use gloo_net::http::Request; use gloo_console::log; +use gloo_timers::future::TimeoutFuture; use std::{ collections::HashSet, - rc::Rc + rc::Rc, + cell::RefCell }; // Since postgres starts ids at 1, we know that no post should have an id of 0, and thus we should @@ -53,6 +55,7 @@ pub enum EditMsg { SetInitial(HashSet, String, String, bool), Title(String), Content(String), + RenderedContent(String), AddTag(String), RemoveTag(String), } @@ -88,10 +91,8 @@ impl Reducible for PostDetails { }.into() }, EditMsg::Title(title) => clone_self!(title), - EditMsg::Content(content) => { - let rendered_content = shared_data::md_to_html(&content); - clone_self!(content, rendered_content) - }, + EditMsg::Content(content) => clone_self!(content), + EditMsg::RenderedContent(rendered_content) => clone_self!(rendered_content), EditMsg::AddTag(tag) => if tag.is_empty() { self } else { @@ -130,6 +131,10 @@ pub struct PostProps { pub fn edit_post(props: &PostProps) -> Html { let post = use_state(|| Option::>::None); let details = use_reducer_eq(|| PostDetails { id: props.id, ..PostDetails::default() }); + // kinda hate storing a Rc inside use_state but whatever, it seems to be the only way + // to actually keep a consistent reference to something inside this function and a promise at + // the same time + let render_uuid = use_state(|| Rc::new(RefCell::new(uuid::Uuid::new_v4()))); // These states aren't used until we're showing a post, but it can't be declared conditionally // (including after a potential return) or else yew gets angry at us and panics at runtime @@ -173,6 +178,30 @@ pub fn edit_post(props: &PostProps) -> Html { details.dispatch(EditMsg::SetInitial(tags, content, title, retrieved_post.draft)); } + fn set_text( + text: String, + render: &UseStateHandle>>, + details: &UseReducerHandle + ) { + let text_clone = text.clone(); + + let new_uuid = ::uuid::Uuid::new_v4(); + *render.borrow_mut() = new_uuid; + let render = render.clone(); + let details = details.clone(); + + details.dispatch(EditMsg::Content(text)); + + wasm_bindgen_futures::spawn_local(async move { + TimeoutFuture::new((text_clone.len() / 100) as u32).await; + + if *render.borrow() != new_uuid { return; } + + let rendered = shared_data::md_to_html(&text_clone); + details.dispatch(EditMsg::RenderedContent(rendered)); + }); + } + // Prepare things for submitting the post to the backend let submit_clone = submit.clone(); let submit_details = details.clone(); @@ -226,20 +255,6 @@ pub fn edit_post(props: &PostProps) -> Html { }); }); - macro_rules! input_callback{ - ($clone:ident, $type:ident, $evtype:ident, $html:ident) => { - move |e: $evtype| if let Some(msg) = e.target() - .and_then(|t| t.dyn_into::<$html>().ok()) - .map(|input| EditMsg::$type(input.value())) { - $clone.dispatch(msg); - } - }; - ($type:ident) => {{ - let details_clone = details.clone(); - Callback::from(input_callback!(details_clone, $type, Event, HtmlInputElement)) - }} - } - // If, at this point, we've uploaded an asset, gotten a response, but not yet inserted it into // the textarea, we need to do so if let AssetUploadState::Resolved(Ok((ref asset_id, false))) = *asset { @@ -251,13 +266,39 @@ pub fn edit_post(props: &PostProps) -> Html { let new_text = format!("{}\n\n{exclamation}[Asset](/api/assets/{asset_id})\n\n", details.content); asset.set(AssetUploadState::Resolved(Ok((asset_id.to_string(), true)))); - details.dispatch(EditMsg::Content(new_text)); + set_text(new_text, &render_uuid, &details); + } + + macro_rules! input_callback{ + ($type:ident) => {{ + let details_clone = details.clone(); + Callback::from(move |e: Event| if let Some(msg) = e.target() + .and_then(|t| t.dyn_into::().ok()) + .map(|input| EditMsg::$type(input.value())) { + details_clone.dispatch(msg); + } + ) + }} } // Get the callbacks for various elements of the editing view - let title_callback = input_callback!(Title); let content_clone = details.clone(); - let content_callback = input_callback!(content_clone, Content, InputEvent, HtmlTextAreaElement); + let render_clone = render_uuid.clone(); + let content_callback = move |e: InputEvent| if let Some(input) = e.target() + .and_then(|t| t.dyn_into::().ok()) { + let new_text = input.value(); + set_text(new_text, &render_clone, &content_clone); + + let style = input.style(); + if let Err(e) = style.set_property("height", "auto") { + log!("Couldn't set height to auto: ", e); + } + let new_height = format!("{}px", input.scroll_height()); + if let Err(e) = style.set_property("height", new_height.as_str()) { + log!("Couldn't update height correctly: ", e); + } + }; + let title_callback = input_callback!(Title); let tag_callback = input_callback!(AddTag); let asset_clone = asset.clone(); @@ -305,6 +346,7 @@ pub fn edit_post(props: &PostProps) -> Html { } textarea { height: 300px; + resize: vertical; } #rendered img { max-width: 100%;