synpad 0.1.0

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

use crate::components::avatar::Avatar;
use crate::state::app_state::{AppState, AppView};

/// Space child room entry for the overview page.
#[derive(Clone, PartialEq)]
struct SpaceRoomEntry {
    room_id: String,
    display_name: String,
    avatar_url: Option<String>,
    topic: Option<String>,
    member_count: u64,
    is_joined: bool,
}

/// Space home/overview page shown when a space is selected.
#[component]
pub fn SpaceHome(space_id: String) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let mut child_rooms = use_signal(Vec::<SpaceRoomEntry>::new);
    let mut is_loading = use_signal(|| true);

    // Get space info
    let state_read = state.read();
    let space_rid: Option<OwnedRoomId> = space_id.as_str().try_into().ok();
    let space_name = space_rid.as_ref()
        .and_then(|rid| state_read.rooms.get(rid))
        .map(|r| r.display_name.clone())
        .unwrap_or_else(|| "Space".to_string());
    let space_topic = space_rid.as_ref()
        .and_then(|rid| state_read.rooms.get(rid))
        .and_then(|r| r.topic.clone());
    let space_avatar = space_rid.as_ref()
        .and_then(|rid| state_read.rooms.get(rid))
        .and_then(|r| r.avatar_url.clone());
    drop(state_read);

    // Load child rooms
    let load_sid = space_id.clone();
    use_effect(move || {
        let sid = load_sid.clone();
        spawn(async move {
            let s = state.read();
            let Ok(space_room_id) = <&str as TryInto<OwnedRoomId>>::try_into(sid.as_str()) else {
                is_loading.set(false);
                return;
            };

            // Collect rooms that belong to this space
            let mut entries = Vec::new();
            for (rid, room) in s.rooms.iter() {
                if room.parent_spaces.contains(&space_room_id) {
                    entries.push(SpaceRoomEntry {
                        room_id: rid.to_string(),
                        display_name: room.display_name.clone(),
                        avatar_url: room.avatar_url.clone(),
                        topic: room.topic.clone(),
                        member_count: room.member_count,
                        is_joined: true,
                    });
                }
            }

            // Also try loading hierarchy from SDK
            let client_clone = s.client.clone();
            drop(s);
            if let Some(client) = client_clone {
                // Use the space hierarchy API to discover child rooms
                use matrix_sdk::ruma::api::client::space::get_hierarchy::v1::Request as HierarchyRequest;
                let mut request = HierarchyRequest::new(space_room_id.clone());
                request.limit = Some(50u32.into());

                match client.send(request).await {
                    Ok(response) => {
                        for child in response.rooms {
                            let child_id = child.summary.room_id.to_string();
                            // Skip the space itself and already-known rooms
                            if child_id == sid || entries.iter().any(|e| e.room_id == child_id) {
                                continue;
                            }
                            entries.push(SpaceRoomEntry {
                                room_id: child_id,
                                display_name: child.summary.name.unwrap_or_else(|| child.summary.room_id.to_string()),
                                avatar_url: child.summary.avatar_url.map(|u: matrix_sdk::ruma::OwnedMxcUri| u.to_string()),
                                topic: child.summary.topic,
                                member_count: child.summary.num_joined_members.into(),
                                is_joined: client.get_room(&child.summary.room_id).is_some(),
                            });
                        }
                    }
                    Err(e) => {
                        tracing::warn!("Failed to load space hierarchy for {sid}: {e}");
                    }
                }
            }

            // Sort by name
            entries.sort_by(|a, b| a.display_name.to_lowercase().cmp(&b.display_name.to_lowercase()));
            child_rooms.set(entries);
            is_loading.set(false);
        });
    });

    rsx! {
        div {
            class: "space-home",
            style: "display: flex; flex-direction: column; height: 100%; overflow-y: auto;",

            // Space header
            div {
                class: "space-home__header",
                style: "padding: 32px 40px; border-bottom: 1px solid var(--border-color, #333); text-align: center;",

                div {
                    style: "display: flex; justify-content: center; margin-bottom: 16px;",
                    Avatar {
                        name: space_name.clone(),
                        url: space_avatar,
                        size: 80,
                    }
                }

                h1 {
                    style: "margin: 0 0 8px 0; font-size: 24px; color: var(--text-primary, #e0e0e0);",
                    "{space_name}"
                }

                if let Some(ref topic) = space_topic {
                    p {
                        style: "margin: 0; color: var(--text-secondary, #888); font-size: 14px; max-width: 500px; margin: 0 auto;",
                        "{topic}"
                    }
                }
            }

            // Room list
            div {
                class: "space-home__content",
                style: "padding: 24px 40px; flex: 1;",

                h2 {
                    style: "font-size: 18px; margin-bottom: 16px; color: var(--text-primary, #e0e0e0);",
                    "Rooms"
                }

                if *is_loading.read() {
                    div {
                        style: "text-align: center; padding: 24px; color: var(--text-secondary, #888);",
                        div { class: "spinner spinner--small" }
                        p { "Loading rooms..." }
                    }
                }

                if !*is_loading.read() && child_rooms.read().is_empty() {
                    div {
                        style: "text-align: center; padding: 32px; color: var(--text-secondary, #888);",
                        p { style: "font-size: 16px; margin-bottom: 8px;", "No rooms in this space yet." }
                        p { style: "font-size: 13px;", "Add rooms to this space from the space settings (right-click on the space icon)." }
                    }
                }

                div {
                    class: "space-home__room-grid",
                    style: "display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px;",

                    for room in child_rooms.read().iter() {
                        {
                            let rid = room.room_id.clone();
                            let rname = room.display_name.clone();
                            let ravatar = room.avatar_url.clone();
                            let rtopic = room.topic.clone();
                            let rmembers = room.member_count;
                            let topic_display = rtopic.clone().unwrap_or_default();
                            let has_topic = rtopic.is_some();
                            rsx! {
                                div {
                                    class: "space-home__room-card",
                                    style: "padding: 16px; border: 1px solid var(--border-color, #333); border-radius: 8px; cursor: pointer; transition: background 0.15s;",
                                    onclick: move |_| {
                                        if let Ok(room_id) = OwnedRoomId::try_from(rid.as_str()) {
                                            let mut s = state.write();
                                            s.active_room_id = Some(room_id.clone());
                                            s.current_view = AppView::Room(room_id);
                                        }
                                    },
                                    div {
                                        style: "display: flex; align-items: center; gap: 12px; margin-bottom: 8px;",
                                        Avatar {
                                            name: rname.clone(),
                                            url: ravatar,
                                            size: 40,
                                        }
                                        div {
                                            h3 {
                                                style: "margin: 0; font-size: 15px; color: var(--text-primary, #e0e0e0);",
                                                "{rname}"
                                            }
                                            span {
                                                style: "font-size: 12px; color: var(--text-secondary, #888);",
                                                "{rmembers} members"
                                            }
                                        }
                                    }
                                    if has_topic {
                                        p {
                                            style: "margin: 0; font-size: 13px; color: var(--text-secondary, #888); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;",
                                            "{topic_display}"
                                        }
                                    }
                                    if !room.is_joined {
                                        {
                                            let join_rid = room.room_id.clone();
                                            rsx! {
                                                button {
                                                    class: "btn btn--primary btn--sm",
                                                    style: "margin-top: 8px;",
                                                    onclick: move |evt| {
                                                        evt.stop_propagation();
                                                        let rid = join_rid.clone();
                                                        spawn(async move {
                                                            let client = { state.read().client.clone() };
                                                            if let Some(client) = client {
                                                                if let Ok(room_id) = OwnedRoomId::try_from(rid.as_str()) {
                                                                    match client.join_room_by_id(&room_id).await {
                                                                        Ok(response) => {
                                                                            tracing::info!("Joined space room: {}", response.room_id());
                                                                            state.write().active_room_id = Some(response.room_id().to_owned());
                                                                        }
                                                                        Err(e) => tracing::error!("Failed to join: {e}"),
                                                                    }
                                                                }
                                                            }
                                                        });
                                                    },
                                                    "Join"
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}