Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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