synpad 0.1.0

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

use crate::room::call::call_state::{CallStatus, CallType};
use crate::room::call::element_call::build_element_call_url;
use crate::state::app_state::AppState;

/// Voice and video call buttons for the room header.
#[component]
pub fn CallButtons(room_id: String) -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let call_active = state.read().call_state.is_active();
    let call_in_this_room = state
        .read()
        .call_state
        .room_id
        .as_ref()
        .map(|r| r.to_string() == room_id)
        .unwrap_or(false);

    let rid_voice = room_id.clone();
    let rid_video = room_id.clone();

    rsx! {
        div {
            class: "call-buttons",

            // Voice call button
            button {
                class: if call_in_this_room { "room-header__action-btn room-header__action-btn--active" } else { "room-header__action-btn" },
                title: "Voice call",
                disabled: call_active && !call_in_this_room,
                onclick: move |_| {
                    if call_in_this_room {
                        // Hang up
                        hang_up_call(&mut state);
                    } else {
                        start_call(&mut state, &rid_voice, CallType::Voice);
                    }
                },
                "📞"
            }

            // Video call button
            button {
                class: if call_in_this_room && state.read().call_state.is_video_enabled {
                    "room-header__action-btn room-header__action-btn--active"
                } else {
                    "room-header__action-btn"
                },
                title: "Video call",
                disabled: call_active && !call_in_this_room,
                onclick: move |_| {
                    if call_in_this_room {
                        hang_up_call(&mut state);
                    } else {
                        start_call(&mut state, &rid_video, CallType::Video);
                    }
                },
                "📹"
            }
        }
    }
}

/// Start a 1:1 VoIP call.
fn start_call(state: &mut Signal<AppState>, room_id: &str, call_type: CallType) {
    if let Ok(rid) = matrix_sdk::ruma::OwnedRoomId::try_from(room_id) {
        let call_id = uuid::Uuid::new_v4().to_string();
        let is_video = matches!(call_type, CallType::Video);
        let widget_url = state
            .read()
            .client
            .as_ref()
            .map(|client| build_element_call_url(client, rid.as_ref()));

        let mut s = state.write();
        s.call_state.status = CallStatus::Connecting;
        s.call_state.room_id = Some(rid.clone());
        s.call_state.call_type = call_type;
        s.call_state.call_id = Some(call_id.clone());
        s.call_state.is_video_enabled = is_video;
        s.call_state.duration_secs = 0;
        s.call_state.is_muted = false;
        s.call_state.is_screen_sharing = false;
        s.call_state.is_on_hold = false;
        s.call_state.is_group_call = true;
        s.call_state.widget_url = widget_url;
        drop(s);

        // Announce the call handoff to Element Call.
        let client = { state.read().client.clone() };
        spawn(async move {
            if let Some(client) = client {
                if let Some(room) = client.get_room(&rid) {
                    use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
                    let msg = RoomMessageEventContent::notice_plain(
                        format!(
                            "Started a {type_str} call in Element Call",
                            type_str = if is_video { "video" } else { "voice" }
                        )
                    );
                    if let Err(e) = room.send(msg).await {
                        tracing::error!("Failed to send call notification: {e}");
                    }
                    tracing::info!("Element Call initiated in room {rid}, call_id={call_id}");
                }
            }
        });
    }
}

/// Hang up the current call.
fn hang_up_call(state: &mut Signal<AppState>) {
    use crate::room::call::call_state::CallEndReason;

    let room_id = state.read().call_state.room_id.clone();
    let call_id = state.read().call_state.call_id.clone();
    let client = state.read().client.clone();
    let mut state_copy = *state;

    {
        let mut s = state.write();
        s.call_state.status = CallStatus::Ended(CallEndReason::HungUp);
        s.call_state.is_on_hold = false;
    }

    // Send m.call.hangup event
    if let (Some(rid), Some(_cid)) = (room_id, call_id) {
        spawn(async move {
            if let Some(client) = client {
                if let Some(room) = client.get_room(&rid) {
                    use matrix_sdk::ruma::events::room::message::RoomMessageEventContent;
                    let msg = RoomMessageEventContent::notice_plain("Call ended");
                    let _ = room.send(msg).await;
                }
            }
        });
    }

    // Reset after a short delay
    spawn(async move {
        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
        let mut s = state_copy.write();
        s.call_state = Default::default();
    });
}