synpad 0.1.0

A full-featured Matrix chat client built with Dioxus
use std::collections::HashMap;

use matrix_sdk::Client;
use matrix_sdk::ruma::OwnedRoomId;

use crate::room::call::call_state::CallState;
use crate::state::notification_state::NotificationState;
use crate::state::room_state::{EditingMessage, ReplyingTo, RoomFilter, RoomSortOrder, RoomSummary};
use crate::state::user_state::UserProfile;
use crate::theme::colors::Theme;

/// The authentication status of the application.
#[derive(Clone, Debug, PartialEq)]
pub enum AuthStatus {
    /// App is loading, checking for stored session
    Unknown,
    /// User is not logged in
    LoggedOut,
    /// Login is in progress
    LoggingIn,
    /// User is logged in
    LoggedIn,
    /// An error occurred during login
    Error(String),
}

/// The current view state of the application.
#[derive(Clone, Debug, PartialEq)]
pub enum AppView {
    /// Viewing the room list / home
    Home,
    /// Viewing a specific room
    Room(OwnedRoomId),
    /// Viewing settings
    Settings,
    /// Welcome/onboarding screen
    Welcome,
}

/// Right panel content state.
#[derive(Clone, Debug, PartialEq)]
pub enum RightPanelView {
    Closed,
    RoomInfo,
    MemberList,
    MemberDetail(String),
    PinnedMessages,
    FilePanel,
    NotificationPanel,
    /// Thread panel: shows thread rooted at the given event ID
    Thread(String),
}

/// The global application state.
#[derive(Clone)]
pub struct AppState {
    /// Current authentication status
    pub auth_status: AuthStatus,

    /// Matrix SDK client (available after login)
    pub client: Option<Client>,

    /// Current user profile
    pub user_profile: Option<UserProfile>,

    /// Current view
    pub current_view: AppView,

    /// All joined rooms
    pub rooms: HashMap<OwnedRoomId, RoomSummary>,

    /// Room IDs in display order (after filtering/sorting)
    pub sorted_room_ids: Vec<OwnedRoomId>,

    /// Currently selected room ID
    pub active_room_id: Option<OwnedRoomId>,

    /// Room filter
    pub room_filter: RoomFilter,

    /// Room sort order
    pub room_sort_order: RoomSortOrder,

    /// Room search query
    pub room_search_query: String,

    /// Right panel state
    pub right_panel: RightPanelView,

    /// Notification state
    pub notifications: NotificationState,

    /// Theme preference
    pub theme: Theme,

    /// Currently replying to a message (shared between timeline and composer)
    pub replying_to: Option<ReplyingTo>,

    /// Currently editing a message (shared between timeline and composer)
    pub editing_message: Option<EditingMessage>,

    /// Sync status
    pub sync_status: SyncStatus,

    /// Incremented after each successful sync to trigger UI refreshes
    pub sync_generation: u64,

    /// Composer message history (for arrow-up recall)
    pub composer_history: Vec<String>,

    /// Current position in composer history (-1 = not browsing)
    pub composer_history_index: i32,

    /// Whether spotlight search overlay is open (Ctrl+K)
    pub spotlight_search_open: bool,

    /// Recent room IDs for breadcrumbs (most recent first, max 10)
    pub breadcrumbs: Vec<OwnedRoomId>,

    /// Labs/experimental feature flags
    pub labs_flags: std::collections::HashMap<String, bool>,

    /// VoIP call state
    pub call_state: CallState,

    /// Whether bubble layout is enabled
    pub bubble_layout: bool,

    /// Whether to use 24-hour time format
    pub use_24h_time: bool,

    /// Whether to show join/leave events in the timeline
    pub show_join_leave: bool,

    /// Active message effect (confetti, fireworks, etc.)
    pub active_effect: Option<crate::components::message_effects::EffectType>,

    /// Whether to send read receipts to other users
    pub send_read_receipts: bool,

    /// Whether to send typing notifications to other users
    pub send_typing_notifications: bool,

    /// Whether to show read receipts from other users
    pub show_read_receipts: bool,

    /// Whether to show display name change events in the timeline
    pub show_display_name_changes: bool,

    /// Whether to show avatar change events in the timeline
    pub show_avatar_changes: bool,

    /// Whether to show session verification prompt after login
    pub show_session_verification: bool,

    /// Whether to show the secure backup setup toast
    pub show_backup_toast: bool,

