synpad 0.1.0

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

use crate::state::app_state::{AppState, AppView};

/// Dialog for joining an existing room by ID or alias.
#[component]
pub fn JoinRoomDialog(on_close: EventHandler<()>) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let mut room_address = use_signal(|| String::new());
    let mut is_joining = use_signal(|| false);
    let mut join_error = use_signal(|| Option::<String>::None);

    let on_join = move |_| {
        let address = room_address.read().trim().to_string();
        if address.is_empty() {
            join_error.set(Some("Please enter a room ID or alias".to_string()));
            return;
        }
        is_joining.set(true);
        join_error.set(None);

        spawn(async move {
            match join_room(state, &address).await {
                Ok(room_id) => {
                    tracing::info!("Joined room: {room_id}");
                    let mut s = state.write();
                    s.active_room_id = Some(room_id.clone());
                    s.current_view = AppView::Room(room_id);
                    drop(s);
                    on_close.call(());
                }
                Err(e) => {
                    tracing::error!("Failed to join room: {e}");
                    join_error.set(Some(e));
                }
            }
            is_joining.set(false);
        });
    };

    let on_keydown = move |evt: Event<KeyboardData>| {
        if evt.key() == Key::Enter {
            evt.prevent_default();
            let address = room_address.read().trim().to_string();
            if address.is_empty() {
                return;
            }
            is_joining.set(true);
            join_error.set(None);

            spawn(async move {
                match join_room(state, &address).await {
                    Ok(room_id) => {
                        let mut s = state.write();
                        s.active_room_id = Some(room_id.clone());
                        s.current_view = AppView::Room(room_id);
                        drop(s);
                        on_close.call(());
                    }
                    Err(e) => {
                        join_error.set(Some(e));
                    }
                }
                is_joining.set(false);
            });
        }
    };

    rsx! {
        div {
            class: "modal-overlay",
            onclick: move |_| on_close.call(()),

            div {
                class: "modal-dialog join-room-dialog",
                onclick: move |evt| evt.stop_propagation(),

                div {
                    class: "modal-dialog__header",
                    h2 { "Join Room" }
                    button {
                        class: "modal-dialog__close",
                        onclick: move |_| on_close.call(()),
                        ""
                    }
                }

                div {
                    class: "modal-dialog__body",

                    if let Some(err) = join_error.read().as_ref() {
                        div {
                            class: "join-room-dialog__error",
                            "{err}"
                        }
                    }

                    div {
                        class: "join-room-dialog__field",
                        label { "Room ID or Alias" }
                        input {
                            r#type: "text",
                            placeholder: "e.g. !roomid:server.org or #alias:server.org",
                            value: "{room_address}",
                            oninput: move |evt| room_address.set(evt.value()),
                            onkeydown: on_keydown,
                            disabled: *is_joining.read(),
                            autofocus: true,
                        }
                    }

                    p {
                        class: "join-room-dialog__hint",
                        "Enter a room ID (starting with !) or a room alias (starting with #)"
                    }
                }

                div {
                    class: "modal-dialog__footer",
                    button {
                        class: "btn btn--secondary",
                        onclick: move |_| on_close.call(()),
                        disabled: *is_joining.read(),
                        "Cancel"
                    }
                    button {
                        class: "btn btn--primary",
                        onclick: on_join,
                        disabled: room_address.read().trim().is_empty() || *is_joining.read(),
                        if *is_joining.read() {
                            "Joining..."
                        } else {
                            "Join"
                        }
                    }
                }
            }
        }
    }
}

/// Join a room by ID or alias via the Matrix SDK.
async fn join_room(
    state: Signal<AppState>,
    address: &str,
) -> Result<OwnedRoomId, String> {
    let client = { state.read().client.clone() };
    let client = client.ok_or_else(|| "Not logged in".to_string())?;

    // Try to join by room ID or alias
    let room = if address.starts_with('!') {
        // Room ID
        let room_id: OwnedRoomId = address
            .try_into()
            .map_err(|e| format!("Invalid room ID: {e}"))?;
        client
            .join_room_by_id(&room_id)
            .await
            .map_err(|e| format!("Failed to join room: {e}"))?
    } else if address.starts_with('#') {
        // Room alias
        use matrix_sdk::ruma::OwnedRoomOrAliasId;
        let alias: OwnedRoomOrAliasId = address
            .try_into()
            .map_err(|e| format!("Invalid room alias: {e}"))?;
        client
            .join_room_by_id_or_alias(alias.as_ref(), &[])
            .await
            .map_err(|e| format!("Failed to join room: {e}"))?
    } else {
        return Err("Address must start with ! (room ID) or # (room alias)".to_string());
    };

    Ok(room.room_id().to_owned())
}