synpad 0.1.0

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

use matrix_sdk::deserialized_responses::RawAnySyncOrStrippedState;
use matrix_sdk::ruma::OwnedRoomId;

use crate::components::modal::Modal;
use crate::state::app_state::{AppState, AppView};

/// Available room versions for upgrade.
static ROOM_VERSIONS: &[(&str, &str)] = &[
    ("1", "Version 1 (original)"),
    ("2", "Version 2 (state resolution v2)"),
    ("3", "Version 3 (event ID format change)"),
    ("4", "Version 4 (URL-safe base64 event IDs)"),
    ("5", "Version 5 (enforce integer power levels)"),
    ("6", "Version 6 (strict JSON canonicalization)"),
    ("7", "Version 7 (knock support)"),
    ("8", "Version 8 (restricted join rules)"),
    ("9", "Version 9 (updated restricted join rules)"),
    ("10", "Version 10 (whole event authorization)"),
    ("11", "Version 11 (latest stable)"),
];

/// State for the upgrade process.
#[derive(Clone, Debug, PartialEq)]
enum UpgradeStep {
    /// Selecting target version
    SelectVersion,
    /// Confirming the upgrade
    Confirm,
    /// Upgrade in progress
    Upgrading,
    /// Upgrade complete
    Done(String),
    /// Upgrade failed
    Error(String),
}

