synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;
use matrix_sdk::ruma::{OwnedEventId, OwnedRoomId};

use crate::components::modal::Modal;
use crate::state::app_state::AppState;
use crate::utils::time_format::format_time;

/// Data for a single edit revision.
#[derive(Clone, Debug)]
struct EditRevision {
    body: String,
    timestamp: u64,
    sender: String,
}

/// Modal to display edit history of a message.
#[component]
pub fn EditHistoryModal(
    room_id: String,
    event_id: String,
    on_close: EventHandler<()>,
) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut revisions = use_signal(Vec::<EditRevision>::new);
    let mut is_loading = use_signal(|| true);
    let mut error_msg = use_signal(|| Option::<String>::None);

    // Load edit history
    let rid = room_id.clone();
    let eid = event_id.clone();
    use_effect(move || {
        let rid = rid.clone();
        let eid = eid.clone();
        spawn(async move {
            let client = { state.read().client.clone() };
            let Some(client) = client else {
                is_loading.set(false);
                return;
            };

            let Ok(room_id) = OwnedRoomId::try_from(rid.as_str()) else {
                is_loading.set(false);
                return;
            };
            let Ok(event_id) = OwnedEventId::try_from(eid.as_str()) else {
                is_loading.set(false);
                return;
            };

            let Some(room) = client.get_room(&room_id) else {
                is_loading.set(false);
                return;
            };

            // Fetch the original event to show at minimum
            match room.event(&event_id, None).await {
                Ok(timeline_event) => {
                    let raw = timeline_event.raw();
                    if let Ok(val) = raw.deserialize_as::<serde_json::Value>() {
                        let mut items = Vec::new();
                        // Extract original content
                        let body = val.get("content")
                            .and_then(|c| c.get("body"))
                            .and_then(|b| b.as_str())
                            .unwrap_or("(unable to read)")
                            .to_string();
                        let sender = val.get("sender")
                            .and_then(|s| s.as_str())
                            .unwrap_or("unknown")
                            .to_string();
                        let ts = val.get("origin_server_ts")
                            .and_then(|t| t.as_u64())
                            .unwrap_or(0);

                        items.push(EditRevision {
                            body,
                            timestamp: ts,
                            sender,
                        });

                        // Check for replacement chain in content.m.new_content
                        if let Some(new_content) = val.get("content").and_then(|c| c.get("m.new_content")) {
                            let new_body = new_content.get("body")
                                .and_then(|b| b.as_str())
                                .unwrap_or("(edited content)")
                                .to_string();
                            items.push(EditRevision {
                                body: new_body,
                                timestamp: ts,
                                sender: items[0].sender.clone(),
                            });
                        }

                        revisions.set(items);
                    }
                }
                Err(e) => {
                    error_msg.set(Some(format!("Failed to load event: {e}")));
                }
            }

            is_loading.set(false);
        });
    });

    rsx! {
        Modal {
            title: "Edit History".to_string(),
            on_close: move |_| on_close.call(()),
            div {
                class: "edit-history",

                if *is_loading.read() {
                    div {
                        class: "edit-history__loading",
                        div { class: "spinner spinner--small" }
                        span { "Loading edit history..." }
                    }
                }

                if let Some(ref err) = *error_msg.read() {
                    div {
                        class: "edit-history__error",
                        "{err}"
                    }
                }

                for (idx, rev) in revisions.read().iter().enumerate() {
                    {
                        let body = rev.body.clone();
                        let sender = rev.sender.clone();
                        let time = format_time(rev.timestamp);
                        let is_latest = idx == revisions.read().len() - 1;
                        let label = if idx == 0 && !is_latest {
                            "Original"
                        } else if is_latest {
                            "Current"
                        } else {
                            "Revision"
                        };
                        rsx! {
                            div {
                                class: if is_latest { "edit-history__revision edit-history__revision--current" } else { "edit-history__revision" },
                                div {
                                    class: "edit-history__revision-header",
                                    span { class: "edit-history__revision-label", "{label}" }
                                    span { class: "edit-history__revision-time", "{time}" }
                                    span { class: "edit-history__revision-sender", "{sender}" }
                                }
                                div {
                                    class: "edit-history__revision-body",
                                    "{body}"
                                }
                            }
                        }
                    }
                }

                if !*is_loading.read() && revisions.read().is_empty() && error_msg.read().is_none() {
                    div {
                        class: "edit-history__empty",
                        "No edit history available."
                    }
                }
            }
        }
    }
}