diff --git a/.github/workflows/pull_request.yaml b/.github/workflows/pull_request.yaml index 3c14f6e4..b4a56aba 100644 --- a/.github/workflows/pull_request.yaml +++ b/.github/workflows/pull_request.yaml @@ -8,6 +8,7 @@ jobs: steps: - uses: actions/checkout@v6 - uses: Swatinem/rust-cache@v2 + - run: cargo fmt --check - uses: browser-actions/setup-chrome@v2 - uses: nanasess/setup-chromedriver@v2 - run: cargo install wasm-bindgen-cli diff --git a/.vscode/extensions.json b/.vscode/extensions.json index dcd5991c..20fba424 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "mhutchie.git-graph", - "github.vscode-github-actions" + "github.vscode-github-actions", + "rust-lang.rust-analyzer" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e730c6d..ef47b992 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,10 @@ "editor.tabSize": 2, "editor.formatOnSave": true, "editor.defaultFormatter": "vscode.markdown-language-features", + "rust-analyzer.check.command": "clippy", + "rust-analyzer.check.extraArgs": [ + "--all-features" + ], "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" }, diff --git a/Cargo.lock b/Cargo.lock index d566bf48..677b941b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "shlex", @@ -264,11 +264,13 @@ name = "definy-event" version = "0.1.0" dependencies = [ "anyhow", + "base64", "chrono", "ed25519-dalek", "rand", "serde", "serde_cbor 0.11.2", + "sha2", "sqlx", "strum", "strum_macros", @@ -898,9 +900,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" @@ -929,9 +931,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.182" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -941,12 +943,13 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ "bitflags", "libc", + "plain", "redox_syscall 0.7.3", ] @@ -1074,9 +1077,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "parking" @@ -1161,6 +1164,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1190,9 +1199,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1487,12 +1496,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1827,9 +1836,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -1842,9 +1851,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.49.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -1859,9 +1868,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -2217,15 +2226,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -2259,30 +2259,13 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", + "windows_i686_gnullvm", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2295,12 +2278,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2313,12 +2290,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2331,24 +2302,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2361,12 +2320,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2379,12 +2332,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2397,12 +2344,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2415,12 +2356,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - [[package]] name = "writeable" version = "0.6.2" @@ -2452,18 +2387,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.40" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", diff --git a/definy-build/src/main.rs b/definy-build/src/main.rs index aad9fc29..03f8e462 100644 --- a/definy-build/src/main.rs +++ b/definy-build/src/main.rs @@ -18,7 +18,7 @@ fn main() -> Result<(), Box> { { let wasm_build_result = std::process::Command::new("cargo") - .args(&[ + .args([ "build", "--release", "-p", @@ -37,7 +37,7 @@ fn main() -> Result<(), Box> { { let wasm_bindgen_result = std::process::Command::new("wasm-bindgen") - .args(&[ + .args([ "--out-dir", "./web-distribution", "--target", diff --git a/definy-client/Cargo.toml b/definy-client/Cargo.toml index 102b4fac..480b1cad 100644 --- a/definy-client/Cargo.toml +++ b/definy-client/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib"] [dependencies] js-sys = "0.3" wasm-bindgen = "0.2" -web-sys = { version = "0.3.85", features = ["console", "Window", "Document", "Element", "Node", "NodeList", "HtmlElement", "Event", "HtmlDialogElement", "Navigator", "Clipboard", "Request", "RequestInit", "Response", "Headers", "Url", "KeyboardEvent", "DomRect", "ScrollIntoViewOptions", "ScrollBehavior", "ScrollLogicalPosition"] } +web-sys = { version = "0.3.91", features = ["console", "Window", "Document", "Element", "Node", "NodeList", "HtmlElement", "Event", "HtmlDialogElement", "Navigator", "Clipboard", "Request", "RequestInit", "Response", "Headers", "Url", "KeyboardEvent", "DomRect", "ScrollIntoViewOptions", "ScrollBehavior", "ScrollLogicalPosition"] } definy-ui = { path = "../definy-ui" } definy-event = { path = "../definy-event" } narumincho-vdom-client = { path = "../narumincho-vdom-client" } @@ -18,8 +18,8 @@ ed25519-dalek = { version = "2.2", features = ["rand_core"] } rand = { version = "0.8", features = ["std_rng"] } getrandom = { version = "0.2", features = ["js"] } base64 = "0.22" -wasm-bindgen-futures = "0.4.58" -chrono = "0.4.43" -anyhow = "1.0.100" +wasm-bindgen-futures = "0.4.64" +chrono = "0.4.44" +anyhow = "1.0.102" serde_cbor = { version = "0.11.2", features = ["std", "tags"] } sha2 = "0.10.9" diff --git a/definy-client/src/keyboard_nav.rs b/definy-client/src/keyboard_nav.rs index c7ec9c80..7f1562c4 100644 --- a/definy-client/src/keyboard_nav.rs +++ b/definy-client/src/keyboard_nav.rs @@ -68,13 +68,13 @@ pub fn handle_keydown(state: AppState, key: String) -> AppState { } else { format!("[data-path='{}']", path_str) }; - if let Ok(Some(el)) = document.query_selector(&selector) { - if let Ok(html_el) = el.dyn_into::() { - let opts = web_sys::ScrollIntoViewOptions::new(); - opts.set_behavior(web_sys::ScrollBehavior::Smooth); - opts.set_block(web_sys::ScrollLogicalPosition::Nearest); - html_el.scroll_into_view_with_scroll_into_view_options(&opts); - } + if let Ok(Some(el)) = document.query_selector(&selector) + && let Ok(html_el) = el.dyn_into::() + { + let opts = web_sys::ScrollIntoViewOptions::new(); + opts.set_behavior(web_sys::ScrollBehavior::Smooth); + opts.set_block(web_sys::ScrollLogicalPosition::Nearest); + html_el.scroll_into_view_with_scroll_into_view_options(&opts); } } @@ -102,12 +102,12 @@ fn get_all_paths(document: &Document) -> Vec<(Vec, web_sys::DomRect)> let mut elements = Vec::new(); if let Ok(nodelist) = document.query_selector_all("[data-path]") { for i in 0..nodelist.length() { - if let Some(node) = nodelist.item(i) { - if let Ok(el) = node.dyn_into::() { - let path_str = el.get_attribute("data-path").unwrap_or_default(); - if let Some(path) = definy_ui::string_to_path(&path_str) { - elements.push((path, el.get_bounding_client_rect())); - } + if let Some(node) = nodelist.item(i) + && let Ok(el) = node.dyn_into::() + { + let path_str = el.get_attribute("data-path").unwrap_or_default(); + if let Some(path) = definy_ui::string_to_path(&path_str) { + elements.push((path, el.get_bounding_client_rect())); } } } @@ -145,14 +145,12 @@ fn focus_input_in_path(document: &Document, current_path: &[PathStep]) { } else { format!("[data-path='{}']", path_str) }; - if let Ok(Some(el)) = document.query_selector(&selector) { - if let Ok(input) = el.query_selector("input, textarea") { - if let Some(input_el) = input { - if let Ok(html_el) = input_el.dyn_into::() { - let _ = html_el.focus(); - } - } - } + if let Ok(Some(el)) = document.query_selector(&selector) + && let Ok(input) = el.query_selector("input, textarea") + && let Some(input_el) = input + && let Ok(html_el) = input_el.dyn_into::() + { + let _ = html_el.focus(); } } diff --git a/definy-client/src/lib.rs b/definy-client/src/lib.rs index 75825666..13f8a17b 100644 --- a/definy-client/src/lib.rs +++ b/definy-client/src/lib.rs @@ -1,3 +1,4 @@ +use definy_event::EventHashId; use definy_ui::AppState; use definy_ui::ResourceHash; use wasm_bindgen::JsValue; @@ -29,11 +30,7 @@ fn read_resource_hash_from_dom() -> Option { .split("';") .next()?; - let wasm = text - .split("module_or_path: \"") - .nth(1)? - .split('"') - .next()?; + let wasm = text.split("module_or_path: \"").nth(1)?.split('"').next()?; Some(ResourceHash { js: js.to_string(), @@ -48,25 +45,17 @@ fn read_ssr_initial_state_text() -> Option { .text_content() } -fn read_ssr_state() -> Option<( - Vec<( - [u8; 32], - Result< - (ed25519_dalek::Signature, definy_event::event::Event), - definy_event::VerifyAndDeserializeError, - >, - )>, - bool, - Vec>, -)> { +fn read_ssr_state() -> Option<(Vec, bool, Vec>)> { let text = SSR_INITIAL_STATE_TEXT.as_ref()?.to_string(); let decoded = definy_ui::decode_ssr_state(text.as_str())?; let event_binaries = decoded.event_binaries; let events = event_binaries .iter() .map(|bytes| { - let hash: [u8; 32] = ::digest(bytes).into(); - (hash, definy_event::verify_and_deserialize(bytes)) + ( + EventHashId::from_bytes(bytes), + definy_event::verify_and_deserialize(bytes), + ) }) .collect(); Some((events, decoded.has_more, event_binaries)) @@ -106,7 +95,10 @@ impl narumincho_vdom_client::App for DefinyApp { .unwrap_or_default(); let url = web_sys::Url::new(&initial_url).unwrap(); let search = url.search(); - search.strip_prefix('?').unwrap_or(search.as_str()).to_string() + search + .strip_prefix('?') + .unwrap_or(search.as_str()) + .to_string() }; let query_params = definy_ui::query::parse_query(Some(query_string.as_str())); @@ -115,48 +107,20 @@ impl narumincho_vdom_client::App for DefinyApp { .and_then(|window| window.document()) .and_then(|document| document.document_element()) .and_then(|element| element.get_attribute("lang")); - let fallback_language = html_lang - .as_deref() - .and_then(definy_ui::language::language_from_tag) - .or_else(definy_ui::language::best_language_from_browser) - .unwrap_or_else(definy_ui::language::default_language); - let language_resolution = if let Some(requested_lang) = query_params.lang { - if let Some(language) = definy_ui::language::language_from_tag(requested_lang.as_str()) { - definy_ui::language::LanguageResolution { - language, - unsupported_query_lang: None, - } - } else { - definy_ui::language::LanguageResolution { - language: fallback_language, - unsupported_query_lang: Some(requested_lang), - } - } - } else { - definy_ui::language::LanguageResolution { - language: fallback_language, - unsupported_query_lang: None, - } - }; - let language_fallback_notice = - language_resolution - .unsupported_query_lang - .as_ref() - .map(|requested| definy_ui::LanguageFallbackNotice { - requested: requested.to_string(), - fallback_to_code: language_resolution.language.code, - }); + let language_resolution = definy_ui::language::resolve_language_with_fallback( + Some(query_string.as_str()), + || { + html_lang + .as_deref() + .and_then(definy_ui::language::language_from_tag) + .or_else(definy_ui::language::best_language_from_browser) + .unwrap_or_else(definy_ui::language::default_language) + }, + ); + let language_fallback_notice = language_resolution.fallback_notice(); wasm_bindgen_futures::spawn_local(async move { if let Some(ssr_event_binaries) = ssr_event_binaries { - let event_pairs = ssr_event_binaries - .into_iter() - .map(|bytes| { - let hash: [u8; 32] = - ::digest(&bytes).into(); - (hash, bytes) - }) - .collect::>(); - let _ = definy_ui::indexed_db::store_events(&event_pairs).await; + let _ = definy_ui::indexed_db::store_events(&ssr_event_binaries).await; } if !has_ssr_events { if let Ok(cached_event_binaries) = @@ -165,8 +129,7 @@ impl narumincho_vdom_client::App for DefinyApp { let mut cached_events = cached_event_binaries .into_iter() .map(|bytes| { - let hash: [u8; 32] = - ::digest(&bytes).into(); + let hash = EventHashId::from_bytes(&bytes); let event = definy_event::verify_and_deserialize(&bytes); (hash, event) }) @@ -186,8 +149,8 @@ impl narumincho_vdom_client::App for DefinyApp { let mut event_cache = state.event_cache.clone(); let mut event_hashes = Vec::new(); for (hash, event) in &cached_events { - event_cache.insert(*hash, event.clone()); - event_hashes.push(*hash); + event_cache.insert(hash.clone(), event.clone()); + event_hashes.push(hash.clone()); } AppState { event_cache, @@ -203,19 +166,15 @@ impl narumincho_vdom_client::App for DefinyApp { } })); } - let events = definy_ui::fetch::get_events( - filter_for_fetch, - Some(20), - Some(0), - ) - .await - .unwrap(); + let events = definy_ui::fetch::get_events(filter_for_fetch, Some(20), Some(0)) + .await + .unwrap(); fire(Box::new(move |state| { let mut event_cache = state.event_cache.clone(); let mut event_hashes = Vec::new(); for (hash, event) in &events { - event_cache.insert(*hash, event.clone()); - event_hashes.push(*hash); + event_cache.insert(hash.clone(), event.clone()); + event_hashes.push(hash.clone()); } AppState { event_cache, @@ -270,8 +229,7 @@ impl narumincho_vdom_client::App for DefinyApp { definy_ui::Location::from_url(&pathname) }; - let (events, is_loading, has_more) = - if let Some((ssr_events, has_more, _)) = ssr_state { + let (events, is_loading, has_more) = if let Some((ssr_events, has_more, _)) = ssr_state { // SSRが送ってきた状態をそのまま採用 (ssr_events, false, has_more) } else { @@ -328,34 +286,30 @@ impl narumincho_vdom_client::App for DefinyApp { language_fallback_notice, ..state }; - if matches!(next.location, Some(definy_ui::Location::Home)) { - if next.event_list_state.filter_event_type != filter_event_type { - next.event_list_state = definy_ui::EventListState { - event_hashes: Vec::new(), - current_offset: 0, - page_size: next.event_list_state.page_size, - is_loading: false, - has_more: true, - filter_event_type, - }; - } + if matches!(next.location, Some(definy_ui::Location::Home)) + && next.event_list_state.filter_event_type != filter_event_type + { + next.event_list_state = definy_ui::EventListState { + event_hashes: Vec::new(), + current_offset: 0, + page_size: next.event_list_state.page_size, + is_loading: false, + has_more: true, + filter_event_type, + }; } - if query_params.lang.is_none() { - if let Some(location) = &next.location { - let url = AppState::build_url( - location, - next.language.code, - filter_event_type, + if query_params.lang.is_none() + && let Some(location) = &next.location + { + let url = AppState::build_url(location, next.language.code, filter_event_type); + if let Some(window) = web_sys::window() + && let Ok(history) = window.history() + { + let _ = history.replace_state_with_url( + &wasm_bindgen::JsValue::NULL, + "", + Some(url.as_str()), ); - if let Some(window) = web_sys::window() { - if let Ok(history) = window.history() { - let _ = history.replace_state_with_url( - &wasm_bindgen::JsValue::NULL, - "", - Some(url.as_str()), - ); - } - } } } return next; @@ -364,6 +318,6 @@ impl narumincho_vdom_client::App for DefinyApp { } fn render(state: &AppState) -> narumincho_vdom::Node { - definy_ui::render(state, &*SSR_RESOURCE_HASH, SSR_INITIAL_STATE_TEXT.as_deref()) + definy_ui::render(state, &SSR_RESOURCE_HASH, SSR_INITIAL_STATE_TEXT.as_deref()) } } diff --git a/definy-event/Cargo.toml b/definy-event/Cargo.toml index 9886470d..3e9f0e1d 100644 --- a/definy-event/Cargo.toml +++ b/definy-event/Cargo.toml @@ -3,16 +3,15 @@ name = "definy-event" version = "0.1.0" edition = "2024" -[lib] -doctest = false - [dependencies] -chrono = { version = "0.4.43", features = ["serde"] } +chrono = { version = "0.4.44", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_cbor = { version = "0.11.2", features = ["std", "tags"] } ed25519-dalek = { version = "2.2", features = ["rand_core", "serde"] } -anyhow = "1.0.100" +anyhow = "1.0.102" rand = "0.8.5" strum_macros = "0.27.2" strum = { version = "0.27.2", features = ["derive"] } sqlx = { version = "0.8.6", features = ["macros"] } +sha2 = "0.10.9" +base64 = "0.22.1" diff --git a/definy-event/src/cbor_datetime_tag1.rs b/definy-event/src/cbor_datetime_tag1.rs index c1c2a0c0..c470fae6 100644 --- a/definy-event/src/cbor_datetime_tag1.rs +++ b/definy-event/src/cbor_datetime_tag1.rs @@ -24,10 +24,9 @@ where let secs = tagged.value.trunc() as i64; let nanos = (tagged.value.fract() * 1_000_000_000.0).round() as u32; - Ok(Utc - .timestamp_opt(secs, nanos) + Utc.timestamp_opt(secs, nanos) .single() - .ok_or_else(|| serde::de::Error::custom("invalid timestamp"))?) + .ok_or_else(|| serde::de::Error::custom("invalid timestamp")) } #[cfg(test)] diff --git a/definy-event/src/event.rs b/definy-event/src/event.rs index 858d9539..4f54e07e 100644 --- a/definy-event/src/event.rs +++ b/definy-event/src/event.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; +use crate::EventHashId; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Event { pub account_id: AccountId, @@ -38,18 +40,18 @@ pub struct PartDefinitionEvent { pub description: Box, pub expression: Expression, #[serde(default)] - pub module_definition_event_hash: Option<[u8; 32]>, + pub module_definition_event_hash: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PartUpdateEvent { pub part_name: Box, pub part_description: Box, - pub part_definition_event_hash: [u8; 32], + pub part_definition_event_hash: EventHashId, #[serde(default = "default_expression")] pub expression: Expression, #[serde(default)] - pub module_definition_event_hash: Option<[u8; 32]>, + pub module_definition_event_hash: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -63,7 +65,7 @@ pub struct ModuleDefinitionEvent { pub struct ModuleUpdateEvent { pub module_name: Box, pub module_description: Box, - pub module_definition_event_hash: [u8; 32], + pub module_definition_event_hash: EventHashId, } fn default_expression() -> Expression { @@ -77,7 +79,7 @@ pub enum PartType { String, Boolean, Type, - TypePart([u8; 32]), + TypePart(EventHashId), List(Box), } @@ -130,7 +132,7 @@ pub struct TypeListExpression { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PartReferenceExpression { - pub part_definition_event_hash: [u8; 32], + pub part_definition_event_hash: EventHashId, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -177,7 +179,7 @@ pub struct TypeLiteralItemExpression { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ConstructorExpression { - pub type_part_definition_event_hash: [u8; 32], + pub type_part_definition_event_hash: EventHashId, pub value: Box, } @@ -192,4 +194,37 @@ pub struct ChangeProfileEvent { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct AccountId(pub Box<[u8; 32]>); +pub struct AccountId(pub ed25519_dalek::VerifyingKey); + +impl std::fmt::Display for AccountId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&base64::Engine::encode( + &base64::engine::general_purpose::URL_SAFE_NO_PAD, + self.0.as_bytes(), + )) + } +} + +impl std::str::FromStr for AccountId { + type Err = AccountIdFromStrError; + + fn from_str(s: &str) -> Result { + let bytes = base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, s) + .map_err(AccountIdFromStrError::DecodeError)?; + + let bytes: [u8; 32] = bytes + .try_into() + .map_err(AccountIdFromStrError::InvalidByteSize)?; + Ok(AccountId( + ed25519_dalek::VerifyingKey::from_bytes(&bytes) + .map_err(AccountIdFromStrError::InvalidBytes)?, + )) + } +} + +#[derive(Debug)] +pub enum AccountIdFromStrError { + DecodeError(base64::DecodeError), + InvalidBytes(ed25519_dalek::SignatureError), + InvalidByteSize(<[u8; 32] as TryFrom>>::Error), +} diff --git a/definy-event/src/event_hash_id.rs b/definy-event/src/event_hash_id.rs new file mode 100644 index 00000000..b183d941 --- /dev/null +++ b/definy-event/src/event_hash_id.rs @@ -0,0 +1,37 @@ +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct EventHashId([u8; 32]); + +impl EventHashId { + pub fn from_bytes(bytes: &[u8]) -> EventHashId { + let hash: [u8; 32] = ::digest(bytes).into(); + EventHashId(hash) + } +} + +impl std::fmt::Display for EventHashId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&base64::Engine::encode( + &base64::engine::general_purpose::URL_SAFE_NO_PAD, + self.0, + )) + } +} + +impl std::str::FromStr for EventHashId { + type Err = EventHashIdFromStrError; + + fn from_str(s: &str) -> Result { + let bytes = base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, s) + .map_err(EventHashIdFromStrError::DecodeError)?; + let bytes: [u8; 32] = bytes + .try_into() + .map_err(EventHashIdFromStrError::InvalidByteSize)?; + Ok(EventHashId(bytes)) + } +} + +#[derive(Debug)] +pub enum EventHashIdFromStrError { + DecodeError(base64::DecodeError), + InvalidByteSize(<[u8; 32] as TryFrom>>::Error), +} diff --git a/definy-event/src/lib.rs b/definy-event/src/lib.rs index 650d51e5..e11ab6ea 100644 --- a/definy-event/src/lib.rs +++ b/definy-event/src/lib.rs @@ -2,8 +2,11 @@ use crate::event::Event; pub mod cbor_datetime_tag1; pub mod event; +mod event_hash_id; pub mod response; +pub use event_hash_id::EventHashId; + #[derive(serde::Serialize, serde::Deserialize)] pub struct SignedEvent { pub signature: ed25519_dalek::Signature, @@ -41,10 +44,8 @@ pub fn verify_and_deserialize( let event: Event = serde_cbor::from_slice(&signed_event.event_binary.value) .map_err(|_| VerifyAndDeserializeError::DecodeError)?; - let public_key = ed25519_dalek::VerifyingKey::from_bytes(event.account_id.0.as_ref()) - .map_err(|_| VerifyAndDeserializeError::DecodeError)?; ed25519_dalek::Verifier::verify( - &public_key, + &event.account_id.0, &signed_event.event_binary.value, &signed_event.signature, ) @@ -63,7 +64,7 @@ mod tests { let signing_key = ed25519_dalek::SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); - let account_id = event::AccountId(Box::new(verifying_key.to_bytes())); + let account_id = event::AccountId(verifying_key); let event = event::Event { account_id: account_id.clone(), @@ -106,7 +107,7 @@ mod tests { let signing_key = ed25519_dalek::SigningKey::generate(&mut csprng); let verifying_key = signing_key.verifying_key(); - let account_id = event::AccountId(Box::new(verifying_key.to_bytes())); + let account_id = event::AccountId(verifying_key); let event = event::Event { account_id, diff --git a/definy-server/Cargo.toml b/definy-server/Cargo.toml index 4169798e..ad6cc08f 100644 --- a/definy-server/Cargo.toml +++ b/definy-server/Cargo.toml @@ -20,7 +20,7 @@ sqlx = { version = "0.8.6", features = [ "macros", "chrono", ] } -anyhow = "1.0.100" +anyhow = "1.0.102" ed25519-dalek = { version = "2.2" } hex = "0.4.3" base64 = "0.22.1" diff --git a/definy-server/src/db.rs b/definy-server/src/db.rs index 27bc9c70..7e39d38e 100644 --- a/definy-server/src/db.rs +++ b/definy-server/src/db.rs @@ -146,7 +146,7 @@ pub async fn get_events( query.push_str(&format!(" OFFSET ${}", bind_index)); let offset_value = std::cmp::min(offset, i64::MAX as usize) as i64; binds.push(BindValue::Offset(offset_value)); - bind_index += 1; + // bind_index += 1; } let mut sql_query = sqlx::query(&query); diff --git a/definy-server/src/event.rs b/definy-server/src/event.rs index 13395818..f6852446 100644 --- a/definy-server/src/event.rs +++ b/definy-server/src/event.rs @@ -42,15 +42,14 @@ pub async fn handle_event_get( .header("Content-Type", "text/html; charset=utf-8") .body(Full::new(Bytes::from("404 Not Found"))), Ok(Some(event_binary)) => { - if let Some(accept) = request.headers().get("accept") { - if let Ok(accept_as_str) = accept.to_str() { - if accept_as_str.contains("text/html") { - return Response::builder() - .status(200) - .header("Content-Type", "text/html; charset=utf-8") - .body(Full::new(Bytes::from("todo"))); - } - } + if let Some(accept) = request.headers().get("accept") + && let Ok(accept_as_str) = accept.to_str() + && accept_as_str.contains("text/html") + { + return Response::builder() + .status(200) + .header("Content-Type", "text/html; charset=utf-8") + .body(Full::new(Bytes::from("todo"))); } Response::builder() .status(200) @@ -72,9 +71,9 @@ pub async fn handle_events( address: SocketAddr, pool: &sqlx::postgres::PgPool, ) -> Result>, hyper::http::Error> { - match request.method() { - &hyper::Method::GET => handle_events_get(request.uri().query(), pool).await, - &hyper::Method::POST => handle_events_post(request, address, pool).await, + match *request.method() { + hyper::Method::GET => handle_events_get(request.uri().query(), pool).await, + hyper::Method::POST => handle_events_post(request, address, pool).await, _ => Response::builder() .status(405) .header("Content-Type", "text/html; charset=utf-8") @@ -104,7 +103,7 @@ async fn handle_events_get( } Ok(events) => { match serde_cbor::to_vec(&definy_event::response::EventsResponse { - events: events, + events, next_cursor: None, }) { Ok(cbor) => Response::builder() diff --git a/definy-server/src/main.rs b/definy-server/src/main.rs index 11579cf0..1a2edbda 100644 --- a/definy-server/src/main.rs +++ b/definy-server/src/main.rs @@ -12,7 +12,6 @@ use hyper::service::service_fn; use hyper::{Request, Response}; use hyper_util::rt::TokioIo; use narumincho_vdom::Route; -use sha2::Digest; use tokio::net::TcpListener; use tokio::sync::RwLock; @@ -98,16 +97,15 @@ async fn main() -> Result<(), anyhow::Error> { const JAVASCRIPT_CONTENT: &[u8] = include_bytes!("../../web-distribution/definy_client.js"); -const JAVASCRIPT_HASH: &'static str = - include_str!("../../web-distribution/definy_client.js.sha256"); +const JAVASCRIPT_HASH: &str = include_str!("../../web-distribution/definy_client.js.sha256"); const WASM_CONTENT: &[u8] = include_bytes!("../../web-distribution/definy_client_bg.wasm"); -const WASM_HASH: &'static str = include_str!("../../web-distribution/definy_client_bg.wasm.sha256"); +const WASM_HASH: &str = include_str!("../../web-distribution/definy_client_bg.wasm.sha256"); const ICON_CONTENT: &[u8] = include_bytes!("../../assets/icon.png"); -const ICON_HASH: &'static str = include_str!("../../web-distribution/icon.png.sha256"); +const ICON_HASH: &str = include_str!("../../web-distribution/icon.png.sha256"); async fn handler( request: Request, @@ -140,15 +138,9 @@ async fn handler( .headers() .get("accept-language") .and_then(|value| value.to_str().ok()); - let language_resolution = definy_ui::language::resolve_language(uri.query(), accept_language); - let language_fallback_notice = - language_resolution - .unsupported_query_lang - .as_ref() - .map(|requested| definy_ui::LanguageFallbackNotice { - requested: requested.to_string(), - fallback_to_code: language_resolution.language.code, - }); + let language_resolution = + definy_ui::language::resolve_language(uri.query(), accept_language); + let language_fallback_notice = language_resolution.fallback_notice(); let pool = state.pool.read().await.clone(); return match pool { Some(pool) => { @@ -195,12 +187,10 @@ async fn handler( None => db_unavailable_response(false), } } else { - match path { - _ => Response::builder() - .status(404) - .header("Content-Type", "text/html; charset=utf-8") - .body(Full::new(Bytes::from("404 Not Found"))), - } + Response::builder() + .status(404) + .header("Content-Type", "text/html; charset=utf-8") + .body(Full::new(Bytes::from("404 Not Found"))) } } } @@ -231,25 +221,25 @@ async fn handle_html( let path = uri.path(); let query = uri.query(); let location = definy_ui::Location::from_url(path); - if let Some(ref location) = location { - if location.to_url() != path { - let mut redirect_url = location.to_url(); - if let Some(query) = query { - if !query.is_empty() { - redirect_url.push('?'); - redirect_url.push_str(query); - } - } - return Response::builder() - .status(301) - .header("Location", redirect_url) - .body(Full::new(Bytes::from("Redirecting..."))); + if let Some(ref location) = location + && location.to_url() != path + { + let mut redirect_url = location.to_url(); + if let Some(query) = query + && !query.is_empty() + { + redirect_url.push('?'); + redirect_url.push_str(query); } + return Response::builder() + .status(301) + .header("Location", redirect_url) + .body(Full::new(Bytes::from("Redirecting..."))); } let filter_event_type = definy_ui::event_filter_from_query(query); - let event_binary_array = - match db::get_events(pool, filter_event_type, Some(20), Some(0)).await { + let event_binary_array = match db::get_events(pool, filter_event_type, Some(20), Some(0)).await + { Ok(events) => events, Err(error) => { eprintln!("Failed to get events for SSR: {:?}", error); @@ -259,9 +249,8 @@ async fn handle_html( let events = event_binary_array .iter() - .into_iter() .map(|event_binary| { - let hash: [u8; 32] = sha2::Sha256::digest(event_binary.as_slice()).into(); + let hash = definy_event::EventHashId::from_bytes(event_binary.as_slice()); ( hash, definy_event::verify_and_deserialize(event_binary.as_slice()), diff --git a/definy-ui/Cargo.toml b/definy-ui/Cargo.toml index 99bf601c..a3ec0029 100644 --- a/definy-ui/Cargo.toml +++ b/definy-ui/Cargo.toml @@ -3,15 +3,12 @@ name = "definy-ui" version = "0.1.0" edition = "2024" -[lib] -doctest = false - [dependencies] narumincho-vdom = { path = "../narumincho-vdom" } base64 = "0.22" ed25519-dalek = { version = "2.2" } definy-event = { path = "../definy-event" } -web-sys = { version = "0.3.85", features = [ +web-sys = { version = "0.3.91", features = [ "console", "Window", "History", @@ -42,20 +39,20 @@ web-sys = { version = "0.3.85", features = [ "CredentialsContainer", "CredentialRequestOptions", ] } -wasm-bindgen = "0.2.108" +wasm-bindgen = "0.2.114" rand = "0.8.5" -wasm-bindgen-futures = "0.4.58" -js-sys = "0.3.76" -anyhow = "1.0.100" -chrono = { version = "0.4.43", features = ["serde"] } +wasm-bindgen-futures = "0.4.64" +js-sys = "0.3.91" +anyhow = "1.0.102" +chrono = { version = "0.4.44", features = ["serde"] } serde_cbor = { version = "0.11.2", features = ["std", "tags"] } serde = { version = "1.0.228", features = ["derive"] } serde_urlencoded = "0.7" sha2 = "0.10.9" [dev-dependencies] -hyper = { version = "1.7", features = ["http1", "server"] } -hyper-util = { version = "0.1.10", features = ["client-legacy", "http1"] } +hyper = { version = "1.8", features = ["http1", "server"] } +hyper-util = { version = "0.1.20", features = ["client-legacy", "http1"] } http-body-util = "0.1.3" -tokio = { version = "1.47", features = ["macros", "rt-multi-thread", "net", "time"] } -serde_json = "1.0.145" +tokio = { version = "1.50", features = ["macros", "rt-multi-thread", "net", "time"] } +serde_json = "1.0.149" diff --git a/definy-ui/src/account_detail.rs b/definy-ui/src/account_detail.rs index 584bb953..8d0e23ae 100644 --- a/definy-ui/src/account_detail.rs +++ b/definy-ui/src/account_detail.rs @@ -1,16 +1,15 @@ +use definy_event::EventHashId; use narumincho_vdom::*; -use crate::{AppState, Location, fetch}; use crate::i18n; +use crate::{AppState, Location, fetch}; pub fn account_detail_view( state: &AppState, account_id: &definy_event::event::AccountId, ) -> Node { let account_name_map = state.account_name_map(); - let account_name = - crate::app_state::account_display_name(&account_name_map, account_id); - let encoded_account_id = crate::hash_format::encode_hash32(account_id.0.as_ref()); + let account_name = crate::app_state::account_display_name(&account_name_map, account_id); let account_events = state .event_cache @@ -18,16 +17,17 @@ pub fn account_detail_view( .filter_map(|(hash, event_result)| { let (_, event) = event_result.as_ref().ok()?; if event.account_id == *account_id { - Some((*hash, event)) + Some((hash, event)) } else { None } }) - .collect::>(); + .collect::>(); - let is_current_account = state.current_key.as_ref().is_some_and(|key| { - key.verifying_key().to_bytes().as_slice() == account_id.0.as_ref() - }); + let is_current_account = state + .current_key + .as_ref() + .is_some_and(|key| key.verifying_key().to_bytes().as_slice() == account_id.0.as_ref()); let profile_form = if is_current_account { Some( @@ -49,20 +49,7 @@ pub fn account_detail_view( .name("profile-name") .value(&state.profile_name_input) .on_change(EventHandler::new(async |set_state| { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document.query_selector("input[name='profile-name']").ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::( - element, - ) - .ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value("input[name='profile-name']"); set_state(Box::new(move |state: AppState| AppState { profile_name_input: value, ..state.clone() @@ -88,9 +75,7 @@ pub fn account_detail_view( wasm_bindgen_futures::spawn_local(async move { let event_binary = match definy_event::sign_and_serialize( definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new( - key.verifying_key().to_bytes(), - )), + account_id: definy_event::event::AccountId(key.verifying_key()), time: chrono::Utc::now(), content: definy_event::event::EventContent::ChangeProfile( @@ -127,23 +112,8 @@ pub fn account_detail_view( fetch::get_events(filter, Some(20), Some(0)).await { set_state_for_async(Box::new(move |state| { - let events_len = events.len(); - let mut event_cache = state.event_cache.clone(); - let mut event_hashes = Vec::new(); - for (hash, event) in events { - event_cache.insert(hash, event); - event_hashes.push(hash); - } let mut next = state.clone(); - next.event_cache = event_cache; - next.event_list_state = crate::EventListState { - event_hashes, - current_offset: 0, - page_size: 20, - is_loading: false, - has_more: events_len == 20, - filter_event_type: filter, - }; + next.apply_latest_events(events, filter); next.profile_name_input = String::new(); crate::app_state::upsert_local_event_record( &mut next, @@ -226,7 +196,7 @@ pub fn account_detail_view( .set("word-break", "break-all") .set("opacity", "0.8"), ) - .children([text(encoded_account_id)]) + .children([text(account_id.to_string())]) .into_node(), Div::new() .style(Style::new().set("color", "var(--text-secondary)")) @@ -268,7 +238,7 @@ pub fn account_detail_view( .map(|(hash, event)| { A::::new() .class("event-card") - .href(state.href_with_lang(Location::Event(hash))) + .href(state.href_with_lang(Location::Event(hash.clone()))) .style( Style::new() .set("display", "grid") @@ -288,7 +258,9 @@ pub fn account_detail_view( .into_node(), Div::new() .children([text( - crate::event_presenter::event_summary_text(state, event), + crate::event_presenter::event_summary_text( + state, event, + ), )]) .into_node(), ]) diff --git a/definy-ui/src/account_list.rs b/definy-ui/src/account_list.rs index 686ff128..d27632dc 100644 --- a/definy-ui/src/account_list.rs +++ b/definy-ui/src/account_list.rs @@ -1,7 +1,7 @@ use narumincho_vdom::*; -use crate::{AppState, Location}; use crate::i18n; +use crate::{AppState, Location}; struct AccountRow { account_id: definy_event::event::AccountId, @@ -40,8 +40,7 @@ pub fn account_list_view(state: &AppState) -> Node { .children( rows.into_iter() .map(|row| { - let encoded = - crate::hash_format::encode_bytes(row.account_id.0.as_ref()); + let encoded = row.account_id.to_string(); let name = crate::app_state::account_display_name( &account_name_map, &row.account_id, diff --git a/definy-ui/src/app_state.rs b/definy-ui/src/app_state.rs index e3a57cc3..8e15a3a8 100644 --- a/definy-ui/src/app_state.rs +++ b/definy-ui/src/app_state.rs @@ -1,6 +1,15 @@ -use definy_event::event::{AccountId, EventType}; +use definy_event::{ + EventHashId, + event::{AccountId, EventType}, +}; use narumincho_vdom::Route; +pub type DecodedEvent = Result< + (ed25519_dalek::Signature, definy_event::event::Event), + definy_event::VerifyAndDeserializeError, +>; +pub type EventWithHash = (EventHashId, DecodedEvent); + #[derive(Clone, PartialEq, Eq, Debug)] pub enum PathStep { Left, @@ -16,23 +25,26 @@ pub enum PathStep { TypeListItem, } -impl PathStep { - pub fn to_string(&self) -> String { - match self { - PathStep::Left => "Left".to_string(), - PathStep::Right => "Right".to_string(), - PathStep::Condition => "Condition".to_string(), - PathStep::Then => "Then".to_string(), - PathStep::Else => "Else".to_string(), - PathStep::LetValue => "LetValue".to_string(), - PathStep::LetBody => "LetBody".to_string(), - PathStep::ListItemValue(index) => format!("ListItemValue({})", index), - PathStep::RecordItemValue(index) => format!("RecordItemValue({})", index), - PathStep::ConstructorValue => "ConstructorValue".to_string(), - PathStep::TypeListItem => "TypeListItem".to_string(), - } +impl std::fmt::Display for PathStep { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + PathStep::Left => "Left", + PathStep::Right => "Right", + PathStep::Condition => "Condition", + PathStep::Then => "Then", + PathStep::Else => "Else", + PathStep::LetValue => "LetValue", + PathStep::LetBody => "LetBody", + PathStep::ListItemValue(index) => return write!(f, "ListItemValue({})", index), + PathStep::RecordItemValue(index) => return write!(f, "RecordItemValue({})", index), + PathStep::ConstructorValue => "ConstructorValue", + PathStep::TypeListItem => "TypeListItem", + }; + write!(f, "{}", s) } +} +impl PathStep { pub fn from_string(s: &str) -> Option { if s == "Left" { Some(PathStep::Left) @@ -79,13 +91,13 @@ pub fn string_to_path(s: &str) -> Option> { s.split('.').map(PathStep::from_string).collect() } -use std::collections::HashMap; +use std::{collections::HashMap, str::FromStr}; #[derive(Clone)] pub struct AppState { pub login_or_create_account_dialog_state: LoginOrCreateAccountDialogState, pub event_cache: HashMap< - [u8; 32], + EventHashId, Result< (ed25519_dalek::Signature, definy_event::event::Event), definy_event::VerifyAndDeserializeError, @@ -118,7 +130,7 @@ pub struct LanguageFallbackNotice { #[derive(Clone)] pub struct EventListState { - pub event_hashes: Vec<[u8; 32]>, + pub event_hashes: Vec, pub current_offset: usize, pub page_size: usize, pub is_loading: bool, @@ -132,17 +144,17 @@ pub struct PartDefinitionFormState { pub part_type_input: Option, pub part_description_input: String, pub composing_expression: definy_event::event::Expression, - pub module_definition_event_hash: Option<[u8; 32]>, + pub module_definition_event_hash: Option, pub eval_result: Option, } #[derive(Clone)] pub struct PartUpdateFormState { - pub part_definition_event_hash: Option<[u8; 32]>, + pub part_definition_event_hash: Option, pub part_name_input: String, pub part_description_input: String, pub expression_input: definy_event::event::Expression, - pub module_definition_event_hash: Option<[u8; 32]>, + pub module_definition_event_hash: Option, } #[derive(Clone)] @@ -154,7 +166,7 @@ pub struct ModuleDefinitionFormState { #[derive(Clone)] pub struct ModuleUpdateFormState { - pub module_definition_event_hash: Option<[u8; 32]>, + pub module_definition_event_hash: Option, pub module_name_input: String, pub module_description_input: String, pub result_message: Option, @@ -172,31 +184,32 @@ impl AppState { &self, ) -> std::collections::HashMap> { let mut account_name_map = std::collections::HashMap::new(); - for event_result in self.event_cache.values() { - if let Ok((_, event)) = event_result { - match &event.content { - definy_event::event::EventContent::CreateAccount(create_account_event) => { - account_name_map - .entry(event.account_id.clone()) - .or_insert_with(|| create_account_event.account_name.clone()); - } - definy_event::event::EventContent::ChangeProfile(change_profile_event) => { - account_name_map - .entry(event.account_id.clone()) - .or_insert_with(|| change_profile_event.account_name.clone()); - } - definy_event::event::EventContent::PartDefinition(_) => {} - definy_event::event::EventContent::PartUpdate(_) => {} - definy_event::event::EventContent::ModuleDefinition(_) => {} - definy_event::event::EventContent::ModuleUpdate(_) => {} + for (_, event) in self.event_cache.values().flatten() { + match &event.content { + definy_event::event::EventContent::CreateAccount(create_account_event) => { + account_name_map + .entry(event.account_id.clone()) + .or_insert_with(|| create_account_event.account_name.clone()); + } + definy_event::event::EventContent::ChangeProfile(change_profile_event) => { + account_name_map + .entry(event.account_id.clone()) + .or_insert_with(|| change_profile_event.account_name.clone()); } + definy_event::event::EventContent::PartDefinition(_) => {} + definy_event::event::EventContent::PartUpdate(_) => {} + definy_event::event::EventContent::ModuleDefinition(_) => {} + definy_event::event::EventContent::ModuleUpdate(_) => {} } } account_name_map } } -pub fn upsert_local_event_record(state: &mut AppState, record: crate::local_event::LocalEventRecord) { +pub fn upsert_local_event_record( + state: &mut AppState, + record: crate::local_event::LocalEventRecord, +) { state .local_event_queue .items @@ -208,7 +221,10 @@ pub fn upsert_local_event_record(state: &mut AppState, record: crate::local_even .sort_by(|a, b| b.updated_at_ms.cmp(&a.updated_at_ms)); } -pub fn replace_local_event_records(state: &mut AppState, records: Vec) { +pub fn replace_local_event_records( + state: &mut AppState, + records: Vec, +) { state.local_event_queue.items = records; state .local_event_queue @@ -223,18 +239,12 @@ pub fn account_display_name( account_name_map .get(account_id) .map(|name| name.to_string()) - .unwrap_or_else(|| crate::hash_format::encode_bytes(account_id.0.as_ref())) + .unwrap_or_else(|| account_id.to_string()) } pub fn build_initial_state( location: Option, - events: Vec<( - [u8; 32], - Result< - (ed25519_dalek::Signature, definy_event::event::Event), - definy_event::VerifyAndDeserializeError, - >, - )>, + events: Vec, event_list_loading: bool, event_list_has_more: bool, current_key: Option, @@ -245,7 +255,7 @@ pub fn build_initial_state( let mut event_cache = HashMap::new(); let mut event_hashes = Vec::new(); for (hash, event) in events { - event_cache.insert(hash, event); + event_cache.insert(hash.clone(), event); event_hashes.push(hash); } @@ -316,7 +326,32 @@ pub fn build_initial_state( } impl AppState { - pub fn build_url(location: &Location, lang_code: &str, event_type: Option) -> String { + pub fn apply_latest_events( + &mut self, + events: Vec, + filter_event_type: Option, + ) { + let events_len = events.len(); + let mut event_hashes = Vec::with_capacity(events_len); + for (hash, event) in events { + self.event_cache.insert(hash.clone(), event); + event_hashes.push(hash); + } + self.event_list_state = EventListState { + event_hashes, + current_offset: 0, + page_size: self.event_list_state.page_size, + is_loading: false, + has_more: events_len == self.event_list_state.page_size, + filter_event_type, + }; + } + + pub fn build_url( + location: &Location, + lang_code: &str, + event_type: Option, + ) -> String { let mut url = location.to_url(); let query = crate::query::build_query(crate::query::QueryParams { lang: Some(lang_code.to_string()), @@ -350,6 +385,64 @@ impl AppState { } } +pub async fn load_more_events(state: AppState, set_state: std::rc::Rc) +where + F: Fn(Box AppState>) + 'static, +{ + let filter = state.event_list_state.filter_event_type; + let page_size = state.event_list_state.page_size; + let is_empty = state.event_list_state.event_hashes.is_empty(); + let current_offset_base = state.event_list_state.current_offset; + set_state(Box::new(|state: AppState| { + let mut next = state.clone(); + next.event_list_state.is_loading = true; + next + })); + let current_offset = if is_empty { + 0 + } else { + current_offset_base + page_size + }; + let events = crate::fetch::get_events(filter, Some(page_size), Some(current_offset)).await; + if let Ok(events) = events { + let events_len = events.len(); + set_state(Box::new(move |state: AppState| { + let mut event_cache = state.event_cache.clone(); + let mut event_hashes = if current_offset == 0 { + Vec::new() + } else { + state.event_list_state.event_hashes.clone() + }; + for (hash, event) in events { + if let std::collections::hash_map::Entry::Vacant(e) = + event_cache.entry(hash.clone()) + { + e.insert(event); + event_hashes.push(hash); + } + } + AppState { + event_cache, + event_list_state: crate::EventListState { + event_hashes, + current_offset, + page_size: state.event_list_state.page_size, + is_loading: false, + has_more: events_len == state.event_list_state.page_size, + filter_event_type: state.event_list_state.filter_event_type, + }, + ..state.clone() + } + })); + } else { + set_state(Box::new(|state: AppState| { + let mut next = state.clone(); + next.event_list_state.is_loading = false; + next + })); + } +} + #[derive(Clone)] pub struct LoginOrCreateAccountDialogState { /// アカウント作成で生成した秘密鍵 @@ -382,9 +475,9 @@ pub enum Location { PartList, ModuleList, LocalEventQueue, - Module([u8; 32]), - Part([u8; 32]), - Event([u8; 32]), + Module(definy_event::EventHashId), + Part(definy_event::EventHashId), + Event(definy_event::EventHashId), Account(AccountId), } @@ -396,25 +489,10 @@ impl narumincho_vdom::Route for Location { Location::PartList => "/parts".to_string(), Location::ModuleList => "/modules".to_string(), Location::LocalEventQueue => "/local-events".to_string(), - Location::Module(hash) => format!( - "/modules/{}", - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, hash) - ), - Location::Part(hash) => format!( - "/parts/{}", - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, hash) - ), - Location::Event(hash) => format!( - "/events/{}", - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, hash) - ), - Location::Account(account_id) => format!( - "/accounts/{}", - base64::Engine::encode( - &base64::engine::general_purpose::URL_SAFE_NO_PAD, - account_id.0.as_ref() - ) - ), + Location::Module(hash) => format!("/modules/{}", hash), + Location::Part(hash) => format!("/parts/{}", hash), + Location::Event(hash) => format!("/events/{}", hash), + Location::Account(account_id) => format!("/accounts/{}", account_id), } } @@ -426,30 +504,22 @@ impl narumincho_vdom::Route for Location { ["parts"] => Some(Location::PartList), ["modules"] => Some(Location::ModuleList), ["local-events"] => Some(Location::LocalEventQueue), - ["modules", hash_str] => decode_32bytes_base64(hash_str).map(Location::Module), - ["parts", hash_str] => decode_32bytes_base64(hash_str).map(Location::Part), - ["events", hash_str] => decode_32bytes_base64(hash_str).map(Location::Event), - ["accounts", account_id_str] => decode_32bytes_base64(account_id_str) - .map(|account_id_bytes| Location::Account(AccountId(Box::new(account_id_bytes)))), + ["modules", hash_str] => Some(Location::Module(EventHashId::from_str(hash_str).ok()?)), + ["parts", hash_str] => Some(Location::Part(EventHashId::from_str(hash_str).ok()?)), + ["events", hash_str] => Some(Location::Event(EventHashId::from_str(hash_str).ok()?)), + ["accounts", account_id_str] => { + Some(Location::Account(AccountId::from_str(account_id_str).ok()?)) + } _ => None, } } } -fn decode_32bytes_base64(value: &str) -> Option<[u8; 32]> { - let bytes = - base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, value).ok()?; - if bytes.len() == 32 { - let mut result = [0u8; 32]; - result.copy_from_slice(&bytes); - Some(result) - } else { - None - } -} - #[cfg(test)] mod tests { + use std::str::FromStr; + + use definy_event::{EventHashId, event::AccountId}; use narumincho_vdom::Route; use super::Location; @@ -461,10 +531,26 @@ mod tests { Location::AccountList, Location::PartList, Location::ModuleList, - Location::Module([2u8; 32]), - Location::Account(definy_event::event::AccountId(Box::new([7u8; 32]))), - Location::Part([9u8; 32]), - Location::Event([3u8; 32]), + Location::Module( + EventHashId::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + .ok() + .unwrap(), + ), + Location::Account( + AccountId::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + .ok() + .unwrap(), + ), + Location::Part( + EventHashId::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + .ok() + .unwrap(), + ), + Location::Event( + EventHashId::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + .ok() + .unwrap(), + ), ]; for case in cases { let url = case.to_url(); diff --git a/definy-ui/src/dom.rs b/definy-ui/src/dom.rs new file mode 100644 index 00000000..a0417079 --- /dev/null +++ b/definy-ui/src/dom.rs @@ -0,0 +1,23 @@ +pub fn get_input_value(selector: &str) -> String { + web_sys::window() + .and_then(|window| window.document()) + .and_then(|document| document.query_selector(selector).ok()) + .flatten() + .and_then(|element| { + wasm_bindgen::JsCast::dyn_into::(element).ok() + }) + .map(|input| input.value()) + .unwrap_or_default() +} + +pub fn get_textarea_value(selector: &str) -> String { + web_sys::window() + .and_then(|window| window.document()) + .and_then(|document| document.query_selector(selector).ok()) + .flatten() + .and_then(|element| { + wasm_bindgen::JsCast::dyn_into::(element).ok() + }) + .map(|textarea| textarea.value()) + .unwrap_or_default() +} diff --git a/definy-ui/src/dropdown.rs b/definy-ui/src/dropdown.rs index eb19ab81..2aa01072 100644 --- a/definy-ui/src/dropdown.rs +++ b/definy-ui/src/dropdown.rs @@ -4,12 +4,14 @@ use narumincho_vdom::*; use std::rc::Rc; use wasm_bindgen::JsCast; +pub type DropdownOnChange = Rc Box AppState>>; + pub fn searchable_dropdown( state: &AppState, name: &str, current_value: &str, options: &[(String, String)], - on_change: Rc Box AppState>>, + on_change: DropdownOnChange, ) -> Node { let is_open = state.active_dropdown_name.as_deref() == Some(name); @@ -50,12 +52,11 @@ pub fn searchable_dropdown( wasm_bindgen::closure::Closure::once_into_js(move || { if let Some(document) = web_sys::window().unwrap().document() { let selector = format!("input[name='search-{}']", n); - if let Ok(Some(element)) = document.query_selector(&selector) { - if let Ok(input) = + if let Ok(Some(element)) = document.query_selector(&selector) + && let Ok(input) = element.dyn_into::() - { - let _ = input.focus(); - } + { + let _ = input.focus(); } } }) @@ -147,29 +148,16 @@ pub fn searchable_dropdown( .set("color", "var(--text-primary)") .set("outline", "none"), ); - search_input - .attributes - .push(( - "placeholder".to_string(), - i18n::tr(state, "Search...", "検索...", "Serĉi...").to_string(), - )); + search_input.attributes.push(( + "placeholder".to_string(), + i18n::tr(state, "Search...", "検索...", "Serĉi...").to_string(), + )); search_input.events.push(( "input".to_string(), EventHandler::new(move |set_state| { let s_name = search_name.clone(); async move { - let value = web_sys::window() - .and_then(|w| w.document()) - .and_then(|d| { - d.query_selector(&format!("input[name='{}']", s_name)) - .ok() - .flatten() - }) - .and_then(|e| { - wasm_bindgen::JsCast::dyn_into::(e).ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value(&format!("input[name='{}']", s_name)); set_state(Box::new(move |state: AppState| AppState { dropdown_search_query: value, ..state diff --git a/definy-ui/src/event_detail.rs b/definy-ui/src/event_detail.rs index ce2403e2..7732936e 100644 --- a/definy-ui/src/event_detail.rs +++ b/definy-ui/src/event_detail.rs @@ -1,10 +1,11 @@ +use definy_event::EventHashId; use definy_event::event::{Event, EventContent}; use narumincho_vdom::*; use crate::Location; use crate::app_state::AppState; -use crate::i18n; use crate::expression_eval::{evaluate_expression, expression_to_source}; +use crate::i18n; fn part_type_text(part_type: &definy_event::event::PartType) -> String { match part_type { @@ -12,9 +13,7 @@ fn part_type_text(part_type: &definy_event::event::PartType) -> String { definy_event::event::PartType::String => "String".to_string(), definy_event::event::PartType::Boolean => "Boolean".to_string(), definy_event::event::PartType::Type => "Type".to_string(), - definy_event::event::PartType::TypePart(hash) => { - format!("TypePart({})", crate::hash_format::short_hash32(hash)) - } + definy_event::event::PartType::TypePart(hash) => format!("TypePart({})", hash), definy_event::event::PartType::List(item_type) => { format!("list<{}>", part_type_text(item_type.as_ref())) } @@ -28,15 +27,18 @@ fn optional_part_type_text(part_type: &Option) -> .unwrap_or_else(|| "None".to_string()) } -pub fn event_detail_view(state: &AppState, target_hash: &[u8; 32]) -> Node { +pub fn event_detail_view( + state: &AppState, + target_hash: &definy_event::EventHashId, +) -> Node { let account_name_map = state.account_name_map(); let mut target_event_opt = None; for (hash, event_result) in &state.event_cache { - if let Ok((_, event)) = event_result { - if hash == target_hash { - target_event_opt = Some(event); - } + if let Ok((_, event)) = event_result + && hash == target_hash + { + target_event_opt = Some(event); } } @@ -88,12 +90,11 @@ pub fn event_detail_view(state: &AppState, target_hash: &[u8; 32]) -> Node>, ) -> Node { - let account_name = - crate::app_state::account_display_name(account_name_map, &event.account_id); + let account_name = crate::app_state::account_display_name(account_name_map, &event.account_id); let root_part_definition_hash = root_part_definition_hash(hash, &event.content); Div::new() @@ -124,14 +125,12 @@ fn render_event_detail( ) .children([ Div::new() - .children([text(&event.time.format("%Y-%m-%d %H:%M:%S").to_string())]) + .children([text(event.time.format("%Y-%m-%d %H:%M:%S").to_string())]) .into_node(), Div::new() .class("mono") .style(Style::new().set("opacity", "0.6")) - .children([text(&crate::hash_format::encode_bytes( - event.account_id.0.as_slice(), - ))]) + .children([text(event.account_id.to_string())]) .into_node(), ]) .into_node(), @@ -215,13 +214,16 @@ fn render_event_detail( let expression = expression.clone(); async move { set_state(Box::new(move |state: AppState| { - let events_vec: Vec<_> = state.event_cache.iter().map(|(h, e)| (*h, e.clone())).collect(); - let eval_result = - evaluate_message_result( - state.language.code, - &expression, - &events_vec, - ); + let events_vec: Vec<_> = state + .event_cache + .iter() + .map(|(h, e)| (h.clone(), e.clone())) + .collect(); + let eval_result = evaluate_message_result( + state.language.code, + &expression, + &events_vec, + ); AppState { event_detail_eval_result: Some(eval_result), ..state.clone() @@ -234,7 +236,7 @@ fn render_event_detail( .into_node() }, A::::new() - .href(state.href_with_lang(Location::Part(*hash))) + .href(state.href_with_lang(Location::Part(hash.clone()))) .style( Style::new() .set("margin-top", "0.45rem") @@ -276,7 +278,12 @@ fn render_event_detail( .style(Style::new().set("font-size", "1.08rem")) .children([text(format!( "{} {}", - i18n::tr(state, "Part updated:", "パーツ更新:", "Parto ĝisdatigita:"), + i18n::tr( + state, + "Part updated:", + "パーツ更新:", + "Parto ĝisdatigita:" + ), part_update_event.part_name ))]) .into_node(), @@ -321,14 +328,12 @@ fn render_event_detail( "partDefinitionEventHash:", "partDefinitionEventHash:" ), - crate::hash_format::encode_hash32( - &part_update_event.part_definition_event_hash, - ) + part_update_event.part_definition_event_hash, ))]) .into_node(), A::::new() .href(state.href_with_lang(Location::Event( - part_update_event.part_definition_event_hash, + part_update_event.part_definition_event_hash.clone(), ))) .children([text(i18n::tr( state, @@ -339,7 +344,7 @@ fn render_event_detail( .into_node(), A::::new() .href(state.href_with_lang(Location::Part( - part_update_event.part_definition_event_hash, + part_update_event.part_definition_event_hash.clone(), ))) .children([text(i18n::tr( state, @@ -359,7 +364,12 @@ fn render_event_detail( .children([ text(format!( "{} {}", - i18n::tr(state, "Module created:", "モジュール作成:", "Modulo kreita:"), + i18n::tr( + state, + "Module created:", + "モジュール作成:", + "Modulo kreita:" + ), module_definition_event.module_name )), if module_definition_event.description.is_empty() { @@ -376,7 +386,7 @@ fn render_event_detail( .into_node() }, A::::new() - .href(state.href_with_lang(Location::Module(*hash))) + .href(state.href_with_lang(Location::Module(hash.clone()))) .children([text(i18n::tr( state, "Open module detail", @@ -398,7 +408,12 @@ fn render_event_detail( .style(Style::new().set("font-size", "1.08rem")) .children([text(format!( "{} {}", - i18n::tr(state, "Module updated:", "モジュール更新:", "Modulo ĝisdatigita:"), + i18n::tr( + state, + "Module updated:", + "モジュール更新:", + "Modulo ĝisdatigita:" + ), module_update_event.module_name ))]) .into_node(), @@ -430,14 +445,12 @@ fn render_event_detail( "moduleDefinitionEventHash:", "moduleDefinitionEventHash:" ), - crate::hash_format::encode_hash32( - &module_update_event.module_definition_event_hash, - ) + module_update_event.module_definition_event_hash, ))]) .into_node(), A::::new() .href(state.href_with_lang(Location::Event( - module_update_event.module_definition_event_hash, + module_update_event.module_definition_event_hash.clone(), ))) .children([text(i18n::tr( state, @@ -448,7 +461,7 @@ fn render_event_detail( .into_node(), A::::new() .href(state.href_with_lang(Location::Module( - module_update_event.module_definition_event_hash, + module_update_event.module_definition_event_hash.clone(), ))) .children([text(i18n::tr( state, @@ -461,7 +474,7 @@ fn render_event_detail( .into_node(), }, if let Some(root_hash) = root_part_definition_hash { - related_part_events_section(state, root_hash) + related_part_events_section(state, &root_hash) } else { Div::new().children([]).into_node() }, @@ -482,7 +495,7 @@ fn render_event_detail( "イベントハッシュ: ", "Evento-hako: ", )), - text(&crate::hash_format::encode_hash32(hash)), + text(hash.to_string()), ]) .into_node(), ]) @@ -491,10 +504,10 @@ fn render_event_detail( fn related_part_events_section( state: &AppState, - root_part_definition_hash: [u8; 32], + root_part_definition_hash: &EventHashId, ) -> Node { let related_events = collect_related_part_events(state, root_part_definition_hash); - let hash_as_base64 = crate::hash_format::encode_hash32(&root_part_definition_hash); + let hash_as_base64 = root_part_definition_hash.to_string(); Div::new() .class("event-detail-card") @@ -565,35 +578,40 @@ fn related_part_events_section( fn collect_related_part_events( state: &AppState, - root_part_definition_hash: [u8; 32], -) -> Vec<([u8; 32], &Event)> { + root_part_definition_hash: &EventHashId, +) -> Vec<(EventHashId, Event)> { let mut events = state .event_cache .iter() .filter_map(|(hash, event_result)| { let (_, event) = event_result.as_ref().ok()?; let is_related = match &event.content { - EventContent::PartDefinition(_) => *hash == root_part_definition_hash, + EventContent::PartDefinition(_) => hash == root_part_definition_hash, EventContent::PartUpdate(part_update) => { - part_update.part_definition_event_hash == root_part_definition_hash + part_update.part_definition_event_hash == *root_part_definition_hash } _ => false, }; if is_related { - Some((*hash, event)) + Some((hash.clone(), event.clone())) } else { None } }) - .collect::>(); + .collect::>(); events.sort_by(|(_, a), (_, b)| b.time.cmp(&a.time)); events } -fn root_part_definition_hash(current_hash: &[u8; 32], content: &EventContent) -> Option<[u8; 32]> { +fn root_part_definition_hash( + current_hash: &definy_event::EventHashId, + content: &EventContent, +) -> Option { match content { - EventContent::PartDefinition(_) => Some(*current_hash), - EventContent::PartUpdate(part_update) => Some(part_update.part_definition_event_hash), + EventContent::PartDefinition(_) => Some(current_hash.clone()), + EventContent::PartUpdate(part_update) => { + Some(part_update.part_definition_event_hash.clone()) + } _ => None, } } @@ -601,13 +619,7 @@ fn root_part_definition_hash(current_hash: &[u8; 32], content: &EventContent) -> fn evaluate_message_result( lang_code: &str, expression: &definy_event::event::Expression, - events: &[( - [u8; 32], - Result< - (ed25519_dalek::Signature, definy_event::event::Event), - definy_event::VerifyAndDeserializeError, - >, - )], + events: &[crate::app_state::EventWithHash], ) -> String { match evaluate_expression(expression, events) { Ok(value) => format!( diff --git a/definy-ui/src/event_list.rs b/definy-ui/src/event_list.rs index 13a2de0a..c31c1128 100644 --- a/definy-ui/src/event_list.rs +++ b/definy-ui/src/event_list.rs @@ -1,12 +1,15 @@ +use std::str::FromStr; + +use definy_event::EventHashId; use definy_event::event::{EventContent, EventType}; use narumincho_vdom::*; use crate::app_state::AppState; use crate::expression_editor::{EditorTarget, render_root_expression_editor}; use crate::expression_eval::{evaluate_expression, expression_to_source}; +use crate::i18n; use crate::module_projection::collect_module_snapshots; use crate::part_projection::collect_part_snapshots; -use crate::i18n; fn update_event_filter_url(event_type: Option, lang_code: &str) { let query = crate::query::build_query(crate::query::QueryParams { @@ -18,10 +21,10 @@ fn update_event_filter_url(event_type: Option, lang_code: &str) { new_url.push('?'); new_url.push_str(query.as_str()); } - if let Some(window) = web_sys::window() { - if let Ok(history) = window.history() { - let _ = history.push_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url)); - } + if let Some(window) = web_sys::window() + && let Ok(history) = window.history() + { + let _ = history.push_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url)); } } @@ -32,7 +35,7 @@ fn part_type_text(part_type: &definy_event::event::PartType) -> String { definy_event::event::PartType::Boolean => "Boolean".to_string(), definy_event::event::PartType::Type => "Type".to_string(), definy_event::event::PartType::TypePart(hash) => { - format!("TypePart({})", crate::hash_format::short_hash32(hash)) + format!("TypePart({})", hash) } definy_event::event::PartType::List(item_type) => { format!("list<{}>", part_type_text(item_type.as_ref())) @@ -54,14 +57,23 @@ pub fn event_list_view(state: &AppState) -> Node { let _page_size = state.event_list_state.page_size; let filter_options = vec![ - ("".to_string(), i18n::tr(&state, "All Events", "すべてのイベント", "Ĉiuj eventoj").to_string()), + ( + "".to_string(), + i18n::tr(&state, "All Events", "すべてのイベント", "Ĉiuj eventoj").to_string(), + ), ( "create_account".to_string(), i18n::tr(&state, "Create Account", "アカウント作成", "Krei konton").to_string(), ), ( "change_profile".to_string(), - i18n::tr(&state, "Change Profile", "プロフィール変更", "Ŝanĝi profilon").to_string(), + i18n::tr( + &state, + "Change Profile", + "プロフィール変更", + "Ŝanĝi profilon", + ) + .to_string(), ), ( "part_definition".to_string(), @@ -73,18 +85,32 @@ pub fn event_list_view(state: &AppState) -> Node { ), ( "module_definition".to_string(), - i18n::tr(&state, "Module Definition", "モジュール定義", "Modulo-difino").to_string(), + i18n::tr( + &state, + "Module Definition", + "モジュール定義", + "Modulo-difino", + ) + .to_string(), ), ( "module_update".to_string(), - i18n::tr(&state, "Module Update", "モジュール更新", "Modulo-ĝisdatigo").to_string(), + i18n::tr( + &state, + "Module Update", + "モジュール更新", + "Modulo-ĝisdatigo", + ) + .to_string(), ), ]; - let current_filter = state.event_list_state.filter_event_type + let current_filter = state + .event_list_state + .filter_event_type .as_ref() .map(|et| et.to_string()) - .unwrap_or_else(|| "".to_string()); + .unwrap_or_default(); let filter_dropdown = crate::dropdown::searchable_dropdown( &state, @@ -174,7 +200,7 @@ pub fn event_list_view(state: &AppState) -> Node { .type_("button") .on_click(EventHandler::new(async |set_state| { set_state(Box::new(|state: AppState| { - let events_vec: Vec<_> = state.event_list_state.event_hashes.iter().filter_map(|hash| state.event_cache.get(hash).map(|event| (*hash, event.clone()))).collect(); + let events_vec: Vec<_> = state.event_list_state.event_hashes.iter().filter_map(|hash| state.event_cache.get(hash).map(|event| (hash.clone(), event.clone()))).collect(); let result = match evaluate_expression( &state.part_definition_form.composing_expression, &events_vec, @@ -218,12 +244,12 @@ pub fn event_list_view(state: &AppState) -> Node { let part_type = state.part_definition_form.part_type_input.clone(); let module_definition_event_hash = - state.part_definition_form.module_definition_event_hash; + state.part_definition_form.module_definition_event_hash.clone(); if part_name.is_empty() { let mut next = state.clone(); next.part_definition_form.eval_result = Some(i18n::tr( - &state, + &state, "Error: part name is required", "エラー: パーツ名は必須です", "Eraro: parto-nomo estas bezonata", @@ -238,9 +264,9 @@ pub fn event_list_view(state: &AppState) -> Node { wasm_bindgen_futures::spawn_local(async move { let event_binary = definy_event::sign_and_serialize( definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new( - key_for_async.verifying_key().to_bytes(), - )), + account_id: definy_event::event::AccountId( + key_for_async.verifying_key() + ), time: chrono::Utc::now(), content: definy_event::event::EventContent::PartDefinition( @@ -277,26 +303,8 @@ pub fn event_list_view(state: &AppState) -> Node { if let Ok(events) = events { set_state_for_async(Box::new( move |state| { - let events_len = events.len(); - let mut event_cache = - state.event_cache.clone(); - let mut event_hashes = - Vec::new(); - for (hash, event) in events { - event_cache.insert(hash, event); - event_hashes.push(hash); - } let mut next = state.clone(); - next.event_cache = event_cache; - next.event_list_state = - crate::EventListState { - event_hashes, - current_offset: 0, - page_size: 20, - is_loading: false, - has_more: events_len == 20, - filter_event_type: None, - }; + next.apply_latest_events(events, None); crate::app_state::upsert_local_event_record( &mut next, record, @@ -316,7 +324,7 @@ pub fn event_list_view(state: &AppState) -> Node { Some(match status { crate::local_event::LocalEventStatus::Queued => { i18n::tr( - &state, + &state, "PartDefinition queued (offline)", "PartDefinition をキューに追加しました (オフライン)", "PartDefinition envicigita (senkonekte)", @@ -325,7 +333,7 @@ pub fn event_list_view(state: &AppState) -> Node { } crate::local_event::LocalEventStatus::Failed => { i18n::tr( - &state, + &state, "PartDefinition failed to send", "PartDefinition の送信に失敗しました", "PartDefinition sendado malsukcesis", @@ -334,7 +342,7 @@ pub fn event_list_view(state: &AppState) -> Node { } crate::local_event::LocalEventStatus::Sent => { i18n::tr( - &state, + &state, "PartDefinition posted", "PartDefinition を投稿しました", "PartDefinition sendita", @@ -413,7 +421,9 @@ pub fn event_list_view(state: &AppState) -> Node { .event_list_state .event_hashes .iter() - .filter_map(|hash| state.event_cache.get(hash).map(|event| (hash, event))) + .filter_map(|hash| { + state.event_cache.get(hash).map(|event| (hash, event)) + }) .map(|(hash, event)| event_view(&state, hash, event, &account_name_map)) .collect::>>() }) @@ -422,7 +432,11 @@ pub fn event_list_view(state: &AppState) -> Node { if state.event_list_state.is_loading { children.push( Div::new() - .style(Style::new().set("text-align", "center").set("padding", "1rem")) + .style( + Style::new() + .set("text-align", "center") + .set("padding", "1rem"), + ) .children([text(i18n::tr( &state, "Loading events...", @@ -433,9 +447,19 @@ pub fn event_list_view(state: &AppState) -> Node { ); } else if state.event_list_state.has_more { let button_text = if state.event_list_state.event_hashes.is_empty() { - i18n::tr(&state, "Load Events", "イベントを読み込む", "Ŝargi eventojn") + i18n::tr( + &state, + "Load Events", + "イベントを読み込む", + "Ŝargi eventojn", + ) } else { - i18n::tr(&state, "Load More Events", "さらに読み込む", "Ŝargi pliajn eventojn") + i18n::tr( + &state, + "Load More Events", + "さらに読み込む", + "Ŝargi pliajn eventojn", + ) }; children.push( Button::new() @@ -443,70 +467,24 @@ pub fn event_list_view(state: &AppState) -> Node { .on_click(EventHandler::new(move |set_state| { let state = state.clone(); async move { - let filter = state.event_list_state.filter_event_type; - let page_size = state.event_list_state.page_size; - let is_empty = state.event_list_state.event_hashes.is_empty(); - let current_offset_base = state.event_list_state.current_offset; let set_state = std::rc::Rc::new(set_state); - set_state(Box::new(|state: AppState| { - let mut next = state.clone(); - next.event_list_state.is_loading = true; - next - })); - let current_offset = if is_empty { - 0 - } else { - current_offset_base + page_size - }; - let events = crate::fetch::get_events( - filter, - Some(page_size), - Some(current_offset), - ).await; - if let Ok(events) = events { - let events_len = events.len(); - set_state(Box::new(move |state: AppState| { - let mut event_cache = state.event_cache.clone(); - let mut event_hashes = if current_offset == 0 { - Vec::new() - } else { - state.event_list_state.event_hashes.clone() - }; - for (hash, event) in events { - if !event_cache.contains_key(&hash) { - event_cache.insert(hash, event); - event_hashes.push(hash); - } - } - AppState { - event_cache, - event_list_state: crate::EventListState { - event_hashes, - current_offset, - page_size: state.event_list_state.page_size, - is_loading: false, - has_more: events_len == state.event_list_state.page_size as usize, - filter_event_type: state.event_list_state.filter_event_type, - }, - ..state.clone() - } - })); - } else { - set_state(Box::new(|state: AppState| { - let mut next = state.clone(); - next.event_list_state.is_loading = false; - next - })); - } + crate::app_state::load_more_events(state, set_state).await; } })) .children([text(button_text)]) .into_node(), ); - } else if state.event_list_state.event_hashes.is_empty() && !state.event_list_state.is_loading { + } else if state.event_list_state.event_hashes.is_empty() + && !state.event_list_state.is_loading + { children.push( Div::new() - .style(Style::new().set("text-align", "center").set("padding", "1rem").set("color", "var(--text-secondary)")) + .style( + Style::new() + .set("text-align", "center") + .set("padding", "1rem") + .set("color", "var(--text-secondary)"), + ) .children([text(i18n::tr( &state, "No events found. Click 'Load Events' to fetch.", @@ -523,7 +501,7 @@ pub fn event_list_view(state: &AppState) -> Node { fn event_view( state: &AppState, - hash: &[u8; 32], + hash: &EventHashId, event_result: &Result< (ed25519_dalek::Signature, definy_event::event::Event), definy_event::VerifyAndDeserializeError, @@ -546,7 +524,7 @@ fn event_view( .set("display", "grid") .set("gap", "0.75rem"), ) - .href(state.href_with_lang(crate::Location::Event(*hash))) + .href(state.href_with_lang(crate::Location::Event(hash.clone()))) .children([ Div::new() .style( @@ -559,14 +537,12 @@ fn event_view( ) .children([ Div::new() - .children([text(&event.time.format("%Y-%m-%d %H:%M:%S").to_string())]) + .children([text(event.time.format("%Y-%m-%d %H:%M:%S").to_string())]) .into_node(), Div::new() .class("mono") .style(Style::new().set("opacity", "0.6")) - .children([text(&crate::hash_format::encode_bytes( - event.account_id.0.as_slice(), - ))]) + .children([text(event.account_id.to_string())]) .into_node(), ]) .into_node(), @@ -575,7 +551,7 @@ fn event_view( .style(Style::new().set("color", "var(--primary)")) .children([ text(i18n::tr( - &state, + state, "Account created:", "アカウント作成:", "Konto kreita:", @@ -587,7 +563,7 @@ fn event_view( .style(Style::new().set("color", "var(--primary)")) .children([ text(i18n::tr( - &state, + state, "Profile changed:", "プロフィール変更:", "Profilo ŝanĝita:", @@ -610,12 +586,10 @@ fn event_view( .set("margin-bottom", "0.25rem") .set("text-decoration", "none"), ) - .children([text( - crate::app_state::account_display_name( - account_name_map, - &event.account_id, - ), - )]) + .children([text(crate::app_state::account_display_name( + account_name_map, + &event.account_id, + ))]) .into_node(), text(format!( "{}: {} = {}", @@ -637,7 +611,7 @@ fn event_view( .into_node() }, A::::new() - .href(state.href_with_lang(crate::Location::Part(*hash))) + .href(state.href_with_lang(crate::Location::Part(hash.clone()))) .style( Style::new() .set("font-size", "0.82rem") @@ -645,7 +619,7 @@ fn event_view( .set("text-decoration", "none"), ) .children([text(i18n::tr( - &state, + state, "Open part detail", "パーツ詳細を開く", "Malfermi partajn detalojn", @@ -668,16 +642,19 @@ fn event_view( .set("margin-bottom", "0.25rem") .set("text-decoration", "none"), ) - .children([text( - crate::app_state::account_display_name( - account_name_map, - &event.account_id, - ), - )]) + .children([text(crate::app_state::account_display_name( + account_name_map, + &event.account_id, + ))]) .into_node(), text(format!( "{} {}", - i18n::tr(&state, "Part updated:", "パーツ更新:", "Parto ĝisdatigita:"), + i18n::tr( + state, + "Part updated:", + "パーツ更新:", + "Parto ĝisdatigita:" + ), part_update_event.part_name )), Div::new() @@ -689,7 +666,7 @@ fn event_view( ) .children([text(format!( "{} {}", - i18n::tr(&state, "expression:", "式:", "esprimo:"), + i18n::tr(state, "expression:", "式:", "esprimo:"), expression_to_source(&part_update_event.expression) ))]) .into_node(), @@ -701,14 +678,12 @@ fn event_view( ) .children([text(format!( "base: {}", - crate::hash_format::encode_hash32( - &part_update_event.part_definition_event_hash, - ) + part_update_event.part_definition_event_hash ))]) .into_node(), A::::new() .href(state.href_with_lang(crate::Location::Part( - part_update_event.part_definition_event_hash, + part_update_event.part_definition_event_hash.clone(), ))) .style( Style::new() @@ -717,7 +692,7 @@ fn event_view( .set("text-decoration", "none"), ) .children([text(i18n::tr( - &state, + state, "Open part detail", "パーツ詳細を開く", "Malfermi partajn detalojn", @@ -740,16 +715,19 @@ fn event_view( .set("margin-bottom", "0.25rem") .set("text-decoration", "none"), ) - .children([text( - crate::app_state::account_display_name( - account_name_map, - &event.account_id, - ), - )]) + .children([text(crate::app_state::account_display_name( + account_name_map, + &event.account_id, + ))]) .into_node(), text(format!( "{} {}", - i18n::tr(&state, "Module created:", "モジュール作成:", "Modulo kreita:"), + i18n::tr( + state, + "Module created:", + "モジュール作成:", + "Modulo kreita:" + ), module_definition_event.module_name )), if module_definition_event.description.is_empty() { @@ -782,16 +760,19 @@ fn event_view( .set("margin-bottom", "0.25rem") .set("text-decoration", "none"), ) - .children([text( - crate::app_state::account_display_name( - account_name_map, - &event.account_id, - ), - )]) + .children([text(crate::app_state::account_display_name( + account_name_map, + &event.account_id, + ))]) .into_node(), text(format!( "{} {}", - i18n::tr(&state, "Module updated:", "モジュール更新:", "Modulo ĝisdatigita:"), + i18n::tr( + state, + "Module updated:", + "モジュール更新:", + "Modulo ĝisdatigita:" + ), module_update_event.module_name )), if module_update_event.module_description.is_empty() { @@ -817,14 +798,12 @@ fn event_view( ) .children([text(format!( "base: {}", - crate::hash_format::encode_hash32( - &module_update_event.module_definition_event_hash, - ) + &module_update_event.module_definition_event_hash, ))]) .into_node(), A::::new() .href(state.href_with_lang(crate::Location::Event( - module_update_event.module_definition_event_hash, + module_update_event.module_definition_event_hash.clone(), ))) .style( Style::new() @@ -833,7 +812,7 @@ fn event_view( .set("text-decoration", "none"), ) .children([text(i18n::tr( - &state, + state, "Open module definition", "モジュール定義を開く", "Malfermi modulo-difinon", @@ -854,10 +833,10 @@ fn event_view( .set("padding", "1rem") .set("color", "var(--error)"), ) - .children([text(&format!( + .children([text(format!( "{}: {:?}", i18n::tr( - &state, + state, "Failed to load events", "イベントの読み込みに失敗しました", "Malsukcesis ŝargi eventojn", @@ -879,15 +858,7 @@ fn part_name_input(state: &AppState) -> Node { input.events.push(( "input".to_string(), EventHandler::new(move |set_state| async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| document.query_selector("input[name='part-name']").ok()) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value("input[name='part-name']"); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); next.part_definition_form.part_name_input = value; @@ -910,19 +881,7 @@ fn part_description_input(state: &AppState) -> Node { textarea.events.push(( "input".to_string(), EventHandler::new(move |set_state| async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document - .query_selector("textarea[name='part-description']") - .ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|textarea| textarea.value()) - .unwrap_or_default(); + let value = crate::dom::get_textarea_value("textarea[name='part-description']"); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); next.part_definition_form.part_description_input = value; @@ -943,7 +902,7 @@ fn part_type_input(state: &AppState) -> Node { .set("font-size", "0.85rem") .set("color", "var(--text-secondary)"), ) - .children([text(i18n::tr(&state, "Part Type", "パーツ型", "Parto-tipo"))]) + .children([text(i18n::tr(state, "Part Type", "パーツ型", "Parto-tipo"))]) .into_node(), render_part_type_editor(state, &state.part_definition_form.part_type_input, 0), ]) @@ -953,20 +912,26 @@ fn part_type_input(state: &AppState) -> Node { fn module_selection_input(state: &AppState) -> Node { let mut options = vec![( "".to_string(), - i18n::tr(&state, "No module", "モジュールなし", "Neniu modulo").to_string(), - )]; - options.extend(collect_module_snapshots(state).into_iter().map(|module| { - ( - crate::hash_format::encode_hash32(&module.definition_event_hash), - module.module_name, + i18n::tr( + &state.clone(), + "No module", + "モジュールなし", + "Neniu modulo", ) - })); + .to_string(), + )]; + options.extend( + collect_module_snapshots(state) + .into_iter() + .map(|module| (module.definition_event_hash.to_string(), module.module_name)), + ); - let current_value = state + let current_value: String = state .part_definition_form .module_definition_event_hash - .map(|hash| crate::hash_format::encode_hash32(&hash)) - .unwrap_or_else(|| "".to_string()); + .clone() + .map(|hash| hash.to_string()) + .unwrap_or_default(); let dropdown = crate::dropdown::searchable_dropdown( state, @@ -977,7 +942,7 @@ fn module_selection_input(state: &AppState) -> Node { Box::new(move |state: AppState| { let mut next = state.clone(); next.part_definition_form.module_definition_event_hash = - crate::hash_format::decode_hash32(&value); + EventHashId::from_str(&value).ok(); next }) }), @@ -992,7 +957,7 @@ fn module_selection_input(state: &AppState) -> Node { .set("font-size", "0.85rem") .set("color", "var(--text-secondary)"), ) - .children([text(i18n::tr(&state, "Module", "モジュール", "Modulo"))]) + .children([text(i18n::tr(state, "Module", "モジュール", "Modulo"))]) .into_node(), dropdown, ]) @@ -1011,30 +976,30 @@ fn render_part_type_editor( if depth == 0 { options.push(( "none".to_string(), - i18n::tr(&state, "None", "なし", "Neniu").to_string(), + i18n::tr(state, "None", "なし", "Neniu").to_string(), )); } options.extend([ ( "number".to_string(), - i18n::tr(&state, "Number", "数値", "Nombro").to_string(), + i18n::tr(state, "Number", "数値", "Nombro").to_string(), ), ( "string".to_string(), - i18n::tr(&state, "String", "文字列", "Teksto").to_string(), + i18n::tr(state, "String", "文字列", "Teksto").to_string(), ), ( "boolean".to_string(), - i18n::tr(&state, "Boolean", "真偽値", "Bulea").to_string(), + i18n::tr(state, "Boolean", "真偽値", "Bulea").to_string(), ), ( "type".to_string(), - i18n::tr(&state, "Type", "型", "Tipo").to_string(), + i18n::tr(state, "Type", "型", "Tipo").to_string(), ), ( "list".to_string(), - i18n::tr(&state, "List<...>", "リスト<...>", "Listo<...>").to_string(), + i18n::tr(state, "List<...>", "リスト<...>", "Listo<...>").to_string(), ), ]); @@ -1043,15 +1008,12 @@ fn render_part_type_editor( .into_iter() .filter(|snapshot| snapshot.part_type == Some(definy_event::event::PartType::Type)) .map(|snapshot| { - let value = format!( - "type_part:{}", - crate::hash_format::encode_hash32(&snapshot.definition_event_hash) - ); + let value = format!("type_part:{}", snapshot.definition_event_hash); ( value, format!( "{} {}", - i18n::tr(&state, "Type Part:", "型パーツ:", "Tipo-parto:"), + i18n::tr(state, "Type Part:", "型パーツ:", "Tipo-parto:"), snapshot.part_name ), ) @@ -1096,7 +1058,7 @@ fn render_part_type_editor( .set("color", "var(--text-secondary)") .set("margin-bottom", "0.25rem"), ) - .children([text(i18n::tr(&state, "Item Type", "要素型", "Ero-tipo"))]) + .children([text(i18n::tr(state, "Item Type", "要素型", "Ero-tipo"))]) .into_node(), render_part_type_editor(state, &Some(item_type.as_ref().clone()), depth + 1), ]) @@ -1164,10 +1126,10 @@ fn next_part_type_from_selected( selected: &str, current: &Option, ) -> Option { - if let Some(encoded) = selected.strip_prefix("type_part:") { - if let Some(hash) = decode_hash32(encoded) { - return Some(definy_event::event::PartType::TypePart(hash)); - } + if let Some(encoded) = selected.strip_prefix("type_part:") + && let Ok(hash) = EventHashId::from_str(encoded) + { + return Some(definy_event::event::PartType::TypePart(hash)); } match selected { "none" => None, @@ -1190,10 +1152,10 @@ fn next_nested_part_type_from_selected( selected: &str, current: &definy_event::event::PartType, ) -> definy_event::event::PartType { - if let Some(encoded) = selected.strip_prefix("type_part:") { - if let Some(hash) = decode_hash32(encoded) { - return definy_event::event::PartType::TypePart(hash); - } + if let Some(encoded) = selected.strip_prefix("type_part:") + && let Ok(hash) = EventHashId::from_str(encoded) + { + return definy_event::event::PartType::TypePart(hash); } match selected { "string" => definy_event::event::PartType::String, @@ -1219,20 +1181,8 @@ fn current_part_type_selection(part_type: &Option Some(definy_event::event::PartType::Boolean) => "boolean".to_string(), Some(definy_event::event::PartType::Type) => "type".to_string(), Some(definy_event::event::PartType::TypePart(hash)) => { - format!("type_part:{}", crate::hash_format::encode_hash32(hash)) + format!("type_part:{}", hash) } Some(definy_event::event::PartType::List(_)) => "list".to_string(), } } - -fn decode_hash32(value: &str) -> Option<[u8; 32]> { - let bytes = - base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, value).ok()?; - if bytes.len() == 32 { - let mut result = [0u8; 32]; - result.copy_from_slice(&bytes); - Some(result) - } else { - None - } -} diff --git a/definy-ui/src/event_presenter.rs b/definy-ui/src/event_presenter.rs index 0be470fd..dc8c2f37 100644 --- a/definy-ui/src/event_presenter.rs +++ b/definy-ui/src/event_presenter.rs @@ -1,22 +1,32 @@ use definy_event::event::{Event, EventContent}; use crate::app_state::AppState; -use crate::i18n; use crate::expression_eval::expression_to_source; +use crate::i18n; pub fn event_summary_text(state: &AppState, event: &Event) -> String { match &event.content { EventContent::CreateAccount(create_account_event) => { format!( "{} {}", - i18n::tr(state, "Account created:", "アカウント作成:", "Konto kreita:"), + i18n::tr( + state, + "Account created:", + "アカウント作成:", + "Konto kreita:" + ), create_account_event.account_name ) } EventContent::ChangeProfile(change_profile_event) => { format!( "{} {}", - i18n::tr(state, "Profile changed:", "プロフィール変更:", "Profilo ŝanĝita:"), + i18n::tr( + state, + "Profile changed:", + "プロフィール変更:", + "Profilo ŝanĝita:" + ), change_profile_event.account_name ) } @@ -45,13 +55,23 @@ pub fn event_summary_text(state: &AppState, event: &Event) -> String { if module_definition_event.description.is_empty() { format!( "{} {}", - i18n::tr(state, "Module created:", "モジュール作成:", "Modulo kreita:"), + i18n::tr( + state, + "Module created:", + "モジュール作成:", + "Modulo kreita:" + ), module_definition_event.module_name ) } else { format!( "{} {} - {}", - i18n::tr(state, "Module created:", "モジュール作成:", "Modulo kreita:"), + i18n::tr( + state, + "Module created:", + "モジュール作成:", + "Modulo kreita:" + ), module_definition_event.module_name, module_definition_event.description ) @@ -61,13 +81,23 @@ pub fn event_summary_text(state: &AppState, event: &Event) -> String { if module_update_event.module_description.is_empty() { format!( "{} {}", - i18n::tr(state, "Module updated:", "モジュール更新:", "Modulo ĝisdatigita:"), + i18n::tr( + state, + "Module updated:", + "モジュール更新:", + "Modulo ĝisdatigita:" + ), module_update_event.module_name ) } else { format!( "{} {} - {}", - i18n::tr(state, "Module updated:", "モジュール更新:", "Modulo ĝisdatigita:"), + i18n::tr( + state, + "Module updated:", + "モジュール更新:", + "Modulo ĝisdatigita:" + ), module_update_event.module_name, module_update_event.module_description ) @@ -78,20 +108,12 @@ pub fn event_summary_text(state: &AppState, event: &Event) -> String { pub fn event_kind_label(state: &AppState, event: &Event) -> String { match &event.content { - EventContent::CreateAccount(_) => i18n::tr( - state, - "CreateAccount", - "アカウント作成", - "Konto-kreo", - ) - .to_string(), - EventContent::ChangeProfile(_) => i18n::tr( - state, - "ChangeProfile", - "プロフィール変更", - "Profil-ŝanĝo", - ) - .to_string(), + EventContent::CreateAccount(_) => { + i18n::tr(state, "CreateAccount", "アカウント作成", "Konto-kreo").to_string() + } + EventContent::ChangeProfile(_) => { + i18n::tr(state, "ChangeProfile", "プロフィール変更", "Profil-ŝanĝo").to_string() + } EventContent::PartDefinition(part_definition) => { format!( "{} {}", @@ -107,14 +129,24 @@ pub fn event_kind_label(state: &AppState, event: &Event) -> String { EventContent::ModuleDefinition(module_definition) => { format!( "{} {}", - i18n::tr(state, "ModuleDefinition:", "モジュール定義:", "Modulo-difino:"), + i18n::tr( + state, + "ModuleDefinition:", + "モジュール定義:", + "Modulo-difino:" + ), module_definition.module_name ) } EventContent::ModuleUpdate(module_update) => { format!( "{} {}", - i18n::tr(state, "ModuleUpdate:", "モジュール更新:", "Modulo-ĝisdatigo:"), + i18n::tr( + state, + "ModuleUpdate:", + "モジュール更新:", + "Modulo-ĝisdatigo:" + ), module_update.module_name ) } diff --git a/definy-ui/src/expression_editor.rs b/definy-ui/src/expression_editor.rs index c7129d9b..cf7afe67 100644 --- a/definy-ui/src/expression_editor.rs +++ b/definy-ui/src/expression_editor.rs @@ -1,5 +1,7 @@ +use definy_event::EventHashId; use narumincho_vdom::*; use std::collections::HashMap; +use std::str::FromStr; use crate::Location; use crate::app_state::AppState; @@ -26,7 +28,7 @@ enum ExpressionType { String, Boolean, Type, - TypePart([u8; 32]), + TypePart(EventHashId), List(Box), Record, Unknown, @@ -49,9 +51,7 @@ impl ExpressionType { ExpressionType::String => "String".to_string(), ExpressionType::Boolean => "Boolean".to_string(), ExpressionType::Type => "Type".to_string(), - ExpressionType::TypePart(hash) => { - format!("TypePart({})", crate::hash_format::short_hash32(hash)) - } + ExpressionType::TypePart(hash) => format!("TypePart({})", hash), ExpressionType::List(item) => format!("list<{}>", item.text()), ExpressionType::Record => "Record".to_string(), ExpressionType::Unknown => "Unknown".to_string(), @@ -75,12 +75,14 @@ pub fn render_root_expression_editor( render_expression_editor( state, expression, - Vec::new(), - target, - Vec::new(), - diagnostics.as_slice(), - false, - true, + ExpressionEditorContext { + path: Vec::new(), + target: target, + scope_variables: Vec::new(), + diagnostics: diagnostics.as_slice(), + structure_locked: false, + allow_kind_change: true, + }, ) } @@ -92,16 +94,26 @@ fn allow_kind_change_for_nested_values(allow_kind_change: bool, path: &[PathStep .any(|step| matches!(step, PathStep::ConstructorValue)) } +pub struct ExpressionEditorContext<'a> { + pub path: Vec, + pub target: EditorTarget, + pub scope_variables: Vec, + pub diagnostics: &'a [TypeDiagnostic], + pub structure_locked: bool, + pub allow_kind_change: bool, +} + fn render_expression_editor( state: &AppState, expression: &definy_event::event::Expression, - path: Vec, - target: EditorTarget, - scope_variables: Vec, - diagnostics: &[TypeDiagnostic], - structure_locked: bool, - allow_kind_change: bool, + context: ExpressionEditorContext, ) -> Node { + let path = context.path; + let target = context.target; + let scope_variables = context.scope_variables; + let diagnostics = context.diagnostics; + let structure_locked = context.structure_locked; + let allow_kind_change = context.allow_kind_change; let current_selection = current_selection_value(expression); let selector_options = selector_options(state, &scope_variables); let warning_message = diagnostics @@ -179,12 +191,14 @@ fn render_expression_editor( render_expression_editor( state, type_list_expression.item_type.as_ref(), - item_type_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: item_type_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -253,12 +267,14 @@ fn render_expression_editor( .children([render_expression_editor( state, record_item.value.as_ref(), - value_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_for_item, + ExpressionEditorContext { + path: value_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_for_item, + }, )]) .into_node(), ); @@ -327,12 +343,14 @@ fn render_expression_editor( render_expression_editor( state, item, - item_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_for_item, + ExpressionEditorContext { + path: item_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_for_item, + }, ), ]) .into_node() @@ -374,12 +392,14 @@ fn render_expression_editor( render_expression_editor( state, add_expression.left.as_ref(), - left_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: left_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -390,12 +410,14 @@ fn render_expression_editor( render_expression_editor( state, add_expression.right.as_ref(), - right_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: right_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -430,12 +452,14 @@ fn render_expression_editor( render_expression_editor( state, if_expression.condition.as_ref(), - cond_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: cond_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -446,12 +470,14 @@ fn render_expression_editor( render_expression_editor( state, if_expression.then_expr.as_ref(), - then_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: then_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -462,12 +488,14 @@ fn render_expression_editor( render_expression_editor( state, if_expression.else_expr.as_ref(), - else_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: else_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -497,12 +525,14 @@ fn render_expression_editor( render_expression_editor( state, equal_expression.left.as_ref(), - left_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: left_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -513,12 +543,14 @@ fn render_expression_editor( render_expression_editor( state, equal_expression.right.as_ref(), - right_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: right_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -555,12 +587,14 @@ fn render_expression_editor( render_expression_editor( state, let_expression.value.as_ref(), - value_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: value_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ), ]) .into_node(), @@ -575,12 +609,14 @@ fn render_expression_editor( render_expression_editor( state, let_expression.body.as_ref(), - body_path, - target, - body_scope, - diagnostics, - structure_locked, - allow_kind_change, + ExpressionEditorContext { + path: body_path, + target: target, + scope_variables: body_scope, + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_change, + }, ) }]) .into_node(), @@ -641,7 +677,12 @@ fn render_expression_editor( if structure_locked { Div::new().children([]).into_node() } else { - remove_record_item_button(state, path.clone(), index, target) + remove_record_item_button( + state, + path.clone(), + index, + target, + ) }, ]) .into_node(), @@ -652,12 +693,14 @@ fn render_expression_editor( render_expression_editor( state, item.value.as_ref(), - value_path, - target, - scope_variables.clone(), - diagnostics, - structure_locked, - allow_kind_for_value, + ExpressionEditorContext { + path: value_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: structure_locked, + allow_kind_change: allow_kind_for_value, + }, ), ]) .into_node(), @@ -686,9 +729,7 @@ fn render_expression_editor( .unwrap_or_else(|| { format!( "(unknown: {})", - crate::hash_format::short_hash32( - &constructor_expression.type_part_definition_event_hash - ) + constructor_expression.type_part_definition_event_hash ) }); children.push( @@ -710,12 +751,14 @@ fn render_expression_editor( render_expression_editor( state, constructor_expression.value.as_ref(), - value_path, - target, - scope_variables.clone(), - diagnostics, - true, - true, + ExpressionEditorContext { + path: value_path, + target: target, + scope_variables: scope_variables.clone(), + diagnostics: diagnostics, + structure_locked: true, + allow_kind_change: true, + }, ), ]) .into_node(), @@ -761,7 +804,7 @@ fn part_type_to_expression_type(part_type: &definy_event::event::PartType) -> Ex definy_event::event::PartType::String => ExpressionType::String, definy_event::event::PartType::Boolean => ExpressionType::Boolean, definy_event::event::PartType::Type => ExpressionType::Type, - definy_event::event::PartType::TypePart(hash) => ExpressionType::TypePart(*hash), + definy_event::event::PartType::TypePart(hash) => ExpressionType::TypePart(hash.clone()), definy_event::event::PartType::List(item_type) => { ExpressionType::List(Box::new(part_type_to_expression_type(item_type.as_ref()))) } @@ -776,11 +819,11 @@ fn expected_type_for_target(state: &AppState, target: EditorTarget) -> Option { - let hash = match state.location { + let hash = match &state.location { Some(Location::Part(hash)) => hash, _ => return None, }; - find_part_snapshot(state, &hash) + find_part_snapshot(state, hash) .and_then(|snapshot| snapshot.part_type) .as_ref() .map(part_type_to_expression_type) @@ -799,16 +842,16 @@ fn collect_type_diagnostics( .filter_map(|snapshot| { snapshot.part_type.as_ref().map(|part_type| { ( - snapshot.definition_event_hash, + snapshot.definition_event_hash.clone(), part_type_to_expression_type(part_type), ) }) }) - .collect::>(); + .collect::>(); let part_snapshot_map = snapshots .into_iter() - .map(|snapshot| (snapshot.definition_event_hash, snapshot)) - .collect::>(); + .map(|snapshot| (snapshot.definition_event_hash.clone(), snapshot)) + .collect::>(); let mut diagnostics = Vec::new(); let env = HashMap::new(); @@ -848,8 +891,8 @@ fn check_expression_type( path: &[PathStep], expected_type: Option, env: &HashMap, - part_type_map: &HashMap<[u8; 32], ExpressionType>, - part_snapshot_map: &HashMap<[u8; 32], PartSnapshot>, + part_type_map: &HashMap, + part_snapshot_map: &HashMap, diagnostics: &mut Vec, ) -> ExpressionType { let actual_type = match expression { @@ -1154,7 +1197,11 @@ fn check_expression_type( diagnostics, ); } - ExpressionType::TypePart(constructor_expression.type_part_definition_event_hash) + ExpressionType::TypePart( + constructor_expression + .type_part_definition_event_hash + .clone(), + ) } }; @@ -1179,8 +1226,8 @@ fn expression_type_from_constructor_shape(shape: &ConstructorValueShape) -> Expr } fn infer_constructor_shape_from_type_part( - part_snapshot_map: &HashMap<[u8; 32], PartSnapshot>, - type_part_definition_event_hash: &[u8; 32], + part_snapshot_map: &HashMap, + type_part_definition_event_hash: &EventHashId, ) -> ConstructorValueShape { let mut visited = Vec::new(); infer_constructor_shape_from_type_part_with_visited( @@ -1191,9 +1238,9 @@ fn infer_constructor_shape_from_type_part( } fn infer_constructor_shape_from_type_part_with_visited( - part_snapshot_map: &HashMap<[u8; 32], PartSnapshot>, - type_part_definition_event_hash: &[u8; 32], - visited: &mut Vec<[u8; 32]>, + part_snapshot_map: &HashMap, + type_part_definition_event_hash: &EventHashId, + visited: &mut Vec, ) -> ConstructorValueShape { if visited.contains(type_part_definition_event_hash) { return ConstructorValueShape::Unknown; @@ -1201,7 +1248,7 @@ fn infer_constructor_shape_from_type_part_with_visited( let Some(snapshot) = part_snapshot_map.get(type_part_definition_event_hash) else { return ConstructorValueShape::Unknown; }; - visited.push(*type_part_definition_event_hash); + visited.push(type_part_definition_event_hash.clone()); let shape = infer_constructor_shape_from_type_expression( snapshot.expression.clone(), part_snapshot_map, @@ -1213,8 +1260,8 @@ fn infer_constructor_shape_from_type_part_with_visited( fn infer_constructor_shape_from_type_expression( expression: definy_event::event::Expression, - part_snapshot_map: &HashMap<[u8; 32], PartSnapshot>, - visited: &mut Vec<[u8; 32]>, + part_snapshot_map: &HashMap, + visited: &mut Vec, ) -> ConstructorValueShape { match expression { definy_event::event::Expression::Number(_) => ConstructorValueShape::Number, @@ -1319,12 +1366,12 @@ fn default_expression_from_constructor_shape( fn constructor_default_value_from_type_part( state: &AppState, - type_part_definition_event_hash: &[u8; 32], + type_part_definition_event_hash: &EventHashId, ) -> definy_event::event::Expression { let part_snapshot_map = collect_part_snapshots(state) .into_iter() - .map(|snapshot| (snapshot.definition_event_hash, snapshot)) - .collect::>(); + .map(|snapshot| (snapshot.definition_event_hash.clone(), snapshot)) + .collect::>(); let shape = infer_constructor_shape_from_type_part(&part_snapshot_map, type_part_definition_event_hash); default_expression_from_constructor_shape(&shape) @@ -1349,10 +1396,10 @@ fn expression_selector( let mut next = state.clone(); let constructor_default = selected_value .strip_prefix("expr:constructor:") - .and_then(decode_hash32) + .and_then(|value| EventHashId::from_str(value).ok()) .map(|type_part_definition_event_hash| { ( - type_part_definition_event_hash, + type_part_definition_event_hash.clone(), constructor_default_value_from_type_part( &next, &type_part_definition_event_hash, @@ -1407,14 +1454,10 @@ fn selector_options(state: &AppState, scope_variables: &[ScopeVariable]) -> Vec< options.extend(snapshots.iter().filter_map(|snapshot| { if snapshot.part_type == Some(definy_event::event::PartType::Type) { Some(( - format!( - "expr:constructor:{}", - crate::hash_format::encode_hash32(&snapshot.definition_event_hash) - ), + format!("expr:constructor:{}", snapshot.definition_event_hash), format!( "Constructor: {} ({})", - snapshot.part_name, - crate::hash_format::short_hash32(&snapshot.definition_event_hash) + snapshot.part_name, snapshot.definition_event_hash ), )) } else { @@ -1424,14 +1467,10 @@ fn selector_options(state: &AppState, scope_variables: &[ScopeVariable]) -> Vec< options.extend(snapshots.into_iter().map(|snapshot| { ( - format!( - "ref:global:{}", - crate::hash_format::encode_hash32(&snapshot.definition_event_hash) - ), + format!("ref:global:{}", snapshot.definition_event_hash), format!( "Global: {} ({})", - snapshot.part_name, - crate::hash_format::short_hash32(&snapshot.definition_event_hash) + snapshot.part_name, snapshot.definition_event_hash ), ) })); @@ -1463,14 +1502,11 @@ fn current_selection_value(expression: &definy_event::event::Expression) -> Stri definy_event::event::Expression::TypeLiteral(_) => "expr:type_literal".to_string(), definy_event::event::Expression::Constructor(constructor_expression) => format!( "expr:constructor:{}", - crate::hash_format::encode_hash32( - &constructor_expression.type_part_definition_event_hash - ) - ), - definy_event::event::Expression::PartReference(part_ref) => format!( - "ref:global:{}", - crate::hash_format::encode_hash32(&part_ref.part_definition_event_hash) + constructor_expression.type_part_definition_event_hash ), + definy_event::event::Expression::PartReference(part_ref) => { + format!("ref:global:{}", part_ref.part_definition_event_hash) + } definy_event::event::Expression::Variable(var_expr) => { format!("ref:local:{}", var_expr.variable_id) } @@ -1481,7 +1517,7 @@ fn apply_selection( root_expression: &mut definy_event::event::Expression, path: &[PathStep], selected_value: &str, - constructor_default: Option<([u8; 32], definy_event::event::Expression)>, + constructor_default: Option<(EventHashId, definy_event::event::Expression)>, ) { let next_variable_id = if selected_value == "expr:let" { next_local_variable_id(root_expression) @@ -1579,7 +1615,7 @@ fn apply_selection( }, ) } else if let Some(encoded) = selected_value.strip_prefix("ref:global:") { - if let Some(hash) = decode_hash32(encoded) { + if let Ok(hash) = EventHashId::from_str(encoded) { definy_event::event::Expression::PartReference( definy_event::event::PartReferenceExpression { part_definition_event_hash: hash, @@ -1602,18 +1638,6 @@ fn apply_selection( } } -fn decode_hash32(value: &str) -> Option<[u8; 32]> { - let bytes = - base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, value).ok()?; - if bytes.len() == 32 { - let mut result = [0u8; 32]; - result.copy_from_slice(&bytes); - Some(result) - } else { - None - } -} - fn number_input(path: Vec, target: EditorTarget, value: i64) -> Node { let name = format!( "{}-expr-number-{}", @@ -1633,14 +1657,9 @@ fn number_input(path: Vec, target: EditorTarget, value: i64) -> Node(element).ok() - }) - .and_then(|input| input.value().parse::().ok()); + let value = crate::dom::get_input_value(selector.as_str()) + .parse::() + .ok(); if let Some(value) = value { set_state(Box::new(move |state: AppState| { @@ -1672,15 +1691,7 @@ fn string_input(path: Vec, target: EditorTarget, value: &str) -> Node< let selector = selector.clone(); let path = path.clone(); async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| document.query_selector(selector.as_str()).ok()) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value(selector.as_str()); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); @@ -1772,15 +1783,7 @@ fn let_name_input(path: Vec, target: EditorTarget, value: &str) -> Nod let selector = selector.clone(); let path = path.clone(); async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| document.query_selector(selector.as_str()).ok()) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value(selector.as_str()); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); @@ -1819,15 +1822,7 @@ fn record_item_key_input( let selector = selector.clone(); let path = path.clone(); async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| document.query_selector(selector.as_str()).ok()) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value(selector.as_str()); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); @@ -1859,7 +1854,12 @@ fn add_record_item_button( })); } })) - .children([text(i18n::tr(state, "+ Add Item", "+ 追加", "+ Aldoni eron"))]) + .children([text(i18n::tr( + state, + "+ Add Item", + "+ 追加", + "+ Aldoni eron", + ))]) .into_node() } @@ -1904,7 +1904,12 @@ fn add_list_item_button( })); } })) - .children([text(i18n::tr(state, "+ Add Item", "+ 追加", "+ Aldoni eron"))]) + .children([text(i18n::tr( + state, + "+ Add Item", + "+ 追加", + "+ Aldoni eron", + ))]) .into_node() } @@ -1937,15 +1942,15 @@ fn selector_prefix(target: EditorTarget) -> &'static str { } } -fn target_expression_mut<'a>( - state: &'a mut AppState, +fn target_expression_mut( + state: &mut AppState, target: EditorTarget, -) -> &'a mut definy_event::event::Expression { +) -> &mut definy_event::event::Expression { match target { EditorTarget::PartDefinition => &mut state.part_definition_form.composing_expression, EditorTarget::PartUpdate => { - if let Some(Location::Part(hash)) = state.location { - state.part_update_form.part_definition_event_hash = Some(hash); + if let Some(Location::Part(hash)) = &state.location { + state.part_update_form.part_definition_event_hash = Some(hash.clone()); } &mut state.part_update_form.expression_input } @@ -2115,10 +2120,9 @@ fn set_record_item_key( ) { if let Some(definy_event::event::Expression::TypeLiteral(record_expr)) = get_mut_expression_at_path(root_expression, path) + && let Some(item) = record_expr.items.get_mut(item_index) { - if let Some(item) = record_expr.items.get_mut(item_index) { - item.key = value.into(); - } + item.key = value.into(); } } @@ -2175,10 +2179,9 @@ fn remove_list_item( ) { if let Some(definy_event::event::Expression::ListLiteral(list_expr)) = get_mut_expression_at_path(root_expression, path) + && item_index < list_expr.items.len() { - if item_index < list_expr.items.len() { - list_expr.items.remove(item_index); - } + list_expr.items.remove(item_index); } } diff --git a/definy-ui/src/expression_eval.rs b/definy-ui/src/expression_eval.rs index 1605047f..8b80e017 100644 --- a/definy-ui/src/expression_eval.rs +++ b/definy-ui/src/expression_eval.rs @@ -35,13 +35,7 @@ impl std::fmt::Display for Value { pub fn evaluate_expression( expression: &definy_event::event::Expression, - events: &[( - [u8; 32], - Result< - (ed25519_dalek::Signature, definy_event::event::Event), - definy_event::VerifyAndDeserializeError, - >, - )], + events: &[crate::app_state::EventWithHash], ) -> Result { // Try to evaluate purely via WebAssembly first! if let Some(result) = evaluate_via_wasm(expression) { @@ -178,13 +172,7 @@ fn is_boolean_expression(expr: &definy_event::event::Expression) -> bool { fn evaluate_expression_with_depth( expression: &definy_event::event::Expression, - events: &[( - [u8; 32], - Result< - (ed25519_dalek::Signature, definy_event::event::Event), - definy_event::VerifyAndDeserializeError, - >, - )], + events: &[crate::app_state::EventWithHash], env: &std::collections::HashMap, depth: usize, ) -> Result { @@ -422,9 +410,9 @@ pub fn expression_to_source(expression: &definy_event::event::Expression) -> Str } } definy_event::event::Expression::PartReference(part_reference_expression) => { - crate::hash_format::encode_hash32( - &part_reference_expression.part_definition_event_hash, - ) + part_reference_expression + .part_definition_event_hash + .to_string() } definy_event::event::Expression::Let(let_expression) => { let mut body_scope = scope.to_vec(); @@ -473,9 +461,7 @@ pub fn expression_to_source(expression: &definy_event::event::Expression) -> Str definy_event::event::Expression::Constructor(constructor_expression) => { let source = format!( "constructor {} {}", - crate::hash_format::encode_hash32( - &constructor_expression.type_part_definition_event_hash - ), + constructor_expression.type_part_definition_event_hash, render(constructor_expression.value.as_ref(), true, scope) ); if is_child { @@ -492,6 +478,8 @@ pub fn expression_to_source(expression: &definy_event::event::Expression) -> Str #[cfg(test)] mod tests { + use std::str::FromStr; + use super::{evaluate_expression, expression_to_source}; #[test] @@ -764,17 +752,22 @@ mod tests { #[test] fn evaluate_part_reference_by_definition_hash() { - let definition_hash = [42u8; 32]; + let definition_hash = + definy_event::EventHashId::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") + .unwrap(); let part_expression = definy_event::event::Expression::Number(definy_event::event::NumberExpression { value: 99, }); let events = vec![( - definition_hash, + definition_hash.clone(), Ok(( ed25519_dalek::Signature::from_bytes(&[0u8; 64]), definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new([1u8; 32])), + account_id: definy_event::event::AccountId::from_str( + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + ) + .unwrap(), time: chrono::DateTime::UNIX_EPOCH, content: definy_event::event::EventContent::PartDefinition( definy_event::event::PartDefinitionEvent { @@ -791,7 +784,7 @@ mod tests { let reference = definy_event::event::Expression::PartReference( definy_event::event::PartReferenceExpression { - part_definition_event_hash: definition_hash, + part_definition_event_hash: definition_hash.clone(), }, ); @@ -801,7 +794,7 @@ mod tests { ); assert_eq!( expression_to_source(&reference), - crate::hash_format::encode_hash32(&definition_hash) + definition_hash.to_string() ); } } diff --git a/definy-ui/src/fetch.rs b/definy-ui/src/fetch.rs index 871110b6..561e0c06 100644 --- a/definy-ui/src/fetch.rs +++ b/definy-ui/src/fetch.rs @@ -1,4 +1,4 @@ -use sha2; +use definy_event::EventHashId; use wasm_bindgen::JsValue; pub async fn get_events_raw( @@ -46,7 +46,7 @@ pub async fn get_events( offset: Option, ) -> anyhow::Result< Vec<( - [u8; 32], + definy_event::EventHashId, Result< (ed25519_dalek::Signature, definy_event::event::Event), definy_event::VerifyAndDeserializeError, @@ -58,14 +58,7 @@ pub async fn get_events( let value = serde_cbor::from_slice::(&response_body_bytes)?; - let event_pairs = value - .events - .into_iter() - .filter_map(|bytes| { - let hash: [u8; 32] = ::digest(&bytes).into(); - Some((hash, bytes)) - }) - .collect::)>>(); + let event_pairs = value.events.into_iter().collect::>>(); if let Err(error) = crate::indexed_db::store_events(&event_pairs).await { web_sys::console::warn_1(&error); @@ -73,9 +66,14 @@ pub async fn get_events( Ok(event_pairs .into_iter() - .map(|(hash, bytes)| (hash, definy_event::verify_and_deserialize(&bytes))) + .map(|bytes| { + ( + EventHashId::from_bytes(&bytes), + definy_event::verify_and_deserialize(&bytes), + ) + }) .collect:: Result { - let hash: [u8; 32] = ::digest(signated_event).into(); + let hash = EventHashId::from_bytes(signated_event); let now_ms = chrono::Utc::now().timestamp_millis(); let (status, last_error) = if force_offline { diff --git a/definy-ui/src/hash_format.rs b/definy-ui/src/hash_format.rs deleted file mode 100644 index a6ecfde6..00000000 --- a/definy-ui/src/hash_format.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub fn encode_hash32(hash: &[u8; 32]) -> String { - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, hash) -} - -pub fn encode_bytes(bytes: &[u8]) -> String { - base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, bytes) -} - -pub fn short_hash32(hash: &[u8; 32]) -> String { - encode_hash32(hash).chars().take(10).collect() -} - -pub fn decode_hash32(value: &str) -> Option<[u8; 32]> { - let bytes = - base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, value).ok()?; - if bytes.len() == 32 { - let mut result = [0u8; 32]; - result.copy_from_slice(&bytes); - Some(result) - } else { - None - } -} diff --git a/definy-ui/src/header.rs b/definy-ui/src/header.rs index 75c4fd7d..3a89c044 100644 --- a/definy-ui/src/header.rs +++ b/definy-ui/src/header.rs @@ -2,8 +2,8 @@ use std::rc::Rc; use narumincho_vdom::*; -use crate::{AppState, Location}; use crate::i18n; +use crate::{AppState, Location}; pub fn header(state: &AppState) -> Node { let mut children = vec![header_main(state)]; @@ -85,7 +85,12 @@ fn header_main(state: &AppState) -> Node { .set("font-size", "0.9rem") .set("color", "var(--text-secondary)"), ) - .children([text(i18n::tr(state, "Local Events", "ローカルイベント", "Lokaj eventoj"))]) + .children([text(i18n::tr( + state, + "Local Events", + "ローカルイベント", + "Lokaj eventoj", + ))]) .into_node(), A::::new() .href(state.href_with_lang(Location::AccountList)) @@ -121,9 +126,7 @@ fn header_main(state: &AppState) -> Node { { let account_button = match &state.current_key { Some(secret_key) => { - let account_id = definy_event::event::AccountId(Box::new( - secret_key.verifying_key().to_bytes(), - )); + let account_id = definy_event::event::AccountId(secret_key.verifying_key()); let account_name = state.account_name_map().get(&account_id).cloned(); Button::new() @@ -199,8 +202,8 @@ fn language_dropdown(state: &AppState) -> Node { options.as_slice(), Rc::new(|value| { Box::new(move |state: AppState| { - let selected = crate::language::language_from_tag(value.as_str()) - .unwrap_or(state.language); + let selected = + crate::language::language_from_tag(value.as_str()).unwrap_or(state.language); if selected.code == state.language.code { return state; } @@ -210,14 +213,14 @@ fn language_dropdown(state: &AppState) -> Node { selected.code, state.event_list_state.filter_event_type, ); - if let Some(window) = web_sys::window() { - if let Ok(history) = window.history() { - let _ = history.push_state_with_url( - &wasm_bindgen::JsValue::NULL, - "", - Some(url.as_str()), - ); - } + if let Some(window) = web_sys::window() + && let Ok(history) = window.history() + { + let _ = history.push_state_with_url( + &wasm_bindgen::JsValue::NULL, + "", + Some(url.as_str()), + ); } AppState { language: selected, @@ -258,8 +261,9 @@ fn language_dropdown(state: &AppState) -> Node { fn popover(state: &AppState) -> Node { let account_link = state.current_key.as_ref().map(|key| { - let account_id = definy_event::event::AccountId(Box::new(key.verifying_key().to_bytes())); - let account_name = crate::app_state::account_display_name(&state.account_name_map(), &account_id); + let account_id = definy_event::event::AccountId(key.verifying_key()); + let account_name = + crate::app_state::account_display_name(&state.account_name_map(), &account_id); A::::new() .href(state.href_with_lang(Location::Account(account_id))) .style( @@ -314,7 +318,12 @@ fn popover(state: &AppState) -> Node { .children([text(if state.force_offline { i18n::tr(state, "Offline: On", "オフライン: オン", "Senkonekte: En") } else { - i18n::tr(state, "Offline: Off", "オフライン: オフ", "Senkonekte: Malŝaltita") + i18n::tr( + state, + "Offline: Off", + "オフライン: オフ", + "Senkonekte: Malŝaltita", + ) })]) .style( Style::new() diff --git a/definy-ui/src/indexed_db.rs b/definy-ui/src/indexed_db.rs index dabd84e8..e7dbf02e 100644 --- a/definy-ui/src/indexed_db.rs +++ b/definy-ui/src/indexed_db.rs @@ -1,13 +1,14 @@ -use wasm_bindgen::closure::Closure; +use definy_event::EventHashId; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; +use wasm_bindgen::closure::Closure; use wasm_bindgen_futures::JsFuture; const DB_NAME: &str = "definy"; const DB_VERSION: u32 = 2; const EVENTS_STORE: &str = "events"; -pub async fn store_events(events: &[([u8; 32], Vec)]) -> Result<(), JsValue> { +pub async fn store_events(events: &[Vec]) -> Result<(), JsValue> { if events.is_empty() { return Ok(()); } @@ -17,11 +18,12 @@ pub async fn store_events(events: &[([u8; 32], Vec)]) -> Result<(), JsValue> db.transaction_with_str_and_mode(EVENTS_STORE, web_sys::IdbTransactionMode::Readwrite)?; let store = transaction.object_store(EVENTS_STORE)?; - for (hash, bytes) in events { - let key = JsValue::from_str(&crate::hash_format::encode_hash32(hash)); + for event_bytes in events { + let hash = definy_event::EventHashId::from_bytes(event_bytes); + let key = JsValue::from_str(&hash.to_string()); let record = crate::local_event::LocalEventRecord { - hash: *hash, - event_binary: bytes.clone(), + hash, + event_binary: event_bytes.clone(), status: crate::local_event::LocalEventStatus::Sent, updated_at_ms: chrono::Utc::now().timestamp_millis(), last_error: None, @@ -38,7 +40,10 @@ pub async fn store_events(events: &[([u8; 32], Vec)]) -> Result<(), JsValue> pub async fn load_event_binaries() -> Result>, JsValue> { let records = load_event_records().await?; - Ok(records.into_iter().map(|record| record.event_binary).collect()) + Ok(records + .into_iter() + .map(|record| record.event_binary) + .collect()) } pub async fn store_event_record( @@ -49,20 +54,21 @@ pub async fn store_event_record( db.transaction_with_str_and_mode(EVENTS_STORE, web_sys::IdbTransactionMode::Readwrite)?; let store = transaction.object_store(EVENTS_STORE)?; - let key = JsValue::from_str(&crate::hash_format::encode_hash32(&record.hash)); - let value = serde_cbor::to_vec(record).map_err(|error| JsValue::from_str(&format!("{error:?}")))?; + let key = JsValue::from_str(&record.hash.to_string()); + let value = + serde_cbor::to_vec(record).map_err(|error| JsValue::from_str(&format!("{error:?}")))?; let value = js_sys::Uint8Array::from(value.as_slice()); let request = store.put_with_key(&value, &key)?; let _ = request_to_jsvalue(request).await?; Ok(()) } -pub async fn remove_event_record(hash: &[u8; 32]) -> Result<(), JsValue> { +pub async fn remove_event_record(hash: &EventHashId) -> Result<(), JsValue> { let db = open_db().await?; let transaction = db.transaction_with_str_and_mode(EVENTS_STORE, web_sys::IdbTransactionMode::Readwrite)?; let store = transaction.object_store(EVENTS_STORE)?; - let key = JsValue::from_str(&crate::hash_format::encode_hash32(hash)); + let key = JsValue::from_str(&hash.to_string()); let request = store.delete(&key)?; let _ = request_to_jsvalue(request).await?; Ok(()) @@ -78,12 +84,10 @@ pub async fn load_event_records() -> Result(&bytes) - { + if let Ok(record) = serde_cbor::from_slice::(&bytes) { records.push(record); } else { - let hash: [u8; 32] = ::digest(&bytes).into(); + let hash = EventHashId::from_bytes(&bytes); records.push(crate::local_event::LocalEventRecord { hash, event_binary: bytes, @@ -106,21 +110,21 @@ async fn open_db() -> Result { let upgrade_request = request.clone(); let on_upgrade = Closure::wrap(Box::new(move |_event: web_sys::IdbVersionChangeEvent| { - if let Ok(result) = upgrade_request.result() { - if let Ok(db) = result.dyn_into::() { - let store_names = db.object_store_names(); - let mut has_events = false; - for index in 0..store_names.length() { - if let Some(name) = store_names.get(index) { - if name == EVENTS_STORE { - has_events = true; - } - } - } - if !has_events { - let _ = db.create_object_store(EVENTS_STORE); + if let Ok(result) = upgrade_request.result() + && let Ok(db) = result.dyn_into::() + { + let store_names = db.object_store_names(); + let mut has_events = false; + for index in 0..store_names.length() { + if let Some(name) = store_names.get(index) + && name == EVENTS_STORE + { + has_events = true; } } + if !has_events { + let _ = db.create_object_store(EVENTS_STORE); + } } }) as Box); request.set_onupgradeneeded(Some(on_upgrade.as_ref().unchecked_ref())); @@ -132,30 +136,29 @@ async fn open_db() -> Result { let request_for_handler = success_request.clone(); let success_request = success_request.clone(); let reject_for_success = reject.clone(); - let on_success = Closure::once(Box::new(move |_event: web_sys::Event| { - match success_request.result() { - Ok(result) => match result.dyn_into::() { - Ok(db) => { - let _ = resolve.call1(&JsValue::NULL, &db); - } + let on_success = + Closure::once(Box::new( + move |_event: web_sys::Event| match success_request.result() { + Ok(result) => match result.dyn_into::() { + Ok(db) => { + let _ = resolve.call1(&JsValue::NULL, &db); + } + Err(error) => { + let _ = reject_for_success.call1(&JsValue::NULL, &error); + } + }, Err(error) => { let _ = reject_for_success.call1(&JsValue::NULL, &error); } }, - Err(error) => { - let _ = reject_for_success.call1(&JsValue::NULL, &error); - } - } - }) as Box); + ) as Box); request_for_handler.set_onsuccess(Some(on_success.as_ref().unchecked_ref())); on_success.forget(); let reject_for_error = reject.clone(); let on_error = Closure::once(Box::new(move |_event: web_sys::Event| { - let _ = reject_for_error.call1( - &JsValue::NULL, - &JsValue::from_str("indexedDB open failed"), - ); + let _ = + reject_for_error.call1(&JsValue::NULL, &JsValue::from_str("indexedDB open failed")); }) as Box); error_request.set_onerror(Some(on_error.as_ref().unchecked_ref())); on_error.forget(); @@ -172,16 +175,17 @@ async fn request_to_jsvalue(request: web_sys::IdbRequest) -> Result { - let _ = resolve.call1(&JsValue::NULL, &result); - } - Err(error) => { - let _ = reject_for_success.call1(&JsValue::NULL, &error); - } - } - }) as Box); + let on_success = + Closure::once(Box::new( + move |_event: web_sys::Event| match success_request.result() { + Ok(result) => { + let _ = resolve.call1(&JsValue::NULL, &result); + } + Err(error) => { + let _ = reject_for_success.call1(&JsValue::NULL, &error); + } + }, + ) as Box); request_for_handler.set_onsuccess(Some(on_success.as_ref().unchecked_ref())); on_success.forget(); diff --git a/definy-ui/src/language.rs b/definy-ui/src/language.rs index 0f71d3d9..3c0b48fc 100644 --- a/definy-ui/src/language.rs +++ b/definy-ui/src/language.rs @@ -15,132 +15,28 @@ pub struct LanguageResolution { pub unsupported_query_lang: Option, } +impl LanguageResolution { + pub fn fallback_notice(&self) -> Option { + self.unsupported_query_lang + .as_ref() + .map(|req| crate::LanguageFallbackNotice { + requested: req.clone(), + fallback_to_code: self.language.code, + }) + } +} + pub const SUPPORTED_LANGUAGES: &[Language] = &[ Language { code: "en", english_name: "English", native_name: "English", }, - Language { - code: "zh", - english_name: "Chinese", - native_name: "中文", - }, - Language { - code: "hi", - english_name: "Hindi", - native_name: "हिन्दी", - }, - Language { - code: "es", - english_name: "Spanish", - native_name: "Español", - }, - Language { - code: "fr", - english_name: "French", - native_name: "Français", - }, - Language { - code: "ar", - english_name: "Arabic", - native_name: "العربية", - }, - Language { - code: "bn", - english_name: "Bengali", - native_name: "বাংলা", - }, - Language { - code: "pt", - english_name: "Portuguese", - native_name: "Português", - }, - Language { - code: "ru", - english_name: "Russian", - native_name: "Русский", - }, - Language { - code: "ur", - english_name: "Urdu", - native_name: "اردو", - }, - Language { - code: "id", - english_name: "Indonesian", - native_name: "Bahasa Indonesia", - }, - Language { - code: "de", - english_name: "German", - native_name: "Deutsch", - }, Language { code: "ja", english_name: "Japanese", native_name: "日本語", }, - Language { - code: "sw", - english_name: "Swahili", - native_name: "Kiswahili", - }, - Language { - code: "mr", - english_name: "Marathi", - native_name: "मराठी", - }, - Language { - code: "te", - english_name: "Telugu", - native_name: "తెలుగు", - }, - Language { - code: "tr", - english_name: "Turkish", - native_name: "Türkçe", - }, - Language { - code: "ta", - english_name: "Tamil", - native_name: "தமிழ்", - }, - Language { - code: "vi", - english_name: "Vietnamese", - native_name: "Tiếng Việt", - }, - Language { - code: "ko", - english_name: "Korean", - native_name: "한국어", - }, - Language { - code: "it", - english_name: "Italian", - native_name: "Italiano", - }, - Language { - code: "th", - english_name: "Thai", - native_name: "ไทย", - }, - Language { - code: "nl", - english_name: "Dutch", - native_name: "Nederlands", - }, - Language { - code: "pl", - english_name: "Polish", - native_name: "Polski", - }, - Language { - code: "fa", - english_name: "Persian", - native_name: "فارسی", - }, Language { code: "eo", english_name: "Esperanto", @@ -148,10 +44,6 @@ pub const SUPPORTED_LANGUAGES: &[Language] = &[ }, ]; -pub fn supported_languages() -> &'static [Language] { - SUPPORTED_LANGUAGES -} - pub fn default_language() -> Language { SUPPORTED_LANGUAGES[0] } @@ -162,7 +54,7 @@ pub fn language_from_tag(tag: &str) -> Option { return None; } let primary = tag - .split(|c| c == '-' || c == '_') + .split(['-', '_']) .next() .unwrap_or(tag) .to_ascii_lowercase(); @@ -185,10 +77,10 @@ pub fn preferred_languages() -> Vec { let mut ordered = Vec::new(); let mut seen = HashSet::new(); for tag in browser_language_tags() { - if let Some(lang) = language_from_tag(tag.as_str()) { - if seen.insert(lang.code) { - ordered.push(lang); - } + if let Some(lang) = language_from_tag(tag.as_str()) + && seen.insert(lang.code) + { + ordered.push(lang); } } for lang in SUPPORTED_LANGUAGES.iter().copied() { @@ -208,7 +100,10 @@ pub fn best_language_from_browser() -> Option { None } -pub fn resolve_language(query: Option<&str>, accept_language: Option<&str>) -> LanguageResolution { +pub fn resolve_language_with_fallback( + query: Option<&str>, + fallback: impl FnOnce() -> Language, +) -> LanguageResolution { let params = parse_query(query); if let Some(requested_lang) = params.lang { if let Some(language) = language_from_tag(requested_lang.as_str()) { @@ -217,27 +112,29 @@ pub fn resolve_language(query: Option<&str>, accept_language: Option<&str>) -> L unsupported_query_lang: None, }; } - let fallback = language_from_accept_language(accept_language).unwrap_or_else(default_language); return LanguageResolution { - language: fallback, + language: fallback(), unsupported_query_lang: Some(requested_lang), }; } LanguageResolution { - language: language_from_accept_language(accept_language).unwrap_or_else(default_language), + language: fallback(), unsupported_query_lang: None, } } +pub fn resolve_language(query: Option<&str>, accept_language: Option<&str>) -> LanguageResolution { + resolve_language_with_fallback(query, || { + language_from_accept_language(accept_language).unwrap_or_else(default_language) + }) +} + pub fn best_language_from_accept_language(header: Option<&str>) -> Language { language_from_accept_language(header).unwrap_or_else(default_language) } pub fn language_from_accept_language(header: Option<&str>) -> Option { - let header = match header { - Some(header) => header, - None => return None, - }; + let header = header?; let mut candidates = Vec::new(); for part in header.split(',') { let part = part.trim(); @@ -254,10 +151,10 @@ pub fn language_from_accept_language(header: Option<&str>) -> Option { let param = param.trim(); let mut kv = param.splitn(2, '='); let key = kv.next().unwrap_or("").trim().to_ascii_lowercase(); - if key == "q" { - if let Some(val) = kv.next() { - q = val.trim().parse::().unwrap_or(1.0); - } + if key == "q" + && let Some(val) = kv.next() + { + q = val.trim().parse::().unwrap_or(1.0); } } candidates.push((tag.to_string(), q)); diff --git a/definy-ui/src/lib.rs b/definy-ui/src/lib.rs index 8fd01c0d..a1911c9c 100644 --- a/definy-ui/src/lib.rs +++ b/definy-ui/src/lib.rs @@ -1,6 +1,7 @@ mod account_detail; mod account_list; -mod app_state; +pub mod app_state; +pub mod dom; pub mod dropdown; mod event_detail; mod event_filter; @@ -9,7 +10,6 @@ mod event_presenter; mod expression_editor; mod expression_eval; pub mod fetch; -mod hash_format; mod header; pub mod i18n; pub mod indexed_db; @@ -22,19 +22,19 @@ mod message; mod module_detail; mod module_list; mod module_projection; -pub mod query; pub mod navigator_credential; mod not_found; mod page_title; mod part_detail; mod part_list; mod part_projection; +pub mod query; pub mod wasm_emitter; pub use app_state::*; pub use event_filter::*; -pub use message::Message; pub use local_event::*; +pub use message::Message; use narumincho_vdom::*; @@ -111,29 +111,26 @@ pub fn render( .children([text(include_str!("../main.css"))]) .into_node(), ]; - match resource_hash { - Some(r) => { - if let Some(ssr_initial_state_json) = ssr_initial_state_json { - head_children.push( - Script::new() - .id(SSR_INITIAL_STATE_ELEMENT_ID) - .type_("application/json") - .children([text(ssr_initial_state_json)]) - .into_node(), - ); - } + if let Some(r) = resource_hash { + if let Some(ssr_initial_state_json) = ssr_initial_state_json { head_children.push( Script::new() - .type_("module") - .children([text(format!( - "import init from '/{}'; -init({{ module_or_path: \"/{}\" }});", - r.js, r.wasm - ))]) + .id(SSR_INITIAL_STATE_ELEMENT_ID) + .type_("application/json") + .children([text(ssr_initial_state_json)]) .into_node(), ); } - _ => {} + head_children.push( + Script::new() + .type_("module") + .children([text(format!( + "import init from '/{}'; + init({{ module_or_path: \"/{}\" }});", + r.js, r.wasm + ))]) + .into_node(), + ); } Html::new() .attribute("lang", state.language.code) diff --git a/definy-ui/src/local_event.rs b/definy-ui/src/local_event.rs index 396ee1d3..12e3f72a 100644 --- a/definy-ui/src/local_event.rs +++ b/definy-ui/src/local_event.rs @@ -7,7 +7,7 @@ pub enum LocalEventStatus { #[derive(Clone, serde::Serialize, serde::Deserialize)] pub struct LocalEventRecord { - pub hash: [u8; 32], + pub hash: definy_event::EventHashId, pub event_binary: Vec, pub status: LocalEventStatus, pub updated_at_ms: i64, diff --git a/definy-ui/src/local_event_queue.rs b/definy-ui/src/local_event_queue.rs index 85ecffaa..dfa1fd8f 100644 --- a/definy-ui/src/local_event_queue.rs +++ b/definy-ui/src/local_event_queue.rs @@ -1,14 +1,14 @@ use narumincho_vdom::*; -use crate::app_state::{replace_local_event_records, AppState}; +use crate::app_state::{AppState, replace_local_event_records}; use crate::i18n; use crate::local_event::LocalEventStatus; fn status_label(state: &AppState, status: &LocalEventStatus) -> &'static str { match status { - LocalEventStatus::Queued => i18n::tr(&state, "Queued", "送信待ち", "Atendanta"), - LocalEventStatus::Sent => i18n::tr(&state, "Sent", "送信済み", "Sendita"), - LocalEventStatus::Failed => i18n::tr(&state, "Failed", "送信失敗", "Malsukcesis"), + LocalEventStatus::Queued => i18n::tr(state, "Queued", "送信待ち", "Atendanta"), + LocalEventStatus::Sent => i18n::tr(state, "Sent", "送信済み", "Sendita"), + LocalEventStatus::Failed => i18n::tr(state, "Failed", "送信失敗", "Malsukcesis"), } } @@ -23,7 +23,7 @@ fn status_color(status: &LocalEventStatus) -> &'static str { fn format_time_ms(state: &AppState, time_ms: i64) -> String { chrono::DateTime::::from_timestamp_millis(time_ms) .map(|t| t.format("%Y-%m-%d %H:%M:%S").to_string()) - .unwrap_or_else(|| i18n::tr(&state, "unknown", "不明", "nekonata").to_string()) + .unwrap_or_else(|| i18n::tr(state, "unknown", "不明", "nekonata").to_string()) } pub fn local_event_queue_view(state: &AppState) -> Node { @@ -47,11 +47,15 @@ pub fn local_event_queue_view(state: &AppState) -> Node { } Err(error) => { next.local_event_queue.is_loading = false; - next.local_event_queue.last_error = - Some(format!( - "{}: {error:?}", - i18n::tr(&state, "Failed to load local events", "ローカルイベントの読み込みに失敗しました", "Malsukcesis ŝargi lokajn eventojn") - )); + next.local_event_queue.last_error = Some(format!( + "{}: {error:?}", + i18n::tr( + &state, + "Failed to load local events", + "ローカルイベントの読み込みに失敗しました", + "Malsukcesis ŝargi lokajn eventojn" + ) + )); } } next @@ -65,7 +69,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { .set("padding", "0.4rem 0.8rem") .set("border-radius", "0.5rem"), ) - .children([text(i18n::tr(&state, "Refresh", "更新", "Refreŝigi"))]) + .children([text(i18n::tr(state, "Refresh", "更新", "Refreŝigi"))]) .into_node(); let offline_toggle = Button::new() @@ -85,9 +89,14 @@ pub fn local_event_queue_view(state: &AppState) -> Node { .set("border-radius", "0.5rem"), ) .children([text(if state.force_offline { - i18n::tr(&state, "Offline: On", "オフライン: オン", "Senkonekte: En") + i18n::tr(state, "Offline: On", "オフライン: オン", "Senkonekte: En") } else { - i18n::tr(&state, "Offline: Off", "オフライン: オフ", "Senkonekte: Malŝaltita") + i18n::tr( + state, + "Offline: Off", + "オフライン: オフ", + "Senkonekte: Malŝaltita", + ) })]) .into_node(); @@ -97,7 +106,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { Div::new() .style(Style::new().set("color", "var(--text-secondary)")) .children([text(i18n::tr( - &state, + state, "No local events", "ローカルイベントはありません", "Neniuj lokaj eventoj", @@ -107,7 +116,6 @@ pub fn local_event_queue_view(state: &AppState) -> Node { } else { for record in &state.local_event_queue.items { let status = record.status.clone(); - let hash = record.hash; let status_badge = Div::new() .style( Style::new() @@ -124,17 +132,17 @@ pub fn local_event_queue_view(state: &AppState) -> Node { let summary = match definy_event::verify_and_deserialize(&record.event_binary) { Ok((_, event)) => crate::event_presenter::event_summary_text(state, &event), - Err(_) => i18n::tr(&state, "Invalid event", "無効なイベント", "Nevalida evento") + Err(_) => i18n::tr(state, "Invalid event", "無効なイベント", "Nevalida evento") .to_string(), }; let mut actions = Vec::new(); if status != LocalEventStatus::Sent { - let hash = hash; + let hash = record.hash.clone(); actions.push( Button::new() .on_click(EventHandler::new(move |set_state| { - let hash = hash; + let hash = hash.clone(); async move { let result = crate::indexed_db::remove_event_record(&hash).await; set_state(Box::new(move |state: AppState| { @@ -149,7 +157,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { next.local_event_queue.last_error = Some(format!( "{}: {error:?}", i18n::tr( - &state, + &state, "Failed to cancel queued event", "キュー済みイベントのキャンセルに失敗しました", "Malsukcesis nuligi envicigitan eventon", @@ -169,7 +177,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { .set("padding", "0.3rem 0.6rem") .set("border-radius", "0.45rem"), ) - .children([text(i18n::tr(&state, "Cancel", "キャンセル", "Nuligi"))]) + .children([text(i18n::tr(state, "Cancel", "キャンセル", "Nuligi"))]) .into_node(), ); } @@ -193,11 +201,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { list_items.push( Div::new() .class("event-card") - .style( - Style::new() - .set("display", "grid") - .set("gap", "0.4rem"), - ) + .style(Style::new().set("display", "grid").set("gap", "0.4rem")) .children([ Div::new() .style( @@ -217,7 +221,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { .set("font-family", "'JetBrains Mono', monospace") .set("display", "inline-flex"), ) - .children([text(crate::hash_format::short_hash32(&hash))]) + .children([text(record.hash.to_string())]) .into_node(), ]) .into_node(), @@ -270,7 +274,7 @@ pub fn local_event_queue_view(state: &AppState) -> Node { .children([ H2::new() .children([text(i18n::tr( - &state, + state, "Local Events", "ローカルイベント", "Lokaj eventoj", @@ -302,7 +306,12 @@ pub fn local_event_queue_view(state: &AppState) -> Node { .set("color", "var(--text-secondary)") .set("font-size", "0.82rem"), ) - .children([text(i18n::tr(&state, "Loading...", "読み込み中...", "Ŝargado..."))]) + .children([text(i18n::tr( + state, + "Loading...", + "読み込み中...", + "Ŝargado...", + ))]) .into_node() } else if let Some(error) = &state.local_event_queue.last_error { Div::new() diff --git a/definy-ui/src/login_or_create_account_dialog.rs b/definy-ui/src/login_or_create_account_dialog.rs index 294a3f94..e1bdccc8 100644 --- a/definy-ui/src/login_or_create_account_dialog.rs +++ b/definy-ui/src/login_or_create_account_dialog.rs @@ -3,8 +3,7 @@ use narumincho_vdom::*; use crate::{ LoginOrCreateAccountDialogState, app_state::{AppState, CreatingAccountState}, - fetch, - i18n, + fetch, i18n, }; /// ログインまたはアカウント作成ダイアログ @@ -158,7 +157,12 @@ pub fn login_or_create_account_dialog(state: &AppState) -> Node { } })); })) - .children([text(i18n::tr(state, "Sign Up", "サインアップ", "Registriĝi"))]) + .children([text(i18n::tr( + state, + "Sign Up", + "サインアップ", + "Registriĝi", + ))]) .into_node(), ]) .into_node(), @@ -176,30 +180,17 @@ pub fn login_or_create_account_dialog(state: &AppState) -> Node { fn login_view(state: &AppState) -> Node { Form::new() .on_submit(EventHandler::new(async |set_state| { - let password = wasm_bindgen::JsCast::dyn_into::( - web_sys::window() - .unwrap() - .document() - .unwrap() - .query_selector("input[name='password']") - .unwrap() - .unwrap(), - ) - .unwrap() - .value(); + let password = crate::dom::get_input_value("input[name='password']"); - match crate::navigator_credential::parse_password(password) { - Some(signing_key) => { - dialog_close(); + if let Some(signing_key) = crate::navigator_credential::parse_password(password) { + dialog_close(); - set_state(Box::new(|state: AppState| -> AppState { - AppState { - current_key: Some(signing_key), - ..state.clone() - } - })); - } - None => {} + set_state(Box::new(|state: AppState| -> AppState { + AppState { + current_key: Some(signing_key), + ..state.clone() + } + })); } })) .style(Style::new().set("display", "grid").set("gap", "1.5rem")) @@ -266,17 +257,7 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node let set_state_for_async = set_state.clone(); let generated_key = generated_key_for_submit.clone(); async move { - let username = wasm_bindgen::JsCast::dyn_into::( - web_sys::window() - .unwrap() - .document() - .unwrap() - .query_selector("input[name='username']") - .unwrap() - .unwrap(), - ) - .unwrap() - .value(); + let username = crate::dom::get_input_value("input[name='username']"); if let Some(key) = &generated_key { let key = key.clone(); @@ -284,9 +265,7 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node wasm_bindgen_futures::spawn_local(async move { let event_binary = definy_event::sign_and_serialize( definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new( - key.verifying_key().to_bytes(), - )), + account_id: definy_event::event::AccountId(key.verifying_key()), time: chrono::Utc::now(), content: definy_event::event::EventContent::CreateAccount( definy_event::event::CreateAccountEvent { @@ -304,37 +283,30 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node let status = record.status.clone(); let status_for_state = status.clone(); let message = match status { - crate::local_event::LocalEventStatus::Sent => { - i18n::tr_lang( - lang_code, - "Account created", - "アカウントを作成しました", - "Konto kreita", - ) - .to_string() - } - crate::local_event::LocalEventStatus::Queued => { - i18n::tr_lang( - lang_code, - "Queued: network unavailable", - "キュー済み: ネットワーク未接続", - "En vico: reto nedisponebla", - ) - .to_string() - } + crate::local_event::LocalEventStatus::Sent => i18n::tr_lang( + lang_code, + "Account created", + "アカウントを作成しました", + "Konto kreita", + ) + .to_string(), + crate::local_event::LocalEventStatus::Queued => i18n::tr_lang( + lang_code, + "Queued: network unavailable", + "キュー済み: ネットワーク未接続", + "En vico: reto nedisponebla", + ) + .to_string(), crate::local_event::LocalEventStatus::Failed => { - record - .last_error - .clone() - .unwrap_or_else(|| { - i18n::tr_lang( - lang_code, - "Failed to send", - "送信に失敗しました", - "Sendado malsukcesis", - ) - .to_string() - }) + record.last_error.clone().unwrap_or_else(|| { + i18n::tr_lang( + lang_code, + "Failed to send", + "送信に失敗しました", + "Sendado malsukcesis", + ) + .to_string() + }) } }; set_state_for_async(Box::new(move |state: AppState| { @@ -381,12 +353,7 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node .class("form-group") .children([ Label::new() - .children([text(i18n::tr( - state, - "Username", - "ユーザー名", - "Uzantnomo", - ))]) + .children([text(i18n::tr(state, "Username", "ユーザー名", "Uzantnomo"))]) .into_node(), Input::new() .type_("text") @@ -420,7 +387,7 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node .set("word-break", "break-all"), ) .children(match &dialog_state.generated_key { - Some(key) => vec![text(&base64::Engine::encode( + Some(key) => vec![text(base64::Engine::encode( &base64::engine::general_purpose::URL_SAFE_NO_PAD, key.verifying_key().to_bytes(), ))], @@ -443,14 +410,12 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node Div::new() .class("hint") .style(Style::new().set("margin-bottom", "0.5rem")) - .children([text( - i18n::tr( - state, - "If you lose your secret key, you will not be able to log in again.", - "秘密鍵を失うと再ログインできません。", - "Se vi perdas la sekretan ŝlosilon, vi ne povos denove ensaluti.", - ), - )]) + .children([text(i18n::tr( + state, + "If you lose your secret key, you will not be able to log in again.", + "秘密鍵を失うと再ログインできません。", + "Se vi perdas la sekretan ŝlosilon, vi ne povos denove ensaluti.", + ))]) .into_node(), Div::new() .style(Style::new().set("display", "flex").set("gap", "0.5rem")) @@ -521,9 +486,12 @@ fn create_account_view(state: &AppState, force_offline: bool) -> Node CreatingAccountState::CreateAccount => { i18n::tr(state, "Sign Up", "サインアップ", "Registriĝi") } - CreatingAccountState::CreateAccountRequesting => { - i18n::tr(state, "Signing Up...", "サインアップ中...", "Registriĝante...") - } + CreatingAccountState::CreateAccountRequesting => i18n::tr( + state, + "Signing Up...", + "サインアップ中...", + "Registriĝante...", + ), CreatingAccountState::Success => { i18n::tr(state, "Success", "成功", "Sukceso") } diff --git a/definy-ui/src/module_detail.rs b/definy-ui/src/module_detail.rs index 7b819207..e925c199 100644 --- a/definy-ui/src/module_detail.rs +++ b/definy-ui/src/module_detail.rs @@ -1,40 +1,39 @@ +use definy_event::EventHashId; use narumincho_vdom::*; +use crate::Location; use crate::app_state::AppState; +use crate::i18n; use crate::module_projection::find_module_snapshot; use crate::part_projection::collect_part_snapshots; -use crate::Location; -use crate::i18n; -pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> Node { +pub fn module_detail_view(state: &AppState, definition_event_hash: &EventHashId) -> Node { let Some(module_snapshot) = find_module_snapshot(state, definition_event_hash) else { return Div::new() .class("page-shell") .style(crate::layout::page_shell_style("1rem")) - .children([ - H2::new() - .style(Style::new().set("font-size", "1.3rem")) - .children([text(i18n::tr( - &state, - "Module not found", - "モジュールが見つかりません", - "Modulo ne trovita", - ))]) - .into_node(), - ]) + .children([H2::new() + .style(Style::new().set("font-size", "1.3rem")) + .children([text(i18n::tr( + state, + "Module not found", + "モジュールが見つかりません", + "Modulo ne trovita", + ))]) + .into_node()]) .into_node(); }; let parts_in_module = collect_part_snapshots(state) .into_iter() - .filter(|snapshot| snapshot.module_definition_event_hash == Some(*definition_event_hash)) + .filter(|snapshot| { + snapshot.module_definition_event_hash == Some(definition_event_hash.clone()) + }) .collect::>(); let account_name_map = state.account_name_map(); - let author_name = crate::app_state::account_display_name( - &account_name_map, - &module_snapshot.account_id, - ); + let author_name = + crate::app_state::account_display_name(&account_name_map, &module_snapshot.account_id); let (initial_name, initial_description) = effective_module_update_form(state, definition_event_hash, Some(&module_snapshot)); @@ -69,7 +68,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> ) .children([text(format!( "{} {}", - i18n::tr(&state, "latest author:", "最新の投稿者:", "lasta aŭtoro:"), + i18n::tr(state, "latest author:", "最新の投稿者:", "lasta aŭtoro:"), author_name ))]) .into_node(), @@ -81,7 +80,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> module_snapshot.latest_event_hash, ))) .children([text(i18n::tr( - &state, + state, "Latest event", "最新イベント", "Lasta evento", @@ -92,7 +91,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> module_snapshot.definition_event_hash, ))) .children([text(i18n::tr( - &state, + state, "Definition event", "定義イベント", "Difina evento", @@ -103,7 +102,12 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> ]) .into_node(), if state.current_key.is_some() { - module_update_form(state, definition_event_hash, &initial_name, &initial_description) + module_update_form( + state, + definition_event_hash, + &initial_name, + &initial_description, + ) } else { Div::new() .class("event-detail-card") @@ -113,7 +117,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> .set("color", "var(--text-secondary)"), ) .children([text(i18n::tr( - &state, + state, "Login required to update modules.", "モジュール更新にはログインが必要です。", "Ensaluto necesas por ĝisdatigi modulojn.", @@ -123,7 +127,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> Div::new() .style(Style::new().set("margin-top", "1rem")) .children([text(i18n::tr( - &state, + state, "Parts in this module", "このモジュールのパーツ", "Partoj en ĉi tiu modulo", @@ -138,7 +142,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> .set("color", "var(--text-secondary)"), ) .children([text(i18n::tr( - &state, + state, "No parts in this module yet.", "このモジュールにはまだパーツがありません。", "Ankoraŭ neniuj partoj en ĉi tiu modulo.", @@ -201,7 +205,12 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> ) .children([text(format!( "{} {}", - i18n::tr(&state, "latest author:", "最新の投稿者:", "lasta aŭtoro:"), + i18n::tr( + state, + "latest author:", + "最新の投稿者:", + "lasta aŭtoro:" + ), part_author ))]) .into_node(), @@ -217,7 +226,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> part.definition_event_hash, ))) .children([text(i18n::tr( - &state, + state, "Open part detail", "パーツ詳細を開く", "Malfermi partajn detalojn", @@ -228,7 +237,7 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> part.latest_event_hash, ))) .children([text(i18n::tr( - &state, + state, "Latest event", "最新イベント", "Lasta evento", @@ -249,11 +258,13 @@ pub fn module_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> fn module_update_form( state: &AppState, - definition_event_hash: &[u8; 32], + definition_event_hash: &EventHashId, initial_name: &str, initial_description: &str, ) -> Node { - let root_module_definition_hash = *definition_event_hash; + let definition_event_hash_name = definition_event_hash.clone(); + let definition_event_hash_description = definition_event_hash.clone(); + let definition_event_hash_send_button = definition_event_hash.clone(); Div::new() .class("event-detail-card") @@ -267,7 +278,7 @@ fn module_update_form( Div::new() .style(Style::new().set("font-weight", "600")) .children([text(i18n::tr( - &state, + state, "Update module", "モジュールを更新", "Ĝisdatigi modulon", @@ -278,22 +289,9 @@ fn module_update_form( .name("module-update-name") .value(initial_name) .on_change(EventHandler::new(move |set_state| { - let root_module_definition_hash = root_module_definition_hash; + let root_module_definition_hash = definition_event_hash_name.clone(); async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document - .query_selector("input[name='module-update-name']") - .ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element) - .ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value("input[name='module-update-name']"); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); next.module_update_form.module_definition_event_hash = @@ -304,42 +302,20 @@ fn module_update_form( } })) .into_node(), - { - let mut description = Textarea::new() + Textarea::new() .name("module-update-description") .value(initial_description) - .style(Style::new().set("min-height", "5rem")); - description.attributes.push(( - "placeholder".to_string(), - i18n::tr( - &state, + .style(Style::new().set("min-height", "5rem")) + .attribute("placeholder", i18n::tr( + state, "module description (supports multiple lines)", "モジュール説明 (複数行対応)", "modula priskribo (subtenas plurajn liniojn)", - ) - .to_string(), - )); - description.events.push(( - "input".to_string(), - EventHandler::new(move |set_state| { - let root_module_definition_hash = root_module_definition_hash; + )) + .on_input(EventHandler::new(move |set_state| { + let root_module_definition_hash = definition_event_hash_description.clone(); async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document - .query_selector("textarea[name='module-update-description']") - .ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::( - element, - ) - .ok() - }) - .map(|textarea| textarea.value()) - .unwrap_or_default(); + let value = crate::dom::get_textarea_value("textarea[name='module-update-description']"); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); next.module_update_form.module_definition_event_hash = @@ -348,13 +324,12 @@ fn module_update_form( next })); } - }), - )); - description.into_node() - }, + })).into_node(), Button::new() .type_("button") - .on_click(EventHandler::new(move |set_state| async move { + .on_click(EventHandler::new(move |set_state| { + let root_module_definition_hash = definition_event_hash_send_button.clone(); + async move { let set_state = std::rc::Rc::new(set_state); let set_state_for_async = set_state.clone(); set_state(Box::new(move |state: AppState| { @@ -372,7 +347,7 @@ fn module_update_form( return next; }; let (module_name, module_description) = - effective_module_update_form(&state, &root_module_definition_hash, None); + effective_module_update_form(&state, &root_module_definition_hash.clone(), None); let module_name = module_name.trim().to_string(); if module_name.is_empty() { let mut next = state.clone(); @@ -385,21 +360,17 @@ fn module_update_form( ).to_string()); return next; } - let module_description = module_description; let force_offline = state.force_offline; wasm_bindgen_futures::spawn_local(async move { let event_binary = match definy_event::sign_and_serialize( definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new( - key.verifying_key().to_bytes(), - )), + account_id: definy_event::event::AccountId(key.verifying_key()), time: chrono::Utc::now(), content: definy_event::event::EventContent::ModuleUpdate( definy_event::event::ModuleUpdateEvent { module_name: module_name.into(), module_description: module_description.into(), - module_definition_event_hash: - root_module_definition_hash, + module_definition_event_hash: root_module_definition_hash.clone(), }, ), }, @@ -438,23 +409,8 @@ fn module_update_form( crate::fetch::get_events(None, Some(20), Some(0)).await { set_state_for_async(Box::new(move |state| { - let events_len = events.len(); - let mut event_cache = state.event_cache.clone(); - let mut event_hashes = Vec::new(); - for (hash, event) in events { - event_cache.insert(hash, event); - event_hashes.push(hash); - } let mut next = state.clone(); - next.event_cache = event_cache; - next.event_list_state = crate::EventListState { - event_hashes, - current_offset: 0, - page_size: 20, - is_loading: false, - has_more: events_len == 20, - filter_event_type: None, - }; + next.apply_latest_events(events, None); crate::app_state::upsert_local_event_record( &mut next, record, @@ -552,9 +508,9 @@ fn module_update_form( }); state })); - })) + }})) .children([text(i18n::tr( - &state, + state, "Send ModuleUpdate", "ModuleUpdate を送信", "Sendi ModuleUpdate", @@ -578,10 +534,12 @@ fn module_update_form( fn effective_module_update_form( state: &AppState, - definition_event_hash: &[u8; 32], + definition_event_hash: &EventHashId, snapshot: Option<&crate::module_projection::ModuleSnapshot>, ) -> (String, String) { - if state.module_update_form.module_definition_event_hash == Some(*definition_event_hash) { + if let Some(hash) = &state.module_update_form.module_definition_event_hash + && hash == definition_event_hash + { return ( state.module_update_form.module_name_input.clone(), state.module_update_form.module_description_input.clone(), diff --git a/definy-ui/src/module_list.rs b/definy-ui/src/module_list.rs index 90d48303..152c0bdc 100644 --- a/definy-ui/src/module_list.rs +++ b/definy-ui/src/module_list.rs @@ -1,5 +1,5 @@ +use definy_event::EventHashId; use narumincho_vdom::*; -use sha2::Digest; use crate::app_state::AppState; use crate::i18n; @@ -21,7 +21,7 @@ pub fn module_list_view(state: &AppState) -> Node { .set("color", "var(--text-secondary)"), ) .children([text(i18n::tr( - &state, + state, "Login required to create modules.", "モジュール作成にはログインが必要です。", "Ensaluto necesas por krei modulojn.", @@ -38,7 +38,7 @@ pub fn module_list_view(state: &AppState) -> Node { children.push( H2::new() .style(Style::new().set("font-size", "1.3rem")) - .children([text(i18n::tr(&state, "Modules", "モジュール", "Moduloj"))]) + .children([text(i18n::tr(state, "Modules", "モジュール", "Moduloj"))]) .into_node(), ); if let Some(form) = create_form { @@ -69,7 +69,7 @@ pub fn module_list_view(state: &AppState) -> Node { .set("color", "var(--text-secondary)"), ) .children([text(i18n::tr( - &state, + state, "No modules yet.", "まだモジュールがありません。", "Ankoraŭ neniuj moduloj.", @@ -125,7 +125,7 @@ pub fn module_list_view(state: &AppState) -> Node { .set("color", "var(--text-secondary)"), ) .children([text(i18n::tr( - &state, + state, "definition event missing", "定義イベントが見つかりません", "difina evento mankas", @@ -163,13 +163,17 @@ pub fn module_list_view(state: &AppState) -> Node { ) .children([ A::::new() - .href(state.href_with_lang( - crate::Location::Module( - module.definition_event_hash, + .href( + state.href_with_lang( + crate::Location::Module( + module + .definition_event_hash + .clone(), + ), ), - )) + ) .children([text(i18n::tr( - &state, + state, "Open module detail", "モジュール詳細を開く", "Malfermi modulajn detalojn", @@ -182,7 +186,7 @@ pub fn module_list_view(state: &AppState) -> Node { ), )) .children([text(i18n::tr( - &state, + state, "Latest event", "最新イベント", "Lasta evento", @@ -195,7 +199,7 @@ pub fn module_list_view(state: &AppState) -> Node { ), )) .children([text(i18n::tr( - &state, + state, "Definition event", "定義イベント", "Difina evento", @@ -224,7 +228,7 @@ fn module_create_form(state: &AppState) -> Node { Div::new() .style(Style::new().set("font-size", "0.9rem")) .children([text(i18n::tr( - &state, + state, "Create module", "モジュールを作成", "Krei modulon", @@ -266,9 +270,9 @@ fn module_create_form(state: &AppState) -> Node { wasm_bindgen_futures::spawn_local(async move { let event_binary = definy_event::sign_and_serialize( definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new( - key_for_async.verifying_key().to_bytes(), - )), + account_id: definy_event::event::AccountId( + key_for_async.verifying_key(), + ), time: chrono::Utc::now(), content: definy_event::event::EventContent::ModuleDefinition( definy_event::event::ModuleDefinitionEvent { @@ -296,13 +300,10 @@ fn module_create_form(state: &AppState) -> Node { record, ); if status == crate::local_event::LocalEventStatus::Sent { - let hash: [u8; 32] = - ::digest(&event_binary) - .into(); let event = definy_event::verify_and_deserialize( event_binary.as_slice(), ); - next.event_cache.insert(hash, event); + next.event_cache.insert(EventHashId::from_bytes(&event_binary), event); next.module_definition_form.result_message = None; } else { next.module_definition_form.result_message = Some( @@ -365,7 +366,7 @@ fn module_create_form(state: &AppState) -> Node { next })); })) - .children([text(i18n::tr(&state, "Create", "作成", "Krei"))]) + .children([text(i18n::tr(state, "Create", "作成", "Krei"))]) .into_node(), ]) .into_node() @@ -376,24 +377,14 @@ fn module_name_input(state: &AppState) -> Node { .name("module-name") .type_("text") .value(&state.module_definition_form.module_name_input); - input - .attributes - .push(( - "placeholder".to_string(), - i18n::tr(&state, "module name", "モジュール名", "modula nomo").to_string(), - )); + input.attributes.push(( + "placeholder".to_string(), + i18n::tr(state, "module name", "モジュール名", "modula nomo").to_string(), + )); input.events.push(( "input".to_string(), EventHandler::new(move |set_state| async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| document.query_selector("input[name='module-name']").ok()) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); + let value = crate::dom::get_input_value("input[name='module-name']"); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); next.module_definition_form.module_name_input = value; @@ -412,7 +403,7 @@ fn module_description_input(state: &AppState) -> Node { textarea.attributes.push(( "placeholder".to_string(), i18n::tr( - &state, + state, "description (optional)", "説明 (任意)", "priskribo (nedeviga)", @@ -422,19 +413,7 @@ fn module_description_input(state: &AppState) -> Node { textarea.events.push(( "input".to_string(), EventHandler::new(move |set_state| async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document - .query_selector("textarea[name='module-description']") - .ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element).ok() - }) - .map(|textarea| textarea.value()) - .unwrap_or_default(); + let value = crate::dom::get_textarea_value("textarea[name='module-description']"); set_state(Box::new(move |state: AppState| { let mut next = state.clone(); next.module_definition_form.module_description_input = value; diff --git a/definy-ui/src/module_projection.rs b/definy-ui/src/module_projection.rs index 92bc35f1..20f2f3f0 100644 --- a/definy-ui/src/module_projection.rs +++ b/definy-ui/src/module_projection.rs @@ -1,11 +1,14 @@ -use definy_event::event::{AccountId, Event, EventContent}; +use definy_event::{ + EventHashId, + event::{AccountId, Event, EventContent}, +}; use crate::AppState; #[derive(Clone)] pub struct ModuleSnapshot { - pub definition_event_hash: [u8; 32], - pub latest_event_hash: [u8; 32], + pub definition_event_hash: EventHashId, + pub latest_event_hash: EventHashId, pub account_id: AccountId, pub module_name: String, pub module_description: String, @@ -19,19 +22,19 @@ pub fn collect_module_snapshots(state: &AppState) -> Vec { .iter() .filter_map(|(hash, event_result)| { let (_, event) = event_result.as_ref().ok()?; - Some((*hash, event.clone())) + Some((hash.clone(), event.clone())) }) - .collect::>(); + .collect::>(); events.sort_by_key(|(_, event)| event.time); - let mut map = std::collections::HashMap::<[u8; 32], ModuleSnapshot>::new(); + let mut map = std::collections::HashMap::::new(); for (event_hash, event) in events { match &event.content { EventContent::ModuleDefinition(module_definition) => { map.insert( - event_hash, + event_hash.clone(), ModuleSnapshot { - definition_event_hash: event_hash, + definition_event_hash: event_hash.clone(), latest_event_hash: event_hash, account_id: event.account_id.clone(), module_name: module_definition.module_name.to_string(), @@ -43,17 +46,17 @@ pub fn collect_module_snapshots(state: &AppState) -> Vec { } EventContent::ModuleUpdate(module_update) => { let entry = map - .entry(module_update.module_definition_event_hash) + .entry(module_update.module_definition_event_hash.clone()) .or_insert_with(|| ModuleSnapshot { - definition_event_hash: module_update.module_definition_event_hash, - latest_event_hash: event_hash, + definition_event_hash: module_update.module_definition_event_hash.clone(), + latest_event_hash: event_hash.clone(), account_id: event.account_id.clone(), module_name: String::new(), module_description: String::new(), updated_at: event.time, has_definition: false, }); - entry.latest_event_hash = event_hash; + entry.latest_event_hash = event_hash.clone(); entry.account_id = event.account_id.clone(); entry.module_name = module_update.module_name.to_string(); entry.module_description = module_update.module_description.to_string(); @@ -70,7 +73,7 @@ pub fn collect_module_snapshots(state: &AppState) -> Vec { pub fn find_module_snapshot( state: &AppState, - definition_event_hash: &[u8; 32], + definition_event_hash: &EventHashId, ) -> Option { collect_module_snapshots(state) .into_iter() @@ -79,23 +82,23 @@ pub fn find_module_snapshot( pub fn resolve_module_name( state: &AppState, - definition_event_hash: &[u8; 32], + definition_event_hash: &EventHashId, ) -> Option { let mut events = state .event_cache .iter() .filter_map(|(hash, event_result)| { let (_, event) = event_result.as_ref().ok()?; - Some((*hash, event)) + Some((hash, event)) }) - .collect::>(); + .collect::>(); events.sort_by_key(|(_, event)| event.time); let mut name = None::; for (hash, event) in events { match &event.content { definy_event::event::EventContent::ModuleDefinition(module_definition) - if &hash == definition_event_hash => + if hash == definition_event_hash => { name = Some(module_definition.module_name.to_string()); } diff --git a/definy-ui/src/page_title.rs b/definy-ui/src/page_title.rs index bbf86ec7..f452c9ca 100644 --- a/definy-ui/src/page_title.rs +++ b/definy-ui/src/page_title.rs @@ -1,5 +1,7 @@ -use crate::{AppState, Location}; +use definy_event::EventHashId; + use crate::i18n; +use crate::{AppState, Location}; #[derive(Clone, Copy)] enum RouteId { @@ -58,9 +60,7 @@ pub fn page_title_text(state: &AppState) -> String { | Some(Location::PartList) | Some(Location::ModuleList) | Some(Location::LocalEventQueue) - | None => { - route_id.title_prefix(state).to_string() - } + | None => route_id.title_prefix(state).to_string(), Some(Location::Account(account_id)) => { let account_name = crate::app_state::account_display_name(&state.account_name_map(), account_id); @@ -68,15 +68,13 @@ pub fn page_title_text(state: &AppState) -> String { } Some(Location::Part(definition_event_hash)) => { let part_name = resolve_part_name(state, definition_event_hash) - .unwrap_or_else(|| short_hash(definition_event_hash)); + .unwrap_or_else(|| definition_event_hash.to_string()); format!("{}/{}", route_id.title_prefix(state), part_name) } Some(Location::Module(definition_event_hash)) => { - let module_name = crate::module_projection::resolve_module_name( - state, - definition_event_hash, - ) - .unwrap_or_else(|| short_hash(definition_event_hash)); + let module_name = + crate::module_projection::resolve_module_name(state, definition_event_hash) + .unwrap_or_else(|| definition_event_hash.to_string()); format!("{}/{}", route_id.title_prefix(state), module_name) } Some(Location::Event(event_hash)) => { @@ -138,7 +136,7 @@ pub fn page_title_text(state: &AppState) -> String { }; Some(label) }) - .unwrap_or_else(|| short_hash(event_hash)); + .unwrap_or_else(|| event_hash.to_string()); format!("{}/{}", route_id.title_prefix(state), event_label) } } @@ -148,15 +146,15 @@ pub fn document_title_text(state: &AppState) -> String { format!("{} | definy", page_title_text(state)) } -fn resolve_part_name(state: &AppState, definition_event_hash: &[u8; 32]) -> Option { +fn resolve_part_name(state: &AppState, definition_event_hash: &EventHashId) -> Option { let mut events = state .event_cache .iter() .filter_map(|(hash, event_result)| { let (_, event) = event_result.as_ref().ok()?; - Some((*hash, event)) + Some((hash.clone(), event)) }) - .collect::>(); + .collect::>(); events.sort_by_key(|(_, event)| event.time); let mut name = None::; @@ -179,7 +177,3 @@ fn resolve_part_name(state: &AppState, definition_event_hash: &[u8; 32]) -> Opti } name } - -fn short_hash(hash: &[u8; 32]) -> String { - crate::hash_format::short_hash32(hash) -} diff --git a/definy-ui/src/part_detail.rs b/definy-ui/src/part_detail.rs index 54d2cb06..cd0f5456 100644 --- a/definy-ui/src/part_detail.rs +++ b/definy-ui/src/part_detail.rs @@ -1,3 +1,6 @@ +use std::str::FromStr; + +use definy_event::EventHashId; use narumincho_vdom::*; use crate::Location; @@ -8,7 +11,7 @@ use crate::i18n; use crate::module_projection::collect_module_snapshots; use crate::part_projection::{collect_related_part_events, find_part_snapshot}; -pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> Node { +pub fn part_detail_view(state: &AppState, definition_event_hash: &EventHashId) -> Node { let snapshot = find_part_snapshot(state, definition_event_hash); let related_events = collect_related_part_events(state, definition_event_hash); @@ -20,7 +23,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N A::::new() .href(state.href_with_lang(Location::PartList)) .children([text(i18n::tr( - &state, + state, "← Back to Parts", "← パーツ一覧へ戻る", "← Reen al partoj", @@ -47,7 +50,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N ) .children([text(format!( "{} {}", - i18n::tr(&state, "Updated at:", "更新日時:", "Ĝisdatigita je:"), + i18n::tr(state, "Updated at:", "更新日時:", "Ĝisdatigita je:"), snapshot.updated_at.format("%Y-%m-%d %H:%M:%S") ))]) .into_node(), @@ -55,7 +58,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N Div::new() .style(Style::new().set("color", "var(--text-secondary)")) .children([text(i18n::tr( - &state, + state, "(no description)", "(説明なし)", "(sen priskribo)", @@ -76,7 +79,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N ) .children([text(format!( "{} {}", - i18n::tr(&state, "expression:", "式:", "esprimo:"), + i18n::tr(state, "expression:", "式:", "esprimo:"), expression_to_source(&snapshot.expression) ))]) .into_node(), @@ -84,13 +87,11 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N .style(Style::new().set("display", "flex").set("gap", "0.6rem")) .children([ A::::new() - .href( - state.href_with_lang(Location::Event( - *definition_event_hash, - )), - ) + .href(state.href_with_lang(Location::Event( + definition_event_hash.clone(), + ))) .children([text(i18n::tr( - &state, + state, "Definition event", "定義イベント", "Difina evento", @@ -101,7 +102,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N snapshot.latest_event_hash, ))) .children([text(i18n::tr( - &state, + state, "Latest event", "最新イベント", "Lasta evento", @@ -123,7 +124,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N .children([ Div::new() .style(Style::new().set("font-weight", "600")) - .children([text(i18n::tr(&state, "History", "履歴", "Historio"))]) + .children([text(i18n::tr(state, "History", "履歴", "Historio"))]) .into_node(), Div::new() .style(Style::new().set("display", "grid").set("gap", "0.4rem")) @@ -171,7 +172,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N A::::new() .href(state.href_with_lang(Location::PartList)) .children([text(i18n::tr( - &state, + state, "← Back to Parts", "← パーツ一覧へ戻る", "← Reen al partoj", @@ -180,7 +181,7 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N Div::new() .style(Style::new().set("color", "var(--text-secondary)")) .children([text(i18n::tr( - &state, + state, "Part not found", "パーツが見つかりません", "Parto ne trovita", @@ -191,24 +192,23 @@ pub fn part_detail_view(state: &AppState, definition_event_hash: &[u8; 32]) -> N .into_node() } -fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node { - let root_part_definition_hash = *definition_event_hash; - let hash_as_base64 = crate::hash_format::encode_hash32(definition_event_hash); +fn part_update_form(state: &AppState, definition_event_hash: &EventHashId) -> Node { + let hash_as_base64 = definition_event_hash.to_string(); let (initial_name, initial_description, initial_expression, initial_module_hash) = effective_part_update_form(state, definition_event_hash); let dropdown_name = format!("part-update-module-{}", hash_as_base64); let mut module_options = vec![( "".to_string(), - i18n::tr(&state, "No module", "モジュールなし", "Neniu modulo").to_string(), + i18n::tr(state, "No module", "モジュールなし", "Neniu modulo").to_string(), )]; - module_options.extend(collect_module_snapshots(state).into_iter().map(|module| { - ( - crate::hash_format::encode_hash32(&module.definition_event_hash), - module.module_name, - ) - })); + + module_options.extend( + collect_module_snapshots(state) + .into_iter() + .map(|module| (module.definition_event_hash.to_string(), module.module_name)), + ); let current_module_value = initial_module_hash - .map(|hash| crate::hash_format::encode_hash32(&hash)) + .map(|hash| hash.to_string()) .unwrap_or_else(|| "".to_string()); Div::new() @@ -223,7 +223,7 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< Div::new() .style(Style::new().set("font-weight", "600")) .children([text(i18n::tr( - &state, + state, "Create PartUpdate event", "PartUpdate イベントを作成", "Krei PartUpdate eventon", @@ -240,7 +240,7 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< .children([text(format!( "{} {}", i18n::tr( - &state, + state, "partDefinitionEventHash:", "partDefinitionEventHash:", "partDefinitionEventHash:" @@ -252,32 +252,22 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< .type_("text") .name("part-update-name") .value(initial_name.as_str()) - .on_change(EventHandler::new(move |set_state| { - let root_part_definition_hash = root_part_definition_hash; - async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document - .query_selector("input[name='part-update-name']") - .ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::(element) - .ok() - }) - .map(|input| input.value()) - .unwrap_or_default(); - set_state(Box::new(move |state: AppState| { - let mut next = state.clone(); - next.part_update_form.part_definition_event_hash = - Some(root_part_definition_hash); - next.part_update_form.part_name_input = value; - next - })); - } - })) + .on_change({ + let definition_event_hash = definition_event_hash.clone(); + EventHandler::new(move |set_state| { + let definition_event_hash = definition_event_hash.clone(); + async move { + let value = crate::dom::get_input_value("input[name='part-update-name']"); + set_state(Box::new(move |state: AppState| { + let mut next = state.clone(); + next.part_update_form.part_definition_event_hash = + Some(definition_event_hash.clone()); + next.part_update_form.part_name_input = value; + next + })); + } + }) + }) .into_node(), Div::new() .style(Style::new().set("display", "grid").set("gap", "0.35rem")) @@ -288,69 +278,51 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< .set("font-size", "0.85rem") .set("color", "var(--text-secondary)"), ) - .children([text(i18n::tr(&state, "Module", "モジュール", "Modulo"))]) + .children([text(i18n::tr(state, "Module", "モジュール", "Modulo"))]) .into_node(), crate::dropdown::searchable_dropdown( state, dropdown_name.as_str(), ¤t_module_value, &module_options, - std::rc::Rc::new(move |value| { - let root_part_definition_hash = root_part_definition_hash; - Box::new(move |state: AppState| { - let mut next = state.clone(); - next.part_update_form.part_definition_event_hash = - Some(root_part_definition_hash); - next.part_update_form.module_definition_event_hash = - crate::hash_format::decode_hash32(&value); - next - }) + std::rc::Rc::new({ + let definition_event_hash = definition_event_hash.clone(); + move |value| { + let definition_event_hash = definition_event_hash.clone(); + Box::new(move |state: AppState| { + let mut next = state.clone(); + next.part_update_form.part_definition_event_hash = + Some(definition_event_hash.clone()); + next.part_update_form.module_definition_event_hash = + definy_event::EventHashId::from_str(&value).ok(); + next + }) + } }), ), ]) .into_node(), - { - let mut description = Textarea::new() - .name("part-update-description") - .value(initial_description.as_str()) - .style(Style::new().set("min-height", "5rem")); - description.attributes.push(( - "placeholder".to_string(), - "part description (supports multiple lines)".to_string(), - )); - description.events.push(( - "input".to_string(), + Textarea::new() + .name("part-update-description") + .value(initial_description.as_str()) + .style(Style::new().set("min-height", "5rem")) + .attribute("placeholder", "part description (supports multiple lines)") + .on_input({ + let definition_event_hash = definition_event_hash.clone(); EventHandler::new(move |set_state| { - let root_part_definition_hash = root_part_definition_hash; + let definition_event_hash = definition_event_hash.clone(); async move { - let value = web_sys::window() - .and_then(|window| window.document()) - .and_then(|document| { - document - .query_selector("textarea[name='part-update-description']") - .ok() - }) - .flatten() - .and_then(|element| { - wasm_bindgen::JsCast::dyn_into::( - element, - ) - .ok() - }) - .map(|textarea| textarea.value()) - .unwrap_or_default(); - set_state(Box::new(move |state: AppState| { - let mut next = state.clone(); - next.part_update_form.part_definition_event_hash = - Some(root_part_definition_hash); - next.part_update_form.part_description_input = value; - next - })); - } - }), - )); - description.into_node() - }, + let value = crate::dom::get_textarea_value("textarea[name='part-update-description']"); + set_state(Box::new(move |state: AppState| { + let mut next = state.clone(); + next.part_update_form.part_definition_event_hash = + Some(definition_event_hash.clone()); + next.part_update_form.part_description_input = value; + next + })); + } + }) + }).into_node(), Div::new() .style( Style::new() @@ -358,7 +330,7 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< .set("font-size", "0.9rem"), ) .children([text(i18n::tr( - &state, + state, "Expression Builder", "式ビルダー", "Esprimo-konstruilo", @@ -375,17 +347,20 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< ) .children([text(format!( "{} {}", - i18n::tr(&state, "Current:", "現在:", "Nuna:"), + i18n::tr(state, "Current:", "現在:", "Nuna:"), expression_to_source(&initial_expression) ))]) .into_node(), - { - Button::new() - .type_("button") - .on_click(EventHandler::new(move |set_state| async move { - let set_state = std::rc::Rc::new(set_state); - let set_state_for_async = set_state.clone(); - set_state(Box::new(move |state: AppState| { + Button::new() + .type_("button") + .on_click({ + let definition_event_hash = definition_event_hash.clone(); + EventHandler::new(move |set_state| { + let definition_event_hash = definition_event_hash.clone(); + async move { + let set_state = std::rc::Rc::new(set_state); + let set_state_for_async = set_state.clone(); + set_state(Box::new(move |state: AppState| { let key = if let Some(key) = &state.current_key { key.clone() } else { @@ -408,13 +383,13 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< current_expression, current_module_hash, ) = - effective_part_update_form(&state, &root_part_definition_hash); + effective_part_update_form(&state, &definition_event_hash); let part_name = current_part_name.trim().to_string(); if part_name.is_empty() { return AppState { event_detail_eval_result: Some( i18n::tr_lang( - &state.language.code, + state.language.code, "Error: part name is required", "エラー: パーツ名は必須です", "Eraro: parto-nomo estas bezonata", @@ -431,16 +406,14 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< wasm_bindgen_futures::spawn_local(async move { let event_binary = match definy_event::sign_and_serialize( definy_event::event::Event { - account_id: definy_event::event::AccountId(Box::new( - key.verifying_key().to_bytes(), - )), + account_id: definy_event::event::AccountId(key + .verifying_key()), time: chrono::Utc::now(), content: definy_event::event::EventContent::PartUpdate( definy_event::event::PartUpdateEvent { part_name: part_name.into(), part_description: part_description.into(), - part_definition_event_hash: - root_part_definition_hash, + part_definition_event_hash: definition_event_hash.clone(), expression, module_definition_event_hash, }, @@ -480,34 +453,19 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< crate::fetch::get_events(None, Some(20), Some(0)).await { set_state_for_async(Box::new(move |state| { - let events_len = events.len(); - let mut event_cache = state.event_cache.clone(); - let mut event_hashes = Vec::new(); - for (hash, event) in events { - event_cache.insert(hash, event); - event_hashes.push(hash); - } let mut next = state.clone(); - next.event_cache = event_cache; - next.event_list_state = crate::EventListState { - event_hashes, - current_offset: 0, - page_size: 20, - is_loading: false, - has_more: events_len == 20, - filter_event_type: None, - }; + next.apply_latest_events(events, None); crate::app_state::upsert_local_event_record( &mut next, record, ); if let Some(snapshot) = find_part_snapshot( &next, - &root_part_definition_hash, + &definition_event_hash, ) { next.part_update_form .part_definition_event_hash = - Some(root_part_definition_hash); + Some(definition_event_hash.clone()); next.part_update_form.part_name_input = snapshot.part_name; next.part_update_form @@ -604,15 +562,17 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< }); state })); - })) - .children([text(i18n::tr( - &state, + } + }) + }) + .children([text(i18n::tr( + state, "Send PartUpdate", "PartUpdate を送信", "Sendi PartUpdate", ))]) .into_node() - }, + , match &state.event_detail_eval_result { Some(result) => Div::new() .class("mono") @@ -631,19 +591,19 @@ fn part_update_form(state: &AppState, definition_event_hash: &[u8; 32]) -> Node< fn effective_part_update_form( state: &AppState, - definition_event_hash: &[u8; 32], + definition_event_hash: &EventHashId, ) -> ( String, String, definy_event::event::Expression, - Option<[u8; 32]>, + Option, ) { - if state.part_update_form.part_definition_event_hash == Some(*definition_event_hash) { + if state.part_update_form.part_definition_event_hash == Some(definition_event_hash.clone()) { return ( state.part_update_form.part_name_input.clone(), state.part_update_form.part_description_input.clone(), state.part_update_form.expression_input.clone(), - state.part_update_form.module_definition_event_hash, + state.part_update_form.module_definition_event_hash.clone(), ); } if let Some(snapshot) = find_part_snapshot(state, definition_event_hash) { @@ -658,6 +618,6 @@ fn effective_part_update_form( state.part_update_form.part_name_input.clone(), state.part_update_form.part_description_input.clone(), state.part_update_form.expression_input.clone(), - state.part_update_form.module_definition_event_hash, + state.part_update_form.module_definition_event_hash.clone(), ) } diff --git a/definy-ui/src/part_list.rs b/definy-ui/src/part_list.rs index 7f5d1ec3..074ade19 100644 --- a/definy-ui/src/part_list.rs +++ b/definy-ui/src/part_list.rs @@ -1,9 +1,9 @@ use narumincho_vdom::*; use crate::Location; -use crate::i18n; use crate::app_state::AppState; use crate::expression_eval::expression_to_source; +use crate::i18n; use crate::part_projection::collect_part_snapshots; fn part_type_text(part_type: &definy_event::event::PartType) -> String { @@ -12,9 +12,7 @@ fn part_type_text(part_type: &definy_event::event::PartType) -> String { definy_event::event::PartType::String => "String".to_string(), definy_event::event::PartType::Boolean => "Boolean".to_string(), definy_event::event::PartType::Type => "Type".to_string(), - definy_event::event::PartType::TypePart(hash) => { - format!("TypePart({})", crate::hash_format::short_hash32(hash)) - } + definy_event::event::PartType::TypePart(hash) => format!("TypePart({})", hash), definy_event::event::PartType::List(item_type) => { format!("list<{}>", part_type_text(item_type.as_ref())) } @@ -123,7 +121,7 @@ pub fn part_list_view(state: &AppState) -> Node { }, A::::new() .href(state.href_with_lang(Location::Part( - part.definition_event_hash, + part.definition_event_hash.clone(), ))) .children([text(i18n::tr( state, @@ -165,7 +163,12 @@ pub fn part_list_view(state: &AppState) -> Node { ) .children([text(format!( "{} {}", - i18n::tr(state, "latest author:", "最新の投稿者:", "lasta aŭtoro:"), + i18n::tr( + state, + "latest author:", + "最新の投稿者:", + "lasta aŭtoro:" + ), account_name ))]) .into_node(), diff --git a/definy-ui/src/part_projection.rs b/definy-ui/src/part_projection.rs index 6cd18fb5..0955f6e6 100644 --- a/definy-ui/src/part_projection.rs +++ b/definy-ui/src/part_projection.rs @@ -1,17 +1,20 @@ -use definy_event::event::{AccountId, Event, EventContent, Expression}; +use definy_event::{ + EventHashId, + event::{AccountId, Event, EventContent, Expression}, +}; use crate::AppState; #[derive(Clone)] pub struct PartSnapshot { - pub definition_event_hash: [u8; 32], - pub latest_event_hash: [u8; 32], + pub definition_event_hash: EventHashId, + pub latest_event_hash: EventHashId, pub account_id: AccountId, pub part_name: String, pub part_type: Option, pub part_description: String, pub expression: Expression, - pub module_definition_event_hash: Option<[u8; 32]>, + pub module_definition_event_hash: Option, pub updated_at: chrono::DateTime, pub has_definition: bool, } @@ -22,26 +25,28 @@ pub fn collect_part_snapshots(state: &AppState) -> Vec { .iter() .filter_map(|(hash, event_result)| { let (_, event) = event_result.as_ref().ok()?; - Some((*hash, event.clone())) + Some((hash.clone(), event.clone())) }) - .collect::>(); + .collect::>(); events.sort_by_key(|(_, event)| event.time); - let mut map = std::collections::HashMap::<[u8; 32], PartSnapshot>::new(); + let mut map = std::collections::HashMap::::new(); for (event_hash, event) in events { match &event.content { EventContent::PartDefinition(part_definition) => { map.insert( - event_hash, + event_hash.clone(), PartSnapshot { - definition_event_hash: event_hash, + definition_event_hash: event_hash.clone(), latest_event_hash: event_hash, account_id: event.account_id.clone(), part_name: part_definition.part_name.to_string(), part_type: part_definition.part_type.clone(), part_description: part_definition.description.to_string(), expression: part_definition.expression.clone(), - module_definition_event_hash: part_definition.module_definition_event_hash, + module_definition_event_hash: part_definition + .module_definition_event_hash + .clone(), updated_at: event.time, has_definition: true, }, @@ -49,26 +54,29 @@ pub fn collect_part_snapshots(state: &AppState) -> Vec { } EventContent::PartUpdate(part_update) => { let entry = map - .entry(part_update.part_definition_event_hash) + .entry(part_update.part_definition_event_hash.clone()) .or_insert_with(|| PartSnapshot { - definition_event_hash: part_update.part_definition_event_hash, - latest_event_hash: event_hash, + definition_event_hash: part_update.part_definition_event_hash.clone(), + latest_event_hash: event_hash.clone(), account_id: event.account_id.clone(), part_name: String::new(), part_type: None, part_description: String::new(), expression: part_update.expression.clone(), - module_definition_event_hash: part_update.module_definition_event_hash, + module_definition_event_hash: part_update + .module_definition_event_hash + .clone(), updated_at: event.time, has_definition: false, }); - entry.latest_event_hash = event_hash; + entry.latest_event_hash = event_hash.clone(); entry.account_id = event.account_id.clone(); entry.part_name = part_update.part_name.to_string(); entry.part_description = part_update.part_description.to_string(); entry.expression = part_update.expression.clone(); if part_update.module_definition_event_hash.is_some() { - entry.module_definition_event_hash = part_update.module_definition_event_hash; + entry.module_definition_event_hash = + part_update.module_definition_event_hash.clone(); } entry.updated_at = event.time; } @@ -83,7 +91,7 @@ pub fn collect_part_snapshots(state: &AppState) -> Vec { pub fn find_part_snapshot( state: &AppState, - definition_event_hash: &[u8; 32], + definition_event_hash: &EventHashId, ) -> Option { collect_part_snapshots(state) .into_iter() @@ -92,8 +100,8 @@ pub fn find_part_snapshot( pub fn collect_related_part_events( state: &AppState, - definition_event_hash: &[u8; 32], -) -> Vec<([u8; 32], Event)> { + definition_event_hash: &EventHashId, +) -> Vec<(EventHashId, Event)> { let mut events = state .event_cache .iter() @@ -107,12 +115,12 @@ pub fn collect_related_part_events( _ => false, }; if is_related { - Some((*hash, event.clone())) + Some((hash.clone(), event.clone())) } else { None } }) - .collect::>(); + .collect::>(); events.sort_by(|(_, a), (_, b)| b.time.cmp(&a.time)); events } diff --git a/definy-ui/src/wasm_emitter.rs b/definy-ui/src/wasm_emitter.rs index b44523f3..a9d512e1 100644 --- a/definy-ui/src/wasm_emitter.rs +++ b/definy-ui/src/wasm_emitter.rs @@ -43,18 +43,11 @@ pub fn compile_expression_to_wasm(expression: &Expression) -> Result, St module.extend_from_slice(&WASM_VERSION); // Type Section (1 type: () -> i64) - let mut type_section = Vec::new(); - type_section.push(1); // 1 type - type_section.push(0x60); // func type - type_section.push(0); // 0 params - type_section.push(1); // 1 result - type_section.push(I64); // result: i64 + let type_section = vec![1, 0x60, 0, 1, I64]; emit_section(&mut module, TYPE_SECTION, &type_section); // Function Section (1 function of type index 0) - let mut function_section = Vec::new(); - function_section.push(1); // 1 function - function_section.push(0); // type index 0 + let function_section = vec![1, 0]; emit_section(&mut module, FUNCTION_SECTION, &function_section); // Export Section (Export function 0 as "evaluate") diff --git a/definy-ui/tests/browser_e2e.rs b/definy-ui/tests/browser_e2e.rs index f3c98b3f..5a34754c 100644 --- a/definy-ui/tests/browser_e2e.rs +++ b/definy-ui/tests/browser_e2e.rs @@ -19,8 +19,7 @@ use tokio::time::{Duration, sleep}; const TEST_JAVASCRIPT_CONTENT: &[u8] = include_bytes!("../../web-distribution/definy_client.js"); const TEST_JAVASCRIPT_HASH: &str = include_str!("../../web-distribution/definy_client.js.sha256"); const TEST_WASM_CONTENT: &[u8] = include_bytes!("../../web-distribution/definy_client_bg.wasm"); -const TEST_WASM_HASH: &str = - include_str!("../../web-distribution/definy_client_bg.wasm.sha256"); +const TEST_WASM_HASH: &str = include_str!("../../web-distribution/definy_client_bg.wasm.sha256"); const TEST_ICON_CONTENT: &[u8] = include_bytes!("../../assets/icon.png"); const TEST_ICON_HASH: &str = include_str!("../../web-distribution/icon.png.sha256"); @@ -330,7 +329,7 @@ impl WebDriverClient { ) -> Result> { let payload = serde_json::json!({ "script": script, "args": args }); let sync_path = format!("/session/{}/execute/sync", self.session_id); - match webdriver_request( + if let Ok(response) = webdriver_request( &self.client, &self.base_url, Method::POST, @@ -339,10 +338,10 @@ impl WebDriverClient { ) .await { - Ok(response) => { - return Ok(response.get("value").cloned().unwrap_or(serde_json::Value::Null)); - } - Err(_) => {} + return Ok(response + .get("value") + .cloned() + .unwrap_or(serde_json::Value::Null)); } let fallback_path = format!("/session/{}/execute", self.session_id); @@ -354,7 +353,10 @@ impl WebDriverClient { Some(payload), ) .await?; - Ok(response.get("value").cloned().unwrap_or(serde_json::Value::Null)) + Ok(response + .get("value") + .cloned() + .unwrap_or(serde_json::Value::Null)) } } diff --git a/narumincho-vdom-client/Cargo.toml b/narumincho-vdom-client/Cargo.toml index 93ede3b8..f1f19f85 100644 --- a/narumincho-vdom-client/Cargo.toml +++ b/narumincho-vdom-client/Cargo.toml @@ -3,12 +3,9 @@ name = "narumincho-vdom-client" version = "0.1.0" edition = "2024" -[lib] -doctest = false - [dependencies] narumincho-vdom = { path = "../narumincho-vdom" } js-sys = "0.3" wasm-bindgen = "0.2" -web-sys = { version = "0.3.85", features = ["console", "Window", "Document", "Element", "Node", "NodeList", "HtmlElement", "HtmlInputElement", "Text", "EventTarget", "Event", "CssStyleDeclaration", "MouseEvent", "PopStateEvent", "History", "Location"] } -wasm-bindgen-futures = "0.4.58" +web-sys = { version = "0.3.91", features = ["console", "Window", "Document", "Element", "Node", "NodeList", "HtmlElement", "HtmlInputElement", "Text", "EventTarget", "Event", "CssStyleDeclaration", "MouseEvent", "PopStateEvent", "History", "Location"] } +wasm-bindgen-futures = "0.4.64" diff --git a/narumincho-vdom-client/src/diff.rs b/narumincho-vdom-client/src/diff.rs index 1aed6b6e..289cfbe6 100644 --- a/narumincho-vdom-client/src/diff.rs +++ b/narumincho-vdom-client/src/diff.rs @@ -135,7 +135,8 @@ fn diff_recursive( .iter() .map(child_key) .collect::>>(); - let has_keys = old_keys.iter().any(|k| k.is_some()) || new_keys.iter().any(|k| k.is_some()); + let has_keys = + old_keys.iter().any(|k| k.is_some()) || new_keys.iter().any(|k| k.is_some()); if has_keys { let all_old_keyed = old_keys.iter().all(|k| k.is_some()); @@ -150,7 +151,7 @@ fn diff_recursive( .map(|k| k.as_ref().unwrap().clone()) .collect::>(); if old_key_list != new_key_list { - if old_element.children.len() > 0 { + if !old_element.children.is_empty() { patches.push(( path.clone(), Patch::RemoveChildren(old_element.children.len()), diff --git a/narumincho-vdom-client/src/lib.rs b/narumincho-vdom-client/src/lib.rs index 09f658cb..49510ea6 100644 --- a/narumincho-vdom-client/src/lib.rs +++ b/narumincho-vdom-client/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::type_complexity)] use std::rc::Rc; use js_sys::Reflect; @@ -8,10 +9,10 @@ use wasm_bindgen::closure::Closure; mod diff; -pub const DOCUMENT: std::sync::LazyLock = std::sync::LazyLock::new(|| { +pub static DOCUMENT: std::sync::LazyLock = std::sync::LazyLock::new(|| { let window = web_sys::window().expect("no global `window` exists"); - let document = window.document().expect("should have a document on window"); - document + + window.document().expect("should have a document on window") }); pub trait App { @@ -117,93 +118,73 @@ pub fn start>() { if let Some(window) = web_sys::window() { // --- 1. Web Navigation API listener (if supported) --- - if let Ok(navigation) = Reflect::get(&window, &JsValue::from_str("navigation")) { - if !navigation.is_undefined() { - let dispatch_for_nav = Rc::clone(&dispatch_impl); - let on_navigate = Closure::wrap(Box::new(move |event: web_sys::Event| { - if let Ok(can_intercept) = - Reflect::get(&event, &JsValue::from_str("canIntercept")) + if let Ok(navigation) = Reflect::get(&window, &JsValue::from_str("navigation")) + && !navigation.is_undefined() + { + let dispatch_for_nav = Rc::clone(&dispatch_impl); + let on_navigate = Closure::wrap(Box::new(move |event: web_sys::Event| { + if let Ok(can_intercept) = Reflect::get(&event, &JsValue::from_str("canIntercept")) + && can_intercept.is_truthy() + && let Ok(_user_initiated) = + Reflect::get(&event, &JsValue::from_str("userInitiated")) + { + // Only intercept if it was a user click (not script navigation, etc) if we want + // Or always intercept. Let's intercept and call .intercept() if available + if let Ok(destination) = Reflect::get(&event, &JsValue::from_str("destination")) + && let Ok(url_val) = Reflect::get(&destination, &JsValue::from_str("url")) + && let Some(url_str) = url_val.as_string() { - if can_intercept.is_truthy() { - if let Ok(_user_initiated) = - Reflect::get(&event, &JsValue::from_str("userInitiated")) - { - // Only intercept if it was a user click (not script navigation, etc) if we want - // Or always intercept. Let's intercept and call .intercept() if available - if let Ok(destination) = - Reflect::get(&event, &JsValue::from_str("destination")) - { - if let Ok(url_val) = - Reflect::get(&destination, &JsValue::from_str("url")) - { - if let Some(url_str) = url_val.as_string() { - // The modern way to handle this in Navigation API is to call event.intercept() - // preventDefault() cancels the navigation entirely (e.g., URL bar doesn't update). - // Usually, VDOM routers want the URL bar to update. - let intercept_func = Reflect::get( - &event, - &JsValue::from_str("intercept"), - ) - .unwrap_or(JsValue::UNDEFINED); - - let dispatch = Rc::clone(&dispatch_for_nav); - - if intercept_func.is_function() { - let url_for_intercept = url_str.clone(); - let intercept_handler = - Closure::wrap(Box::new(move || { - let dispatch_inner = Rc::clone(&dispatch); - let url_for_closure = - url_for_intercept.clone(); - dispatch_inner(Box::new( - move |state: State| { - A::on_navigate( - state, - url_for_closure, - ) - }, - )); - }) - as Box); - - let intercept_options = js_sys::Object::new(); - Reflect::set( - &intercept_options, - &JsValue::from_str("handler"), - intercept_handler.as_ref(), - ) - .unwrap(); - - intercept_func - .unchecked_into::() - .call1(&event, &intercept_options) - .unwrap(); - intercept_handler.forget(); - } else { - event.prevent_default(); - dispatch(Box::new(move |state: State| { - A::on_navigate(state, url_str.clone()) - })); - } - } - } - } - } + // The modern way to handle this in Navigation API is to call event.intercept() + // preventDefault() cancels the navigation entirely (e.g., URL bar doesn't update). + // Usually, VDOM routers want the URL bar to update. + let intercept_func = Reflect::get(&event, &JsValue::from_str("intercept")) + .unwrap_or(JsValue::UNDEFINED); + + let dispatch = Rc::clone(&dispatch_for_nav); + + if intercept_func.is_function() { + let url_for_intercept = url_str.clone(); + let intercept_handler = Closure::wrap(Box::new(move || { + let dispatch_inner = Rc::clone(&dispatch); + let url_for_closure = url_for_intercept.clone(); + dispatch_inner(Box::new(move |state: State| { + A::on_navigate(state, url_for_closure) + })); + }) + as Box); + + let intercept_options = js_sys::Object::new(); + Reflect::set( + &intercept_options, + &JsValue::from_str("handler"), + intercept_handler.as_ref(), + ) + .unwrap(); + + intercept_func + .unchecked_into::() + .call1(&event, &intercept_options) + .unwrap(); + intercept_handler.forget(); + } else { + event.prevent_default(); + dispatch(Box::new(move |state: State| { + A::on_navigate(state, url_str.clone()) + })); } } - }) - as Box); - - let _ = Reflect::get(&navigation, &JsValue::from_str("addEventListener")) - .unwrap() - .unchecked_into::() - .call2( - &navigation, - &JsValue::from_str("navigate"), - on_navigate.as_ref(), - ); - on_navigate.forget(); - } + } + }) as Box); + + let _ = Reflect::get(&navigation, &JsValue::from_str("addEventListener")) + .unwrap() + .unchecked_into::() + .call2( + &navigation, + &JsValue::from_str("navigate"), + on_navigate.as_ref(), + ); + on_navigate.forget(); } // --- 2. Fallback or standard Click Event Interception (for browsers without modern Navigation API) --- @@ -232,14 +213,14 @@ pub fn start>() { let dispatch = Rc::clone(&dispatch_for_click); let href_clone = href.clone(); // Update history API manually since we intercepted the click - if let Some(window) = web_sys::window() { - if let Ok(history) = window.history() { - let _ = history.push_state_with_url( - &JsValue::NULL, - "", - Some(&href_clone), - ); - } + if let Some(window) = web_sys::window() + && let Ok(history) = window.history() + { + let _ = history.push_state_with_url( + &JsValue::NULL, + "", + Some(&href_clone), + ); } dispatch(Box::new(move |state: State| { // Provide the full URL to on_navigate, or just the path if on_navigate handles it. @@ -287,7 +268,7 @@ pub fn apply( dispatch: &Rc State>)>, ) { for (path, patch) in patches { - if let Some(node) = find_node(root, &path) { + if let Some(node) = find_node(root, path) { apply_patch( node, patch, @@ -296,7 +277,7 @@ pub fn apply( ); } else { web_sys::console::error_1(&format!("Node not found at path {:?}", path).into()); - log_missing_path(root, &path); + log_missing_path(root, path); } } } @@ -356,26 +337,26 @@ fn apply_patch( node: web_sys::Node, patch: &diff::Patch, dispatch: &Rc State>)>, - callback_key_symbol: &js_sys::Symbol, + _callback_key_symbol: &js_sys::Symbol, ) { match patch { diff::Patch::Replace(new_node) => { if let Some(parent) = node.parent_node() { - let new_web_node = create_web_sys_node(&new_node, dispatch, callback_key_symbol); + let new_web_node = create_web_sys_node(new_node, dispatch, _callback_key_symbol); parent.replace_child(&new_web_node, &node).unwrap(); } } diff::Patch::UpdateText(text) => { - node.set_text_content(Some(&text)); + node.set_text_content(Some(text)); } diff::Patch::AddAttributes(attrs) => { if let Some(element) = node.dyn_ref::() { for (key, value) in attrs { - element.set_attribute(&key, &value).unwrap(); - if key == "value" { - if let Some(input) = element.dyn_ref::() { - input.set_value(value); - } + element.set_attribute(key, value).unwrap(); + if key == "value" + && let Some(input) = element.dyn_ref::() + { + input.set_value(value); } } } @@ -383,7 +364,7 @@ fn apply_patch( diff::Patch::RemoveAttributes(keys) => { if let Some(element) = node.dyn_ref::() { for key in keys { - element.remove_attribute(&key).unwrap(); + element.remove_attribute(key).unwrap(); } } } @@ -423,7 +404,7 @@ fn apply_patch( as Box); element .add_event_listener_with_callback( - &event_name, + event_name, closure.as_ref().unchecked_ref(), ) .unwrap(); @@ -441,7 +422,7 @@ fn apply_patch( if let Ok(value) = Reflect::get(element, &JsValue::from_str(&key)) { if let Some(func) = value.dyn_ref::() { element - .remove_event_listener_with_callback(&event_name, func) + .remove_event_listener_with_callback(event_name, func) .unwrap(); } Reflect::delete_property(element, &JsValue::from_str(&key)).unwrap(); @@ -451,7 +432,7 @@ fn apply_patch( } diff::Patch::AppendChildren(children) => { for child in children { - let child_node = create_web_sys_node(&child, dispatch, callback_key_symbol); + let child_node = create_web_sys_node(child, dispatch, _callback_key_symbol); node.append_child(&child_node).unwrap(); } } @@ -470,7 +451,7 @@ fn apply_patch( fn create_web_sys_node( vdom: &Node, dispatch: &Rc State>)>, - callback_key_symbol: &js_sys::Symbol, + _callback_key_symbol: &js_sys::Symbol, ) -> web_sys::Node { match vdom { Node::Element(el) => { @@ -500,7 +481,7 @@ fn create_web_sys_node( wasm_bindgen_futures::spawn_local(fut); }) as Box); element - .add_event_listener_with_callback(&event_name, closure.as_ref().unchecked_ref()) + .add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref()) .unwrap(); let key = format!("__narumincho_event_{}", event_name); Reflect::set(&element, &JsValue::from_str(&key), closure.as_ref()).unwrap(); @@ -508,7 +489,7 @@ fn create_web_sys_node( } for child in &el.children { element - .append_child(&create_web_sys_node(child, dispatch, callback_key_symbol)) + .append_child(&create_web_sys_node(child, dispatch, _callback_key_symbol)) .unwrap(); } element.into() diff --git a/narumincho-vdom/Cargo.toml b/narumincho-vdom/Cargo.toml index 1a25a58f..f8cfc354 100644 --- a/narumincho-vdom/Cargo.toml +++ b/narumincho-vdom/Cargo.toml @@ -3,8 +3,5 @@ name = "narumincho-vdom" version = "0.1.0" edition = "2024" -[lib] -doctest = false - [dependencies] sha2 = "0.10.9" diff --git a/narumincho-vdom/src/button.rs b/narumincho-vdom/src/button.rs index bc1b5c31..0ee57bf1 100644 --- a/narumincho-vdom/src/button.rs +++ b/narumincho-vdom/src/button.rs @@ -8,6 +8,12 @@ pub struct Button { pub children: Vec>, } +impl Default for Button { + fn default() -> Self { + Self::new() + } +} + impl Button { /// https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/button pub fn new() -> Self { @@ -86,16 +92,17 @@ pub enum CommandValue { Custom(String), } -impl ToString for CommandValue { - fn to_string(&self) -> String { - match self { - CommandValue::ShowModal => "show-modal".to_string(), - CommandValue::Close => "close".to_string(), - CommandValue::RequestClose => "request-close".to_string(), - CommandValue::ShowPopover => "show-popover".to_string(), - CommandValue::HidePopover => "hide-popover".to_string(), - CommandValue::TogglePopover => "toggle-popover".to_string(), - CommandValue::Custom(s) => s.to_string(), - } +impl std::fmt::Display for CommandValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + CommandValue::ShowModal => "show-modal", + CommandValue::Close => "close", + CommandValue::RequestClose => "request-close", + CommandValue::ShowPopover => "show-popover", + CommandValue::HidePopover => "hide-popover", + CommandValue::TogglePopover => "toggle-popover", + CommandValue::Custom(s) => return write!(f, "{}", s), + }; + write!(f, "{}", s) } } diff --git a/narumincho-vdom/src/elements.rs b/narumincho-vdom/src/elements.rs index 29853067..57bbe55c 100644 --- a/narumincho-vdom/src/elements.rs +++ b/narumincho-vdom/src/elements.rs @@ -10,6 +10,12 @@ macro_rules! define_element { pub children: Vec>, } + impl Default for $name { + fn default() -> Self { + Self::new() + } + } + impl $name { pub fn new() -> Self { Self { @@ -64,9 +70,9 @@ macro_rules! define_element { } } - impl Into> for $name { - fn into(self) -> Node { - self.into_node() + impl From<$name> for Node { + fn from(val: $name) -> Self { + val.into_node() } } }; @@ -109,6 +115,7 @@ impl Body { self } } + define_element!( H1, "h1", @@ -247,6 +254,11 @@ impl Textarea { pub fn value(self, value: &str) -> Self { self.attribute("value", value) } + + pub fn on_input(mut self, msg: EventHandler) -> Self { + self.events.push(("input".to_string(), msg)); + self + } } impl Form { @@ -266,6 +278,12 @@ pub struct A { _phantom: std::marker::PhantomData, } +impl Default for A { + fn default() -> Self { + Self::new() + } +} + impl A { pub fn new() -> Self { Self { @@ -324,8 +342,8 @@ impl A { } } -impl Into> for A { - fn into(self) -> Node { - self.into_node() +impl From> for Node { + fn from(val: A) -> Self { + val.into_node() } } diff --git a/narumincho-vdom/src/lib.rs b/narumincho-vdom/src/lib.rs index 05956745..b88cd6fc 100644 --- a/narumincho-vdom/src/lib.rs +++ b/narumincho-vdom/src/lib.rs @@ -37,7 +37,7 @@ pub fn to_string(node: &Node) -> String { html.push_str(" style=\""); for (key, value) in vdom.styles.iter() { html.push_str(key); - html.push_str(":"); + html.push(':'); html.push_str(&attribute_escape(value)); html.push(';'); } diff --git a/narumincho-vdom/src/node.rs b/narumincho-vdom/src/node.rs index 4b633ec3..ffa0f2e7 100644 --- a/narumincho-vdom/src/node.rs +++ b/narumincho-vdom/src/node.rs @@ -77,17 +77,20 @@ impl PartialEq for Node { } } +pub type StateUpdater = Box State>; +pub type StateDispatcher = Box)>; +pub type EventHandlerClosure = + dyn Fn(StateDispatcher) -> Pin>>; + pub struct EventHandler { - pub handler: Rc< - dyn Fn(Box State>)>) -> Pin>>, - >, + pub handler: Rc>, pub parameter_hash: u64, } impl EventHandler { pub fn new(f: F) -> Self where - F: Fn(Box State>)>) -> Fut + 'static, + F: Fn(StateDispatcher) -> Fut + 'static, Fut: Future + 'static, { EventHandler { @@ -98,7 +101,7 @@ impl EventHandler { pub fn with_parameter(f: F, p: P) -> Self where - F: Fn(Box State>)>, &P) -> Fut + 'static, + F: Fn(StateDispatcher, &P) -> Fut + 'static, Fut: Future + 'static, P: Hash + 'static, { @@ -116,7 +119,7 @@ impl Clone for EventHandler { fn clone(&self) -> Self { Self { handler: Rc::clone(&self.handler), - parameter_hash: self.parameter_hash.clone(), + parameter_hash: self.parameter_hash, } } } diff --git a/narumincho-vdom/src/route.rs b/narumincho-vdom/src/route.rs index 1675dd8d..79441928 100644 --- a/narumincho-vdom/src/route.rs +++ b/narumincho-vdom/src/route.rs @@ -8,9 +8,9 @@ pub enum Href { Internal(L), } -impl Into for Href { - fn into(self) -> String { - match self { +impl From> for String { + fn from(val: Href) -> Self { + match val { Href::External(url) => url, Href::Internal(location) => location.to_url(), } diff --git a/narumincho-vdom/src/style.rs b/narumincho-vdom/src/style.rs index 98e97259..e09ee034 100644 --- a/narumincho-vdom/src/style.rs +++ b/narumincho-vdom/src/style.rs @@ -3,6 +3,12 @@ use std::collections::HashMap; #[derive(Debug, Clone, PartialEq)] pub struct Style(HashMap); +impl Default for Style { + fn default() -> Self { + Self::new() + } +} + impl Style { pub fn new() -> Self { Self(HashMap::new()) diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..d0ead5ec --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["clippy", "rustfmt"]