Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ jobs:
uses: actions/checkout@v3
with:
submodules: true
- name: Install Rust (1.83 w/ clippy)
uses: dtolnay/rust-toolchain@1.83
- name: Install Rust (1.87 w/ clippy)
uses: dtolnay/rust-toolchain@1.87
with:
components: clippy
- name: Install Rust (nightly w/ rustfmt)
Expand Down
21 changes: 14 additions & 7 deletions docs/iamb.5
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,25 @@ The room to show by default instead of the
.Sy :welcome
window.

.IT Sy cache_policy
Configure how downloaded files and image previews are cached internally.
Specifying this object will replace the upstream defaults and possibly lead to unconstrained cache growth.
Possible keys and their effect are documented at:
.br
.Lk https://docs.rs/matrix-sdk/latest/matrix_sdk/media/struct.MediaRetentionPolicy.html#fields

.It Sy image_preview
Enable image previews and configure it.
An empty object will enable the feature with default settings, omitting it will disable the feature.
The available fields in this object are:
.Bl -tag -width Ds
.It Sy lazy_load
If
.Sy true
(the default), download and render image previews when viewing a message with an image.
If
.Sy false ,
load previews as soon as a message with an image is received.
.It Sy size
An optional object with
.Sy width
Expand Down Expand Up @@ -538,8 +552,6 @@ Configured as an object under the key
.It Sy cache
Specifies where to store assets and temporary data in.
(For example,
.Sy image_preview
and
.Sy logs
will also go in here by default.)
Defaults to
Expand All @@ -555,11 +567,6 @@ Specifies where to store downloaded files.
Defaults to
.Ev $XDG_DOWNLOAD_DIR .

.It Sy image_previews
Specifies where to store automatically downloaded image previews.
Defaults to
.Ev ${cache}/image_preview_downloads .

.It Sy logs
Specifies where to store log files.
Defaults to
Expand Down
147 changes: 87 additions & 60 deletions src/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use std::time::{Duration, Instant};

use emojis::Emoji;
use matrix_sdk::ruma::events::receipt::ReceiptThread;
use matrix_sdk::ruma::events::room::MediaSource;
use matrix_sdk::ruma::room_version_rules::RedactionRules;
use matrix_sdk::ruma::OwnedMxcUri;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
Expand Down Expand Up @@ -90,10 +92,9 @@ use modalkit::{
prelude::{CommandType, WordStyle},
};

use crate::config::ImagePreviewProtocolValues;
use crate::message::ImageStatus;
use crate::config::{ImagePreviewProtocolValues, ImagePreviewSize};
use crate::notifications::NotificationHandle;
use crate::preview::{source_from_event, spawn_insert_preview};
use crate::preview::{source_from_event, PreviewKind, PreviewManager};
use crate::{
message::{Message, MessageEvent, MessageKey, MessageTimeStamp, Messages},
worker::Requester,
Expand Down Expand Up @@ -688,7 +689,7 @@ pub type IambResult<T> = UIResult<T, IambInfo>;
///
/// The event identifier used as a key here is the ID for the reaction, and not for the message
/// it's reacting to.
pub type MessageReactions = HashMap<OwnedEventId, (String, OwnedUserId)>;
pub type MessageReactions = HashMap<OwnedEventId, (String, OwnedUserId, Option<MediaSource>)>;

/// Errors encountered during application use.
#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -994,22 +995,25 @@ impl RoomInfo {
}

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

let mut seen_user_reactions = BTreeSet::new();

