synpad 0.1.0

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

use anyhow::Result;
use dioxus::prelude::*;
use matrix_sdk::Client;

use crate::notifications::push_rules::notification_level_from_user_defined_mode;
use crate::state::app_state::{AppState, SyncStatus};
use crate::state::room_state::{RoomMembership, RoomSummary};
use crate::state::user_state::UserProfile;

/// Start the sync loop.
///
/// This function runs indefinitely, processing sync responses
/// and updating the application state signals.
pub async fn start_sync_loop(client: Client, mut state: Signal<AppState>) {
    tracing::info!("Starting sync loop...");
    state.write().sync_status = SyncStatus::Syncing;

    // Load user profile
    if let Err(e) = load_user_profile(&client, &mut state).await {
        tracing::error!("Failed to load user profile: {e}");
    }

    // Run the sync loop using the SDK's built-in sync
    let sync_settings = matrix_sdk::config::SyncSettings::default()
        .timeout(std::time::Duration::from_secs(30));

    loop {
        match client.sync_once(sync_settings.clone()).await {
            Ok(_response) => {
                {
                    let mut w = state.write();
                    w.sync_status = SyncStatus::Synced;
                    w.sync_generation += 1;
                }
                refresh_room_list(&client, &mut state).await;
            }
            Err(e) => {
                tracing::error!("Sync error: {e}");
                state.write().sync_status = SyncStatus::Error(e.to_string());
                // Wait before retrying
                tokio::time::sleep(std::time::Duration::from_secs(5)).await;
            }
        }
    }
}

/// Load the current user's profile.
async fn load_user_profile(client: &Client, state: &mut Signal<AppState>) -> Result<()> {
    let user_id = client
        .user_id()
        .ok_or_else(|| anyhow::anyhow!("No user ID"))?;

    // Get the user's display name and avatar from the account
    let account = client.account();
    let display_name = account.get_display_name().await?.unwrap_or_default();
    let avatar_url = account.get_avatar_url().await?.map(|u| u.to_string());

    state.write().user_profile = Some(UserProfile {
        user_id: Some(user_id.to_owned()),
        display_name: Some(display_name),
        avatar_url,
        email: None,
        status_message: None,
    });

    Ok(())
}

/// Refresh the room list from the client's current state.
async fn refresh_room_list(client: &Client, state: &mut Signal<AppState>) {
    let mut rooms = HashMap::new();
    let notification_settings = client.notification_settings().await;

    for room in client.joined_rooms() {
        let room_id = room.room_id().to_owned();

        let display_name = match room.display_name().await {
            Ok(name) => name.to_string(),
            Err(_) => room_id.to_string(),
        };

        let avatar_url = room.avatar_url().map(|u| u.to_string());

        let topic = room.topic();

        let is_direct = room.is_direct().await.unwrap_or(false);

        let unread = room.unread_notification_counts();
        let member_count = room.joined_members_count();
        let notification_level = notification_level_from_user_defined_mode(
            notification_settings
                .get_user_defined_room_notification_mode(room.room_id())
                .await,
        );

        let summary = RoomSummary {
            room_id: room_id.clone(),
            display_name,
            avatar_url,
            topic,
            is_direct,
            is_favorite: false,
            // TODO: Check encryption status once API is available
            is_encrypted: false,
            is_tombstoned: false,
            tombstone_successor: None,
            tombstone_body: None,
            unread_count: unread.notification_count.into(),
            highlight_count: unread.highlight_count.into(),
            last_activity_ts: None,
            last_event_preview: None,
            last_event_sender: None,
            member_count,
            typing_members: Vec::new(),
            notification_level,
            parent_spaces: Vec::new(),
            membership: RoomMembership::Joined,
        };

        rooms.insert(room_id, summary);
    }

    // Update notification counts
    let total_unread: u64 = rooms.values().map(|r| r.unread_count).sum();
    let total_highlights: u64 = rooms.values().map(|r| r.highlight_count).sum();

    // Check for new notifications and fire desktop notifications
    {
        let prev_unread = state.read().notifications.total_unread;
        if total_unread > prev_unread {
            // New messages arrived - fire desktop notification for the most active room
            let new_msgs: Vec<_> = rooms.values()
                .filter(|r| {
                    let prev = state.read().rooms.get(&r.room_id).map(|p| p.unread_count).unwrap_or(0);
                    r.unread_count > prev
                })
                .collect();

            for room in new_msgs.iter().take(3) {
                let room_name = room.display_name.clone();
                let sender = room.last_event_sender.clone().unwrap_or_default();
                let body = room.last_event_preview.clone().unwrap_or_else(|| "New message".to_string());
                let rid = room.room_id.to_string();
                spawn(async move {
                    crate::notifications::desktop::show_desktop_notification(
                        &room_name, &sender, &body, &rid,
                    ).await;
                });
            }
        }
    }

    let mut state_write = state.write();
    state_write.rooms = rooms;
    state_write.notifications.total_unread = total_unread;
    state_write.notifications.total_highlights = total_highlights;
    state_write.update_sorted_rooms();
}