synpad 0.1.0

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

use crate::components::modal::Modal;
use crate::state::app_state::AppState;

/// Ban list entry.
#[derive(Clone, Debug, PartialEq)]
struct BanEntry {
    user_id: String,
    display_name: Option<String>,
    reason: Option<String>,
    banned_by: Option<String>,
}

/// Ban list management dialog (Mjolnir-style).
/// Shows banned users and allows unbanning.
#[component]
pub fn BanListDialog(room_id: String, on_close: EventHandler<()>) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut bans = use_signal(Vec::<BanEntry>::new);
    let mut loading = use_signal(|| true);
    let mut error = use_signal(|| Option::<String>::None);
    let mut unbanning = use_signal(|| Option::<String>::None);
    let mut ban_user_id = use_signal(|| String::new());
    let mut ban_reason = use_signal(|| String::new());
    let mut banning = use_signal(|| false);

    let rid = room_id.clone();
    use_effect(move || {
        let rid = rid.clone();
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                if let Ok(room_id) = matrix_sdk::ruma::OwnedRoomId::try_from(rid.as_str()) {
                    if let Some(room) = client.get_room(&room_id) {
                        // Fetch banned members
                        match room.members(matrix_sdk::RoomMemberships::BAN).await {
                            Ok(members) => {
                                let banned: Vec<BanEntry> = members
                                    .iter()
                                    .map(|m| BanEntry {
                                        user_id: m.user_id().to_string(),
                                        display_name: m.display_name().map(|n| n.to_string()),
                                        reason: None,
                                        banned_by: None,
                                    })
                                    .collect();
                                bans.set(banned);
                            }
                            Err(e) => {
                                error.set(Some(format!("Failed to load bans: {e}")));
                            }
                        }
                    }
                }
            }
            loading.set(false);
        });
    });

    let rid_ban = room_id.clone();
    let on_ban = move |_| {
        let uid = ban_user_id.read().trim().to_string();
        let reason = ban_reason.read().trim().to_string();
        if uid.is_empty() {
            return;
        }
        banning.set(true);
        let rid = rid_ban.clone();
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                if let Ok(room_id) = matrix_sdk::ruma::OwnedRoomId::try_from(rid.as_str()) {
                    if let Some(room) = client.get_room(&room_id) {
                        if let Ok(user_id) = matrix_sdk::ruma::OwnedUserId::try_from(uid.as_str()) {
                            let reason_opt = if reason.is_empty() { None } else { Some(reason.as_str()) };
                            match room.ban_user(&user_id, reason_opt).await {
                                Ok(_) => {
                                    tracing::info!("Banned {uid}");
                                    bans.write().push(BanEntry {
                                        user_id: uid.clone(),
                                        display_name: None,
                                        reason: if reason.is_empty() { None } else { Some(reason) },
                                        banned_by: None,
                                    });
                                    ban_user_id.set(String::new());
                                    ban_reason.set(String::new());
                                }
                                Err(e) => {
                                    error.set(Some(format!("Failed to ban: {e}")));
                                }
                            }
                        }
                    }
                }
            }
            banning.set(false);
        });
    };

    rsx! {
        Modal {
            title: "Ban List".to_string(),
            on_close: move |_| on_close.call(()),

            div {
                class: "ban-list-dialog",

                // Add ban form
                div {
                    class: "ban-list-dialog__add",
                    h4 { "Ban a user" }
                    div {
                        class: "ban-list-dialog__form",
                        input {
                            r#type: "text",
                            class: "settings-input",
                            placeholder: "@user:server.com",
                            value: "{ban_user_id}",
                            oninput: move |evt| ban_user_id.set(evt.value()),
                        }
                        input {
                            r#type: "text",
                            class: "settings-input",
                            placeholder: "Reason (optional)",
                            value: "{ban_reason}",
                            oninput: move |evt| ban_reason.set(evt.value()),
                        }
                        button {
                            class: "btn btn--danger btn--sm",
                            disabled: *banning.read(),
                            onclick: on_ban,
                            if *banning.read() { "Banning..." } else { "Ban" }
                        }
                    }
                }

                if let Some(ref err) = *error.read() {
                    div { class: "ban-list-dialog__error", "{err}" }
                }

                // Ban list
                div {
                    class: "ban-list-dialog__list",
                    {
                        let count = bans.read().len();
                        rsx! { h4 { "Banned users ({count})" } }
                    }

                    if *loading.read() {
                        div { class: "ban-list-dialog__loading", div { class: "spinner" } }
                    }

                    if bans.read().is_empty() && !*loading.read() {
                        p { class: "ban-list-dialog__empty", "No banned users." }
                    }

                    for ban in bans.read().iter() {
                        {
                            let uid = ban.user_id.clone();
                            let name = ban.display_name.clone().unwrap_or_else(|| uid.clone());
                            let reason = ban.reason.clone();
                            let uid_unban = uid.clone();
                            let is_unbanning = unbanning.read().as_ref() == Some(&uid);
                            let rid = room_id.clone();
                            rsx! {
                                div {
                                    class: "ban-list-dialog__entry",
                                    div {
                                        class: "ban-list-dialog__entry-info",
                                        span { class: "ban-list-dialog__entry-name", "{name}" }
                                        span { class: "ban-list-dialog__entry-id", "{uid}" }
                                        if let Some(ref r) = reason {
                                            span { class: "ban-list-dialog__entry-reason", "Reason: {r}" }
                                        }
                                    }
                                    button {
                                        class: "btn btn--secondary btn--sm",
                                        disabled: is_unbanning,
                                        onclick: move |_| {
                                            let uid = uid_unban.clone();
                                            let rid = rid.clone();
                                            unbanning.set(Some(uid.clone()));
                                            spawn(async move {
                                                let client = { state.read().client.clone() };
                                                if let Some(client) = client {
                                                    if let Ok(room_id) = matrix_sdk::ruma::OwnedRoomId::try_from(rid.as_str()) {
                                                        if let Some(room) = client.get_room(&room_id) {
                                                            if let Ok(user_id) = matrix_sdk::ruma::OwnedUserId::try_from(uid.as_str()) {
                                                                match room.unban_user(&user_id, None).await {
                                                                    Ok(_) => {
                                                                        bans.write().retain(|b| b.user_id != uid);
                                                                    }
                                                                    Err(e) => {
                                                                        error.set(Some(format!("Failed to unban: {e}")));
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                                unbanning.set(None);
                                            });
                                        },
                                        if is_unbanning { "..." } else { "Unban" }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}