diff --git a/src/backend/manager.rs b/src/backend/manager.rs index c25fba0..10b41f6 100644 --- a/src/backend/manager.rs +++ b/src/backend/manager.rs @@ -490,6 +490,16 @@ impl Manager { Thread::Contact(uuid) }; + // Fast path: if we already know this thread, return the cached + // `Channel` so callers see notify-signals (e.g. `is-typing`) on the + // same GObject instance the UI is bound to. Without this, the + // incoming `TypingMessage` handler would toggle `is-typing` on a + // freshly-built orphan `Channel` and the typing indicator would + // silently never light up for known contacts. + if let Some(cached) = self.imp().channels.borrow().get(&thread).cloned() { + return cached; + } + let contact = Contact::from_service_address(&uuid, self).await; let channel = Channel::from_contact_or_group(contact, group, self).await; channel.initialize_avatar().await; @@ -977,3 +987,49 @@ mod imp { } } } + +#[cfg(test)] +mod tests { + use super::*; + use libsignal_service::prelude::Uuid; + use libsignal_service::protocol::ServiceId; + + /// Regression test for the channel-cache miss in + /// [`Manager::channel_from_uuid_or_group`]. + /// + /// The function previously always built and returned a fresh + /// `Channel` GObject, even when one for the same thread already + /// lived in the cache. Property notifications (e.g. `is-typing` + /// for incoming `TypingMessage`s) were therefore emitted on an + /// orphan instance the UI was not bound to, and indicators + /// silently never reflected the change. This test asserts the + /// invariant that a cached thread short-circuits to the cached + /// `Channel` instance. + #[test] + fn cached_thread_returns_cached_channel() { + gtk::init().expect("gtk to initialize"); + let manager: Manager = glib::Object::new(); + let cached: Channel = glib::Object::new(); + let uuid = Uuid::from_u128(0x1); + let thread = Thread::Contact(ServiceId::Aci(uuid.into())); + + manager + .imp() + .channels + .borrow_mut() + .insert(thread, cached.clone()); + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio current-thread runtime to build"); + let result = runtime + .block_on(manager.channel_from_uuid_or_group(ServiceId::Aci(uuid.into()), &None)); + + assert!( + cached.eq(&result), + "channel_from_uuid_or_group must return the cached `Channel` instance \ + so notify-signals reach widgets bound to it" + ); + } +}