Skip to content

Commit

Permalink
Merge pull request #90 from kevinaboos/fetch_user_profile_room_info
Browse files Browse the repository at this point in the history
Fetch a user's room membership info upon showing the user profile sliding pane
  • Loading branch information
kevinaboos authored Jul 19, 2024
2 parents 81ea0bd + 0852138 commit cd552d2
Show file tree
Hide file tree
Showing 6 changed files with 536 additions and 147 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ rangemap = "1.5.0"
tokio = { version = "1.33.0", features = ["macros", "rt-multi-thread"] }
tracing-subscriber = "0.3.17"
unicode-segmentation = "1.10.1"
url = "2.2.2"
url = "2.5.0"
emojis = "0.6.1"


Expand Down
161 changes: 119 additions & 42 deletions src/home/room_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ use matrix_sdk::ruma::{
guest_access::GuestAccess,
history_visibility::HistoryVisibility,
join_rules::JoinRule, message::{MessageFormat, MessageType, RoomMessageEventContent}, MediaSource,
}, AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, SyncMessageLikeEvent
}, uint, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId,
},
AnySyncMessageLikeEvent, AnySyncTimelineEvent, FullStateEventContent, SyncMessageLikeEvent,
}, matrix_uri::MatrixId, uint, MatrixToUri, MatrixUri, MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId,
};
use matrix_sdk_ui::timeline::{
self, AnyOtherFullStateEventContent, BundledReactions, EventTimelineItem, MemberProfileChange, MembershipChange, RoomMembershipChange, TimelineDetails, TimelineItem, TimelineItemContent, TimelineItemKind, VirtualTimelineItem
Expand All @@ -21,7 +22,7 @@ use matrix_sdk_ui::timeline::{
use rangemap::RangeSet;
use crate::{
media_cache::{MediaCache, MediaCacheEntry, AVATAR_CACHE},
profile::user_profile::{ShowUserProfileAction, UserProfileInfo, UserProfileSlidingPaneWidgetExt},
profile::user_profile::{AvatarInfo, ShowUserProfileAction, UserProfile, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt},
shared::{avatar::{AvatarRef, AvatarWidgetRefExt}, html_or_plaintext::HtmlOrPlaintextWidgetRefExt, text_or_image::TextOrImageWidgetRefExt},
sliding_sync::{submit_async_request, take_timeline_update_receiver, MatrixRequest},
utils::{self, unix_time_millis_to_datetime, MediaFormatConst},
Expand Down Expand Up @@ -558,6 +559,7 @@ impl Widget for RoomScreen {
// Handle events and actions at the RoomScreen level.
fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope){
let pane = self.user_profile_sliding_pane(id!(user_profile_sliding_pane));
let timeline = self.timeline(id!(timeline));

if let Event::Actions(actions) = event {
// Handle the send message button being clicked.
Expand Down Expand Up @@ -586,14 +588,93 @@ impl Widget for RoomScreen {
for action in actions {
// Handle the action that requests to show the user profile sliding pane.
if let ShowUserProfileAction::ShowUserProfile(avatar_info) = action.as_widget_action().cast() {
pane.set_info(UserProfileInfo {
avatar_info,
room_name: self.room_name.clone(),
});
pane.show(cx);
// TODO: Hack for error that when you first open the modal, doesnt draw until an event
// this forces the entire ui to rerender, still weird that only happens the first time.
self.redraw(cx);
timeline.show_user_profile(
cx,
&pane,
UserProfilePaneInfo {
avatar_info,
room_name: self.room_name.clone(),
room_member: None,
},
);
}

// Handle a link being clicked.
if let HtmlLinkAction::Clicked { url, .. } = action.as_widget_action().cast() {
let mut link_was_handled = false;
if let Ok(matrix_to_uri) = MatrixToUri::parse(&url) {
match matrix_to_uri.id() {
MatrixId::Room(room_id) => {
log!("TODO: open room {}", room_id);
link_was_handled = true;
}
MatrixId::RoomAlias(room_alias) => {
log!("TODO: open room alias {}", room_alias);
link_was_handled = true;
}
MatrixId::User(user_id) => {
log!("Opening matrix.to user link for {}", user_id);

// There is no synchronous way to get the user's full profile info
// including the details of their room membership,
// so we fill in with the details we *do* know currently,
// show the UserProfileSlidingPane, and then after that,
// the UserProfileSlidingPane itself will fire off
// an async request to get the rest of the details.
timeline.show_user_profile(
cx,
&pane,
UserProfilePaneInfo {
avatar_info: AvatarInfo {
user_profile: UserProfile {
user_id: user_id.to_owned(),
username: None,
avatar_img_data: None,
},
room_id: self.room_id.clone().unwrap(),
},
room_name: self.room_name.clone(),
// TODO: provide the extra `via` parameters from `matrix_to_uri.via()`.
room_member: None,
},
);
link_was_handled = true;
}
MatrixId::Event(room_id, event_id) => {
log!("TODO: open event {} in room {}", event_id, room_id);
link_was_handled = true;
}
_ => { }
}
}

if let Ok(matrix_uri) = MatrixUri::parse(&url) {
match matrix_uri.id() {
MatrixId::Room(room_id) => {
log!("TODO: open room {}", room_id);
link_was_handled = true;
}
MatrixId::RoomAlias(room_alias) => {
log!("TODO: open room alias {}", room_alias);
link_was_handled = true;
}
MatrixId::User(user_id) => {
log!("TODO: open user {}", user_id);
link_was_handled = true;
}
MatrixId::Event(room_id, event_id) => {
log!("TODO: open event {} in room {}", event_id, room_id);
link_was_handled = true;
}
_ => { }
}
}

if !link_was_handled {
if let Err(e) = robius_open::Uri::new(&url).open() {
error!("Failed to open URL {:?}. Error: {:?}", url, e);
}
}
}
}
}
Expand Down Expand Up @@ -832,6 +913,20 @@ impl TimelineRef {
let Some(mut timeline) = self.borrow_mut() else { return };
timeline.room_id = Some(room_id);
}

/// Shows the user profile sliding pane with the given avatar info.
fn show_user_profile(
&self,
cx: &mut Cx,
pane: &UserProfileSlidingPaneRef,
info: UserProfilePaneInfo,
) {
let Some(mut inner) = self.borrow_mut() else { return };
pane.set_info(info);
pane.show(cx);
// Not sure if this redraw is necessary
inner.redraw(cx);
}
}

impl Widget for Timeline {
Expand All @@ -854,25 +949,6 @@ impl Widget for Timeline {
| StackNavigationTransitionAction::None => { }
}

// Handle a link being clicked.
if let HtmlLinkAction::Clicked { url, .. } = action.as_widget_action().cast() {
if url.starts_with("https://matrix.to/#/") {
log!("TODO: handle Matrix link internally: {url:?}");
// TODO: show a pop-up pane with the user's profile, or a room preview pane.
//
// There are four kinds of matrix.to schemes:
// See here: <https://github.com/matrix-org/matrix.to?tab=readme-ov-file#url-scheme>
// 1. Rooms: https://matrix.to/#/#matrix:matrix.org
// 2. Rooms by ID: https://matrix.to/#/!cURbafjkfsMDVwdRDQ:matrix.org
// 3. Users: https://matrix.to/#/@matthew:matrix.org
// 4. Messages: https://matrix.to/#/#matrix:matrix.org/$1448831580433WbpiJ:jki.re
} else {
if let Err(e) = robius_open::Uri::new(&url).open() {
error!("Failed to open URL {:?}. Error: {:?}", url, e);
}
}
}

// Handle other actions here
// TODO: handle actions upon an item being clicked.
// for (item_id, item) in self.list.items_with_actions(&actions) {
Expand Down Expand Up @@ -950,6 +1026,7 @@ impl Widget for Timeline {
self.view.handle_event(cx, event, scope);
}


fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep {
let Some(tl_state) = self.tl_state.as_mut() else {
return DrawStep::done()
Expand Down Expand Up @@ -1727,36 +1804,36 @@ fn set_avatar_and_get_username(
};

// Set the username to the display name if available, otherwise the user ID after the '@'.
username = profile.display_name
.as_ref()
.cloned()
.unwrap_or_else(|| user_id.to_string());
let username_opt = profile.display_name.clone();
username = username_opt.clone().unwrap_or_else(|| user_id.to_string());

// Draw the avatar image if available, otherwise set the avatar to text.
let drew_avatar_img = avatar_img.map(|data|
avatar.show_image(
Some((username.clone(), user_id.to_owned(), room_id.to_owned(), data.clone())),
Some((user_id.to_owned(), username_opt.clone(), room_id.to_owned(), data.clone())),
|img| utils::load_png_or_jpg(&img, cx, &data)
).is_ok()
).unwrap_or(false);

if !drew_avatar_img {
avatar.show_text(
Some((user_id.to_owned(), room_id.to_owned())),
username.clone(),
Some((user_id.to_owned(), username_opt, room_id.to_owned())),
&username,
);
}
}
other => {
// log!("populate_message_view(): sender profile not ready yet for event {_other:?}");

// If the profile is not ready, use the user ID for both the username and the avatar.
not_ready => {
// log!("populate_message_view(): sender profile not ready yet for event {not_ready:?}");
username = user_id.to_string();
avatar.show_text(
Some((user_id.to_owned(), room_id.to_owned())),
username.clone(),
Some((user_id.to_owned(), None, room_id.to_owned())),
&username,
);
// If there was an error fetching the profile, treat that condition as fully drawn,
// since we don't yet have a good way to re-request profile information.
profile_drawn = matches!(other, TimelineDetails::Error(_));
profile_drawn = matches!(not_ready, TimelineDetails::Error(_));
}
}

Expand Down
75 changes: 69 additions & 6 deletions src/media_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,73 @@ use makepad_widgets::{error, log};
use matrix_sdk::{ruma::{OwnedMxcUri, events::room::MediaSource}, media::{MediaRequest, MediaFormat}};
use crate::{home::room_screen::TimelineUpdate, sliding_sync::{self, MatrixRequest}, utils::{MediaFormatConst, MEDIA_THUMBNAIL_FORMAT}};

pub static AVATAR_CACHE: Mutex<MediaCache> = Mutex::new(MediaCache::new(MEDIA_THUMBNAIL_FORMAT, None));

pub type MediaCacheEntryRef = Arc<Mutex<MediaCacheEntry>>;

pub static AVATAR_CACHE: MediaCacheLocked = MediaCacheLocked(Mutex::new(MediaCache::new(MEDIA_THUMBNAIL_FORMAT, None)));

pub struct MediaCacheLocked(Mutex<MediaCache>);
impl Deref for MediaCacheLocked {
type Target = Mutex<MediaCache>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl MediaCacheLocked {
/// Similar to [`Self::try_get_media_or_fetch()`], but immediately fires off an async request
/// on the current task to fetch the media, blocking until the request completes.
///
/// Unlike other functions, this is intended for use in background tasks or other async contexts
/// where it is not latency-sensitive, and safe to block on the async request.
/// Thus, it must be implemented on the `MediaCacheLocked` type, which is safe to hold a reference to
/// across an await point, whereas a mutable reference to a locked `MediaCache` is not (i.e., a `MutexGuard`).
pub async fn get_media_or_fetch_async(
&self,
client: &matrix_sdk::Client,
mxc_uri: OwnedMxcUri,
media_format: Option<MediaFormat>,
) -> Option<Arc<[u8]>> {
let destination = {
match self.lock().unwrap().entry(mxc_uri.clone()) {
Entry::Vacant(vacant) => vacant
.insert(Arc::new(Mutex::new(MediaCacheEntry::Requested)))
.clone(),
Entry::Occupied(occupied) => match occupied.get().lock().unwrap().deref() {
MediaCacheEntry::Loaded(data) => return Some(data.clone()),
MediaCacheEntry::Failed => return None,
// If already requested (a fetch is in process),
// we return None for now and allow the `insert_into_cache` function
// emit a UI Signal when the fetch completes,
// which will trigger a re-draw of the UI,
// and thus a re-fetch of any visible avatars.
MediaCacheEntry::Requested => return None,
}
}
};

let media_request = MediaRequest {
source: MediaSource::Plain(mxc_uri),
format: media_format.unwrap_or_else(|| self.lock().unwrap().default_format.clone().into()),
};

let res = client
.media()
.get_media_content(&media_request, true)
.await;
let res: matrix_sdk::Result<Arc<[u8]>> = res.map(|d| d.into());
let retval = res
.as_ref()
.ok()
.cloned();
insert_into_cache(
&destination,
media_request,
res,
None,
);
retval
}
}

/// An entry in the media cache.
#[derive(Debug, Clone)]
pub enum MediaCacheEntry {
Expand Down Expand Up @@ -97,18 +160,18 @@ impl MediaCache {
);
MediaCacheEntry::Requested
}

}

/// Insert data into a previously-requested media cache entry.
fn insert_into_cache(
fn insert_into_cache<D: Into<Arc<[u8]>>>(
value_ref: &Mutex<MediaCacheEntry>,
_request: MediaRequest,
data: matrix_sdk::Result<Vec<u8>>,
data: matrix_sdk::Result<D>,
update_sender: Option<crossbeam_channel::Sender<TimelineUpdate>>,
) {
let new_value = match data {
Ok(data) => {
let data = data.into();

// debugging: dump out the media image to disk
if false {
Expand All @@ -127,7 +190,7 @@ fn insert_into_cache(
}
}

MediaCacheEntry::Loaded(data.into())
MediaCacheEntry::Loaded(data)
}
Err(e) => {
error!("Failed to fetch media for {:?}: {e:?}", _request.source);
Expand Down
Loading

0 comments on commit cd552d2

Please sign in to comment.