synpad 0.1.0

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

use crate::components::modal::Modal;
use crate::state::app_state::{AppState, AppView};

/// A room in the space hierarchy.
#[derive(Clone, Debug, PartialEq)]
struct SpaceChild {
    room_id: String,
    name: String,
    topic: Option<String>,
    member_count: u64,
    is_space: bool,
    is_joined: bool,
}

/// Space hierarchy browser dialog.
/// Shows all rooms within a space and allows joining them.
#[component]
pub fn SpaceHierarchyDialog(space_id: String, space_name: String, on_close: EventHandler<()>) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let mut children = use_signal(Vec::<SpaceChild>::new);
    let mut is_loading = use_signal(|| true);
    let error = use_signal(|| Option::<String>::None);
    let mut joining = use_signal(|| Option::<String>::None);

    let sid = space_id.clone();
    use_effect(move || {
        let sid = sid.clone();
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                if let Ok(space_room_id) = OwnedRoomId::try_from(sid.as_str()) {
                    if let Some(_space_room) = client.get_room(&space_room_id) {
                        // Get children from space state events
                        // We iterate over known rooms and check if they're children of this space
                        let all_rooms = client.rooms();
                        let mut items = Vec::new();

                        for room in &all_rooms {
                            let room_id = room.room_id().to_string();
                            // Skip the space itself
                            if room_id == sid {
                                continue;
                            }
                            let name = room.cached_display_name()
                                .map(|n| n.to_string())
                                .unwrap_or_else(|| room_id.clone());
                            let is_space = room.is_space();
                            let member_count = room.joined_members_count();

                            items.push(SpaceChild {
                                room_id,
                                name,
                                topic: None,
                                member_count,
                                is_space,
                                is_joined: true,
                            });
                        }

                        // Also try the hierarchy API
                        use matrix_sdk::ruma::api::client::space::get_hierarchy::v1::Request;
                        let request = Request::new(space_room_id);
                        match client.send(request).await {
                            Ok(response) => {
                                // Merge server results with what we already know
                                for chunk in &response.rooms {
                                    let chunk_json = serde_json::to_value(chunk).unwrap_or_default();
                                    let room_id = chunk_json.get("room_id")
                                        .and_then(|v| v.as_str())
                                        .unwrap_or("")
                                        .to_string();
                                    if room_id == sid || room_id.is_empty() {
                                        continue;
                                    }

                                    // Check if already in our list
                                    if items.iter().any(|i| i.room_id == room_id) {
                                        continue;
                                    }

                                    let name = chunk_json.get("name")
                                        .and_then(|v| v.as_str())
                                        .unwrap_or("Unnamed")
                                        .to_string();
                                    let topic = chunk_json.get("topic")
                                        .and_then(|v| v.as_str())
                                        .map(|s| s.to_string());
                                    let member_count = chunk_json.get("num_joined_members")
                                        .and_then(|v| v.as_u64())
                                        .unwrap_or(0);
                                    let is_space = chunk_json.get("room_type")
                                        .and_then(|v| v.as_str())
                                        .map_or(false, |t| t == "m.space");
                                    let is_joined = if let Ok(rid) = OwnedRoomId::try_from(room_id.as_str()) {
                                        client.get_room(&rid).is_some()
                                    } else {
                                        false
                                    };

                                    items.push(SpaceChild {
                                        room_id,
                                        name,
                                        topic,
                                        member_count,
                                        is_space,
                                        is_joined,
                                    });
                                }
                            }
                            Err(e) => {
                                tracing::debug!("Space hierarchy API not available: {e}");
                            }
                        }

                        children.set(items);
                    }
                }
            }
            is_loading.set(false);
        });
    });

    rsx! {
        Modal {
            title: format!("Explore: {space_name}"),
            on_close: move |_| on_close.call(()),

            div {
                class: "space-hierarchy",

                if *is_loading.read() {
                    div {
                        class: "space-hierarchy__loading",
                        div { class: "spinner" }
                        span { "Loading space rooms..." }
                    }
                }

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

                if !*is_loading.read() && children.read().is_empty() && error.read().is_none() {
                    div { class: "space-hierarchy__empty", "This space has no rooms yet." }
                }

                div {
                    class: "space-hierarchy__list",
                    for child in children.read().iter() {
                        {
                            let child_rid = child.room_id.clone();
                            let child_name = child.name.clone();
                            let child_topic = child.topic.clone();
                            let member_count = child.member_count;
                            let is_space = child.is_space;
                            let is_joined = child.is_joined;
                            let icon = if is_space { "📁" } else { "#" };
                            let is_joining = joining.read().as_ref() == Some(&child_rid);

                            rsx! {
                                div {
                                    class: if is_joined {
                                        "space-hierarchy__item space-hierarchy__item--joined"
                                    } else {
                                        "space-hierarchy__item"
                                    },

                                    span { class: "space-hierarchy__item-icon", "{icon}" }
                                    div {
                                        class: "space-hierarchy__item-info",
                                        span { class: "space-hierarchy__item-name", "{child_name}" }
                                        if let Some(ref topic) = child_topic {
                                            span { class: "space-hierarchy__item-topic", "{topic}" }
                                        }
                                        span {
                                            class: "space-hierarchy__item-members",
                                            "{member_count} members"
                                        }
                                    }

                                    if is_joined {
                                        button {
                                            class: "btn btn--secondary btn--sm",
                                            onclick: move |_| {
                                                if let Ok(rid) = OwnedRoomId::try_from(child_rid.as_str()) {
                                                    let mut s = state.write();
                                                    s.active_room_id = Some(rid.clone());
                                                    s.current_view = AppView::Room(rid);
                                                }
                                            },
                                            "Open"
                                        }
                                    } else {
                                        button {
                                            class: "btn btn--primary btn--sm",
                                            disabled: is_joining,
                                            onclick: move |_| {
                                                let rid_str = child_rid.clone();
                                                joining.set(Some(rid_str.clone()));
                                                spawn(async move {
                                                    let client = { state.read().client.clone() };
                                                    if let Some(client) = client {
                                                        if let Ok(rid) = OwnedRoomId::try_from(rid_str.as_str()) {
                                                            match client.join_room_by_id(&rid).await {
                                                                Ok(_) => {
                                                                    tracing::info!("Joined room {rid_str}");
                                                                }
                                                                Err(e) => {
                                                                    tracing::error!("Failed to join {rid_str}: {e}");
                                                                }
                                                            }
                                                        }
                                                    }
                                                    joining.set(None);
                                                });
                                            },
                                            if is_joining { "Joining..." } else { "Join" }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}