Skip to content

Commit cfa2f0a

Browse files
committed
Render images in reactions
1 parent 843848b commit cfa2f0a

File tree

7 files changed

+238
-105
lines changed

7 files changed

+238
-105
lines changed

src/base.rs

Lines changed: 80 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use std::time::{Duration, Instant};
1313

1414
use emojis::Emoji;
1515
use matrix_sdk::ruma::events::receipt::ReceiptThread;
16+
use matrix_sdk::ruma::events::room::MediaSource;
17+
use matrix_sdk::ruma::OwnedMxcUri;
1618
use ratatui::{
1719
buffer::Buffer,
1820
layout::{Alignment, Rect},
@@ -90,9 +92,9 @@ use modalkit::{
9092
prelude::{CommandType, WordStyle},
9193
};
9294

93-
use crate::config::ImagePreviewProtocolValues;
95+
use crate::config::{ImagePreviewProtocolValues, ImagePreviewSize};
9496
use crate::notifications::NotificationHandle;
95-
use crate::preview::{source_from_event, PreviewManager};
97+
use crate::preview::{source_from_event, PreviewKind, PreviewManager};
9698
use crate::{
9799
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
98100
worker::Requester,
@@ -682,7 +684,7 @@ pub type IambResult<T> = UIResult<T, IambInfo>;
682684
///
683685
/// The event identifier used as a key here is the ID for the reaction, and not for the message
684686
/// it's reacting to.
685-
pub type MessageReactions = HashMap<OwnedEventId, (String, OwnedUserId)>;
687+
pub type MessageReactions = HashMap<OwnedEventId, (String, OwnedUserId, Option<MediaSource>)>;
686688

687689
/// Errors encountered during application use.
688690
#[derive(thiserror::Error, Debug)]
@@ -984,22 +986,25 @@ impl RoomInfo {
984986
}
985987

986988
/// Get the reactions and their counts for a message.
987-
pub fn get_reactions(&self, event_id: &EventId) -> Vec<(&str, usize)> {
989+
pub fn get_reactions(&self, event_id: &EventId) -> Vec<(&str, usize, &Option<MediaSource>)> {
988990
if let Some(reacts) = self.reactions.get(event_id) {
989991
let mut counts = HashMap::new();
990992

991993
let mut seen_user_reactions = BTreeSet::new();
992994

993-
for (key, user) in reacts.values() {
995+
for (key, user, source) in reacts.values() {
994996
if !seen_user_reactions.contains(&(key, user)) {
995997
seen_user_reactions.insert((key, user));
996-
let count = counts.entry(key.as_str()).or_default();
997-
*count += 1;
998+
let count = counts.entry(key.as_str()).or_insert((0, source));
999+
count.0 += 1;
9981000
}
9991001
}
10001002

1001-
let mut reactions = counts.into_iter().collect::<Vec<_>>();
1002-
reactions.sort();
1003+
let mut reactions = counts
1004+
.into_iter()
1005+
.map(|(key, (count, source))| (key, count, source))
1006+
.collect::<Vec<_>>();
1007+
reactions.sort_by_key(|item| (item.0, item.1));
10031008

10041009
reactions
10051010
} else {
@@ -1060,24 +1065,46 @@ impl RoomInfo {
10601065
}
10611066

10621067
/// Insert a reaction to a message.
1063-
pub fn insert_reaction(&mut self, react: ReactionEvent) {
1064-
match react {
1065-
MessageLikeEvent::Original(react) => {
1066-
let rel_id = react.content.relates_to.event_id;
1067-
let key = react.content.relates_to.key;
1068+
fn insert_reaction(&mut self, react: ReactionEvent, source: Option<MediaSource>) {
1069+
let MessageLikeEvent::Original(react) = react else {
1070+
return;
1071+
};
1072+
let rel_id = react.content.relates_to.event_id;
1073+
let key = react.content.relates_to.key;
10681074

1069-
let message = self.reactions.entry(rel_id.clone()).or_default();
1070-
let event_id = react.event_id;
1071-
let user_id = react.sender;
1075+
let message = self.reactions.entry(rel_id.clone()).or_default();
1076+
let event_id = react.event_id;
1077+
let user_id = react.sender;
10721078

1073-
message.insert(event_id.clone(), (key, user_id));
1079+
message.insert(event_id.clone(), (key, user_id, source));
10741080

1075-
let loc = EventLocation::Reaction(rel_id);
1076-
self.keys.insert(event_id, loc);
1077-
},
1078-
MessageLikeEvent::Redacted(_) => {
1079-
return;
1080-
},
1081+
let loc = EventLocation::Reaction(rel_id);
1082+
self.keys.insert(event_id, loc);
1083+
}
1084+
1085+
/// Insert a reaction to a message.
1086+
pub fn insert_reaction_with_preview(
1087+
&mut self,
1088+
react: ReactionEvent,
1089+
settings: &ApplicationSettings,
1090+
previews: &mut PreviewManager,
1091+
worker: &Requester,
1092+
) {
1093+
let MessageLikeEvent::Original(ref orig_react) = react else {
1094+
return;
1095+
};
1096+
let image_uri = OwnedMxcUri::from(orig_react.content.relates_to.key.as_str());
1097+
let source = if image_uri.is_valid() && settings.tunables.image_preview.is_some() {
1098+
Some(MediaSource::Plain(image_uri))
1099+
} else {
1100+
None
1101+
};
1102+
1103+
self.insert_reaction(react, source.clone());
1104+
1105+
if let (Some(source), Some(_)) = (source, &settings.tunables.image_preview) {
1106+
let size = ImagePreviewSize { width: 2, height: 1 };
1107+
previews.register_preview(settings, source, PreviewKind::Reaction, size, worker);
10811108
}
10821109
}
10831110

@@ -1219,7 +1246,13 @@ impl RoomInfo {
12191246
(self.get_event_mut(&event_id), &settings.tunables.image_preview)
12201247
{
12211248
msg.image_preview = Some(source.clone());
1222-
previews.register_preview(settings, source, image_preview.size, worker)
1249+
previews.register_preview(
1250+
settings,
1251+
source,
1252+
PreviewKind::Message,
1253+
image_preview.size,
1254+
worker,
1255+
)
12231256
}
12241257
}
12251258
}
@@ -1376,7 +1409,7 @@ impl RoomInfo {
13761409
if let Some(reactions) = self.reactions.get(event_id) {
13771410
reactions
13781411
.values()
1379-
.any(|(annotation, user)| annotation == emoji && user == user_id)
1412+
.any(|(annotation, user, _)| annotation == emoji && user == user_id)
13801413
} else {
13811414
false
13821415
}
@@ -2129,16 +2162,17 @@ pub mod tests {
21292162

21302163
for i in 0..3 {
21312164
let event_id = format!("$house_{}", i);
2132-
info.insert_reaction(MessageLikeEvent::Original(
2133-
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
2165+
info.insert_reaction(
2166+
MessageLikeEvent::Original(matrix_sdk::ruma::events::OriginalMessageLikeEvent {
21342167
content: content.clone(),
21352168
event_id: OwnedEventId::from_str(&event_id).unwrap(),
21362169
sender: owned_user_id!("@foo:example.org"),
21372170
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
21382171
room_id: owned_room_id!("!foo:example.org"),
21392172
unsigned: MessageLikeUnsigned::new(),
2140-
},
2141-
));
2173+
}),
2174+
None,
2175+
);
21422176
}
21432177

21442178
let content = ReactionEventContent::new(Annotation::new(
@@ -2148,36 +2182,40 @@ pub mod tests {
21482182

21492183
for i in 0..2 {
21502184
let event_id = format!("$smile_{}", i);
2151-
info.insert_reaction(MessageLikeEvent::Original(
2152-
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
2185+
info.insert_reaction(
2186+
MessageLikeEvent::Original(matrix_sdk::ruma::events::OriginalMessageLikeEvent {
21532187
content: content.clone(),
21542188
event_id: OwnedEventId::from_str(&event_id).unwrap(),
21552189
sender: owned_user_id!("@foo:example.org"),
21562190
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
21572191
room_id: owned_room_id!("!foo:example.org"),
21582192
unsigned: MessageLikeUnsigned::new(),
2159-
},
2160-
));
2193+
}),
2194+
None,
2195+
);
21612196
}
21622197

21632198
for i in 2..4 {
21642199
let event_id = format!("$smile_{}", i);
2165-
info.insert_reaction(MessageLikeEvent::Original(
2166-
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
2200+
info.insert_reaction(
2201+
MessageLikeEvent::Original(matrix_sdk::ruma::events::OriginalMessageLikeEvent {
21672202
content: content.clone(),
21682203
event_id: OwnedEventId::from_str(&event_id).unwrap(),
21692204
sender: owned_user_id!("@bar:example.org"),
21702205
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
21712206
room_id: owned_room_id!("!foo:example.org"),
21722207
unsigned: MessageLikeUnsigned::new(),
2173-
},
2174-
));
2208+
}),
2209+
None,
2210+
);
21752211
}
21762212

2177-
assert_eq!(info.get_reactions(&owned_event_id!("$my_reaction")), vec![
2178-
("🏠", 1),
2179-
("🙂", 2)
2180-
]);
2213+
let reacts: Vec<_> = info
2214+
.get_reactions(&owned_event_id!("$my_reaction"))
2215+
.into_iter()
2216+
.map(|(key, count, _)| (key, count))
2217+
.collect();
2218+
assert_eq!(reacts, vec![("🏠", 1), ("🙂", 2)]);
21812219
}
21822220

21832221
#[test]

src/message/mod.rs

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ use modalkit::prelude::*;
5959
use ratatui_image::protocol::Protocol;
6060

6161
use crate::config::ImagePreviewSize;
62-
use crate::preview::{ImageStatus, PreviewManager};
62+
use crate::preview::{ImageStatus, PreviewKind, PreviewManager};
6363
use crate::{
6464
base::RoomInfo,
6565
config::ApplicationSettings,
@@ -769,16 +769,37 @@ impl<'a> MessageFormatter<'a> {
769769
proto
770770
}
771771

772-
fn push_reactions(&mut self, counts: Vec<(&'a str, usize)>, style: Style, text: &mut Text<'a>) {
772+
fn push_reactions(
773+
&mut self,
774+
counts: Vec<(&'a str, usize, &'a Option<MediaSource>)>,
775+
style: Style,
776+
text: &mut Text<'a>,
777+
settings: &ApplicationSettings,
778+
previews: &'a PreviewManager,
779+
) -> Vec<ProtocolPreview<'a>> {
773780
let mut emojis = printer::TextPrinter::new(self.width(), style, false, self.settings);
774781
let mut reactions = 0;
782+
let mut protos = Vec::new();
775783

776-
for (key, count) in counts {
784+
for (key, count, source) in counts {
777785
if reactions != 0 {
778786
emojis.push_str(" ", style);
779787
}
780788

781-
let name = if self.settings.tunables.reaction_shortcode_display {
789+
let proto = match source
790+
.as_ref()
791+
.and_then(|source| previews.get(source, PreviewKind::Reaction))
792+
{
793+
Some(ImageStatus::Loaded(backend)) => Some(Some(backend)),
794+
// Use empty space as placeholder
795+
Some(ImageStatus::Queued(_)) | Some(ImageStatus::Downloading(_)) => Some(None),
796+
// Fall back to text
797+
None | Some(ImageStatus::Error(_)) => None,
798+
};
799+
800+
let name = if proto.is_some() {
801+
" "
802+
} else if self.settings.tunables.reaction_shortcode_display {
782803
if let Some(emoji) = emojis::get(key) {
783804
if let Some(short) = emoji.shortcode() {
784805
short
@@ -797,6 +818,13 @@ impl<'a> MessageFormatter<'a> {
797818
};
798819

799820
emojis.push_str("[", style);
821+
if let Some(Some(proto)) = proto {
822+
let (x, y) = emojis.curosor_pos();
823+
let y = (y + text.lines.len()) as u16;
824+
let x = x as u16 + self.cols.user_gutter_width(settings);
825+
826+
protos.push((proto, x, y));
827+
}
800828
emojis.push_str(name, style);
801829
emojis.push_str(" ", style);
802830
emojis.push_span_nobreak(Span::styled(count.to_string(), style));
@@ -808,6 +836,8 @@ impl<'a> MessageFormatter<'a> {
808836
if reactions > 0 {
809837
self.push_text(emojis.finish(), style, text);
810838
}
839+
840+
protos
811841
}
812842

813843
fn push_thread_reply_count(&mut self, len: usize, text: &mut Text<'a>) {
@@ -1033,7 +1063,8 @@ impl Message {
10331063

10341064
if settings.tunables.reaction_display {
10351065
let reactions = info.get_reactions(self.event.event_id());
1036-
fmt.push_reactions(reactions, style, &mut text);
1066+
let react_protos = fmt.push_reactions(reactions, style, &mut text, settings, previews);
1067+
protos.extend(react_protos);
10371068
}
10381069

10391070
if let Some(thread) = info.get_thread(Some(self.event.event_id())) {
@@ -1076,21 +1107,24 @@ impl Message {
10761107
}
10771108

10781109
let mut proto = None;
1079-
let placeholder =
1080-
match self.image_preview.as_ref().and_then(|source| previews.get(source)) {
1081-
None => None,
1082-
Some(ImageStatus::Queued(image_preview_size)) => {
1083-
placeholder_frame(Some("Queued..."), width, image_preview_size)
1084-
},
1085-
Some(ImageStatus::Downloading(image_preview_size)) => {
1086-
placeholder_frame(Some("Downloading..."), width, image_preview_size)
1087-
},
1088-
Some(ImageStatus::Loaded(backend)) => {
1089-
proto = Some(backend);
1090-
placeholder_frame(Some("Cut off..."), width, &backend.area().into())
1091-
},
1092-
Some(ImageStatus::Error(err)) => Some(format!("[Image error: {err}]\n")),
1093-
};
1110+
let placeholder = match self
1111+
.image_preview
1112+
.as_ref()
1113+
.and_then(|source| previews.get(source, PreviewKind::Message))
1114+
{
1115+
None => None,
1116+
Some(ImageStatus::Queued(image_preview_size)) => {
1117+
placeholder_frame(Some("Queued..."), width, image_preview_size)
1118+
},
1119+
Some(ImageStatus::Downloading(image_preview_size)) => {
1120+
placeholder_frame(Some("Downloading..."), width, image_preview_size)
1121+
},
1122+
Some(ImageStatus::Loaded(backend)) => {
1123+
proto = Some(backend);
1124+
placeholder_frame(Some("Cut off..."), width, &backend.area().into())
1125+
},
1126+
Some(ImageStatus::Error(err)) => Some(format!("[Image error: {err}]\n")),
1127+
};
10941128

10951129
if let Some(placeholder) = placeholder {
10961130
msg.to_mut().insert_str(0, &placeholder);

src/message/printer.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ impl<'a> TextPrinter<'a> {
5757
}
5858
}
5959

60+
/// The position where the next text will be printed. (x, y)
61+
pub fn curosor_pos(&self) -> (usize, usize) {
62+
(self.curr_width, self.text.lines.len().saturating_sub(1))
63+
}
64+
6065
/// Configure the alignment for each line.
6166
pub fn align(mut self, alignment: Alignment) -> Self {
6267
self.alignment = alignment;

0 commit comments

Comments
 (0)