synpad 0.1.0

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

use crate::components::avatar::Avatar;
use crate::state::app_state::{AppState, RightPanelView};
use crate::state::room_state::{MemberMembership, RoomMember};

/// Right panel listing room members with a search field.
#[component]
pub fn MemberListPanel() -> Element {
    let mut state = use_context::<Signal<AppState>>();
    let mut search_query = use_signal(|| String::new());
    let mut members = use_signal(Vec::<RoomMember>::new);
    let mut members_loaded_for = use_signal(|| Option::<String>::None);

    let state_read = state.read();
    let active_room_id = state_read.active_room_id.clone();
    let member_count = state_read
        .active_room()
        .map(|r| r.member_count)
        .unwrap_or(0);
    drop(state_read);

    // Load members from SDK when room changes
    let room_id_str = active_room_id.as_ref().map(|id| id.to_string());
    if room_id_str != *members_loaded_for.read() {
        members_loaded_for.set(room_id_str.clone());
        if let Some(rid) = room_id_str {
            spawn(async move {
                let client = { state.read().client.clone() };
                if let Some(client) = client {
                    if let Ok(room_id) = OwnedRoomId::try_from(rid.as_str()) {
                        if let Some(room) = client.get_room(&room_id) {
                            match room.members(matrix_sdk::RoomMemberships::JOIN).await {
                                Ok(room_members) => {
                                    let list: Vec<RoomMember> = room_members.iter().map(|m| {
                                        RoomMember {
                                            user_id: m.user_id().to_owned(),
                                            display_name: m.display_name().map(|s| s.to_string()),
                                            avatar_url: m.avatar_url().map(|u| u.to_string()),
                                            power_level: match m.power_level() {
                                            matrix_sdk::ruma::events::room::power_levels::UserPowerLevel::Int(i) => i64::from(i),
                                            _ => 100,
                                        },
                                            is_ignored: false,
                                            membership: MemberMembership::Join,
                                        }
                                    }).collect();
                                    members.set(list);
                                }
                                Err(e) => tracing::error!("Failed to load members: {e}"),
                            }
                        }
                    }
                }
            });
        }
    }

    let query = search_query.read().to_lowercase();
    let members_read = members.read();
    let filtered_members: Vec<&RoomMember> = members_read
        .iter()
        .filter(|m| {
            if query.is_empty() {
                return true;
            }
            let name_match = m
                .display_name
                .as_deref()
                .unwrap_or("")
                .to_lowercase()
                .contains(&query);
            let id_match = m.user_id.to_string().to_lowercase().contains(&query);
            name_match || id_match
        })
        .collect();

    let close_panel = move |_| {
        state.write().right_panel = RightPanelView::Closed;
    };

    let back_to_info = move |_| {
        state.write().right_panel = RightPanelView::RoomInfo;
    };

    rsx! {
        div {
            class: "member-list-panel",

            // Header
            header {
                class: "member-list-panel__header",
                button {
                    class: "member-list-panel__back-btn",
                    title: "Back to room info",
                    onclick: back_to_info,
                    ""
                }
                h3 {
                    class: "member-list-panel__title",
                    "Members"
                }
                span {
                    class: "member-list-panel__count",
                    "{member_count}"
                }
                button {
                    class: "member-list-panel__close-btn",
                    title: "Close panel",
                    onclick: close_panel,
                    ""
                }
            }

            // Search input
            div {
                class: "member-list-panel__search",
                input {
                    class: "member-list-panel__search-input",
                    r#type: "text",
                    placeholder: "Search members...",
                    value: "{search_query}",
                    oninput: move |evt| {
                        search_query.set(evt.value());
                    },
                }
            }

            // Invite button
            div {
                class: "member-list-panel__invite",
                button {
                    class: "member-list-panel__invite-btn",
                    "Invite to this room"
                }
            }

            // Member list
            div {
                class: "member-list-panel__list",

                if filtered_members.is_empty() {
                    p {
                        class: "member-list-panel__empty",
                        if query.is_empty() {
                            "Loading members..."
                        } else {
                            "No members found."
                        }
                    }
                }

                for member in filtered_members.iter() {
                    MemberListItem {
                        key: "{member.user_id}",
                        user_id: member.user_id.to_string(),
                        display_name: member.display_name.clone().unwrap_or_else(|| member.user_id.to_string()),
                        avatar_url: member.avatar_url.clone(),
                        power_level: member.power_level,
                        membership: member.membership.clone(),
                    }
                }
            }
        }
    }
}

/// A single member entry in the member list.
#[component]
fn MemberListItem(
    user_id: String,
    display_name: String,
    avatar_url: Option<String>,
    power_level: i64,
    membership: MemberMembership,
) -> Element {
    let mut state = use_context::<Signal<AppState>>();

    let user_id_clone = user_id.clone();
    let on_click = move |_| {
        state.write().right_panel = RightPanelView::MemberDetail(user_id_clone.clone());
    };

    let role_label = match power_level {
        100.. => "Admin",
        50..=99 => "Moderator",
        _ => "",
    };

    rsx! {
        div {
            class: "member-list-item",
            onclick: on_click,

            Avatar {
                name: display_name.clone(),
                url: avatar_url,
                size: 32,
            }

            div {
                class: "member-list-item__info",
                span {
                    class: "member-list-item__name",
                    "{display_name}"
                }
                span {
                    class: "member-list-item__user-id",
                    "{user_id}"
                }
            }

            if !role_label.is_empty() {
                span {
                    class: "member-list-item__role",
                    "{role_label}"
                }
            }
        }
    }
}