use dioxus::prelude::*; use dioxus_web::events::FormEvent; use mize::Mize; use matrix_sdk::SyncRoomEvent; const ROOM_ID: &str = "!XwqdXGPlmaLatdGUiN:ppc.social"; /// Props for the chat component #[derive(Props, Clone)] pub struct ChatProps { pub mize: Mize, pub initial_timeline: Vec, pub initial_draft: String, pub initial_edit_id: Option, } /// Message component - renders a single message #[component] pub fn Message(cx: Scope, event: SyncRoomEvent) -> Element { let sender = event.sender().to_string(); let timestamp = event.origin_server_ts(); // Extract display name from MXID (basic version) let display_name = if let Some(at_pos) = sender.find('@') { if let Some(colon_pos) = sender.rfind(':') { &sender[at_pos + 1..colon_pos] } else { &sender[at_pos + 1..] } } else { &sender }; // Format timestamp let time_str = { let dt = js_sys::Date::new(&(timestamp as f64)); dt.to_locale_string() }; // Extract message content let content = event .as_original() .and_then(|e| e.content().deserialize().ok()) .and_then(|c: matrix_sdk::ruma::events::room::message::RoomMessageEventContent| { c.body().map(|s| s.to_string()) }) .unwrap_or_default(); // Check if this is an edit let is_edit = event .as_original() .map(|e| { e.content() .get("m.relates_to") .and_then(|v| v.as_object()) .and_then(|o| o.get("rel_type")) .and_then(|v| v.as_str()) == Some("m.replace") }) .unwrap_or(false); cx.render(rsx! { div { class: "matrix-chat-event", style: "margin: 14px 16px; padding-bottom: 10px; border-bottom: 1px solid var(--background-modifier-border);", div { class: "matrix-chat-msghead", style: "font-size: 1em; font-weight: 500; color: var(--interactive-accent); display: flex; align-items: center; gap: 10px;", span { display_name } span { style: "font-size: 0.85em; color: var(--text-muted); margin-left: auto;", {time_str.clone()} if is_edit { " ยท edited" } } } div { class: "matrix-chat-msgbody", style: "margin: 6px 0 0 0; font-size: 1em;", {content} } } }) } /// Editor component - message input area #[component] pub fn Editor(cx: Scope, mize: Mize, draft: UseSignal, edit_id: UseSignal>) -> Element { let on_input = move |evt: FormEvent| { draft.set(evt.value.clone()); }; let on_send = move |_| { let mize_clone = mize.clone(); let draft_val = draft.read().clone(); let edit_id_val = edit_id.read().clone(); cx.spawn(async move { if let Ok(mut matrix_part) = mize_clone.get_part_native::("matrix") { matrix_part.message_draft = draft_val; matrix_part.edit_event_id = edit_id_val; if let Err(e) = matrix_part.send_message().await { console_log!("Failed to send message: {:?}", e); } // Clear local draft draft.set(String::new()); edit_id.set(None); } }); }; let on_keydown = move |evt: dioxus_web::events::KeyboardEvent| { if (evt.ctrl_key() || evt.meta_key()) && evt.key() == "Enter" { on_send(evt); } }; let on_cancel = move |_| { draft.set(String::new()); edit_id.set(None); }; cx.render(rsx! { div { class: "matrix-chat-editor", style: "border-top: 1px solid var(--background-modifier-border); background: var(--background-primary); padding: 8px 12px;", div { class: "matrix-chat-editor-inner", style: "border: 1px solid var(--background-modifier-border); border-radius: 6px; overflow: hidden; background: var(--background-secondary);", textarea { r#type: "text", rows: "3", style: "width: 99%; min-height: 40px; resize: vertical; background: transparent; border: none; outline: none; padding: 8px; font-family: inherit; font-size: inherit; color: var(--text-normal);", value: "{draft}", oninput: on_input, onkeydown: on_keydown, } } button { r#type: "button", style: "margin-top: 3px; float: right; padding: 6px 14px; font-size: 0.97em; border-radius: 5px; border: none; background: var(--interactive-accent); color: white; cursor: pointer;", onclick: on_send, if edit_id.read().is_some() { "Save" } else { "Send" } } if edit_id.read().is_some() { button { r#type: "button", style: "margin-right: 12px; padding: 6px 12px; font-size: 0.93em; border-radius: 5px; border: none; color: var(--text-normal); background: var(--background-secondary); cursor: pointer;", onclick: on_cancel, "Cancel" } } } }) } /// Timeline component - list of messages #[component] pub fn Timeline(cx: Scope, mize: Mize, timeline: UseSignal>) -> Element { // Periodically refresh timeline from MatrixPart cx.use_effect((), move |_| { let mize_clone = mize.clone(); let timeline_clone = timeline.clone(); // Set up interval to poll for updates let interval = js_sys::set_interval( &Closure::::wrap(Box::new(move || { if let Ok(matrix_part) = mize_clone.get_part_native::("matrix") { *timeline_clone.write() = matrix_part.timeline.clone(); } })), 1000, // 1 second ); // Cleanup on unmount move || { js_sys::clear_interval(interval); } }); cx.render(rsx! { div { class: "matrix-chat-scroll", style: "flex-grow: 1; overflow-y: auto; padding: 8px 0; background: var(--background-secondary);", // Room name header div { class: "matrix-chat-roomname", style: "font-weight: 600; text-align: center; font-size: 1.1em; margin: 2px 0 12px 0; border-bottom: 1px solid var(--background-modifier-border); color: var(--text-accent); background: var(--background-primary);", "Matrix Chat - {ROOM_ID}" } // Messages for event in timeline.read().iter() { rsx! { Message { event: event.clone() } } } } }) } /// Root chat component #[component] pub fn Chat(cx: Scope, props: ChatProps) -> Element { let timeline = use_signal(&cx, || props.initial_timeline); let draft = use_signal(&cx, || props.initial_draft); let edit_id = use_signal(&cx, || props.initial_edit_id); cx.render(rsx! { div { class: "matrix-chat-root", style: "height: 100%; display: flex; flex-direction: column; background: var(--background-primary); color: var(--text-normal);", // Title div { class: "matrix-chat-title", style: "padding: 12px; font-weight: bold; font-size: 1.2em; text-align: center; border-bottom: 1px solid var(--background-modifier-border);", "Matrix Chat" } // Info div { class: "matrix-chat-info", style: "font-size: 0.85em; color: var(--text-muted); text-align: center; margin-bottom: 5px;", "Room: {ROOM_ID}" } // Timeline Timeline { mize: props.mize.clone(), timeline } // Editor Editor { mize: props.mize.clone(), draft, edit_id } } }) } /// Mount the Dioxus app into a DOM element pub fn mount_chat_app( element: web_sys::HtmlElement, initial_timeline: Vec, initial_draft: String, initial_edit_id: Option, mize: Mize, ) { dioxus_web::launch( dioxus::prelude::App::new(move || rsx! { Chat { mize: mize.clone(), initial_timeline: initial_timeline.clone(), initial_draft: initial_draft.clone(), initial_edit_id: initial_edit_id.clone(), } }), element, ); } // Console log macro #[macro_export] macro_rules! console_log { ($($t:tt)*) => { web_sys::console::log_1(&format!($($t)*).into()); }; }