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/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 78a2d833..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}; @@ -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). @@ -388,11 +384,25 @@ 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 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 `handle_ignore_user_list_subscriber()`.` + submit_async_request(MatrixRequest::PaginateRoomTimeline { + room_id, + num_events: 50, + forwards: false, + }); }); } @@ -560,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. /// @@ -638,6 +664,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. @@ -707,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"); @@ -889,6 +918,50 @@ 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:?}"); + 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, + // 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,