From 102bda929c662004159693daaeed6eb27147b53b Mon Sep 17 00:00:00 2001 From: JSKitty Date: Sat, 4 Oct 2025 19:53:29 +0100 Subject: [PATCH 1/2] cleanup: deprecated code cleanup --- src-tauri/src/chat.rs | 26 -- src-tauri/src/lib.rs | 689 ------------------------------------------ 2 files changed, 715 deletions(-) diff --git a/src-tauri/src/chat.rs b/src-tauri/src/chat.rs index 530108bb..28b30ffb 100644 --- a/src-tauri/src/chat.rs +++ b/src-tauri/src/chat.rs @@ -244,32 +244,6 @@ impl ChatMetadata { } } -// Database structures for persistence -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct SlimChat { - pub id: String, - pub chat_type: ChatType, - pub participants: Vec, - pub last_read: String, - pub created_at: u64, - pub metadata: ChatMetadata, - pub muted: bool, -} - -impl From<&Chat> for SlimChat { - fn from(chat: &Chat) -> Self { - SlimChat { - id: chat.id.clone(), - chat_type: chat.chat_type.clone(), - participants: chat.participants.clone(), - last_read: chat.last_read.clone(), - created_at: chat.created_at, - metadata: chat.metadata.clone(), - muted: chat.muted, - } - } -} - //// Marks a specific message as read for a chat. /// Behavior: /// - If message_id is Some(id): set chat.last_read = id. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 90a84c81..2efd98dd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1687,683 +1687,6 @@ async fn handle_reaction(reaction: Reaction, _contact: &str) -> bool { reaction_added } -// OLD IMPLEMENTATION BELOW - TO BE REMOVED AFTER VERIFICATION -/* -#[tauri::command] -async fn handle_event_old(event: Event, is_new: bool) -> bool { - let client = NOSTR_CLIENT.get().expect("Nostr client not initialized"); - - // Grab our pubkey - let signer = client.signer().await.unwrap(); - let my_public_key = signer.get_public_key().await.unwrap(); - - // Unwrap the gift wrap - match client.unwrap_gift_wrap(&event).await { - Ok(UnwrappedGift { rumor, sender }) => { - // Handle MLS Welcome messages (group invites) - these need special processing - if rumor.kind == Kind::MlsWelcome { - // Convert rumor Event -> UnsignedEvent - let unsigned_opt = serde_json::to_string(&rumor) - .ok() - .and_then(|s| nostr_sdk::UnsignedEvent::from_json(s.as_bytes()).ok()); - - if let Some(unsigned) = unsigned_opt { - // Outer giftwrap id is our wrapper id for dedup/logs - let wrapper_id = event.id; - let app_handle = TAURI_APP.get().cloned(); - - // Use blocking thread for non-Send MLS engine - let processed = tokio::task::spawn_blocking(move || { - if app_handle.is_none() { - return false; - } - let handle = app_handle.unwrap(); - let svc = MlsService::new_persistent(&handle); - if let Ok(mls) = svc { - if let Ok(engine) = mls.engine() { - match engine.process_welcome(&wrapper_id, &unsigned) { - Ok(_) => { - println!("[MLS][live][welcome] processed wrapper_id={}", wrapper_id); - return true; - } - Err(e) => { - eprintln!("[MLS][live][welcome] process_welcome failed wrapper_id={} err={}", wrapper_id, e); - return false; - } - } - } - } - false - }) - .await - .unwrap_or(false); - - if processed { - // Notify UI so invites list can refresh via list_pending_mls_welcomes() - if let Some(app) = TAURI_APP.get() { - let _ = app.emit("mls_invite_received", serde_json::json!({ - "wrapper_event_id": wrapper_id.to_hex() - })); - } - return true; - } else { - return false; - } - } else { - eprintln!("[MLS][live][welcome] failed to convert rumor to UnsignedEvent"); - return false; - } - } - - // Check if it's mine - let is_mine = sender == my_public_key; - - // Attempt to get contact public key (bech32) - let contact: String = if is_mine { - // Try to get the first public key from tags - match rumor.tags.public_keys().next() { - Some(pub_key) => match pub_key.to_bech32() { - Ok(p_tag_pubkey_bech32) => p_tag_pubkey_bech32, - Err(_) => { - eprintln!("Failed to convert public key to bech32"); - // If conversion fails, fall back to sender - sender - .to_bech32() - .expect("Failed to convert sender's public key to bech32") - } - }, - None => { - eprintln!("No public key tag found"); - // If no public key found in tags, fall back to sender - sender - .to_bech32() - .expect("Failed to convert sender's public key to bech32") - } - } - } else { - // If not is_mine, just use sender's bech32 - sender - .to_bech32() - .expect("Failed to convert sender's public key to bech32") - }; - - // Direct Message (NIP-17) - if rumor.kind == Kind::PrivateDirectMessage { - // Check if the message replies to anything - let mut replied_to = String::new(); - match rumor.tags.find(TagKind::e()) { - Some(tag) => { - if tag.is_reply() { - // Add the referred Event ID to our `replied_to` field - replied_to = tag.content().unwrap().to_string(); - } - } - None => (), - }; - - // Extract milliseconds from custom tag if present - let ms_timestamp = match rumor.tags.find(TagKind::Custom(Cow::Borrowed("ms"))) { - Some(ms_tag) => { - // Get the ms value and append it to the timestamp - if let Some(ms_str) = ms_tag.content() { - if let Ok(ms_value) = ms_str.parse::() { - // Validate that ms is between 0-999 - if ms_value <= 999 { - rumor.created_at.as_u64() * 1000 + ms_value - } else { - // Invalid ms value, ignore it - rumor.created_at.as_u64() * 1000 - } - } else { - rumor.created_at.as_u64() * 1000 - } - } else { - rumor.created_at.as_u64() * 1000 - } - } - None => rumor.created_at.as_u64() * 1000 - }; - - // Create the Message - let msg = Message { - id: rumor.id.unwrap().to_hex(), - content: rumor.content, - replied_to, - preview_metadata: None, - at: ms_timestamp, - attachments: Vec::new(), - reactions: Vec::new(), - mine: is_mine, - pending: false, - failed: false, - }; - - // Send an OS notification for incoming messages (do this before locking state) - if !is_mine && is_new { - // Clone necessary data for notification (avoid holding lock during notification) - let display_info = { - let state = STATE.lock().await; - match state.get_profile(&contact) { - Some(profile) => { - if profile.muted { - None // Profile is muted, don't send notification - } else { - // Profile is not muted, send notification - let display_name = if !profile.nickname.is_empty() { - profile.nickname.clone() - } else if !profile.name.is_empty() { - profile.name.clone() - } else { - String::from("New Message") - }; - Some((display_name, msg.content.clone())) - } - } - // No profile, send notification with default name - None => Some((String::from("New Message"), msg.content.clone())), - } - }; - - // Send notification outside of state lock - if let Some((display_name, content)) = display_info { - show_notification(display_name, content); - } - } - - // Add the message to the state and handle database save in one operation to avoid multiple locks - let (was_msg_added_to_state, _should_emit, _should_save) = { - let mut state = STATE.lock().await; - let was_added = state.add_message_to_participant(&contact, msg.clone()); - (was_added, was_added, was_added) - }; - - // If accepted in-state: commit to the DB and emit to the frontend - if was_msg_added_to_state { - // Send it to the frontend - if let Some(handle) = TAURI_APP.get() { - handle.emit("message_new", serde_json::json!({ - "message": &msg, - "chat_id": &contact - })).unwrap(); - } - - // Save the chat/messages to DB (chat_id = contact npub for DMs) - if let Some(handle) = TAURI_APP.get() { - // Get all messages for this chat and save them - let all_messages = { - let state = STATE.lock().await; - state.get_chat(&contact).map(|chat| chat.messages.clone()).unwrap_or_default() - }; - let _ = save_chat_messages(handle.clone(), &contact, &all_messages).await; - } - // Ensure OS badge is updated immediately after accepting the message - if let Some(handle) = TAURI_APP.get() { - let _ = update_unread_counter(handle.clone()).await; - } - } - - was_msg_added_to_state - } - // Emoji Reaction (NIP-25) - else if rumor.kind == Kind::Reaction { - match rumor.tags.find(TagKind::e()) { - Some(react_reference_tag) => { - // Add the reaction to the appropriate chat message - let reaction = Reaction { - id: rumor.id.unwrap().to_hex(), - reference_id: react_reference_tag.content().unwrap().to_string(), - author_id: rumor.pubkey.to_hex(), - emoji: rumor.content.clone(), - }; - - // Find the chat containing the referenced message and add the reaction - // Use a single lock scope to avoid nested locks - let (reaction_added, chat_id_for_save, _message_for_save) = { - let mut state = STATE.lock().await; - let reaction_added = if let Some((chat_id, msg_mut)) = state.find_chat_and_message_mut(&react_reference_tag.content().unwrap()) { - msg_mut.add_reaction(reaction, Some(chat_id)) - } else { - // Message not found in any chat - this can happen during sync - // TODO: track these "ahead" reactions and re-apply them once sync has finished - false - }; - - // If reaction was added, get the message for saving - let message_for_save = if reaction_added { - // Find the message again for saving - state.find_message(&react_reference_tag.content().unwrap()) - .map(|(chat, message)| (chat.id().clone(), message.clone())) - } else { - None - }; - - (reaction_added, message_for_save.as_ref().map(|(chat_id, _)| chat_id.clone()), message_for_save.map(|(_, message)| message)) - }; - - // Save all messages for the chat with the new reaction to our DB (outside of state lock) - if let Some(chat_id) = chat_id_for_save { - if let Some(handle) = TAURI_APP.get() { - // Get all messages for this chat - let all_messages = { - let state = STATE.lock().await; - state.get_chat(&chat_id).map(|chat| chat.messages.clone()).unwrap_or_default() - }; - let _ = save_chat_messages(handle.clone(), &chat_id, &all_messages).await; - } - } - - reaction_added - } - None => false, - } - } - // Files and Images - else if rumor.kind == Kind::from_u16(15) { - // Extract our AES-GCM decryption key and nonce - let decryption_key = rumor.tags.find(TagKind::Custom(Cow::Borrowed("decryption-key"))).unwrap().content().unwrap(); - let decryption_nonce = rumor.tags.find(TagKind::Custom(Cow::Borrowed("decryption-nonce"))).unwrap().content().unwrap(); - - // Extract the original file hash (ox tag) if present - let original_file_hash = rumor.tags.find(TagKind::Custom(Cow::Borrowed("ox"))).map(|tag| tag.content().unwrap_or("")); - - // Extract the content storage URL - let content_url = rumor.content; - - // Extract image metadata if provided - let img_meta: Option = { - let blurhash_opt = rumor.tags.find(TagKind::Custom(Cow::Borrowed("blurhash"))) - .and_then(|tag| tag.content()) - .map(|s| s.to_string()); - - let dimensions_opt = rumor.tags.find(TagKind::Custom(Cow::Borrowed("dim"))) - .and_then(|tag| tag.content()) - .and_then(|s| { - // Parse "width-x-height" format - let parts: Vec<&str> = s.split('x').collect(); - if parts.len() == 2 { - let width = parts[0].parse::().ok()?; - let height = parts[1].parse::().ok()?; - Some((width, height)) - } else { - None - } - }); - - // Only create ImageMetadata if we have all required fields - match (blurhash_opt, dimensions_opt) { - (Some(blurhash), Some((width, height))) => { - Some(message::ImageMetadata { - blurhash, - width, - height, - }) - }, - _ => None - } - }; - - // Figure out the file extension from the mime-type - let mime_type = rumor.tags.find(TagKind::Custom(Cow::Borrowed("file-type"))).unwrap().content().unwrap(); - let extension = crate::util::extension_from_mime(mime_type); - - let handle = TAURI_APP.get().unwrap(); - // Choose the appropriate base directory based on platform - let base_directory = if cfg!(target_os = "ios") { - tauri::path::BaseDirectory::Document - } else { - tauri::path::BaseDirectory::Download - }; - - // Resolve the directory path using the determined base directory - let dir = handle.path().resolve("vector", base_directory).unwrap(); - - // Grab the reported file size - it's noteworthy that this COULD be missing or wrong, so must be treated as an assumption or guide - let reported_size = rumor.tags - .find(TagKind::Custom(Cow::Borrowed("size"))) - .map_or(0, |tag| tag.content().unwrap_or("0").parse().unwrap_or(0)); - - // Check for existing local files based on ox tag first - let mut file_hash = String::new(); - let mut file_path = std::path::PathBuf::new(); - let mut size: u64 = reported_size; - let mut downloaded: bool = false; - let mut found_existing_file = false; - - // If we have an ox tag (original file hash), check if a local file exists with that hash - if let Some(ox_hash) = original_file_hash { - if !ox_hash.is_empty() { - // Check if a local file exists with this hash across all messages - let state = STATE.lock().await; - for chat in &state.chats { - for message in &chat.messages { - for attachment in &message.attachments { - if attachment.id == ox_hash && attachment.downloaded { - // Found existing attachment with same original hash - file_path = std::path::PathBuf::from(&attachment.path); - file_hash = ox_hash.to_string(); - size = attachment.size; - downloaded = true; - found_existing_file = true; - break; - } - } - } - } - } - } - - if !found_existing_file { - // Check if a hash-based file might already exist - // We need to download to check the hash, but only for small files during sync - if is_new && reported_size > 0 && reported_size <= 262144 { - // Try to download and check if hash file exists - let temp_attachment = Attachment { - id: decryption_nonce.to_string(), - key: decryption_key.to_string(), - nonce: decryption_nonce.to_string(), - extension: extension.to_string(), - url: content_url.clone(), - path: String::new(), // Temporary, will be set below - size: reported_size, - img_meta: img_meta.clone(), - downloading: false, - downloaded: false - }; - - // Download to check hash - if let Ok(encrypted_data) = net::download_silent(&content_url, Some(std::time::Duration::from_secs(5))).await { - // Calculate hash without saving - if let Ok(decrypted_data) = crypto::decrypt_data(&encrypted_data, &temp_attachment.key, &temp_attachment.nonce) { - file_hash = calculate_file_hash(&decrypted_data); - let hash_file_path = dir.join(format!("{}.{}", file_hash, extension)); - - if hash_file_path.exists() { - // Hash file already exists! - file_path = hash_file_path; - downloaded = true; - size = reported_size; - } else { - // Save the new file with hash name - if let Err(_e) = std::fs::write(&hash_file_path, decrypted_data) { - downloaded = false; - size = reported_size; - // Still set path to where it WILL be downloaded - file_path = hash_file_path; - } else { - file_path = hash_file_path; - downloaded = true; - size = reported_size; - } - } - } else { - // Failed to decrypt for hash check, fall back to nonce placeholder - file_path = dir.join(format!("{}.{}", decryption_nonce, extension)); - downloaded = false; - size = reported_size; - } - } else { - // Failed to download for hash check, fall back to nonce placeholder - file_path = dir.join(format!("{}.{}", decryption_nonce, extension)); - downloaded = false; - size = reported_size; - } - } else { - // File too large, size unknown, or during historical sync - // Use nonce as placeholder - this will be updated to hash-based - // when the file is actually downloaded via download_attachment - file_path = dir.join(format!("{}.{}", decryption_nonce, extension)); - downloaded = false; - size = reported_size; - } - } - - // Check if the message replies to anything - let mut replied_to = String::new(); - match rumor.tags.find(TagKind::e()) { - Some(tag) => { - if tag.is_reply() { - // Add the referred Event ID to our `replied_to` field - replied_to = tag.content().unwrap().to_string(); - } - } - None => (), - }; - - // Extract milliseconds from custom tag if present (same as for text messages) - let ms_timestamp = match rumor.tags.find(TagKind::Custom(Cow::Borrowed("ms"))) { - Some(ms_tag) => { - // Get the ms value and append it to the timestamp - if let Some(ms_str) = ms_tag.content() { - if let Ok(ms_value) = ms_str.parse::() { - // Validate that ms is between 0-999 - if ms_value <= 999 { - rumor.created_at.as_u64() * 1000 + ms_value - } else { - // Invalid ms value, ignore it - rumor.created_at.as_u64() * 1000 - } - } else { - rumor.created_at.as_u64() * 1000 - } - } else { - rumor.created_at.as_u64() * 1000 - } - } - None => rumor.created_at.as_u64() * 1000 - }; - - // Create an attachment - // Note: The path will be updated to hash-based when the file is downloaded - let mut attachments = Vec::new(); - let attachment = Attachment { - id: if downloaded { file_hash } else { decryption_nonce.to_string() }, - key: decryption_key.to_string(), - nonce: decryption_nonce.to_string(), - extension: extension.to_string(), - url: content_url, - path: file_path.to_string_lossy().to_string(), // Will be updated to hash-based path on download - size, - img_meta, - downloading: false, - downloaded - }; - attachments.push(attachment); - - // Create the message - let msg = Message { - id: rumor.id.unwrap().to_hex(), - content: String::new(), - replied_to, - preview_metadata: None, - at: ms_timestamp, - attachments, - reactions: Vec::new(), - mine: is_mine, - pending: false, - failed: false, - }; - - // Send an OS notification for incoming files (do this before locking state) - if !is_mine && is_new { - // Clone necessary data for notification (avoid holding lock during notification) - let display_info = { - let state = STATE.lock().await; - match state.get_profile(&contact) { - Some(profile) => { - if profile.muted { - None // Profile is muted, don't send notification - } else { - // Profile is not muted, send notification - let display_name = if !profile.nickname.is_empty() { - profile.nickname.clone() - } else if !profile.name.is_empty() { - profile.name.clone() - } else { - String::from("New Message") - }; - // Create a "description" of the attachment file - Some((display_name, extension.to_string())) - } - } - // No profile, send notification with default name - None => Some((String::from("New Message"), extension.to_string())), - } - }; - - // Send notification outside of state lock - if let Some((display_name, file_extension)) = display_info { - show_notification(display_name, "Sent a ".to_string() + &get_file_type_description(&file_extension)); - } - } - - // Add the message to the state and handle database save in one operation to avoid multiple locks - let (was_msg_added_to_state, _should_emit, _should_save) = { - let mut state = STATE.lock().await; - let was_added = state.add_message_to_participant(&contact, msg.clone()); - (was_added, was_added, was_added) - }; - - // If accepted in-state: commit to the DB and emit to the frontend - if was_msg_added_to_state { - // Send it to the frontend - if let Some(handle) = TAURI_APP.get() { - handle.emit("message_new", serde_json::json!({ - "message": &msg, - "chat_id": &contact - })).unwrap(); - } - - // Save the chat/messages to DB (chat_id = contact npub for DMs) - if let Some(handle) = TAURI_APP.get() { - // Get all messages for this chat and save them - let all_messages = { - let state = STATE.lock().await; - state.get_chat(&contact).map(|chat| chat.messages.clone()).unwrap_or_default() - }; - let _ = save_chat_messages(handle.clone(), &contact, &all_messages).await; - } - // Ensure OS badge is updated immediately after accepting the attachment - if let Some(handle) = TAURI_APP.get() { - let _ = update_unread_counter(handle.clone()).await; - } - } - - was_msg_added_to_state - } - // Vector-specific events (NIP-78) - else if rumor.kind == Kind::ApplicationSpecificData { - // Ensure the application target is ours - match rumor.tags.find(TagKind::d()) { - Some(d_tag) => { - if d_tag.content().unwrap() == "vector" { - // Typing Indicator - if rumor.content == "typing" { - // A NIP-40 expiry must be present - match rumor.tags.find(TagKind::Expiration) { - Some(ex_tag) => { - // And it must be within 30 seconds - let expiry_timestamp: u64 = - ex_tag.content().unwrap().parse().unwrap_or(0); - // Check if the expiry timestamp is within 30 seconds from now - let current_timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - if expiry_timestamp <= current_timestamp + 30 - && expiry_timestamp > current_timestamp - { - // Now we apply the typing indicator to it's author profile - let mut state = STATE.lock().await; - match state.get_profile_mut(&rumor.pubkey.to_bech32().unwrap()) { - Some(profile) => { - // Apply typing indicator - profile.typing_until = expiry_timestamp; - - // Update the frontend - let handle = TAURI_APP.get().unwrap(); - handle.emit("profile_update", &profile).unwrap(); - true - } - None => false, - } - } else { - false - } - } - None => false, - } - } else { - false - } - } else { - false - } - } - None => false, - } - } - - // Live MLS Welcome inside GiftWrap (process immediately, no manual sync required) - else if rumor.kind == Kind::MlsWelcome { - // Convert rumor Event -> UnsignedEvent - let unsigned_opt = serde_json::to_string(&rumor) - .ok() - .and_then(|s| nostr_sdk::UnsignedEvent::from_json(s.as_bytes()).ok()); - - if let Some(unsigned) = unsigned_opt { - // Outer giftwrap id is our wrapper id for dedup/logs - let wrapper_id = event.id; - let app_handle = TAURI_APP.get().cloned(); - - // Use blocking thread for non-Send MLS engine - let processed = tokio::task::spawn_blocking(move || { - if app_handle.is_none() { - return false; - } - let handle = app_handle.unwrap(); - let svc = MlsService::new_persistent(&handle); - if let Ok(mls) = svc { - if let Ok(engine) = mls.engine() { - match engine.process_welcome(&wrapper_id, &unsigned) { - Ok(_) => { - println!("[MLS][live][welcome] processed wrapper_id={}", wrapper_id); - return true; - } - Err(e) => { - eprintln!("[MLS][live][welcome] process_welcome failed wrapper_id={} err={}", wrapper_id, e); - return false; - } - } - } - } - false - }) - .await - .unwrap_or(false); - - if processed { - // Notify UI so invites list can refresh via list_pending_mls_welcomes() - if let Some(app) = TAURI_APP.get() { - let _ = app.emit("mls_invite_received", serde_json::json!({ - "wrapper_event_id": wrapper_id.to_hex() - })); - } - true - } else { - false - } - } else { - eprintln!("[MLS][live][welcome] failed to convert rumor to UnsignedEvent"); - false - } - } else { - false - } - } - Err(_) => false, - } -}*/ - /* MLS live subscriptions overview (using Marmot/MDK): - GiftWrap subscription (Kind::GiftWrap): @@ -3108,18 +2431,6 @@ fn show_notification_generic(data: NotificationData) { } } -/// Legacy notification function for backwards compatibility -#[tauri::command] -fn show_notification(title: String, content: String) { - let data = NotificationData { - notification_type: NotificationType::DirectMessage, - title, - body: content, - group_name: None, - sender_name: None, - }; - show_notification_generic(data); -} /// Decrypts and saves an attachment to disk /// From 182a0a92257aff0ad65f216027d966b814ef6718 Mon Sep 17 00:00:00 2001 From: JSKitty Date: Sat, 4 Oct 2025 21:09:35 +0100 Subject: [PATCH 2/2] add: Deep Rescan utility tool Deep Rescan is a Dangerzone button that allows users to rescan their entire Vector history, deeper than the default sync - this can be used to recover messages that Vector loses due to unforeseen traditional sync issues (internet outage during section syncs, disk issues, etc). --- src-tauri/src/lib.rs | 83 ++++++++++++++++++++++++++++++++++++++++++-- src/index.html | 1 + src/js/settings.js | 24 +++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2efd98dd..63e79c4a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -100,6 +100,7 @@ lazy_static! { enum SyncMode { ForwardSync, // Initial sync from most recent message going backward BackwardSync, // Syncing historically old messages + DeepRescan, // Deep rescan mode - continues until 30 days of no events Finished // Sync complete } @@ -662,6 +663,22 @@ async fn fetch_messages( state.sync_window_start = new_window_start; state.sync_window_end = new_window_end; + ( + Timestamp::from_secs(new_window_start), + Timestamp::from_secs(new_window_end) + ) + } else if state.sync_mode == SyncMode::DeepRescan { + // Deep rescan mode - scan backwards in 2-day increments until 30 days of no events + let window_start = state.sync_window_start; + + // Move window backward in time in 2-day increments + let new_window_end = window_start; + let new_window_start = window_start - (60 * 60 * 24 * 2); // Always 2 days + + // Update state with new window + state.sync_window_start = new_window_start; + state.sync_window_end = new_window_end; + ( Timestamp::from_secs(new_window_start), Timestamp::from_secs(new_window_end) @@ -715,7 +732,8 @@ async fn fetch_messages( .unwrap() }; - // Process events without holding any locks + // Count total events fetched (for DeepRescan) and new messages added (for other modes) + let total_events_count = events.len() as u16; let mut new_messages_count: u16 = 0; for event in events.into_iter() { // Count the amount of accepted (new) events @@ -732,8 +750,15 @@ async fn fetch_messages( // Increment total iterations counter state.sync_total_iterations += 1; - // Update state based on if messages were found - if new_messages_count > 0 { + // For DeepRescan, use total events count; for other modes, use new messages count + let events_found = if state.sync_mode == SyncMode::DeepRescan { + total_events_count + } else { + new_messages_count + }; + + // Update state based on if events were found + if events_found > 0 { state.sync_empty_iterations = 0; } else { state.sync_empty_iterations += 1; @@ -791,6 +816,17 @@ async fn fetch_messages( state.sync_mode = SyncMode::Finished; continue_sync = false; } + } else if state.sync_mode == SyncMode::DeepRescan { + // For deep rescan, continue until: + // No messages found for 15 consecutive iterations (30 days of no events) + // Each iteration is 2 days, so 15 iterations = 30 days + let enough_empty_iterations = state.sync_empty_iterations >= 15; + + if enough_empty_iterations { + // We've completed deep rescan + state.sync_mode = SyncMode::Finished; + continue_sync = false; + } } else { continue_sync = false; // Unknown state, stop syncing } @@ -2900,6 +2936,45 @@ async fn stop_recording() -> Result, String> { AudioRecorder::global().stop() } +#[tauri::command] +async fn deep_rescan(handle: AppHandle) -> Result { + // Check if a scan is already in progress + { + let state = STATE.lock().await; + if state.is_syncing { + return Err("Already Scanning! Please wait for the current scan to finish.".to_string()); + } + } + + // Start a deep rescan by forcing DeepRescan mode + { + let mut state = STATE.lock().await; + let now = Timestamp::now(); + + // Set up for deep rescan starting from now + state.is_syncing = true; + state.sync_mode = SyncMode::DeepRescan; + state.sync_empty_iterations = 0; + state.sync_total_iterations = 0; + + // Start with a 2-day window from now + let two_days_ago = now.as_u64() - (60 * 60 * 24 * 2); + state.sync_window_start = two_days_ago; + state.sync_window_end = now.as_u64(); + } + + // Trigger the first fetch + fetch_messages(handle, false, None).await; + + Ok(true) +} + +#[tauri::command] +async fn is_scanning() -> bool { + let state = STATE.lock().await; + state.is_syncing +} + #[tauri::command] async fn logout(handle: AppHandle) { // Lock the state to ensure nothing is added to the DB before restart @@ -4581,6 +4656,8 @@ pub fn run() { message::react_to_message, message::fetch_msg_metadata, fetch_messages, + deep_rescan, + is_scanning, warmup_nip96_servers, get_chat_messages, generate_blurhash_preview, diff --git a/src/index.html b/src/index.html index b04fbcaa..1f1b8b48 100644 --- a/src/index.html +++ b/src/index.html @@ -309,6 +309,7 @@