/// Dialog for upgrading a room to a newer room version.
#[component]
pub fn RoomUpgradeDialog(room_id: String, on_close: EventHandler<()>) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let mut step = use_signal(|| UpgradeStep::SelectVersion);
    let mut target_version = use_signal(|| "11".to_string());
    let mut current_version = use_signal(|| "loading...".to_string());

    let rid_for_load = room_id.clone();
    use_effect(move || {
        let rid = rid_for_load.clone();
        spawn(async move {
            let version = load_current_room_version(state, &rid)
                .await
                .unwrap_or_else(|_| "unknown".to_string());
            current_version.set(version);
        });
    });

    let current_step = step.read().clone();
    let selected_version = target_version.read().clone();
    let current_room_version = current_version.read().clone();
    let rid = room_id.clone();

    let on_confirm = move |_| {
        step.set(UpgradeStep::Upgrading);
        let rid = rid.clone();
        let version = target_version.read().clone();

        spawn(async move {
            let result = perform_room_upgrade(state, &rid, &version).await;
            match result {
                Ok(new_room_id) => {
                    step.set(UpgradeStep::Done(new_room_id));
                }
                Err(e) => {
                    step.set(UpgradeStep::Error(e));
                }
            }
        });
    };

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

            div {
                class: "room-upgrade-dialog",

                match current_step {
                    UpgradeStep::SelectVersion => rsx! {
                        div {
                            class: "room-upgrade-dialog__section",
                            p {
                                class: "room-upgrade-dialog__current",
                                "Current room version: "
                                strong { "{current_room_version}" }
                            }
                        }

                        div {
                            class: "room-upgrade-dialog__section",
                            label {
                                class: "room-upgrade-dialog__label",
                                "Upgrade to version:"
                            }
                            select {
                                class: "room-upgrade-dialog__select settings-input",
                                value: "{selected_version}",
                                oninput: move |evt| target_version.set(evt.value()),
                                for (ver, desc) in ROOM_VERSIONS.iter() {
                                    {
                                        let ver_str = ver.to_string();
                                        let desc_str = desc.to_string();
                                        rsx! {
                                            option {
                                                value: "{ver_str}",
                                                "{desc_str}"
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        div {
                            class: "room-upgrade-dialog__actions",
                            button {
                                class: "btn btn--secondary",
                                onclick: move |_| on_close.call(()),
                                "Cancel"
                            }
                            button {
                                class: "btn btn--primary",
                                onclick: move |_| step.set(UpgradeStep::Confirm),
                                "Next"
                            }
                        }
                    },

                    UpgradeStep::Confirm => rsx! {
                        div {
                            class: "room-upgrade-dialog__warning",
                            div {
                                class: "room-upgrade-dialog__warning-icon",
                                "Warning"
                            }
                            div {
                                class: "room-upgrade-dialog__warning-text",
                                p {
                                    "Upgrading this room will create a new room with the selected version. "
                                    "The old room will be marked as replaced."
                                }
                                p {
                                    "All members will be invited to the new room automatically. "
                                    "However, some older clients may not follow the upgrade link."
                                }
                                p {
                                    strong { "This action cannot be undone." }
                                }
                            }
                        }

                        div {
                            class: "room-upgrade-dialog__summary",
                            p {
                                "Upgrading from version "
                                strong { "{current_room_version}" }
                                " to version "
                                strong { "{selected_version}" }
                            }
                        }

                        div {
                            class: "room-upgrade-dialog__actions",
                            button {
                                class: "btn btn--secondary",
                                onclick: move |_| step.set(UpgradeStep::SelectVersion),
                                "Back"
                            }
                            button {
                                class: "btn btn--danger",
                                onclick: on_confirm,
                                "Upgrade Room"
                            }
                        }
                    },

                    UpgradeStep::Upgrading => rsx! {
                        div {
                            class: "room-upgrade-dialog__progress",
                            div { class: "spinner" }
                            p { "Upgrading room... This may take a moment." }
                        }
                    },

                    UpgradeStep::Done(ref new_room_id) => {
                        let new_id = new_room_id.clone();
                        rsx! {
                            div {
                                class: "room-upgrade-dialog__success",
                                div {
                                    class: "room-upgrade-dialog__success-icon",
                                    "Room Upgraded"
                                }
                                p { "The room has been successfully upgraded." }
                                p {
                                    class: "room-upgrade-dialog__new-room-id",
                                    "New room: "
                                    code { "{new_id}" }
                                }
                            }

                            div {
                                class: "room-upgrade-dialog__actions",
                                button {
                                    class: "btn btn--secondary",
                                    onclick: move |_| on_close.call(()),
                                    "Close"
                                }
                                button {
                                    class: "btn btn--primary",
                                    onclick: {
                                        let new_id = new_id.clone();
                                        move |_| {
                                            if let Ok(room_id) = OwnedRoomId::try_from(new_id.as_str()) {
                                                let mut s = state.write();
                                                s.active_room_id = Some(room_id.clone());
                                                s.current_view = AppView::Room(room_id);
                                            }
                                            on_close.call(());
                                        }
                                    },
                                    "Join New Room"
                                }
                            }
                        }
                    },

                    UpgradeStep::Error(ref error_msg) => {
                        let err = error_msg.clone();
                        rsx! {
                            div {
                                class: "room-upgrade-dialog__error",
                                p {
                                    class: "room-upgrade-dialog__error-text",
                                    "Failed to upgrade room: {err}"
                                }
                            }

                            div {
                                class: "room-upgrade-dialog__actions",
                                button {
                                    class: "btn btn--secondary",
                                    onclick: move |_| step.set(UpgradeStep::SelectVersion),
                                    "Try Again"
                                }
                                button {
                                    class: "btn btn--secondary",
                                    onclick: move |_| on_close.call(()),
                                    "Close"
                                }
                            }
                        }
                    },
                }
            }
        }
    }
}

/// Perform the room upgrade via the Matrix API.
pub async fn perform_room_upgrade(
    state: Signal<AppState>,
    room_id_str: &str,
    new_version: &str,
) -> Result<String, 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 _room = client
        .get_room(&room_id)
        .ok_or_else(|| format!("Room not found: {room_id}"))?;

    // Use the room upgrade API endpoint
    use matrix_sdk::ruma::api::client::room::upgrade_room::v3::Request;
    use matrix_sdk::ruma::RoomVersionId;
    let version_id = RoomVersionId::try_from(new_version)
        .map_err(|e| format!("Invalid room version: {e}"))?;
    let request = Request::new(room_id.clone(), version_id);
    match client.send(request).await {
        Ok(response) => {
            let new_room_id = response.replacement_room.to_string();
            tracing::info!(
                "Room {} upgraded to version {}, new room: {}",
                room_id, new_version, new_room_id
            );
            Ok(new_room_id)
        }
        Err(e) => {
            tracing::error!("Room upgrade failed: {e}");
            Err(format!("Room upgrade failed: {e}"))
        }
    }
}

async fn load_current_room_version(
    state: Signal<AppState>,
    room_id_str: &str,
) -> Result<String, 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 room = client
        .get_room(&room_id)
        .ok_or_else(|| format!("Room not found: {room_id}"))?;

    let Some(raw) = room
        .get_state_event(matrix_sdk::ruma::events::StateEventType::RoomCreate, "")
        .await
        .map_err(|e| format!("Failed to read room version: {e}"))?
    else {
        return Ok("unknown".to_string());
    };

    let value = raw_state_event_json(raw)?;
    Ok(value
        .get("content")
        .and_then(|content| content.get("room_version"))
        .and_then(|version| version.as_str())
        .unwrap_or("1")
        .to_string())
}

fn raw_state_event_json(raw: RawAnySyncOrStrippedState) -> Result<serde_json::Value, String> {
    match raw {
        RawAnySyncOrStrippedState::Sync(raw) => raw
            .deserialize_as::<serde_json::Value>()
            .map_err(|e| format!("Failed to parse room state: {e}")),
        RawAnySyncOrStrippedState::Stripped(raw) => raw
            .deserialize_as::<serde_json::Value>()
            .map_err(|e| format!("Failed to parse room state: {e}")),
    }
}

/// Banner component shown when a room has been upgraded (tombstone event).
/// This is displayed at the top of the timeline when the room has a tombstone.
#[component]
pub fn RoomUpgradeBanner(
    successor_room_id: Option<String>,
    tombstone_body: Option<String>,
) -> Element {
    let mut state = use_context::<Signal<AppState>>();

    let body = tombstone_body.unwrap_or_else(|| {
        "This room has been upgraded and is no longer active.".to_string()
    });

    rsx! {
        div {
            class: "room-upgrade-banner",

            div {
                class: "room-upgrade-banner__icon",
                "Upgraded"
            }

            div {
                class: "room-upgrade-banner__content",
                p {
                    class: "room-upgrade-banner__text",
                    "{body}"
                }

                if let Some(ref new_id) = successor_room_id {
                    {
                        let new_id_str = new_id.clone();
                        rsx! {
                            button {
                                class: "btn btn--primary btn--sm room-upgrade-banner__join-btn",
                                onclick: move |_| {
                                    if let Ok(room_id) = OwnedRoomId::try_from(new_id_str.as_str()) {
                                        let mut s = state.write();
                                        s.active_room_id = Some(room_id.clone());
                                        s.current_view = AppView::Room(room_id);
                                    }
                                },
                                "Join New Room"
                            }
                        }
                    }
                }
            }
        }
    }
}