synpad 0.1.0

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

use crate::components::modal::Modal;
use crate::platform::file_dialog::save_file;
use crate::state::app_state::AppState;

/// Export format options.
#[derive(Clone, Debug, PartialEq)]
enum ExportFormat {
    PlainText,
    Html,
    Json,
}

/// Chat export dialog.
#[component]
pub fn ChatExportDialog(
    room_id: String,
    room_name: String,
    on_close: EventHandler<()>,
) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut format = use_signal(|| ExportFormat::PlainText);
    let mut is_exporting = use_signal(|| false);
    let mut export_status = use_signal(|| Option::<String>::None);

    rsx! {
        Modal {
            title: "Export Chat".to_string(),
            on_close: move |_| on_close.call(()),
            div {
                class: "chat-export",

                p {
                    class: "chat-export__info",
                    "Export the message history of \"{room_name}\" to a file."
                }

                div {
                    class: "chat-export__format",
                    label { "Format:" }
                    div {
                        class: "chat-export__format-options",
                        button {
                            class: if *format.read() == ExportFormat::PlainText { "chat-export__format-btn chat-export__format-btn--active" } else { "chat-export__format-btn" },
                            onclick: move |_| format.set(ExportFormat::PlainText),
                            "Plain Text (.txt)"
                        }
                        button {
                            class: if *format.read() == ExportFormat::Html { "chat-export__format-btn chat-export__format-btn--active" } else { "chat-export__format-btn" },
                            onclick: move |_| format.set(ExportFormat::Html),
                            "HTML (.html)"
                        }
                        button {
                            class: if *format.read() == ExportFormat::Json { "chat-export__format-btn chat-export__format-btn--active" } else { "chat-export__format-btn" },
                            onclick: move |_| format.set(ExportFormat::Json),
                            "JSON (.json)"
                        }
                    }
                }

                if let Some(ref status) = *export_status.read() {
                    div {
                        class: "chat-export__status",
                        "{status}"
                    }
                }

                div {
                    class: "chat-export__actions",
                    button {
                        class: "btn btn--secondary",
                        onclick: move |_| on_close.call(()),
                        "Cancel"
                    }
                    button {
                        class: "btn btn--primary",
                        disabled: *is_exporting.read(),
                        onclick: move |_| {
                            let rid = room_id.clone();
                            let rname = room_name.clone();
                            let fmt = format.read().clone();
                            is_exporting.set(true);
                            export_status.set(Some("Exporting...".to_string()));
                            spawn(async move {
                                let client = { state.read().client.clone() };
                                if let Some(client) = client {
                                    if let Ok(room_id) = OwnedRoomId::try_from(rid.as_str()) {
                                        // Load timeline messages
                                        match crate::client::timeline::load_room_messages(&client, &room_id, None).await {
                                            Ok((items, _)) => {
                                                let content = format_export(&rname, &items, &fmt);
                                                let extension = match fmt {
                                                    ExportFormat::PlainText => "txt",
                                                    ExportFormat::Html => "html",
                                                    ExportFormat::Json => "json",
                                                };

                                                match save_file(
                                                    "Save export",
                                                    &format!("{rname}_export.{extension}"),
                                                    content.as_bytes(),
                                                )
                                                .await
                                                {
                                                    Ok(Some(_)) => {
                                                        export_status.set(Some("Export saved successfully!".to_string()));
                                                    }
                                                    Ok(None) => {
                                                        export_status.set(None);
                                                    }
                                                    Err(err) => {
                                                        export_status.set(Some(err));
                                                    }
                                                }
                                            }
                                            Err(e) => {
                                                export_status.set(Some(format!("Failed to load messages: {e}")));
                                            }
                                        }
                                    }
                                }
                                is_exporting.set(false);
                            });
                        },
                        if *is_exporting.read() { "Exporting..." } else { "Export" }
                    }
                }
            }
        }
    }
}

fn format_export(
    room_name: &str,
    items: &[crate::state::room_state::TimelineItemKind],
    format: &ExportFormat,
) -> String {
    use crate::state::room_state::TimelineItemKind;

    match format {
        ExportFormat::PlainText => {
            let separator = "=".repeat(40);
            let mut output = format!("Chat Export: {room_name}\n{separator}\n\n");
            for item in items {
                if let TimelineItemKind::Event(event) = item {
                    let time = crate::utils::time_format::format_time(event.timestamp);
                    let body = event.content.body_text();
                    output.push_str(&format!("[{time}] {}: {body}\n", event.sender_display_name));
                } else if let TimelineItemKind::DaySeparator(date) = item {
                    output.push_str(&format!("\n--- {date} ---\n\n"));
                }
            }
            output
        }
        ExportFormat::Html => {
            let mut output = format!(
                "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Chat Export: {room_name}</title>\
                <style>body{{font-family:sans-serif;max-width:800px;margin:0 auto;padding:20px}} \
                .msg{{margin:8px 0;padding:8px;border-left:3px solid #ccc}} \
                .sender{{font-weight:bold;color:#333}} .time{{color:#888;font-size:0.8em}} \
                .separator{{text-align:center;color:#888;margin:16px 0}}</style></head><body>\
                <h1>Chat Export: {room_name}</h1>"
            );
            for item in items {
                if let TimelineItemKind::Event(event) = item {
                    let time = crate::utils::time_format::format_time(event.timestamp);
                    let body = ammonia::clean(&event.content.body_text());
                    output.push_str(&format!(
                        "<div class=\"msg\"><span class=\"sender\">{}</span> <span class=\"time\">[{time}]</span><br>{body}</div>",
                        ammonia::clean(&event.sender_display_name)
                    ));
                } else if let TimelineItemKind::DaySeparator(date) = item {
                    output.push_str(&format!("<div class=\"separator\">--- {date} ---</div>"));
                }
            }
            output.push_str("</body></html>");
            output
        }
        ExportFormat::Json => {
            let mut events = Vec::new();
            for item in items {
                if let TimelineItemKind::Event(event) = item {
                    events.push(serde_json::json!({
                        "sender": event.sender.to_string(),
                        "sender_display_name": event.sender_display_name,
                        "timestamp": event.timestamp,
                        "content": event.content.body_text(),
                        "event_id": event.event_id.as_ref().map(|e| e.to_string()),
                    }));
                }
            }
            let export = serde_json::json!({
                "room_name": room_name,
                "export_date": chrono::Utc::now().to_rfc3339(),
                "messages": events,
            });
            serde_json::to_string_pretty(&export).unwrap_or_default()
        }
    }
}