Skip to content

Commit 3fa173e

Browse files
authored
Merge pull request #585 from alanpoon/display_link_preview#81_2
2 parents 4c1435d + 1a86620 commit 3fa173e

File tree

9 files changed

+913
-34
lines changed

9 files changed

+913
-34
lines changed

resources/img/default_image.png

5.59 KB
Loading

src/home/link_preview.rs

Lines changed: 631 additions & 0 deletions
Large diffs are not rendered by default.

src/home/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub mod spaces_dock;
2020
pub mod welcome_screen;
2121
pub mod event_reaction_list;
2222
pub mod new_message_context_menu;
23+
pub mod link_preview;
2324

2425
pub fn live_design(cx: &mut Cx) {
2526
home_screen::live_design(cx);
@@ -42,4 +43,5 @@ pub fn live_design(cx: &mut Cx) {
4243
welcome_screen::live_design(cx);
4344
light_themed_dock::live_design(cx);
4445
event_reaction_list::live_design(cx);
46+
link_preview::live_design(cx);
4547
}

src/home/room_screen.rs

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use matrix_sdk_ui::timeline::{
2626
};
2727

2828
use crate::{
29-
app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, editing_pane::EditingPaneState, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::RoomsListRef, tombstone_footer::TombstoneFooterWidgetExt}, location::init_location_subscriber, media_cache::{MediaCache, MediaCacheEntry}, profile::{
29+
app::AppStateAction, avatar_cache, event_preview::{plaintext_body_of_timeline_item, text_preview_of_encrypted_message, text_preview_of_member_profile_change, text_preview_of_other_message_like, text_preview_of_other_state, text_preview_of_redacted_message, text_preview_of_room_membership_change, text_preview_of_timeline_item}, home::{edited_indicator::EditedIndicatorWidgetRefExt, editing_pane::EditingPaneState, link_preview::{LinkPreviewCache, LinkPreviewRef, LinkPreviewWidgetRefExt}, loading_pane::{LoadingPaneState, LoadingPaneWidgetExt}, rooms_list::RoomsListRef, tombstone_footer::TombstoneFooterWidgetExt}, location::init_location_subscriber, media_cache::{MediaCache, MediaCacheEntry}, profile::{
3030
user_profile::{AvatarState, ShowUserProfileAction, UserProfile, UserProfileAndRoomId, UserProfilePaneInfo, UserProfileSlidingPaneRef, UserProfileSlidingPaneWidgetExt},
3131
user_profile_cache,
3232
}, room::typing_notice::TypingNoticeWidgetExt, shared::{
@@ -81,6 +81,7 @@ live_design! {
8181
use crate::home::tombstone_footer::TombstoneFooter;
8282
use crate::rooms_list::*;
8383
use crate::shared::restore_status_view::*;
84+
use crate::home::link_preview::LinkPreview;
8485
use link::tsp_link::TspSignIndicator;
8586

8687
IMG_DEFAULT_AVATAR = dep("crate://self/resources/img/default_avatar.png")
@@ -340,6 +341,7 @@ live_design! {
340341
}
341342

342343
message = <HtmlOrPlaintext> { }
344+
link_preview_view = <LinkPreview> {}
343345

344346
// <LineH> {
345347
// margin: {top: 13.0, bottom: 5.0}
@@ -384,6 +386,7 @@ live_design! {
384386
padding: { left: 10.0 }
385387

386388
message = <HtmlOrPlaintext> { }
389+
link_preview_view = <LinkPreview> {}
387390
<View> {
388391
width: Fill,
389392
height: Fit
@@ -1292,6 +1295,7 @@ impl Widget for RoomScreen {
12921295
msg_like_content,
12931296
prev_event,
12941297
&mut tl_state.media_cache,
1298+
&mut tl_state.link_preview_cache,
12951299
&tl_state.user_power,
12961300
item_drawn_status,
12971301
room_screen_widget_uid,
@@ -2290,7 +2294,8 @@ impl RoomScreen {
22902294
profile_drawn_since_last_update: RangeSet::new(),
22912295
update_receiver,
22922296
request_sender,
2293-
media_cache: MediaCache::new(Some(update_sender)),
2297+
media_cache: MediaCache::new(Some(update_sender.clone())),
2298+
link_preview_cache: LinkPreviewCache::new(Some(update_sender)),
22942299
replying_to: None,
22952300
saved_state: SavedState::default(),
22962301
message_highlight_animation_state: MessageHighlightAnimationState::default(),
@@ -2832,6 +2837,8 @@ struct TimelineUiState {
28322837
/// Currently this excludes avatars, as those are shared across multiple rooms.
28332838
media_cache: MediaCache,
28342839

2840+
/// Cache for link preview data indexed by URL to avoid redundant network requests.
2841+
link_preview_cache: LinkPreviewCache,
28352842
/// Info about the event currently being replied to, if any.
28362843
replying_to: Option<(EventTimelineItem, EmbeddedEvent)>,
28372844

@@ -3000,6 +3007,7 @@ fn populate_message_view(
30003007
msg_like_content: &MsgLikeContent,
30013008
prev_event: Option<&Arc<TimelineItem>>,
30023009
media_cache: &mut MediaCache,
3010+
link_preview_cache: &mut LinkPreviewCache,
30033011
user_power_levels: &UserPowerLevels,
30043012
item_drawn_status: ItemDrawnStatus,
30053013
room_screen_widget_uid: WidgetUid,
@@ -3045,13 +3053,15 @@ fn populate_message_view(
30453053
if existed && item_drawn_status.content_drawn {
30463054
(item, true)
30473055
} else {
3048-
populate_text_message_content(
3056+
new_drawn_status.content_drawn = populate_text_message_content(
30493057
cx,
30503058
&item.html_or_plaintext(id!(content.message)),
30513059
body,
30523060
formatted.as_ref(),
3061+
Some(&mut item.link_preview(id!(content.link_preview_view))),
3062+
Some(media_cache),
3063+
Some(link_preview_cache),
30533064
);
3054-
new_drawn_status.content_drawn = true;
30553065
(item, false)
30563066
}
30573067
}
@@ -3081,13 +3091,15 @@ fn populate_message_view(
30813091
}
30823092
}
30833093
));
3084-
populate_text_message_content(
3094+
new_drawn_status.content_drawn = populate_text_message_content(
30853095
cx,
30863096
&html_or_plaintext_ref,
30873097
body,
30883098
formatted.as_ref(),
3099+
Some(&mut item.link_preview(id!(content.link_preview_view))),
3100+
Some(media_cache),
3101+
Some(link_preview_cache),
30893102
);
3090-
new_drawn_status.content_drawn = true;
30913103
(item, false)
30923104
}
30933105
}
@@ -3121,16 +3133,18 @@ fn populate_message_view(
31213133
.map(|c| format!("\n<i>Admin contact:</i> {}", c))
31223134
.unwrap_or_default(),
31233135
);
3124-
populate_text_message_content(
3136+
new_drawn_status.content_drawn = populate_text_message_content(
31253137
cx,
31263138
&html_or_plaintext_ref,
31273139
&sn.body,
31283140
Some(&FormattedBody {
31293141
format: MessageFormat::Html,
31303142
body: formatted,
31313143
}),
3144+
Some(&mut item.link_preview(id!(content.link_preview_view))),
3145+
Some(media_cache),
3146+
Some(link_preview_cache),
31323147
);
3133-
new_drawn_status.content_drawn = true;
31343148
(item, false)
31353149
}
31363150
}
@@ -3168,14 +3182,17 @@ fn populate_message_view(
31683182
} else {
31693183
(Cow::from(format!("* {} {}", &username, body)), None)
31703184
};
3171-
populate_text_message_content(
3185+
let link_previews_drawn = populate_text_message_content(
31723186
cx,
31733187
&item.html_or_plaintext(id!(content.message)),
31743188
&body,
31753189
formatted.as_ref(),
3190+
Some(&mut item.link_preview(id!(content.link_preview_view))),
3191+
Some(media_cache),
3192+
Some(link_preview_cache),
31763193
);
31773194
set_username_and_get_avatar_retval = Some((username, profile_drawn));
3178-
new_drawn_status.content_drawn = true;
3195+
new_drawn_status.content_drawn = link_previews_drawn;
31793196
(item, false)
31803197
}
31813198
}
@@ -3302,13 +3319,15 @@ fn populate_message_view(
33023319
),
33033320
};
33043321

3305-
populate_text_message_content(
3322+
new_drawn_status.content_drawn = populate_text_message_content(
33063323
cx,
33073324
&item.html_or_plaintext(id!(content.message)),
33083325
&verification.body,
33093326
Some(&formatted),
3327+
Some(&mut item.link_preview(id!(content.link_preview_view))),
3328+
Some(media_cache),
3329+
Some(link_preview_cache),
33103330
);
3311-
new_drawn_status.content_drawn = true;
33123331
(item, false)
33133332
}
33143333
}
@@ -3516,38 +3535,61 @@ fn populate_message_view(
35163535
}
35173536

35183537
/// Draws the Html or plaintext body of the given Text or Notice message into the `message_content_widget`.
3538+
/// Also populates link previews if a link_preview_ref is provided.
3539+
/// Returns whether the text items were fully drawn.
35193540
fn populate_text_message_content(
35203541
cx: &mut Cx,
35213542
message_content_widget: &HtmlOrPlaintextRef,
35223543
body: &str,
35233544
formatted_body: Option<&FormattedBody>,
3524-
) {
3545+
link_preview_ref: Option<&mut LinkPreviewRef>,
3546+
media_cache: Option<&mut MediaCache>,
3547+
link_preview_cache: Option<&mut LinkPreviewCache>,
3548+
) -> bool {
35253549
// The message was HTML-formatted rich text.
3526-
if let Some(fb) = formatted_body.as_ref()
3550+
let links = if let Some(fb) = formatted_body.as_ref()
35273551
.and_then(|fb| (fb.format == MessageFormat::Html).then_some(fb))
35283552
{
3553+
let (linkified_html, links) = utils::linkify_get_urls(
3554+
utils::trim_start_html_whitespace(&fb.body),
3555+
true,
3556+
);
35293557
message_content_widget.show_html(
35303558
cx,
3531-
utils::linkify(
3532-
utils::trim_start_html_whitespace(&fb.body),
3533-
true,
3534-
)
3559+
linkified_html
35353560
);
3561+
links
35363562
}
35373563
// The message was non-HTML plaintext.
35383564
else {
3539-
match utils::linkify(body, false) {
3565+
let (linkified_html, links) = utils::linkify_get_urls(body, false);
3566+
match linkified_html {
35403567
Cow::Owned(linkified_html) => message_content_widget.show_html(cx, &linkified_html),
35413568
Cow::Borrowed(plaintext) => message_content_widget.show_plaintext(cx, plaintext),
35423569
}
3570+
links
3571+
};
3572+
3573+
// Populate link previews if all required parameters are provided
3574+
if let (Some(link_preview_ref), Some(media_cache), Some(link_preview_cache)) =
3575+
(link_preview_ref, media_cache, link_preview_cache) {
3576+
link_preview_ref.populate_below_message(
3577+
cx,
3578+
&links,
3579+
media_cache,
3580+
link_preview_cache,
3581+
&populate_image_message_content,
3582+
)
3583+
} else {
3584+
true
35433585
}
35443586
}
35453587

35463588
/// Draws the given image message's content into the `message_content_widget`.
35473589
///
35483590
/// Returns whether the image message content was fully drawn.
35493591
fn populate_image_message_content(
3550-
cx: &mut Cx2d,
3592+
cx: &mut Cx,
35513593
text_or_image_ref: &TextOrImageRef,
35523594
image_info_source: Option<Box<ImageInfo>>,
35533595
original_source: MediaSource,
@@ -3576,7 +3618,7 @@ fn populate_image_message_content(
35763618

35773619
// A closure that fetches and shows the image from the given `mxc_uri`,
35783620
// marking it as fully drawn if the image was available.
3579-
let mut fetch_and_show_image_uri = |cx: &mut Cx2d, mxc_uri: OwnedMxcUri, image_info: Box<ImageInfo>| {
3621+
let mut fetch_and_show_image_uri = |cx: &mut Cx, mxc_uri: OwnedMxcUri, image_info: Box<ImageInfo>| {
35803622
match media_cache.try_get_media_or_fetch(mxc_uri.clone(), MEDIA_THUMBNAIL_FORMAT.into()) {
35813623
(MediaCacheEntry::Loaded(data), _media_format) => {
35823624
let show_image_result = text_or_image_ref.show_image(cx, |cx, img| {
@@ -3640,6 +3682,10 @@ fn populate_image_message_content(
36403682
fully_drawn = false;
36413683
}
36423684
(MediaCacheEntry::Failed, _media_format) => {
3685+
if text_or_image_ref.view(id!(default_image_view)).visible() {
3686+
fully_drawn = true;
3687+
return;
3688+
}
36433689
text_or_image_ref
36443690
.show_text(cx, format!("{body}\n\nFailed to fetch image from {:?}", mxc_uri));
36453691
// For now, we consider this as being "complete". In the future, we could support
@@ -3649,7 +3695,7 @@ fn populate_image_message_content(
36493695
}
36503696
};
36513697

3652-
let mut fetch_and_show_media_source = |cx: &mut Cx2d, media_source: MediaSource, image_info: Box<ImageInfo>| {
3698+
let mut fetch_and_show_media_source = |cx: &mut Cx, media_source: MediaSource, image_info: Box<ImageInfo>| {
36533699
match media_source {
36543700
MediaSource::Encrypted(encrypted) => {
36553701
// We consider this as "fully drawn" since we don't yet support encryption.
@@ -3841,6 +3887,7 @@ fn populate_location_message_content(
38413887
true
38423888
}
38433889

3890+
38443891
/// Draws a ReplyPreview above a message if it was in-reply to another message.
38453892
///
38463893
/// If the given `in_reply_to` details are `None`,
@@ -3948,7 +3995,8 @@ fn populate_preview_of_timeline_item(
39483995
match m.msgtype() {
39493996
MessageType::Text(TextMessageEventContent { body, formatted, .. })
39503997
| MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => {
3951-
return populate_text_message_content(cx, widget_out, body, formatted.as_ref());
3998+
let _ = populate_text_message_content(cx, widget_out, body, formatted.as_ref(), None, None, None);
3999+
return;
39524000
}
39534001
_ => { } // fall through to the general case for all timeline items below.
39544002
}

src/shared/html_or_plaintext.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ live_design! {
6666
flow: RightWrap,
6767

6868
html_link = <HtmlLink> {
69-
hover_color: #21b070
69+
hover_color: (COLOR_LINK_HOVER)
7070
grab_key_focus: false,
7171
padding: {left: 1.0, right: 1.5},
7272
}

src/shared/styles.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ live_design! {
118118

119119
pub COLOR_TRANSPARENT = #00000000
120120
pub COLOR_WARNING = #fcdb03
121+
122+
pub COLOR_LINK_HOVER = #21B070
123+
121124
// An icon that can be rotated at a custom angle.
122125
pub IconRotated = <Icon> {
123126
draw_icon: {

src/shared/text_or_image.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ live_design! {
1111
use link::widgets::*;
1212

1313
use crate::shared::styles::*;
14+
DEFAULT_IMAGE = dep("crate://self/resources/img/default_image.png")
1415

1516
pub TextOrImage = {{TextOrImage}} {
1617
width: Fill, height: Fit,
@@ -41,6 +42,16 @@ live_design! {
4142
fit: Smallest,
4243
}
4344
}
45+
default_image_view = <View> {
46+
visible: false,
47+
cursor: Default, // Use `Hand` once we support clicking on the image
48+
width: Fill, height: Fit,
49+
image = <Image> {
50+
width: Fill, height: Fit,
51+
fit: Smallest,
52+
source: (DEFAULT_IMAGE)
53+
}
54+
}
4455
}
4556
}
4657

@@ -75,6 +86,7 @@ impl TextOrImage {
7586
/// a message like "Loading..." or an error message.
7687
pub fn show_text<T: AsRef<str>>(&mut self, cx: &mut Cx, text: T) {
7788
self.view(id!(image_view)).set_visible(cx, false);
89+
self.view(id!(default_image_view)).set_visible(cx, false);
7890
self.view(id!(text_view)).set_visible(cx, true);
7991
self.view.label(id!(text_view.label)).set_text(cx, text.as_ref());
8092
self.status = TextOrImageStatus::Text;
@@ -99,6 +111,7 @@ impl TextOrImage {
99111
self.size_in_pixels = size_in_pixels;
100112
self.view(id!(image_view)).set_visible(cx, true);
101113
self.view(id!(text_view)).set_visible(cx, false);
114+
self.view(id!(default_image_view)).set_visible(cx, false);
102115
Ok(())
103116
}
104117
Err(e) => {
@@ -112,6 +125,13 @@ impl TextOrImage {
112125
pub fn status(&self) -> TextOrImageStatus {
113126
self.status
114127
}
128+
129+
/// Displays the default image that is used when no image is available.
130+
pub fn show_default_image(&self, cx: &mut Cx) {
131+
self.view(id!(default_image_view)).set_visible(cx, true);
132+
self.view(id!(text_view)).set_visible(cx, false);
133+
self.view(id!(image_view)).set_visible(cx, false);
134+
}
115135
}
116136

117137
impl TextOrImageRef {
@@ -141,6 +161,13 @@ impl TextOrImageRef {
141161
TextOrImageStatus::Text
142162
}
143163
}
164+
165+
/// See [TextOrImage::show_default_image()].
166+
pub fn show_default_image(&self, cx: &mut Cx) {
167+
if let Some(inner) = self.borrow() {
168+
inner.show_default_image(cx);
169+
}
170+
}
144171
}
145172

146173
/// Whether a `TextOrImage` instance is currently displaying text or an image.

0 commit comments

Comments
 (0)