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;
#[derive(Clone, Debug, PartialEq)]
enum ExportFormat {
PlainText,
Html,
Json,
}
#[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()) {
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()
}
}
}