synpad 0.1.0

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

/// A pending file attachment for display in the attachment bar.
#[derive(Clone, Debug, PartialEq)]
pub struct PendingAttachment {
    pub file_name: String,
    pub file_size: u64,
    pub mime_type: String,
    /// True while uploading
    pub is_uploading: bool,
}

/// Attachment bar showing pending file uploads above the composer.
#[component]
pub fn AttachmentBar(
    attachments: Vec<PendingAttachment>,
    on_remove: EventHandler<usize>,
    on_clear_all: EventHandler<()>,
) -> Element {
    if attachments.is_empty() {
        return rsx! {};
    }

    rsx! {
        div {
            class: "attachment-bar",

            div {
                class: "attachment-bar__header",
                span {
                    class: "attachment-bar__count",
                    "{attachments.len()} file(s) attached"
                }
                button {
                    class: "attachment-bar__clear-btn",
                    onclick: move |_| on_clear_all.call(()),
                    "Clear all"
                }
            }

            div {
                class: "attachment-bar__list",
                for (idx, att) in attachments.iter().enumerate() {
                    {
                        let file_name = att.file_name.clone();
                        let size_text = format_size(att.file_size);
                        let is_uploading = att.is_uploading;
                        let icon = mime_icon(&att.mime_type);
                        rsx! {
                            div {
                                class: if is_uploading { "attachment-bar__item attachment-bar__item--uploading" } else { "attachment-bar__item" },
                                span { class: "attachment-bar__item-icon", "{icon}" }
                                div {
                                    class: "attachment-bar__item-info",
                                    span { class: "attachment-bar__item-name", "{file_name}" }
                                    span { class: "attachment-bar__item-size", "{size_text}" }
                                }
                                if is_uploading {
                                    span { class: "attachment-bar__item-spinner", "..." }
                                } else {
                                    button {
                                        class: "attachment-bar__item-remove",
                                        title: "Remove",
                                        onclick: move |_| on_remove.call(idx),
                                        ""
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

fn format_size(bytes: u64) -> String {
    if bytes >= 1_048_576 {
        format!("{:.1} MB", bytes as f64 / 1_048_576.0)
    } else if bytes >= 1_024 {
        format!("{:.1} KB", bytes as f64 / 1_024.0)
    } else {
        format!("{bytes} B")
    }
}

fn mime_icon(mime: &str) -> &'static str {
    if mime.starts_with("image/") {
        "🖼"
    } else if mime.starts_with("video/") {
        "🎬"
    } else if mime.starts_with("audio/") {
        "🎵"
    } else if mime.contains("pdf") {
        "📕"
    } else {
        "📄"
    }
}