synpad 0.1.0

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

use crate::state::app_state::AppState;

/// Session data for display
#[derive(Clone, Debug, PartialEq)]
struct DeviceInfo {
    device_id: String,
    display_name: String,
    last_seen_ip: String,
    last_seen_ts: String,
    is_current: bool,
    is_verified: bool,
}

/// Device selection state for bulk operations.
#[derive(Clone, Debug, PartialEq)]
struct DeviceSelection {
    selected: Vec<String>,
}

/// Active sessions management panel.
#[component]
pub fn SessionSettings() -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut devices = use_signal(Vec::<DeviceInfo>::new);
    let mut loaded = use_signal(|| false);
    let mut selection = use_signal(|| DeviceSelection { selected: Vec::new() });
    let mut show_rename = use_signal(|| Option::<String>::None);
    let mut rename_value = use_signal(|| String::new());

    // Load devices from SDK
    if !*loaded.read() {
        loaded.set(true);
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                let current_device_id = client.device_id().map(|d| d.to_string()).unwrap_or_default();
                match client.devices().await {
                    Ok(response) => {
                        let list: Vec<DeviceInfo> = response.devices.iter().map(|d| {
                            let device_id = d.device_id.to_string();
                            let is_current = device_id == current_device_id;
                            DeviceInfo {
                                device_id: device_id.clone(),
                                display_name: d.display_name.clone().unwrap_or_else(|| device_id),
                                last_seen_ip: d.last_seen_ip.clone().unwrap_or_default(),
                                last_seen_ts: d.last_seen_ts.map(|ts| {
                                    chrono::DateTime::from_timestamp_millis(ts.0.into())
                                        .map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
                                        .unwrap_or_default()
                                }).unwrap_or_default(),
                                is_current,
                                is_verified: false, // Would need crypto API to check
                            }
                        }).collect();
                        devices.set(list);
                    }
                    Err(e) => tracing::error!("Failed to load devices: {e}"),
                }
            }
        });
    }

    let devices_read = devices.read();
    let current = devices_read.iter().find(|d| d.is_current);
    let others: Vec<&DeviceInfo> = devices_read.iter().filter(|d| !d.is_current).collect();

    rsx! {
        div {
            class: "session-settings",

            h3 { "Sessions" }

            // Current session
            div {
                class: "settings-section",
                h4 { "Current Session" }
                if let Some(device) = current {
                    div {
                        class: "session-card session-card--current",
                        div { class: "session-card__icon", "💻" }
                        div {
                            class: "session-card__info",
                            span { class: "session-card__name", "{device.display_name}" }
                            span { class: "session-card__detail", "Device ID: {device.device_id}" }
                            if !device.last_seen_ip.is_empty() {
                                span { class: "session-card__detail", "IP: {device.last_seen_ip}" }
                            }
                        }
                        span {
                            class: "session-card__badge session-card__badge--verified",
                            "Current"
                        }
                    }
                } else {
                    p { class: "settings-description", "Loading..." }
                }
            }

            // Other sessions
            div {
                class: "settings-section",
                div {
                    style: "display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;",
                    h4 { "Other Sessions ({others.len()})" }
                    if !selection.read().selected.is_empty() {
                        {
                            let count = selection.read().selected.len();
                            rsx! {
                                button {
                                    class: "btn btn--danger btn--sm",
                                    onclick: move |_| {
                                        let selected = selection.read().selected.clone();
                                        spawn(async move {
                                            let client = { state.read().client.clone() };
                                            if let Some(client) = client {
                                                let device_ids: Vec<matrix_sdk::ruma::OwnedDeviceId> = selected.iter()
                                                    .filter_map(|s| <&str as TryInto<matrix_sdk::ruma::OwnedDeviceId>>::try_into(s.as_str()).ok())
                                                    .collect();
                                                match client.delete_devices(&device_ids, None).await {
                                                    Ok(_) => {
                                                        tracing::info!("Signed out {} devices", selected.len());
                                                        let mut d = devices.write();
                                                        d.retain(|dev| !selected.contains(&dev.device_id));
                                                        selection.write().selected.clear();
                                                    }
                                                    Err(e) => tracing::error!("Bulk sign out failed: {e}"),
                                                }
                                            }
                                        });
                                    },
                                    "Sign out {count} selected"
                                }
                            }
                        }
                    }
                }

                if others.is_empty() {
                    p { class: "settings-description", "No other sessions found." }
                }
                for device in others.iter() {
                    {
                        let did = device.device_id.clone();
                        let did_for_select = device.device_id.clone();
                        let did_for_rename = device.device_id.clone();
                        let dname = device.display_name.clone();
                        let dts = device.last_seen_ts.clone();
                        let dip = device.last_seen_ip.clone();
                        let has_ts = !dts.is_empty();
                        let has_ip = !dip.is_empty();
                        let is_selected = selection.read().selected.contains(&did);
                        let is_verified = device.is_verified;
                        rsx! {
                            div {
                                class: if is_selected { "session-card session-card--selected" } else { "session-card" },
                                style: "display: flex; align-items: center; gap: 12px;",

                                // Checkbox for bulk selection
                                input {
                                    r#type: "checkbox",
                                    checked: is_selected,
                                    onchange: move |_| {
                                        let mut sel = selection.write();
                                        if let Some(pos) = sel.selected.iter().position(|s| s == &did_for_select) {
                                            sel.selected.remove(pos);
                                        } else {
                                            sel.selected.push(did_for_select.clone());
                                        }
                                    },
                                }

                                div { class: "session-card__icon", "📱" }
                                div {
                                    class: "session-card__info",
                                    style: "flex: 1;",
                                    div {
                                        style: "display: flex; align-items: center; gap: 6px;",
                                        span { class: "session-card__name", "{dname}" }
                                        if is_verified {
                                            span { class: "session-card__badge session-card__badge--verified", "Verified" }
                                        } else {
                                            span { class: "session-card__badge session-card__badge--unverified", "Unverified" }
                                        }
                                    }
                                    span { class: "session-card__detail", "Device ID: {did}" }
                                    if has_ts {
                                        span { class: "session-card__detail", "Last seen: {dts}" }
                                    }
                                    if has_ip {
                                        span { class: "session-card__detail", "IP: {dip}" }
                                    }
                                }
                                div {
                                    style: "display: flex; gap: 4px;",
                                    button {
                                        class: "btn btn--secondary btn--sm",
                                        title: "Rename device",
                                        onclick: move |_| {
                                            show_rename.set(Some(did_for_rename.clone()));
                                            rename_value.set(dname.clone());
                                        },
                                        ""
                                    }
                                    button {
                                        class: "btn btn--danger btn--sm",
                                        title: "Sign out this device",
                                        onclick: move |_| {
                                            let device_id_str = did.clone();
                                            spawn(async move {
                                                let client = { state.read().client.clone() };
                                                if let Some(client) = client {
                                                    let device_id = <&str as TryInto<matrix_sdk::ruma::OwnedDeviceId>>::try_into(device_id_str.as_str()).unwrap();
                                                    match client.delete_devices(&[device_id], None).await {
                                                        Ok(_) => {
                                                            tracing::info!("Signed out device {device_id_str}");
                                                            let mut d = devices.write();
                                                            d.retain(|dev| dev.device_id != device_id_str);
                                                        }
                                                        Err(e) => {
                                                            tracing::error!("Failed to sign out device: {e}");
                                                        }
                                                    }
                                                }
                                            });
                                        },
                                        "Sign Out"
                                    }
                                }
                            }
                        }
                    }
                }
            }

            // Rename dialog
            if let Some(ref rename_did) = *show_rename.read() {
                {
                    let rename_did_clone = rename_did.clone();
                    rsx! {
                        div {
                            class: "settings-section",
                            h4 { "Rename Device" }
                            div {
                                style: "display: flex; gap: 8px; align-items: center;",
                                input {
                                    r#type: "text",
                                    value: "{rename_value}",
                                    oninput: move |evt| rename_value.set(evt.value()),
                                    style: "flex: 1; padding: 6px 10px;",
                                }
                                button {
                                    class: "btn btn--primary btn--sm",
                                    onclick: move |_| {
                                        let did = rename_did_clone.clone();
                                        let new_name = rename_value.read().clone();
                                        spawn(async move {
                                            let client = { state.read().client.clone() };
                                            if let Some(client) = client {
                                                let device_id = <&str as TryInto<matrix_sdk::ruma::OwnedDeviceId>>::try_into(did.as_str()).unwrap();
                                                use matrix_sdk::ruma::api::client::device::update_device::v3::Request;
                                                let mut request = Request::new(device_id);
                                                request.display_name = Some(new_name.clone());
                                                match client.send(request).await {
                                                    Ok(_) => {
                                                        tracing::info!("Renamed device {did} to {new_name}");
                                                        let mut d = devices.write();
                                                        if let Some(dev) = d.iter_mut().find(|d| d.device_id == did) {
                                                            dev.display_name = new_name;
                                                        }
                                                    }
                                                    Err(e) => tracing::error!("Rename failed: {e}"),
                                                }
                                            }
                                            show_rename.set(None);
                                        });
                                    },
                                    "Save"
                                }
                                button {
                                    class: "btn btn--secondary btn--sm",
                                    onclick: move |_| show_rename.set(None),
                                    "Cancel"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}