for (key, user) in reacts.values() {
for (key, user, source) in reacts.values() {
if !seen_user_reactions.contains(&(key, user)) {
seen_user_reactions.insert((key, user));
let count = counts.entry(key.as_str()).or_default();
*count += 1;
let count = counts.entry(key.as_str()).or_insert((0, source));
count.0 += 1;
}
}

let mut reactions = counts.into_iter().collect::<Vec<_>>();
reactions.sort();
let mut reactions = counts
.into_iter()
.map(|(key, (count, source))| (key, count, source))
.collect::<Vec<_>>();
reactions.sort_by_key(|item| (item.0, item.1));

reactions
} else {
Expand Down Expand Up @@ -1070,24 +1074,46 @@ impl RoomInfo {
}

/// Insert a reaction to a message.
pub fn insert_reaction(&mut self, react: ReactionEvent) {
match react {
MessageLikeEvent::Original(react) => {
let rel_id = react.content.relates_to.event_id;
let key = react.content.relates_to.key;
fn insert_reaction(&mut self, react: ReactionEvent, source: Option<MediaSource>) {
let MessageLikeEvent::Original(react) = react else {
return;
};
let rel_id = react.content.relates_to.event_id;
let key = react.content.relates_to.key;

let message = self.reactions.entry(rel_id.clone()).or_default();
let event_id = react.event_id;
let user_id = react.sender;
let message = self.reactions.entry(rel_id.clone()).or_default();
let event_id = react.event_id;
let user_id = react.sender;

message.insert(event_id.clone(), (key, user_id));
message.insert(event_id.clone(), (key, user_id, source));

let loc = EventLocation::Reaction(rel_id);
self.keys.insert(event_id, loc);
},
MessageLikeEvent::Redacted(_) => {
return;
},
let loc = EventLocation::Reaction(rel_id);
self.keys.insert(event_id, loc);
}

/// Insert a reaction to a message.
pub fn insert_reaction_with_preview(
&mut self,
react: ReactionEvent,
settings: &ApplicationSettings,
previews: &mut PreviewManager,
worker: &Requester,
) {
let MessageLikeEvent::Original(ref orig_react) = react else {
return;
};
let image_uri = OwnedMxcUri::from(orig_react.content.relates_to.key.as_str());
let source = if image_uri.is_valid() && settings.tunables.image_preview.is_some() {
Some(MediaSource::Plain(image_uri))
} else {
None
};

self.insert_reaction(react, source.clone());

if let (Some(source), Some(_)) = (source, &settings.tunables.image_preview) {
let size = ImagePreviewSize { width: 2, height: 1 };
previews.register_preview(settings, source, PreviewKind::Reaction, size, worker);
}
}

Expand Down Expand Up @@ -1233,32 +1259,28 @@ impl RoomInfo {
}
}

/// Insert a new message event, and spawn a task for image-preview if it has an image
/// attachment.
/// Insert a new message event, and prepare for image-preview if it has an image attachment.
pub fn insert_with_preview(
&mut self,
room_id: OwnedRoomId,
store: AsyncProgramStore,
picker: Option<Picker>,
ev: RoomMessageEvent,
settings: &mut ApplicationSettings,
media: matrix_sdk::Media,
settings: &ApplicationSettings,
previews: &mut PreviewManager,
worker: &Requester,
) {
let source = picker.and_then(|_| source_from_event(&ev));
let source = source_from_event(&ev);
self.insert(ev);

if let Some((event_id, source)) = source {
if let (Some(msg), Some(image_preview)) =
(self.get_event_mut(&event_id), &settings.tunables.image_preview)
{
msg.image_preview = ImageStatus::Downloading(image_preview.size.clone());
spawn_insert_preview(
store,
room_id,
event_id,
msg.image_preview = Some(source.clone());
previews.register_preview(
settings,
source,
media,
settings.dirs.image_previews.clone(),
PreviewKind::Message,
image_preview.size,
worker,
)
}
}
Expand Down Expand Up @@ -1416,7 +1438,7 @@ impl RoomInfo {
if let Some(reactions) = self.reactions.get(event_id) {
reactions
.values()
.any(|(annotation, user)| annotation == emoji && user == user_id)
.any(|(annotation, user, _)| annotation == emoji && user == user_id)
} else {
false
}
Expand Down Expand Up @@ -1601,8 +1623,8 @@ pub struct ChatStore {
/// Information gathered by the background thread.
pub sync_info: SyncInfo,

/// Image preview "protocol" picker.
pub picker: Option<Picker>,
/// Rendered image previews.
pub previews: PreviewManager,

/// Last draw time, used to match with RoomInfo's draw_last.
pub draw_curr: Option<Instant>,
Expand All @@ -1628,7 +1650,7 @@ impl ChatStore {
ChatStore {
worker,
settings,
picker,
previews: PreviewManager::new(picker),
cmds: crate::commands::setup_commands(),
emojis: emoji_map(),

Expand Down Expand Up @@ -2196,16 +2218,17 @@ pub mod tests {

for i in 0..3 {
let event_id = format!("$house_{i}");
info.insert_reaction(MessageLikeEvent::Original(
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
info.insert_reaction(
MessageLikeEvent::Original(matrix_sdk::ruma::events::OriginalMessageLikeEvent {
content: content.clone(),
event_id: OwnedEventId::from_str(&event_id).unwrap(),
sender: owned_user_id!("@foo:example.org"),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
room_id: owned_room_id!("!foo:example.org"),
unsigned: MessageLikeUnsigned::new(),
},
));
}),
None,
);
}

let content = ReactionEventContent::new(Annotation::new(
Expand All @@ -2215,36 +2238,40 @@ pub mod tests {

for i in 0..2 {
let event_id = format!("$smile_{i}");
info.insert_reaction(MessageLikeEvent::Original(
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
info.insert_reaction(
MessageLikeEvent::Original(matrix_sdk::ruma::events::OriginalMessageLikeEvent {
content: content.clone(),
event_id: OwnedEventId::from_str(&event_id).unwrap(),
sender: owned_user_id!("@foo:example.org"),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
room_id: owned_room_id!("!foo:example.org"),
unsigned: MessageLikeUnsigned::new(),
},
));
}),
None,
);
}

for i in 2..4 {
let event_id = format!("$smile_{i}");
info.insert_reaction(MessageLikeEvent::Original(
matrix_sdk::ruma::events::OriginalMessageLikeEvent {
info.insert_reaction(
MessageLikeEvent::Original(matrix_sdk::ruma::events::OriginalMessageLikeEvent {
content: content.clone(),
event_id: OwnedEventId::from_str(&event_id).unwrap(),
sender: owned_user_id!("@bar:example.org"),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
room_id: owned_room_id!("!foo:example.org"),
unsigned: MessageLikeUnsigned::new(),
},
));
}),
None,
);
}

assert_eq!(info.get_reactions(&owned_event_id!("$my_reaction")), vec![
("🏠", 1),
("🙂", 2)
]);
let reacts: Vec<_> = info
.get_reactions(&owned_event_id!("$my_reaction"))
.into_iter()
.map(|(key, count, _)| (key, count))
.collect();
assert_eq!(reacts, vec![("🏠", 1), ("🙂", 2)]);
}

#[test]
Expand Down
Loading
Loading