From 3544fbcb2e562ab5648d25bf04614440dc2839a7 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Thu, 1 Aug 2024 10:44:51 -0700 Subject: [PATCH 1/4] Post-`ignore_user` pagination does work to refill the cleared timeline. But, there are still a couple of things we need to fix up: * saving which event ID is currently shown on each timeline, such that we can jump back to displaying that event after the timelines get cleared and then refilled. * subscribing to an ignored user stream, such that we can receive an update when the current logged-in user (un)ignores someone from a different client that we do not control. * See `Client::subscribe_to_ignore_user_list_changes()`: --- src/home/room_screen.rs | 1 + src/sliding_sync.rs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/home/room_screen.rs b/src/home/room_screen.rs index 1405a727..085cdae4 100644 --- a/src/home/room_screen.rs +++ b/src/home/room_screen.rs @@ -995,6 +995,7 @@ impl Widget for Timeline { if clear_cache { tl.content_drawn_since_last_update.clear(); tl.profile_drawn_since_last_update.clear(); + tl.fully_paginated = false; } else { tl.content_drawn_since_last_update.remove(changed_indices.clone()); tl.profile_drawn_since_last_update.remove(changed_indices.clone()); diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 78a2d833..a02ee4dc 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -388,11 +388,26 @@ async fn async_worker(mut receiver: UnboundedReceiver) -> Result< if new_room_member.is_ignored() { "" } else { "un" }, ); enqueue_user_profile_update(UserProfileUpdate::RoomMemberOnly { - room_id, + room_id: room_id.clone(), room_member: new_room_member, }); } } + + // After a successful (un)ignore operation, all timelines get completely cleared, + // so we must re-fetch all timelines for all rooms. + // Start with the current room, since that's the one being displayed. + for room_id_to_paginate in client.get_room(&room_id) + .into_iter() + .chain(client.joined_rooms()) + .map(|room| room.room_id().to_owned()) + { + submit_async_request(MatrixRequest::PaginateRoomTimeline { + room_id: room_id_to_paginate, + num_events: 50, + forwards: false, + }); + } }); } From cbf1191bb128362c0ed0950f4afcd785065b5bf6 Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Thu, 1 Aug 2024 14:52:01 -0700 Subject: [PATCH 2/4] subscribe to updates to the list of ignored users This allows Robrix to be aware of the current logged-in user ignoring or unignoring a user on a different client instance, and respond to the fact that an ignore/unignore action has occurred elsewhere, i.e., *beyond* the ignore user UI flow that is driven from the UserProfileSlidingPane action buttons. As such, we move the logic for re-paginating all cleared timelines to the handler for updates to the ignored users lists. --- src/sliding_sync.rs | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index a02ee4dc..e5e1f137 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -372,10 +372,6 @@ async fn async_worker(mut receiver: UnboundedReceiver) -> Result< if ignore_result.is_err() { return; } - // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. - // Therefore, we need to re-fetch all timelines for all rooms, - // and currently the only way to actually accomplish this is via pagination. - // See: // We need to re-acquire the `RoomMember` object now that its state // has changed, i.e., the user has been (un)ignored). @@ -394,20 +390,19 @@ async fn async_worker(mut receiver: UnboundedReceiver) -> Result< } } - // After a successful (un)ignore operation, all timelines get completely cleared, - // so we must re-fetch all timelines for all rooms. - // Start with the current room, since that's the one being displayed. - for room_id_to_paginate in client.get_room(&room_id) - .into_iter() - .chain(client.joined_rooms()) - .map(|room| room.room_id().to_owned()) - { - submit_async_request(MatrixRequest::PaginateRoomTimeline { - room_id: room_id_to_paginate, - num_events: 50, - forwards: false, - }); - } + // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. + // Therefore, we need to re-fetch all timelines for all rooms, + // and currently the only way to actually accomplish this is via pagination. + // See: + // + // Note that here we only proactively re-paginate the *current* room + // (the one being viewed by the user when this ignore request was issued), + // and all other rooms will be re-paginated in on-demand when they are viewed. + submit_async_request(MatrixRequest::PaginateRoomTimeline { + room_id, + num_events: 50, + forwards: false, + }); }); } @@ -653,6 +648,9 @@ async fn async_main_loop() -> Result<()> { let (client, _token) = login(cli).await?; CLIENT.set(client.clone()).expect("BUG: CLIENT already set!"); + // Listen for updates to the ignored user list. + handle_ignore_user_list_subscriber(client.clone()); + let mut filters = ListFilters::default(); filters.not_room_types = vec!["m.space".into()]; // Ignore spaces for now. @@ -904,6 +902,28 @@ async fn async_main_loop() -> Result<()> { } +fn handle_ignore_user_list_subscriber(client: Client) { + let mut subscriber = client.subscribe_to_ignore_user_list_changes(); + Handle::current().spawn(async move { + while let Some(_ignore_list) = subscriber.next().await { + log!("Received an updated ignored-user list: {_ignore_list:?}"); + + // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. + // Therefore, we need to re-fetch all timelines for all rooms, + // and currently the only way to actually accomplish this is via pagination. + // See: + for joined_room in client.joined_rooms() { + submit_async_request(MatrixRequest::PaginateRoomTimeline { + room_id: joined_room.room_id().to_owned(), + num_events: 50, + forwards: false, + }); + } + } + }); +} + + async fn timeline_subscriber_handler( room_id: OwnedRoomId, timeline: Arc, From 34429265b780830c3ada3212c2c904847785989a Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Thu, 1 Aug 2024 15:02:33 -0700 Subject: [PATCH 3/4] correct inaccurate comment about post-ignore timeline repagination --- src/sliding_sync.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index e5e1f137..54557323 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -397,7 +397,7 @@ async fn async_worker(mut receiver: UnboundedReceiver) -> Result< // // Note that here we only proactively re-paginate the *current* room // (the one being viewed by the user when this ignore request was issued), - // and all other rooms will be re-paginated in on-demand when they are viewed. + // and all other rooms will be re-paginated in `handle_ignore_user_list_subscriber()`.` submit_async_request(MatrixRequest::PaginateRoomTimeline { room_id, num_events: 50, From 20da196a78dd7a70dd4f20471c1e32835be1f6db Mon Sep 17 00:00:00 2001 From: Kevin Boos Date: Thu, 1 Aug 2024 17:13:30 -0700 Subject: [PATCH 4/4] Maintain our own list of ignored users Do not attempt to trust or use the Matrix SDK's list of ignored users, as it does not get updated properly in the current version of the SDK. We now maintain our own list, which does properly get synced when the logged-in users ignores/unignores other users on a different client or session. This includes correctly setting the `ignore_user_button`'s text. --- src/profile/user_profile.rs | 11 +++++--- src/sliding_sync.rs | 50 ++++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/profile/user_profile.rs b/src/profile/user_profile.rs index 61b24c76..ae06e311 100644 --- a/src/profile/user_profile.rs +++ b/src/profile/user_profile.rs @@ -3,7 +3,7 @@ use crossbeam_queue::SegQueue; use makepad_widgets::*; use matrix_sdk::{room::{RoomMember, RoomMemberRole}, ruma::{events::room::member::MembershipState, OwnedRoomId, OwnedUserId, UserId}}; use crate::{ - shared::avatar::AvatarWidgetExt, sliding_sync::{get_client, submit_async_request, MatrixRequest}, utils + shared::avatar::AvatarWidgetExt, sliding_sync::{get_client, is_user_ignored, submit_async_request, MatrixRequest}, utils }; thread_local! { @@ -728,9 +728,12 @@ impl Widget for UserProfileSlidingPane { let ignore_user_button = self.button(id!(ignore_user_button)); ignore_user_button.set_enabled(!is_pane_showing_current_account && info.room_member.is_some()); - ignore_user_button.set_text(info.room_member.as_ref() - .and_then(|rm| rm.is_ignored().then_some("Unignore (Unblock) User")) - .unwrap_or("Ignore (Block) User") + // Unfortunately the Matrix SDK's RoomMember type does not properly track + // the `ignored` state of a user, so we have to maintain it separately. + let is_ignored = info.room_member.as_ref() + .is_some_and(|rm| is_user_ignored(rm.user_id())); + ignore_user_button.set_text( + if is_ignored { "Unignore (Unblock) User" } else { "Ignore (Block) User" } ); self.view.draw_walk(cx, scope, walk) diff --git a/src/sliding_sync.rs b/src/sliding_sync.rs index 54557323..34d87a05 100644 --- a/src/sliding_sync.rs +++ b/src/sliding_sync.rs @@ -6,8 +6,8 @@ use imbl::Vector; use makepad_widgets::{error, log, SignalToUI}; use matrix_sdk::{ config::RequestConfig, media::MediaRequest, room::RoomMember, ruma::{ - api::client::session::get_login_types::v3::LoginType, assign, events::{room::message::RoomMessageEventContent, FullStateEventContent, StateEventType}, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UInt - }, sliding_sync::http::request::{AccountData, E2EE, ListFilters, ToDevice}, Client, Room, SlidingSyncList, SlidingSyncMode + api::client::session::get_login_types::v3::LoginType, assign, events::{room::message::RoomMessageEventContent, FullStateEventContent, StateEventType}, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, UInt, UserId + }, sliding_sync::http::request::{AccountData, ListFilters, ToDevice, E2EE}, Client, Room, SlidingSyncList, SlidingSyncMode }; use matrix_sdk_ui::{timeline::{AnyOtherFullStateEventContent, LiveBackPaginationStatus, TimelineItem, TimelineItemContent}, Timeline}; use tokio::{ @@ -15,7 +15,7 @@ use tokio::{ sync::mpsc::{UnboundedSender, UnboundedReceiver}, task::JoinHandle, }; use unicode_segmentation::UnicodeSegmentation; -use std::{cmp::{max, min}, collections::BTreeMap, ops::Range, sync::{Arc, Mutex, OnceLock}}; +use std::{cmp::{max, min}, collections::{BTreeMap, BTreeSet}, ops::Range, sync::{Arc, Mutex, OnceLock}}; use url::Url; use crate::{home::{room_screen::TimelineUpdate, rooms_list::{self, enqueue_rooms_list_update, RoomPreviewAvatar, RoomPreviewEntry, RoomsListUpdate}}, media_cache::{MediaCacheEntry, AVATAR_CACHE}, profile::user_profile::{enqueue_user_profile_update, UserProfile, UserProfileUpdate}, utils::MEDIA_THUMBNAIL_FORMAT}; @@ -570,6 +570,22 @@ pub fn get_client() -> Option { CLIENT.get().cloned() } +/// The list of users that the current user has chosen to ignore. +/// Ideally we shouldn't have to maintain this list ourselves, +/// but the Matrix SDK doesn't currently properly maintain the list of ignored users. +static IGNORED_USERS: Mutex> = Mutex::new(BTreeSet::new()); + +/// Returns a deep clone of the current list of ignored users. +pub fn get_ignored_users() -> BTreeSet { + IGNORED_USERS.lock().unwrap().clone() +} + +/// Returns whether the given user ID is currently being ignored. +pub fn is_user_ignored(user_id: &UserId) -> bool { + IGNORED_USERS.lock().unwrap().contains(user_id) +} + + /// Returns the timeline update sender and receiver endpoints for the given room, /// if and only if the receiver exists. /// @@ -720,7 +736,7 @@ async fn async_main_loop() -> Result<()> { Some(Err(e)) => { error!("sync loop was stopped by client error processing: {e}"); stream_error = Some(e); - continue; + break; } None => { error!("sync loop ended unexpectedly"); @@ -902,11 +918,33 @@ async fn async_main_loop() -> Result<()> { } +#[allow(unused)] +async fn current_ignore_user_list(client: &Client) -> Option> { + use matrix_sdk::ruma::events::ignored_user_list::IgnoredUserListEventContent; + let ignored_users = client.account() + .account_data::() + .await + .ok()?? + .deserialize() + .ok()? + .ignored_users + .into_keys() + .collect(); + + Some(ignored_users) +} + + fn handle_ignore_user_list_subscriber(client: Client) { let mut subscriber = client.subscribe_to_ignore_user_list_changes(); Handle::current().spawn(async move { - while let Some(_ignore_list) = subscriber.next().await { - log!("Received an updated ignored-user list: {_ignore_list:?}"); + while let Some(ignore_list) = subscriber.next().await { + log!("Received an updated ignored-user list: {ignore_list:?}"); + let ignored_users_set = ignore_list + .into_iter() + .filter_map(|u| OwnedUserId::try_from(u).ok()) + .collect::>(); + *IGNORED_USERS.lock().unwrap() = ignored_users_set; // After successfully (un)ignoring a user, all timelines are fully cleared by the Matrix SDK. // Therefore, we need to re-fetch all timelines for all rooms,