synpad 0.1.0

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

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

/// Dialog for inviting a user to the current room.
#[component]
pub fn InviteDialog(room_id: String, on_close: EventHandler<()>) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut user_id_input = use_signal(|| String::new());
    let mut is_inviting = use_signal(|| false);
    let mut invite_error = use_signal(|| Option::<String>::None);
    let mut invite_success = use_signal(|| Option::<String>::None);

    let rid = room_id.clone();
    let on_invite = move |_| {
        let user_str = user_id_input.read().trim().to_string();
        if user_str.is_empty() {
            invite_error.set(Some("Please enter a user ID".to_string()));
            return;
        }
        is_inviting.set(true);
        invite_error.set(None);
        invite_success.set(None);
        let rid = rid.clone();

        spawn(async move {
            match invite_user(state, &rid, &user_str).await {
                Ok(()) => {
                    tracing::info!("Invited {user_str} to {rid}");
                    invite_success.set(Some(format!("Invited {user_str}")));
                    user_id_input.set(String::new());
                }
                Err(e) => {
                    tracing::error!("Failed to invite: {e}");
                    invite_error.set(Some(e));
                }
            }
            is_inviting.set(false);
        });
    };

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

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

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

                div {
                    class: "modal-dialog__body",

                    if let Some(err) = invite_error.read().as_ref() {
                        div {
                            class: "invite-dialog__error",
                            "{err}"
                        }
                    }

                    if let Some(msg) = invite_success.read().as_ref() {
                        div {
                            class: "invite-dialog__success",
                            "{msg}"
                        }
                    }

                    div {
                        class: "invite-dialog__field",
                        label { "User ID" }
                        input {
                            r#type: "text",
                            placeholder: "@user:server.org",
                            value: "{user_id_input}",
                            oninput: move |evt| user_id_input.set(evt.value()),
                            disabled: *is_inviting.read(),
                            autofocus: true,
                        }
                    }
                }

                div {
                    class: "modal-dialog__footer",
                    button {
                        class: "btn btn--secondary",
                        onclick: move |_| on_close.call(()),
                        "Close"
                    }
                    button {
                        class: "btn btn--primary",
                        onclick: on_invite,
                        disabled: user_id_input.read().trim().is_empty() || *is_inviting.read(),
                        if *is_inviting.read() {
                            "Inviting..."
                        } else {
                            "Invite"
                        }
                    }
                }
            }
        }
    }
}

/// Invite a user to a room via the Matrix SDK.
async fn invite_user(
    state: Signal<AppState>,
    room_id_str: &str,
    user_id_str: &str,
) -> Result<(), String> {
    let client = { state.read().client.clone() };
    let client = client.ok_or_else(|| "Not logged in".to_string())?;

    let room_id: OwnedRoomId = room_id_str
        .try_into()
        .map_err(|e| format!("Invalid room ID: {e}"))?;
    let user_id: OwnedUserId = user_id_str
        .try_into()
        .map_err(|e| format!("Invalid user ID: {e}"))?;

    let room = client
        .get_room(&room_id)
        .ok_or_else(|| format!("Room not found: {room_id}"))?;

    room.invite_user_by_id(&user_id)
        .await
        .map_err(|e| format!("Failed to invite: {e}"))?;

    Ok(())
}