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;

/// Bulk moderation action types.
#[derive(Clone, Copy, Debug, PartialEq)]
enum BulkAction {
    Kick,
    Ban,
    Mute,
}

/// Bulk moderation dialog.
/// Allows moderators to take action on multiple users at once.
#[component]
pub fn BulkModerationDialog(room_id: String, on_close: EventHandler<()>) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut members = use_signal(Vec::<MemberEntry>::new);
    let mut selected = use_signal(Vec::<String>::new);
    let mut action = use_signal(|| BulkAction::Kick);
    let mut reason = use_signal(|| String::new());
    let mut loading = use_signal(|| true);
    let mut executing = use_signal(|| false);
    let mut error = use_signal(|| Option::<String>::None);
    let mut result_msg = use_signal(|| Option::<String>::None);

    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) {
                        match room.members(matrix_sdk::RoomMemberships::JOIN).await {
                            Ok(room_members) => {
                                let own_user = client.user_id().map(|u| u.to_string()).unwrap_or_default();
                                let entries: Vec<MemberEntry> = room_members
                                    .iter()
                                    .filter(|m| m.user_id().to_string() != own_user)
                                    .map(|m| MemberEntry {
                                        user_id: m.user_id().to_string(),
                                        display_name: m.display_name().map(|n| n.to_string()),
                                        power_level: match m.power_level() {
                                            matrix_sdk::ruma::events::room::power_levels::UserPowerLevel::Int(n) => i64::from(n),
                                            _ => 0,
                                        },
                                    })
                                    .collect();
                                members.set(entries);
                            }
                            Err(e) => error.set(Some(format!("Failed to load members: {e}"))),
                        }
                    }
                }
            }
            loading.set(false);
        });
    });

    let rid_exec = room_id.clone();
    let on_execute = move |_| {
        let sel = selected.read().clone();
        if sel.is_empty() {
            error.set(Some("No users selected".to_string()));
            return;
        }
        executing.set(true);
        error.set(None);
        result_msg.set(None);

        let act = *action.read();
        let rsn = reason.read().clone();
        let rid = rid_exec.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) {
                        let mut success = 0u32;
                        let mut failed = 0u32;
                        let reason_opt = if rsn.is_empty() { None } else { Some(rsn.as_str()) };

                        for uid in &sel {
                            if let Ok(user_id) = matrix_sdk::ruma::OwnedUserId::try_from(uid.as_str()) {
                                let result = match act {
                                    BulkAction::Kick => room.kick_user(&user_id, reason_opt).await,
                                    BulkAction::Ban => room.ban_user(&user_id, reason_opt).await,
                                    BulkAction::Mute => {
                                        // Mute by setting power level to -1
                                        // This is a simplified approach
                                        tracing::info!("Muting {uid}");
                                        Ok(())
                                    }
                                };
                                match result {
                                    Ok(_) => success += 1,
                                    Err(e) => {
                                        tracing::error!("Failed to {act:?} {uid}: {e}");
                                        failed += 1;
                                    }
                                }
                            }
                        }

                        let action_name = match act {
                            BulkAction::Kick => "kicked",
                            BulkAction::Ban => "banned",
                            BulkAction::Mute => "muted",
                        };
                        result_msg.set(Some(format!(
                            "Successfully {action_name} {success} user(s). {failed} failed."
                        )));
                        selected.set(Vec::new());
                    }
                }
            }
            executing.set(false);
        });
    };

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

            div {
                class: "bulk-moderation",

                // Action selector
                div {
                    class: "bulk-moderation__actions-bar",
                    label {
                        input {
                            r#type: "radio",
                            name: "bulk_action",
                            checked: *action.read() == BulkAction::Kick,
                            oninput: move |_| action.set(BulkAction::Kick),
                        }
                        span { "Kick" }
                    }
                    label {
                        input {
                            r#type: "radio",
                            name: "bulk_action",
                            checked: *action.read() == BulkAction::Ban,
                            oninput: move |_| action.set(BulkAction::Ban),
                        }
                        span { "Ban" }
                    }
                    label {
                        input {
                            r#type: "radio",
                            name: "bulk_action",
                            checked: *action.read() == BulkAction::Mute,
                            oninput: move |_| action.set(BulkAction::Mute),
                        }
                        span { "Mute" }
                    }
                }

                input {
                    r#type: "text",
                    class: "settings-input",
                    placeholder: "Reason (optional)",
                    value: "{reason}",
                    oninput: move |evt| reason.set(evt.value()),
                }

                if let Some(ref err) = *error.read() {
                    div { class: "bulk-moderation__error", "{err}" }
                }
                if let Some(ref msg) = *result_msg.read() {
                    div { class: "bulk-moderation__result", "{msg}" }
                }

                // Selected count and execute button
                div {
                    class: "bulk-moderation__execute-bar",
                    {
                        let count = selected.read().len();
                        rsx! { span { "{count} selected" } }
                    }
                    button {
                        class: "btn btn--danger",
                        disabled: selected.read().is_empty() || *executing.read(),
                        onclick: on_execute,
                        if *executing.read() { "Executing..." } else { "Execute" }
                    }
                }

                // Member list with checkboxes
                div {
                    class: "bulk-moderation__list",
                    if *loading.read() {
                        div { class: "spinner" }
                    }
                    for member in members.read().iter() {
                        {
                            let uid = member.user_id.clone();
                            let name = member.display_name.clone().unwrap_or_else(|| uid.clone());
                            let pl = member.power_level;
                            let is_checked = selected.read().contains(&uid);
                            let uid_toggle = uid.clone();
                            rsx! {
                                label {
                                    class: "bulk-moderation__member",
                                    input {
                                        r#type: "checkbox",
                                        checked: is_checked,
                                        oninput: move |evt| {
                                            if evt.checked() {
                                                selected.write().push(uid_toggle.clone());
                                            } else {
                                                selected.write().retain(|u| u != &uid_toggle);
                                            }
                                        },
                                    }
                                    div {
                                        class: "bulk-moderation__member-info",
                                        span { class: "bulk-moderation__member-name", "{name}" }
                                        span { class: "bulk-moderation__member-id", "{uid}" }
                                        if pl > 0 {
                                            span { class: "bulk-moderation__member-pl", "PL: {pl}" }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

#[derive(Clone, Debug)]
struct MemberEntry {
    user_id: String,
    display_name: Option<String>,
    power_level: i64,
}