    /// List of ignored user IDs
    pub ignored_users: Vec<String>,

    /// List of low-priority room IDs
    pub low_priority_rooms: Vec<String>,

    /// Custom user status message (displayed next to name and in presence indicator)
    pub custom_status: Option<String>,
}

/// Sync loop status.
#[derive(Clone, Debug, PartialEq)]
pub enum SyncStatus {
    /// Not yet started
    Idle,
    /// Currently syncing
    Syncing,
    /// Synced and receiving updates
    Synced,
    /// Error during sync
    Error(String),
}

impl Default for AppState {
    fn default() -> Self {
        Self {
            auth_status: AuthStatus::Unknown,
            client: None,
            user_profile: None,
            current_view: AppView::Home,
            rooms: HashMap::new(),
            sorted_room_ids: Vec::new(),
            active_room_id: None,
            room_filter: RoomFilter::All,
            room_sort_order: RoomSortOrder::Activity,
            room_search_query: String::new(),
            replying_to: None,
            editing_message: None,
            right_panel: RightPanelView::Closed,
            notifications: NotificationState::default(),
            theme: Theme::Dark,
            sync_status: SyncStatus::Idle,
            sync_generation: 0,
            composer_history: Vec::new(),
            composer_history_index: -1,
            spotlight_search_open: false,
            breadcrumbs: Vec::new(),
            labs_flags: std::collections::HashMap::new(),
            call_state: CallState::default(),
            bubble_layout: false,
            use_24h_time: false,
            show_join_leave: true,
            active_effect: None,
            send_read_receipts: true,
            send_typing_notifications: true,
            show_read_receipts: true,
            show_display_name_changes: true,
            show_avatar_changes: true,
            show_session_verification: false,
            show_backup_toast: false,
            ignored_users: Vec::new(),
            low_priority_rooms: Vec::new(),
            custom_status: None,
        }
    }
}

impl AppState {
    /// Get the active room summary, if any.
    pub fn active_room(&self) -> Option<&RoomSummary> {
        self.active_room_id
            .as_ref()
            .and_then(|id| self.rooms.get(id))
    }

    /// Update the sorted room list based on current filter and sort order.
    pub fn update_sorted_rooms(&mut self) {
        let query = self.room_search_query.to_lowercase();
        let mut ids: Vec<OwnedRoomId> = self
            .rooms
            .iter()
            .filter(|(_, room)| {
                // Apply filter
                match &self.room_filter {
                    RoomFilter::All => true,
                    RoomFilter::DirectMessages => room.is_direct,
                    RoomFilter::Rooms => !room.is_direct,
                    RoomFilter::Favorites => room.is_favorite,
                    RoomFilter::Space(space_id) => {
                        room.parent_spaces.contains(space_id)
                    }
                }
            })
            .filter(|(_, room)| {
                // Apply search
                if query.is_empty() {
                    true
                } else {
                    room.display_name.to_lowercase().contains(&query)
                }
            })
            .map(|(id, _)| id.clone())
            .collect();

        // Apply sort
        match self.room_sort_order {
            RoomSortOrder::Activity => {
                ids.sort_by(|a, b| {
                    let a_ts = self
                        .rooms
                        .get(a)
                        .and_then(|r| r.last_activity_ts)
                        .unwrap_or(0);
                    let b_ts = self
                        .rooms
                        .get(b)
                        .and_then(|r| r.last_activity_ts)
                        .unwrap_or(0);
                    b_ts.cmp(&a_ts)
                });
            }
            RoomSortOrder::Alphabetical => {
                ids.sort_by(|a, b| {
                    let a_name = self
                        .rooms
                        .get(a)
                        .map(|r| r.display_name.as_str())
                        .unwrap_or("");
                    let b_name = self
                        .rooms
                        .get(b)
                        .map(|r| r.display_name.as_str())
                        .unwrap_or("");
                    a_name.to_lowercase().cmp(&b_name.to_lowercase())
                });
            }
            RoomSortOrder::Unread => {
                ids.sort_by(|a, b| {
                    let a_unread = self
                        .rooms
                        .get(a)
                        .map(|r| r.unread_count)
                        .unwrap_or(0);
                    let b_unread = self
                        .rooms
                        .get(b)
                        .map(|r| r.unread_count)
                        .unwrap_or(0);
                    b_unread.cmp(&a_unread)
                });
            }
        }

        self.sorted_room_ids = ids;
    }
}