From 989c9ad356e92c254dadda8f76a8f9eef737030b Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Tue, 9 Jun 2026 17:49:02 -0700 Subject: [PATCH] Ensure that content in re-used widgets (timelines) gets properly set The timeline portallist in a room can frequently re-use a widget, so we need to make sure that ALL content that a widget might show gets properly populated/set such that no old content is displayed. Generally this is unlikely or impossible, but we might as well be sure. This includes: * Clear link previews on messages that can't have link previews, e.g., redacted, file, audio, video, location, other unsupported messages. * Hide the edited and TSP indicators * Don't show reactions on redacted messages * Rebuild the row of avatars for a read receipts when it visibly changes. * Space lobby loading spinner --- src/home/edited_indicator.rs | 13 +++++++++++++ src/home/link_preview.rs | 15 +++++++++++++++ src/home/room_read_receipt.rs | 9 ++++++++- src/home/room_screen.rs | 28 ++++++++++++++++++++++------ src/home/space_lobby.rs | 1 + src/tsp/tsp_sign_indicator.rs | 13 +++++++++++++ 6 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/home/edited_indicator.rs b/src/home/edited_indicator.rs index bc1933263..332c7ce58 100644 --- a/src/home/edited_indicator.rs +++ b/src/home/edited_indicator.rs @@ -109,6 +109,12 @@ impl EditedIndicator { self.visible = true; self.redraw(cx); } + + /// Hides this indicator, e.g., for a message that has not been edited. + pub fn hide(&mut self, cx: &mut Cx) { + self.visible = false; + self.redraw(cx); + } } impl EditedIndicatorRef { @@ -118,6 +124,13 @@ impl EditedIndicatorRef { inner.set_latest_edit(cx, event_tl_item); } } + + /// See [`EditedIndicator::hide()`]. + pub fn hide(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.hide(cx); + } + } } diff --git a/src/home/link_preview.rs b/src/home/link_preview.rs index aa4ce9c00..5dc1ccb6a 100644 --- a/src/home/link_preview.rs +++ b/src/home/link_preview.rs @@ -346,6 +346,21 @@ impl LinkPreviewRef { } } + /// Clears any displayed link preview(s), resetting this widget to its empty state. + /// + /// Needed for messages that never show link previews (e.g. redacted messages). + pub fn clear(&mut self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.children.clear(); + inner.last_populated_links.clear(); + inner.show_collapsible_buttons = false; + inner.is_expanded = false; + inner.hidden_links_count = 0; + inner.update_button_and_visibility(cx); + inner.redraw(cx); + } + } + /// Shows the collapsible button for the link preview. /// /// This function is usually called when the link preview is updated. diff --git a/src/home/room_read_receipt.rs b/src/home/room_read_receipt.rs index d2bad9726..42cb29995 100644 --- a/src/home/room_read_receipt.rs +++ b/src/home/room_read_receipt.rs @@ -161,7 +161,14 @@ impl AvatarRow { event_id: Option<&EventId>, receipts_map: &IndexMap, ) { - if receipts_map.len() != self.buttons.len() { + // Rebuild the list of avatars if anything visible changes. + let receipts_changed = self.read_receipts.as_ref().is_none_or(|existing| { + existing.len() != receipts_map.len() || + !existing.keys().rev().take(MAX_VISIBLE_AVATARS_IN_READ_RECEIPT).eq( + receipts_map.keys().rev().take(MAX_VISIBLE_AVATARS_IN_READ_RECEIPT) + ) + }); + if receipts_changed { self.buttons.clear(); for _ in 0..cmp::min(MAX_VISIBLE_AVATARS_IN_READ_RECEIPT, receipts_map.len()) { self.buttons.push(( diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 8ff9b82c8..fb9842e9c 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -3685,6 +3685,7 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); let is_location_fully_drawn = populate_location_message_content( cx, &html_or_plaintext_ref, @@ -3713,6 +3714,7 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); new_drawn_status.content_drawn = populate_file_message_content( cx, &html_or_plaintext_ref, @@ -3740,6 +3742,7 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); new_drawn_status.content_drawn = populate_audio_message_content( cx, &html_or_plaintext_ref, @@ -3767,6 +3770,7 @@ fn populate_message_view( } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); new_drawn_status.content_drawn = populate_video_message_content( cx, &html_or_plaintext_ref, @@ -3819,6 +3823,7 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); item.label(cx, ids!(content.message)).set_text( cx, &format!("[Unsupported {:?}]", msg_like_content.kind), @@ -3883,6 +3888,8 @@ fn populate_message_view( (item, true) } else { let html_or_plaintext_ref = item.html_or_plaintext(cx, ids!(content.message)); + // Redacted messages have no link preview; clear any stale one from a reused row. + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); // Apply a smaller font size for redacted messages. let mut html_widget = html_or_plaintext_ref.html(cx, ids!(html_view.html)); script_apply_eval!(cx, html_widget, { @@ -3908,6 +3915,7 @@ fn populate_message_view( if existed && item_drawn_status.content_drawn { (item, true) } else { + item.link_preview(cx, ids!(content.link_preview_view)).clear(cx); item.label(cx, ids!(content.message)).set_text( cx, &format!("[Unsupported {:?}] ", other), @@ -3923,9 +3931,13 @@ fn populate_message_view( // If we didn't use a cached item, we need to draw all other message content: // the reactions, the read receipts avatar row, the reply preview. if !used_cached_item { + // Redacted messages must never show reactions, even if the SDK still reports some. + let reactions = (!matches!(msg_like_content.kind, MsgLikeKind::Redacted)) + .then(|| event_tl_item.content().reactions()) + .flatten(); item.reaction_list(cx, ids!(content.reaction_list)).set_list( cx, - event_tl_item.content().reactions(), + reactions, timeline_kind.clone(), timeline_event_id.clone(), item_id, @@ -4043,12 +4055,13 @@ fn populate_message_view( item.timestamp(cx, ids!(profile.timestamp)).set_date_time(cx, dt); } - // Set the "edited" indicator if this message was edited. + // Set the "edited" indicator if this message was edited, otherwise hide it + // (this widget may be reused for a non-edited message at the same row). + let edited_indicator = item.edited_indicator(cx, ids!(profile.edited_indicator)); if msg_like_content.as_message().is_some_and(|m| m.is_edited()) { - item.edited_indicator(cx, ids!(profile.edited_indicator)).set_latest_edit( - cx, - event_tl_item, - ); + edited_indicator.set_latest_edit(cx, event_tl_item); + } else { + edited_indicator.hide(cx); } #[cfg(feature = "tsp")] { @@ -4081,6 +4094,9 @@ fn populate_message_view( log!("TSP signature state for event {:?} is {:?}", event_tl_item.event_id(), tsp_sign_state); item.tsp_sign_indicator(cx, ids!(profile.tsp_sign_indicator)) .show_with_state(cx, tsp_sign_state); + } else { + // Hide the TSP indicator (in case we reused the message widget at this item index). + item.tsp_sign_indicator(cx, ids!(profile.tsp_sign_indicator)).hide(cx); } } diff --git a/src/home/space_lobby.rs b/src/home/space_lobby.rs index de0591517..d78395445 100644 --- a/src/home/space_lobby.rs +++ b/src/home/space_lobby.rs @@ -1182,6 +1182,7 @@ impl Widget for SpaceLobbyScreen { let item = if self.is_loading && item_id == 0 { let item = list.item(cx, item_id, id!(status_label)); item.child_by_path(ids!(label)).as_label().set_text(cx, "Loading rooms and spaces..."); + item.child_by_path(ids!(loading_spinner)).set_visible(cx, true); item } // No entries found diff --git a/src/tsp/tsp_sign_indicator.rs b/src/tsp/tsp_sign_indicator.rs index 2c95bd168..3731e29ab 100644 --- a/src/tsp/tsp_sign_indicator.rs +++ b/src/tsp/tsp_sign_indicator.rs @@ -141,6 +141,12 @@ impl TspSignIndicator { self.visible = true; self.redraw(cx); } + + /// Hides this indicator, e.g., for a message that has no TSP signature. + pub fn hide(&mut self, cx: &mut Cx) { + self.visible = false; + self.redraw(cx); + } } impl TspSignIndicatorRef { @@ -150,6 +156,13 @@ impl TspSignIndicatorRef { inner.show_with_state(cx, state); } } + + /// See [`TspSignIndicator::hide()`]. + pub fn hide(&self, cx: &mut Cx) { + if let Some(mut inner) = self.borrow_mut() { + inner.hide(cx); + } + } }