diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 4205fd097..46340cc25 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -1811,6 +1811,11 @@ impl RoomScreen { .update_tombstone_footer(cx, tl.kind.room_id(), Some(&successor_room_details)); tl.tombstone_info = Some(successor_room_details); } + TimelineUpdate::RoomEncrypted => { + tl.is_encrypted = true; + self.view.room_input_bar(cx, ids!(room_input_bar)) + .update_encryption_status(cx, true); + } TimelineUpdate::LinkPreviewFetched => {} TimelineUpdate::FileUploadStarted { upload_id, file_name, in_reply_to, abort_handle } => { self.view.room_input_bar(cx, ids!(room_input_bar)) @@ -2491,7 +2496,7 @@ impl RoomScreen { return; } if !self.is_loaded && self.all_rooms_loaded { - panic!("BUG: timeline {kind} is not loaded, but its RoomScreen \ + error!("BUG: timeline {kind} is not loaded, but its RoomScreen \ was not waiting for its timeline to be loaded either."); } return; @@ -2501,6 +2506,7 @@ impl RoomScreen { update_sender, request_sender, successor_room, + is_encrypted, } = timeline_endpoints; // Start with the basic tombstone info, and fetch the full details @@ -2521,6 +2527,7 @@ impl RoomScreen { // This doesn't mean that the user can actually perform all actions; // the power levels will be updated from the homeserver once the room is opened. user_power: UserPowerLevels::all(), + is_encrypted, // Room members start as None and get populated when fetched from the server room_members: None, // We assume timelines being viewed for the first time haven't been fully paginated. @@ -2733,6 +2740,7 @@ impl RoomScreen { saved_room_input_bar_state, tl_state.user_power, tl_state.tombstone_info.as_ref(), + tl_state.is_encrypted, ); } @@ -3042,6 +3050,9 @@ pub enum TimelineUpdate { PinnedEvents(Vec), /// An update containing the currently logged-in user's power levels for this room. UserPowerLevels(UserPowerLevels), + /// A notice that this room has been changed to use encryption. + /// It's only possible to go from unencrypted --> encrypted, not the other way. + RoomEncrypted, /// An update to the currently logged-in user's own read receipt for this room. OwnUserReadReceipt(Receipt), /// A notice that the given room has been tombstoned (closed) @@ -3193,6 +3204,9 @@ struct TimelineUiState { /// The power levels of the currently logged-in user in this room. user_power: UserPowerLevels, + /// Whether this room is encrypted. Once enabled it can never be disabled. + is_encrypted: bool, + /// The list of room members for this room. room_members: Option>>, diff --git a/src/room/room_input_bar.rs b/src/room/room_input_bar.rs index c0a61c1a5..2cdde437e 100644 --- a/src/room/room_input_bar.rs +++ b/src/room/room_input_bar.rs @@ -587,6 +587,17 @@ impl RoomInputBar { self.view.view(cx, ids!(can_not_send_message_notice)).set_visible(cx, !can_send); } + /// Sets the message input's placeholder to reflect this room's encryption status. + fn update_encryption_status(&mut self, cx: &mut Cx, is_encrypted: bool) { + let empty_text = if is_encrypted { + "Send an encrypted message..." + } else { + "Send an unencrypted message..." + }; + self.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) + .set_empty_text(cx, empty_text.to_string()); + } + /// Returns true if the TSP signing checkbox is checked, false otherwise. /// /// If TSP is not enabled, this will always return false. @@ -755,6 +766,12 @@ impl RoomInputBarRef { inner.update_tombstone_footer(cx, tombstoned_room_id, successor_room_details); } + /// Updates the message input's placeholder based on this room's encryption status. + pub fn update_encryption_status(&self, cx: &mut Cx, is_encrypted: bool) { + let Some(mut inner) = self.borrow_mut() else { return }; + inner.update_encryption_status(cx, is_encrypted); + } + /// Opens the native picker to upload a photo or video into this room. pub fn open_photo_video_picker( &self, @@ -816,6 +833,7 @@ impl RoomInputBarRef { saved_state: RoomInputBarState, user_power_levels: UserPowerLevels, tombstone_info: Option<&SuccessorRoomDetails>, + is_encrypted: bool, ) { let Some(mut inner) = self.borrow_mut() else { return }; let RoomInputBarState { @@ -831,6 +849,7 @@ impl RoomInputBarRef { // This must happen before we restore the state of the `EditingPane`, // because the call to `show_editing_pane()` might re-update the `input_bar`'s visibility. inner.update_user_power_levels(cx, user_power_levels); + inner.update_encryption_status(cx, is_encrypted); // 1. Restore the state of the TextInput within the MentionableTextInput. inner.text_input(cx, ids!(input_bar.mentionable_text_input.text_input)) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 89a32a0d1..063a32594 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -2363,6 +2363,7 @@ pub struct TimelineEndpoints { pub update_receiver: crossbeam_channel::Receiver, pub request_sender: TimelineRequestSender, pub successor_room: Option, + pub is_encrypted: bool, } /// Info about a timeline for a joined room or a thread in a joined room. @@ -2371,8 +2372,7 @@ struct PerTimelineDetails { timeline: Arc, /// A clone-able sender for updates to this timeline. timeline_update_sender: crossbeam_channel::Sender, - /// A tuple of two separate channel endpoints that can only be taken *once* by the main UI thread. - /// + /// A tuple of two separate channel endpoints that can only be taken *once* by the main UI thread: /// 1. The single receiver that can receive updates from this timeline. /// * When a new room is joined (or a thread is opened), an unbounded crossbeam channel will be created /// and its sender given to a background task (the `timeline_subscriber_handler()`) @@ -2585,6 +2585,7 @@ pub fn take_timeline_endpoints(kind: &TimelineKind) -> Option update_receiver, request_sender, successor_room: details.timeline.room().successor_room(), + is_encrypted: details.timeline.room().encryption_state().is_encrypted(), }) } @@ -2624,6 +2625,7 @@ struct RoomListServiceRoomInfo { is_direct: bool, is_marked_unread: bool, is_tombstoned: bool, + is_encrypted: bool, tags: Option, user_power_levels: Option, latest_event_timestamp: Option, @@ -2655,6 +2657,7 @@ impl RoomListServiceRoomInfo { is_direct: is_direct.unwrap_or(false), is_marked_unread: room.is_marked_unread(), is_tombstoned: room.is_tombstoned(), + is_encrypted: room.encryption_state().is_encrypted(), tags: tags.ok().flatten(), user_power_levels, latest_event_timestamp: room.latest_event_timestamp(), @@ -3473,6 +3476,18 @@ async fn update_room( error!("BUG: could not find JoinedRoomDetails for room {new_room_id} where power levels changed."); } } + + if !old_room.is_encrypted && new_room.is_encrypted { + if let Some(timeline_update_sender) = get_timeline_update_sender(&new_room_id) { + log!("Room {new_room_id} is now encrypted."); + match timeline_update_sender.send(TimelineUpdate::RoomEncrypted) { + Ok(_) => SignalToUI::set_ui_signal(), + Err(_) => error!("Failed to send the RoomEncrypted update to room {new_room_id}"), + } + } else { + error!("BUG: could not find JoinedRoomDetails for room {new_room_id} that became encrypted."); + } + } } Ok(()) }