synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use dioxus::prelude::*;
use matrix_sdk::ruma::api::client::room::create_room::v3::Request as CreateRoomRequest;
use matrix_sdk::ruma::api::client::room::Visibility;
use matrix_sdk::ruma::OwnedRoomId;

use crate::components::modal::Modal;
use crate::state::app_state::AppState;

/// Dialog for creating a new space.
#[component]
pub fn CreateSpaceDialog(on_close: EventHandler<()>) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut space_name = use_signal(|| String::new());
    let mut space_topic = use_signal(|| String::new());
    let mut is_public = use_signal(|| false);
    let mut is_creating = use_signal(|| false);
    let mut create_error = use_signal(|| Option::<String>::None);
    let mut create_success = use_signal(|| false);

    let on_create = move |_| {
        let name = space_name.read().clone();
        if name.trim().is_empty() {
            create_error.set(Some("Space name is required".to_string()));
            return;
        }
        is_creating.set(true);
        create_error.set(None);
        let topic = space_topic.read().clone();
        let public = *is_public.read();

        spawn(async move {
            match create_space(state, &name, &topic, public).await {
                Ok(room_id) => {
                    tracing::info!("Space created: {room_id}");
                    create_success.set(true);
                }
                Err(e) => {
                    tracing::error!("Failed to create space: {e}");
                    create_error.set(Some(e));
                }
            }
            is_creating.set(false);
        });
    };

    let is_busy = *is_creating.read();
    let succeeded = *create_success.read();
    let err_msg = create_error.read().clone();

    rsx! {
        Modal {
            title: "Create Space".to_string(),
            on_close: move |_| on_close.call(()),

            div {
                class: "create-space-dialog",

                if succeeded {
                    div {
                        style: "padding: 16px; text-align: center;",
                        p {
                            style: "color: var(--success-text, #4caf50); font-size: 16px; margin-bottom: 12px;",
                            "Space created successfully!"
                        }
                        button {
                            style: "padding: 8px 20px; border: none; border-radius: 6px; background: var(--accent-color, #4a9eff); color: white; cursor: pointer;",
                            onclick: move |_| on_close.call(()),
                            "Close"
                        }
                    }
                }

                if !succeeded {
                    div {
                        class: "create-space-dialog__form",

                        // Error display
                        if let Some(ref err) = err_msg {
                            div {
                                style: "padding: 8px 12px; background: var(--error-bg, #3a1a1a); color: var(--error-text, #f44336); border-radius: 6px; margin-bottom: 12px;",
                                "{err}"
                            }
                        }

                        // Space name
                        div {
                            style: "margin-bottom: 16px;",
                            label {
                                style: "display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary, #888);",
                                "Space Name"
                            }
                            input {
                                style: "width: 100%; padding: 8px 12px; border: 1px solid var(--border-color, #333); border-radius: 6px; background: var(--bg-primary, #0d0d1a); color: var(--text-primary, #e0e0e0); font-size: 14px;",
                                r#type: "text",
                                placeholder: "e.g. My Team",
                                value: "{space_name}",
                                oninput: move |evt| space_name.set(evt.value()),
                                disabled: is_busy,
                                autofocus: true,
                            }
                        }

                        // Space topic
                        div {
                            style: "margin-bottom: 16px;",
                            label {
                                style: "display: block; font-size: 13px; font-weight: 600; margin-bottom: 4px; color: var(--text-secondary, #888);",
                                "Topic (optional)"
                            }
                            input {
                                style: "width: 100%; padding: 8px 12px; border: 1px solid var(--border-color, #333); border-radius: 6px; background: var(--bg-primary, #0d0d1a); color: var(--text-primary, #e0e0e0); font-size: 14px;",
                                r#type: "text",
                                placeholder: "What is this space about?",
                                value: "{space_topic}",
                                oninput: move |evt| space_topic.set(evt.value()),
                                disabled: is_busy,
                            }
                        }

                        // Options
                        div {
                            style: "margin-bottom: 16px;",
                            label {
                                style: "display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px;",
                                input {
                                    r#type: "checkbox",
                                    checked: *is_public.read(),
                                    oninput: move |evt| is_public.set(evt.value() == "true"),
                                }
                                span { "Public space (anyone can find and join)" }
                            }
                        }

                        // Action buttons
                        div {
                            style: "display: flex; justify-content: flex-end; gap: 8px;",
                            button {
                                style: "padding: 8px 16px; border-radius: 6px; border: 1px solid var(--border-color, #333); background: transparent; color: var(--text-primary, #e0e0e0); cursor: pointer;",
                                onclick: move |_| on_close.call(()),
                                disabled: is_busy,
                                "Cancel"
                            }
                            button {
                                style: "padding: 8px 16px; border-radius: 6px; border: none; background: var(--accent-color, #4a9eff); color: white; cursor: pointer;",
                                onclick: on_create,
                                disabled: space_name.read().trim().is_empty() || is_busy,
                                if is_busy { "Creating..." } else { "Create Space" }
                            }
                        }
                    }
                }
            }
        }
    }
}

/// Create a new space via the Matrix SDK.
async fn create_space(
    state: Signal<AppState>,
    name: &str,
    topic: &str,
    is_public: bool,
) -> Result<OwnedRoomId, String> {
    let client = { state.read().client.clone() };
    let client = client.ok_or_else(|| "Not logged in".to_string())?;

    let mut request = CreateRoomRequest::new();
    request.name = Some(name.to_string());
    if !topic.is_empty() {
        request.topic = Some(topic.to_string());
    }
    request.visibility = if is_public {
        Visibility::Public
    } else {
        Visibility::Private
    };

    // Set room type to space via creation_content
    let creation_content = serde_json::json!({
        "type": "m.space"
    });
    request.creation_content = Some(matrix_sdk::ruma::serde::Raw::from_json(
        serde_json::value::to_raw_value(&creation_content)
            .map_err(|e| format!("Failed to serialize creation content: {e}"))?
    ));

    let response = client
        .create_room(request)
        .await
        .map_err(|e| format!("Failed to create space: {e}"))?;

    Ok(response.room_id().to_owned())
}