diff --git a/ui/src/components/app/freenet_api/freenet_synchronizer.rs b/ui/src/components/app/freenet_api/freenet_synchronizer.rs index 658548ee..b0113afe 100644 --- a/ui/src/components/app/freenet_api/freenet_synchronizer.rs +++ b/ui/src/components/app/freenet_api/freenet_synchronizer.rs @@ -31,6 +31,9 @@ pub enum SynchronizerMessage { ConnectionLost, /// Sent when page becomes visible after being hidden (e.g., after sleep/wake) PageBecameVisible, + /// Sent to refresh all room states after reconnection (e.g., after sleep/wake) + /// This fetches current state for all rooms to catch any updates missed during suspension + RefreshAllRooms, ApiResponse(Result), AcceptInvitation { owner_vk: VerifyingKey, @@ -206,16 +209,53 @@ impl FreenetSynchronizer { error!("Failed to send Connect message after wake: {}", e); } } else { - // Connection appears active, trigger a room sync to verify - // This will fail fast if the connection is actually dead - info!("Connection appears active, triggering room sync to verify"); + // Connection appears active, but we may have missed updates during suspension. + // First verify connection with ProcessRooms, then refresh all rooms to + // catch any updates that arrived while the page was hidden/PC was suspended. + info!("Connection appears active, refreshing all rooms to catch missed updates"); if let Err(e) = - message_tx.unbounded_send(SynchronizerMessage::ProcessRooms) + message_tx.unbounded_send(SynchronizerMessage::RefreshAllRooms) { - error!("Failed to send ProcessRooms message after wake: {}", e); + error!("Failed to send RefreshAllRooms message after wake: {}", e); } } } + SynchronizerMessage::RefreshAllRooms => { + // Refresh all room states by sending GET requests + // This catches any updates missed during PC suspension or page being hidden + info!("Refreshing all rooms to catch missed updates"); + if !connection_manager.is_connected() { + info!( + "Connection not ready, deferring refresh and attempting to connect" + ); + if let Err(e) = message_tx.unbounded_send(SynchronizerMessage::Connect) + { + error!("Failed to send Connect message: {}", e); + } + continue; + } + if let Err(e) = response_handler + .get_room_synchronizer_mut() + .refresh_all_rooms() + .await + { + error!("Error refreshing rooms: {}", e); + // Check if this is a WebSocket error that needs reconnection + let error_str = e.to_string(); + if error_str.contains("WebSocket") || error_str.contains("not open") { + warn!( + "WebSocket error during room refresh, triggering reconnection" + ); + if let Err(e) = + message_tx.unbounded_send(SynchronizerMessage::ConnectionLost) + { + error!("Failed to send ConnectionLost: {}", e); + } + } + } else { + info!("Successfully refreshed all rooms"); + } + } SynchronizerMessage::Connect => { info!("Connecting to Freenet"); match connection_manager diff --git a/ui/src/components/app/freenet_api/response_handler/get_response.rs b/ui/src/components/app/freenet_api/response_handler/get_response.rs index 064a2a3e..314f37f0 100644 --- a/ui/src/components/app/freenet_api/response_handler/get_response.rs +++ b/ui/src/components/app/freenet_api/response_handler/get_response.rs @@ -28,7 +28,7 @@ pub async fn handle_get_response( // First try to find the owner_vk from SYNC_INFO let owner_vk = SYNC_INFO.read().get_owner_vk_for_instance_id(key.id()); - // If we couldn't find it in SYNC_INFO, try to find it in PENDING_INVITES by checking contract keys + // If we couldn't find it in SYNC_INFO, try fallback mechanisms let owner_vk = if owner_vk.is_none() { // This is a fallback mechanism in case SYNC_INFO wasn't properly set up warn!( @@ -36,6 +36,7 @@ pub async fn handle_get_response( key.id() ); + // First try PENDING_INVITES let pending_invites = PENDING_INVITES.read(); let mut found_owner_vk = None; @@ -50,15 +51,34 @@ pub async fn handle_get_response( break; } } + drop(pending_invites); + + // If not in pending invites, try ROOMS (for refresh after suspension) + if found_owner_vk.is_none() { + let rooms = ROOMS.read(); + for (owner_key, room_data) in rooms.map.iter() { + if room_data.contract_key.id() == key.id() { + info!( + "Found matching owner key in existing rooms: {:?}", + MemberId::from(*owner_key) + ); + found_owner_vk = Some(*owner_key); + break; + } + } + } found_owner_vk } else { owner_vk }; - // Now check if this is for a pending invitation + // Now check if this is for a pending invitation or an existing room needing refresh if let Some(owner_vk) = owner_vk { - if PENDING_INVITES.read().map.contains_key(&owner_vk) { + let is_pending_invite = PENDING_INVITES.read().map.contains_key(&owner_vk); + let is_existing_room = ROOMS.read().map.contains_key(&owner_vk); + + if is_pending_invite { info!("This is a subscription for a pending invitation, adding state"); let retrieved_state: ChatRoomStateV1 = from_cbor_slice::(&state); @@ -304,6 +324,45 @@ pub async fn handle_get_response( info!("Successfully triggered synchronization after joining room"); } } + } else if is_existing_room { + // This is a refresh GET for an already-subscribed room (e.g., after wake from suspension) + info!("Processing GET response for existing room (refresh after suspension)"); + let retrieved_state: ChatRoomStateV1 = from_cbor_slice::(&state); + + ROOMS.with_mut(|rooms| { + if let Some(room_data) = rooms.map.get_mut(&owner_vk) { + // Create parameters for merge + let params = ChatRoomParametersV1 { owner: owner_vk }; + + // Clone current state to avoid borrow issues during merge + let current_state = room_data.room_state.clone(); + + // Merge the retrieved state into the existing state + match room_data + .room_state + .merge(¤t_state, ¶ms, &retrieved_state) + { + Ok(_) => { + info!( + "Successfully merged refreshed state for room {:?}", + MemberId::from(owner_vk) + ); + } + Err(e) => { + error!( + "Failed to merge refreshed state for room {:?}: {}", + MemberId::from(owner_vk), + e + ); + } + } + } + }); + + // Update sync info to reflect we received fresh state + SYNC_INFO.with_mut(|sync_info| { + sync_info.update_sync_status(&owner_vk, RoomSyncStatus::Subscribed); + }); } } diff --git a/ui/src/components/app/freenet_api/room_synchronizer.rs b/ui/src/components/app/freenet_api/room_synchronizer.rs index e7afebdb..17b03109 100644 --- a/ui/src/components/app/freenet_api/room_synchronizer.rs +++ b/ui/src/components/app/freenet_api/room_synchronizer.rs @@ -461,6 +461,69 @@ impl RoomSynchronizer { }); } + /// Refresh all room states by sending GET requests. + /// This is used after PC suspension/wake to catch any updates that were missed + /// while the page was hidden or the machine was suspended. + pub async fn refresh_all_rooms(&self) -> Result<(), SynchronizerError> { + info!("Refreshing all rooms to catch missed updates"); + + // Check if WebAPI is available + let web_api_available = WEB_API.read().is_some(); + if !web_api_available { + warn!("WebAPI not available, skipping room refresh"); + return Err(SynchronizerError::ApiNotInitialized); + } + + // Collect all room owner keys that we're currently tracking + let room_owners: Vec = ROOMS.read().map.keys().copied().collect(); + + if room_owners.is_empty() { + info!("No rooms to refresh"); + return Ok(()); + } + + info!("Refreshing {} rooms", room_owners.len()); + + for owner_vk in room_owners { + let contract_key = owner_vk_to_contract_key(&owner_vk); + + // Send a GET request to fetch the current state + // This will trigger a response that merges any missed updates + let get_request = ContractRequest::Get { + key: *contract_key.id(), + return_contract_code: false, + subscribe: false, // Already subscribed, just need the state + }; + + let client_request = ClientRequest::ContractOp(get_request); + + if let Some(web_api) = WEB_API.write().as_mut() { + match web_api.send(client_request).await { + Ok(_) => { + info!( + "Sent refresh GET request for room {:?}", + MemberId::from(owner_vk) + ); + } + Err(e) => { + // Don't fail the entire refresh if one room fails + error!( + "Error sending refresh GET for room {:?}: {}", + MemberId::from(owner_vk), + e + ); + } + } + } else { + warn!("WebAPI became unavailable during refresh"); + return Err(SynchronizerError::ApiNotInitialized); + } + } + + info!("Finished sending refresh requests for all rooms"); + Ok(()) + } + /// Subscribe to a contract after a successful GET or PUT operation pub async fn subscribe_to_contract( &self, diff --git a/ui/src/example_data.rs b/ui/src/example_data.rs index 89294221..ca83e8a7 100644 --- a/ui/src/example_data.rs +++ b/ui/src/example_data.rs @@ -344,12 +344,16 @@ mod tests { ); // Verify room has at least basic configuration - assert!(!room_data - .room_state - .configuration - .configuration - .name - .is_empty()); + assert!( + room_data + .room_state + .configuration + .configuration + .display + .name + .declared_len() + > 0 + ); // Verify members list exists assert!(!room_data.room_state.members.members.is_empty());