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::local_search::{SearchIndex, SearchResult};
use crate::utils::time_format::format_datetime;

/// Local search dialog component for searching locally indexed (decrypted) messages.
///
/// This is useful for encrypted rooms where server-side search cannot access
/// the plaintext message content.
#[component]
pub fn LocalSearchDialog(
    search_index: Signal<SearchIndex>,
    on_close: EventHandler<()>,
    on_navigate: EventHandler<(String, String)>,
) -> Element {
    let state = use_context::<Signal<AppState>>();
    let mut query = use_signal(|| String::new());
    let mut selected_room_filter = use_signal(|| String::new());

    // Read query and filter values
    let query_val = query.read().clone();
    let filter_val = selected_room_filter.read().clone();

    // Perform search
    let room_filter_opt = if filter_val.is_empty() {
        None
    } else {
        Some(filter_val.as_str())
    };

    let results: Vec<SearchResult> = if query_val.len() >= 2 {
        let idx = search_index.read();
        idx.search(&query_val, room_filter_opt)
    } else {
        Vec::new()
    };

    let result_count = results.len();
    let total_indexed = search_index.read().message_count();

    // Get indexed room IDs for the filter dropdown
    let indexed_rooms: Vec<(String, String)> = {
        let idx = search_index.read();
        let room_ids = idx.indexed_room_ids();
        let s = state.read();
        room_ids
            .into_iter()
            .map(|rid| {
                let display = s
                    .rooms
                    .iter()
                    .find(|(id, _)| id.as_str() == rid.as_str())
                    .map(|(_, room)| room.display_name.clone())
                    .unwrap_or_else(|| rid.clone());
                (rid, display)
            })
            .collect()
    };

    // Build status text
    let status_text = if query_val.len() < 2 {
        String::from("Type at least 2 characters to search")
    } else {
        let count = result_count;
        let suffix = if count == 1 { "" } else { "s" };
        format!("{} result{} found", count, suffix)
    };

    rsx! {
        div {
            class: "local-search-overlay",
            onclick: move |_| on_close.call(()),

            div {
                class: "local-search-dialog",
                onclick: move |evt| evt.stop_propagation(),

                // Header
                div {
                    class: "local-search-dialog__header",
                    h2 { class: "local-search-dialog__title", "Search Messages" }
                    p {
                        class: "local-search-dialog__subtitle",
                        "Searching locally indexed messages"
                    }
                    button {
                        class: "local-search-dialog__close",
                        onclick: move |_| on_close.call(()),
                        ""
                    }
                }

                // Search input and filter row
                div {
                    class: "local-search-dialog__controls",

                    div {
                        class: "local-search-dialog__input-row",
                        span { class: "local-search-dialog__search-icon", "🔍" }
                        input {
                            r#type: "text",
                            class: "local-search-dialog__input",
                            placeholder: "Search in encrypted messages...",
                            value: "{query_val}",
                            oninput: move |evt| {
                                query.set(evt.value());
                            },
                            onkeydown: move |evt: Event<KeyboardData>| {
                                if evt.key() == Key::Escape {
                                    on_close.call(());
                                }
                            },
                            autofocus: true,
                        }
                    }

                    // Room filter dropdown
                    if !indexed_rooms.is_empty() {
                        div {
                            class: "local-search-dialog__filter-row",
                            label {
                                class: "local-search-dialog__filter-label",
                                "Room:"
                            }
                            select {
                                class: "local-search-dialog__filter-select",
                                value: "{filter_val}",
                                onchange: move |evt| {
                                    selected_room_filter.set(evt.value());
                                },
                                option {
                                    value: "",
                                    "All rooms"
                                }
                                for (rid, display_name) in indexed_rooms.iter() {
                                    {
                                        let rid_val = rid.clone();
                                        let display = display_name.clone();
                                        rsx! {
                                            option {
                                                value: "{rid_val}",
                                                "{display}"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                // Stats bar
                div {
                    class: "local-search-dialog__stats",
                    span { class: "local-search-dialog__stats-text", "{status_text}" }
                    span {
                        class: "local-search-dialog__stats-count",
                        "{total_indexed} messages indexed"
                    }
                }

                // Results list
                div {
                    class: "local-search-dialog__results",

                    if query_val.len() >= 2 && results.is_empty() {
                        div {
                            class: "local-search-dialog__empty",
                            span { class: "local-search-dialog__empty-icon", "🔎" }
                            p { "No matching messages found" }
                            p {
                                class: "local-search-dialog__empty-hint",
                                "Only messages you have viewed can be searched"
                            }
                        }
                    }

                    for result in results.iter() {
                        {
                            let room_id = result.room_id.clone();
                            let event_id = result.event_id.clone();
                            let sender = result.sender.clone();
                            let snippet = result.snippet.clone();
                            let ts = result.timestamp;
                            let time_str = format_datetime(ts);

                            // Resolve room display name
                            let room_display = {
                                let s = state.read();
                                s.rooms
                                    .iter()
                                    .find(|(id, _)| id.as_str() == room_id.as_str())
                                    .map(|(_, room)| room.display_name.clone())
                                    .unwrap_or_else(|| room_id.clone())
                            };

                            let nav_room = room_id.clone();
                            let nav_event = event_id.clone();

                            rsx! {
                                button {
                                    class: "local-search-dialog__result",
                                    onclick: move |_| {
                                        let r = nav_room.clone();
                                        let e = nav_event.clone();
                                        tracing::info!("Navigate to event {} in room {}", e, r);
                                        on_navigate.call((r, e));
                                    },
                                    div {
                                        class: "local-search-dialog__result-header",
                                        span {
                                            class: "local-search-dialog__result-sender",
                                            "{sender}"
                                        }
                                        span {
                                            class: "local-search-dialog__result-room",
                                            "{room_display}"
                                        }
                                        span {
                                            class: "local-search-dialog__result-time",
                                            "{time_str}"
                                        }
                                    }
                                    div {
                                        class: "local-search-dialog__result-snippet",
                                        dangerous_inner_html: "{snippet}",
                                    }
                                }
                            }
                        }
                    }
                }

                // Footer
                div {
                    class: "local-search-dialog__footer",
                    span { "Esc to close" }
                    if result_count > 0 {
                        span { "Click a result to navigate" }
                    }
                }
            }
        }
    }
}