synpad 0.1.0

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

use crate::state::app_state::AppState;
use crate::state::room_state::ReactionGroup;

/// Send a reaction to a message.
async fn toggle_reaction(
    state: Signal<AppState>,
    room_id: &str,
    event_id: &str,
    emoji: &str,
) -> Result<(), String> {
    let client = { state.read().client.clone() };
    let client = client.ok_or_else(|| "Not logged in".to_string())?;

    let room_id: matrix_sdk::ruma::OwnedRoomId = room_id
        .try_into()
        .map_err(|e| format!("Invalid room ID: {e}"))?;
    let target_event_id: matrix_sdk::ruma::OwnedEventId = event_id
        .try_into()
        .map_err(|e| format!("Invalid event ID: {e}"))?;

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

    use matrix_sdk::ruma::events::reaction::ReactionEventContent;
    use matrix_sdk::ruma::events::relation::Annotation;

    let annotation = Annotation::new(target_event_id, emoji.to_string());
    let content = ReactionEventContent::new(annotation);
    room.send(content)
        .await
        .map_err(|e| format!("Failed to send reaction: {e}"))?;

    Ok(())
}

/// Reactions display below a message.
#[component]
pub fn ReactionList(room_id: String, event_id: String, reactions: Vec<ReactionGroup>) -> Element {
    let state = use_context::<Signal<AppState>>();

    rsx! {
        div {
            class: "reaction-list",
            for reaction in reactions.iter() {
                {
                    let emoji = reaction.key.clone();
                    let rid = room_id.clone();
                    let eid = event_id.clone();
                    let already_reacted = reaction.user_reacted;
                    rsx! {
                        button {
                            class: if already_reacted {
                                "reaction-list__item reaction-list__item--active"
                            } else {
                                "reaction-list__item"
                            },
                            title: "{reaction.senders.len()} reactions",
                            onclick: move |_| {
                                if !already_reacted {
                                    let rid = rid.clone();
                                    let eid = eid.clone();
                                    let emoji = emoji.clone();
                                    spawn(async move {
                                        if let Err(e) = toggle_reaction(state, &rid, &eid, &emoji).await {
                                            tracing::error!("Failed to toggle reaction: {e}");
                                        }
                                    });
                                }
                            },
                            span { class: "reaction-list__emoji", "{reaction.key}" }
                            span { class: "reaction-list__count", "{reaction.count}" }
                        }
                    }
                }
            }
            // Add reaction button
            button {
                class: "reaction-list__add",
                title: "Add reaction",
                "+"
            }
        }
    }
}