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::components::spinner::Spinner;
use crate::state::app_state::AppState;

/// Data for a public room from the directory.
#[derive(Clone, Debug)]
struct PublicRoomEntry {
    room_id: String,
    name: String,
    topic: Option<String>,
    avatar_url: Option<String>,
    member_count: u64,
    is_world_readable: bool,
    alias: Option<String>,
}

/// Room directory page - browse and join public rooms.
#[component]
pub fn RoomDirectory() -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let mut rooms = use_signal(Vec::<PublicRoomEntry>::new);
    let mut search_query = use_signal(|| String::new());
    let mut is_loading = use_signal(|| false);
    let mut loaded = use_signal(|| false);
    let mut error_msg = use_signal(|| Option::<String>::None);
    let mut joining_room = use_signal(|| Option::<String>::None);

    // Load room directory on mount
    if !*loaded.read() {
        loaded.set(true);
        is_loading.set(true);
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                match load_public_rooms(&client, None).await {
                    Ok(entries) => rooms.set(entries),
                    Err(e) => error_msg.set(Some(e)),
                }
            }
            is_loading.set(false);
        });
    }

    let mut do_search = move || {
        let query = search_query.read().clone();
        let query_opt = if query.trim().is_empty() { None } else { Some(query) };
        is_loading.set(true);
        error_msg.set(None);
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                match load_public_rooms(&client, query_opt.as_deref()).await {
                    Ok(entries) => rooms.set(entries),
                    Err(e) => error_msg.set(Some(e)),
                }
            }
            is_loading.set(false);
        });
    };

    let close_directory = move |_| {
        state.write().active_room_id = None;
    };

    let rooms_read = rooms.read();
    let has_error = error_msg.read().is_some();
    let err_text = error_msg.read().clone().unwrap_or_default();

    rsx! {
        div {
            class: "room-directory",

            header {
                class: "room-directory__header",
                h2 { "Room Directory" }
                button {
                    class: "room-directory__close-btn",
                    onclick: close_directory,
                    "✕"
                }
            }

            // Search bar
            div {
                class: "room-directory__search",
                input {
                    r#type: "text",
                    class: "room-directory__search-input",
                    placeholder: "Search public rooms...",
                    value: "{search_query}",
                    oninput: move |evt| search_query.set(evt.value()),
                    onkeydown: move |evt: Event<KeyboardData>| {
                        if evt.key() == Key::Enter {
                            do_search();
                        }
                    },
                }
                button {
                    class: "btn btn--primary",
                    onclick: move |_| do_search(),
                    "Search"
                }
            }

            if has_error {
                div {
                    class: "room-directory__error",
                    "{err_text}"
                }
            }

            // Results
            div {
                class: "room-directory__list",

                if *is_loading.read() {
                    div {
                        class: "room-directory__loading",
                        Spinner {}
                        span { "Loading rooms..." }
                    }
                }

                if !*is_loading.read() && rooms_read.is_empty() {
                    div {
                        class: "room-directory__empty",
                        p { "No public rooms found." }
                    }
                }

                for entry in rooms_read.iter() {
                    {
                        let room_id = entry.room_id.clone();
                        let entry_name = entry.name.clone();
                        let entry_topic = entry.topic.clone();
                        let entry_avatar = entry.avatar_url.clone();
                        let entry_members = entry.member_count;
                        let entry_alias = entry.alias.clone();
                        let is_joining = joining_room.read().as_deref() == Some(room_id.as_str());
                        rsx! {
                            div {
                                class: "room-directory__entry",
                                key: "{room_id}",
                                Avatar {
                                    name: entry_name.clone(),
                                    url: entry_avatar,
                                    size: 40,
                                }
                                div {
                                    class: "room-directory__entry-info",
                                    span {
                                        class: "room-directory__entry-name",
                                        "{entry_name}"
                                    }
                                    if let Some(ref alias) = entry_alias {
                                        span {
                                            class: "room-directory__entry-alias",
                                            "{alias}"
                                        }
                                    }
                                    if let Some(ref topic) = entry_topic {
                                        span {
                                            class: "room-directory__entry-topic",
                                            "{topic}"
                                        }
                                    }
                                    span {
                                        class: "room-directory__entry-members",
                                        "{entry_members} members"
                                    }
                                }
                                button {
                                    class: "btn btn--primary btn--sm",
                                    disabled: is_joining,
                                    onclick: move |_| {
                                        let rid = room_id.clone();
                                        joining_room.set(Some(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 room {}", response.room_id());
                                                            state.write().active_room_id = Some(response.room_id().to_owned());
                                                        }
                                                        Err(e) => {
                                                            tracing::error!("Failed to join room: {e}");
                                                        }
                                                    }
                                                }
                                            }
                                            joining_room.set(None);
                                        });
                                    },
                                    if is_joining { "Joining..." } else { "Join" }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

/// Load public rooms from the homeserver directory.
async fn load_public_rooms(
    client: &matrix_sdk::Client,
    search_term: Option<&str>,
) -> Result<Vec<PublicRoomEntry>, String> {
    use matrix_sdk::ruma::api::client::directory::get_public_rooms_filtered::v3::Request;
    use matrix_sdk::ruma::directory::Filter;
    use matrix_sdk::ruma::uint;

    let mut filter = Filter::new();
    if let Some(term) = search_term {
        filter.generic_search_term = Some(term.to_owned());
    }

    let mut request = Request::new();
    request.filter = filter;
    request.limit = Some(uint!(50));

    match client.send(request).await {
        Ok(response) => {
            let entries: Vec<PublicRoomEntry> = response
                .chunk
                .into_iter()
                .map(|room| PublicRoomEntry {
                    room_id: room.room_id.to_string(),
                    name: room
                        .name
                        .unwrap_or_else(|| room.room_id.to_string()),
                    topic: room.topic,
                    avatar_url: room.avatar_url.map(|u| u.to_string()),
                    member_count: room.num_joined_members.into(),
                    is_world_readable: room.world_readable,
                    alias: room.canonical_alias.map(|a| a.to_string()),
                })
                .collect();
            Ok(entries)
        }
        Err(e) => Err(format!("Failed to load room directory: {e}")),
    }
}