270 lines
9.1 KiB
Rust
270 lines
9.1 KiB
Rust
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<SyncRoomEvent>,
|
|
pub initial_draft: String,
|
|
pub initial_edit_id: Option<String>,
|
|
}
|
|
|
|
/// 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<String>, edit_id: UseSignal<Option<String>>) -> 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::<crate::matrix::MatrixPart>("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<Vec<SyncRoomEvent>>) -> 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::<dyn FnMut()>::wrap(Box::new(move || {
|
|
if let Ok(matrix_part) = mize_clone.get_part_native::<crate::matrix::MatrixPart>("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<SyncRoomEvent>,
|
|
initial_draft: String,
|
|
initial_edit_id: Option<String>,
|
|
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());
|
|
};
|
|
}
|