What's New:

Dangerzone

+ diff --git a/src/js/settings.js b/src/js/settings.js index 2a0c2947..e416cad4 100644 --- a/src/js/settings.js +++ b/src/js/settings.js @@ -596,6 +596,30 @@ domSettingsLogout.onclick = async (evt) => { await invoke('logout'); }; +// Listen for Deep Rescan clicks +const domSettingsDeepRescan = document.getElementById('deep-rescan-btn'); +domSettingsDeepRescan.onclick = async (evt) => { + try { + // Prompt for confirmation first + const fConfirm = await popupConfirm('Deep Rescan', 'This will forcefully sync your message history backwards in two-day sections until 30 days of no events are found. This may take some time. Continue?', false, '', 'vector_warning.svg'); + if (!fConfirm) return; + + // Check if already scanning (only after user confirms) + const isScanning = await invoke('is_scanning'); + if (isScanning) { + await popupConfirm('Already Scanning!', 'Please wait for the current scan to finish before starting a deep rescan.', true, '', 'vector_warning.svg'); + return; + } + + // Start the deep rescan + await invoke('deep_rescan'); + await popupConfirm('Deep Rescan Started', 'The deep rescan has been initiated. You can continue using the app while it runs in the background.', true, '', 'vector-check.svg'); + } catch (error) { + console.error('Deep rescan failed:', error); + await popupConfirm('Deep Rescan Failed', error.toString(), true, '', 'vector_warning.svg'); + } +}; + // Listen for Export Account clicks domSettingsExport.onclick = async (evt) => { try {