synpad 0.1.0

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

use crate::state::app_state::AppState;
use crate::utils::time_format::format_time;

/// A search result from the server.
#[derive(Clone, Debug)]
struct SearchResult {
    event_id: String,
    sender: String,
    sender_display_name: String,
    body: String,
    timestamp: u64,
}

/// Server-side message search panel.
#[component]
pub fn ServerSearchPanel(
    room_id: String,
    on_close: EventHandler<()>,
) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut query = use_signal(|| String::new());
    let mut results = use_signal(Vec::<SearchResult>::new);
    let mut is_searching = use_signal(|| false);
    let mut searched = use_signal(|| false);
    let _room_id_sig = use_signal(|| room_id.clone());

    let mut do_search = move || {
        let q = query.read().clone();
        if q.trim().is_empty() {
            return;
        }
        is_searching.set(true);
        searched.set(true);
        results.set(Vec::new());
        spawn(async move {
            let client = { state.read().client.clone() };
            if let Some(client) = client {
                use matrix_sdk::ruma::api::client::search::search_events::v3::{
                    Request, Criteria,
                };
                use matrix_sdk::ruma::assign;

                let criteria = Criteria::new(q);
                let categories = assign!(matrix_sdk::ruma::api::client::search::search_events::v3::Categories::new(), {
                    room_events: Some(criteria),
                });
                let request = Request::new(categories);
                match client.send(request).await {
                    Ok(response) => {
                        let mut items = Vec::new();
                        let room_events = response.search_categories.room_events;
                        for result in room_events.results {
                            if let Some(event) = result.result {
                                let json = event.json();
                                if let Ok(val) = serde_json::from_str::<serde_json::Value>(json.get()) {
                                    let event_id = val.get("event_id")
                                        .and_then(|e: &serde_json::Value| e.as_str())
                                        .unwrap_or("")
                                        .to_string();
                                    let sender = val.get("sender")
                                        .and_then(|s: &serde_json::Value| s.as_str())
                                        .unwrap_or("")
                                        .to_string();
                                    let body = val.get("content")
                                        .and_then(|c: &serde_json::Value| c.get("body"))
                                        .and_then(|b: &serde_json::Value| b.as_str())
                                        .unwrap_or("")
                                        .to_string();
                                    let ts = val.get("origin_server_ts")
                                        .and_then(|t: &serde_json::Value| t.as_u64())
                                        .unwrap_or(0);

                                    items.push(SearchResult {
                                        event_id,
                                        sender: sender.clone(),
                                        sender_display_name: sender,
                                        body,
                                        timestamp: ts,
                                    });
                                }
                            }
                        }
                        results.set(items);
                    }
                    Err(e) => {
                        tracing::error!("Search failed: {e}");
                    }
                }
            }
            is_searching.set(false);
        });
    };

    rsx! {
        div {
            class: "server-search",

            div {
                class: "server-search__header",
                h3 { "Search Messages" }
                button {
                    class: "server-search__close",
                    onclick: move |_| on_close.call(()),
                    ""
                }
            }

            div {
                class: "server-search__input-row",
                input {
                    r#type: "text",
                    class: "server-search__input",
                    placeholder: "Search messages on server...",
                    value: "{query}",
                    oninput: move |evt| query.set(evt.value()),
                    onkeydown: move |evt: Event<KeyboardData>| {
                        if evt.key() == Key::Enter {
                            do_search();
                        }
                    },
                    autofocus: true,
                }
                button {
                    class: "btn btn--primary",
                    onclick: move |_| do_search(),
                    "Search"
                }
            }

            div {
                class: "server-search__results",
                if *is_searching.read() {
                    div {
                        class: "server-search__loading",
                        div { class: "spinner spinner--small" }
                        span { "Searching..." }
                    }
                }

                if *searched.read() && !*is_searching.read() && results.read().is_empty() {
                    div {
                        class: "server-search__empty",
                        "No results found."
                    }
                }

                for result in results.read().iter() {
                    {
                        let body = result.body.clone();
                        let sender = result.sender_display_name.clone();
                        let time = format_time(result.timestamp);
                        rsx! {
                            div {
                                class: "server-search__result",
                                div {
                                    class: "server-search__result-header",
                                    span { class: "server-search__result-sender", "{sender}" }
                                    span { class: "server-search__result-time", "{time}" }
                                }
                                div {
                                    class: "server-search__result-body",
                                    "{body}"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}