1use gettextrs::gettext;
2use gtk::{gio, prelude::*};
3use matrix_sdk::Client;
4use ruma::events::{
5 room::message::{
6 AudioMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent,
7 MessageType, VideoMessageEventContent,
8 },
9 sticker::StickerEventContent,
10};
11use tracing::{debug, error};
12
13use crate::{
14 components::ContentType,
15 prelude::*,
16 toast,
17 utils::{
18 File,
19 media::{
20 FrameDimensions, MediaFileError,
21 image::{
22 Blurhash, Image, ImageError, ImageRequestPriority, ImageSource,
23 ThumbnailDownloader, ThumbnailSettings,
24 },
25 },
26 save_data_to_tmp_file,
27 },
28};
29
30macro_rules! filename {
32 ($message:ident, $mime_fallback:expr) => {{
33 let filename = $message.filename();
34
35 if filename.is_empty() {
36 let mimetype = $message
37 .info
38 .as_ref()
39 .and_then(|info| info.mimetype.as_deref());
40
41 $crate::utils::media::filename_for_mime(mimetype, $mime_fallback)
42 } else {
43 filename.to_owned()
44 }
45 }};
46}
47
48#[derive(Debug, Clone)]
50pub(crate) enum MediaMessage {
51 Audio(AudioMessageEventContent),
53 File(FileMessageEventContent),
55 Image(ImageMessageEventContent),
57 Video(VideoMessageEventContent),
59 Sticker(Box<StickerEventContent>),
61}
62
63impl MediaMessage {
64 pub(crate) fn from_message(msgtype: &MessageType) -> Option<Self> {
66 match msgtype {
67 MessageType::Audio(c) => Some(Self::Audio(c.clone())),
68 MessageType::File(c) => Some(Self::File(c.clone())),
69 MessageType::Image(c) => Some(Self::Image(c.clone())),
70 MessageType::Video(c) => Some(Self::Video(c.clone())),
71 _ => None,
72 }
73 }
74
75 pub(crate) fn filename(&self) -> String {
79 match self {
80 Self::Audio(c) => filename!(c, Some(mime::AUDIO)),
81 Self::File(c) => filename!(c, None),
82 Self::Image(c) => filename!(c, Some(mime::IMAGE)),
83 Self::Video(c) => filename!(c, Some(mime::VIDEO)),
84 Self::Sticker(c) => c.body.clone(),
85 }
86 }
87
88 pub(crate) fn caption(&self) -> Option<(&str, Option<&FormattedBody>)> {
92 match self {
93 Self::Audio(c) => c.caption().map(|caption| (caption, c.formatted.as_ref())),
94 Self::File(c) => c.caption().map(|caption| (caption, c.formatted.as_ref())),
95 Self::Image(c) => c.caption().map(|caption| (caption, c.formatted.as_ref())),
96 Self::Video(c) => c.caption().map(|caption| (caption, c.formatted.as_ref())),
97 Self::Sticker(_) => None,
98 }
99 }
100
101 pub(crate) async fn into_content(self, client: &Client) -> Result<Vec<u8>, matrix_sdk::Error> {
105 let media = client.media();
106
107 macro_rules! content {
108 ($event_content:expr) => {{
109 Ok(
110 $crate::spawn_tokio!(
111 async move { media.get_file(&$event_content, true).await }
112 )
113 .await
114 .unwrap()?
115 .expect("All media message types have a file"),
116 )
117 }};
118 }
119
120 match self {
121 Self::Audio(c) => content!(c),
122 Self::File(c) => content!(c),
123 Self::Image(c) => content!(c),
124 Self::Video(c) => content!(c),
125 Self::Sticker(c) => content!(*c),
126 }
127 }
128
129 pub(crate) async fn into_tmp_file(self, client: &Client) -> Result<File, MediaFileError> {
134 let data = self.into_content(client).await?;
135 Ok(save_data_to_tmp_file(data).await?)
136 }
137
138 pub(crate) async fn save_to_file(self, client: &Client, parent: &impl IsA<gtk::Widget>) {
142 let filename = self.filename();
143
144 let data = match self.into_content(client).await {
145 Ok(data) => data,
146 Err(error) => {
147 error!("Could not retrieve media file: {error}");
148 toast!(parent, error.to_user_facing());
149
150 return;
151 }
152 };
153
154 let dialog = gtk::FileDialog::builder()
155 .title(gettext("Save File"))
156 .modal(true)
157 .accept_label(gettext("Save"))
158 .initial_name(filename)
159 .build();
160
161 match dialog
162 .save_future(parent.root().and_downcast_ref::<gtk::Window>())
163 .await
164 {
165 Ok(file) => {
166 if let Err(error) = file.replace_contents(
167 &data,
168 None,
169 false,
170 gio::FileCreateFlags::REPLACE_DESTINATION,
171 gio::Cancellable::NONE,
172 ) {
173 error!("Could not save file: {error}");
174 toast!(parent, gettext("Could not save file"));
175 }
176 }
177 Err(error) => {
178 if error.matches(gtk::DialogError::Dismissed) {
179 debug!("File dialog dismissed by user");
180 } else {
181 error!("Could not access file: {error}");
182 toast!(parent, gettext("Could not access file"));
183 }
184 }
185 }
186 }
187}
188
189impl From<AudioMessageEventContent> for MediaMessage {
190 fn from(value: AudioMessageEventContent) -> Self {
191 Self::Audio(value)
192 }
193}
194
195impl From<FileMessageEventContent> for MediaMessage {
196 fn from(value: FileMessageEventContent) -> Self {
197 Self::File(value)
198 }
199}
200
201impl From<StickerEventContent> for MediaMessage {
202 fn from(value: StickerEventContent) -> Self {
203 Self::Sticker(value.into())
204 }
205}
206
207#[derive(Debug, Clone)]
209pub(crate) enum VisualMediaMessage {
210 Image(ImageMessageEventContent),
212 Video(VideoMessageEventContent),
214 Sticker(Box<StickerEventContent>),
216}
217
218impl VisualMediaMessage {
219 pub(crate) fn from_message(msgtype: &MessageType) -> Option<Self> {
221 match msgtype {
222 MessageType::Image(c) => Some(Self::Image(c.clone())),
223 MessageType::Video(c) => Some(Self::Video(c.clone())),
224 _ => None,
225 }
226 }
227
228 pub(crate) fn filename(&self) -> String {
232 match self {
233 Self::Image(c) => filename!(c, Some(mime::IMAGE)),
234 Self::Video(c) => filename!(c, Some(mime::VIDEO)),
235 Self::Sticker(c) => c.body.clone(),
236 }
237 }
238
239 pub(crate) fn dimensions(&self) -> Option<FrameDimensions> {
241 let (width, height) = match self {
242 Self::Image(c) => c.info.as_ref().map(|i| (i.width, i.height))?,
243 Self::Video(c) => c.info.as_ref().map(|i| (i.width, i.height))?,
244 Self::Sticker(c) => (c.info.width, c.info.height),
245 };
246 FrameDimensions::from_options(width, height)
247 }
248
249 pub(crate) fn visual_media_type(&self) -> VisualMediaType {
251 match self {
252 Self::Image(_) => VisualMediaType::Image,
253 Self::Sticker(_) => VisualMediaType::Sticker,
254 Self::Video(_) => VisualMediaType::Video,
255 }
256 }
257
258 pub(crate) fn content_type(&self) -> ContentType {
260 match self {
261 Self::Image(_) | Self::Sticker(_) => ContentType::Image,
262 Self::Video(_) => ContentType::Video,
263 }
264 }
265
266 pub(crate) fn blurhash(&self) -> Option<Blurhash> {
268 match self {
269 Self::Image(image_content) => image_content.info.as_deref()?.blurhash.clone(),
270 Self::Sticker(sticker_content) => sticker_content.info.blurhash.clone(),
271 Self::Video(video_content) => video_content.info.as_deref()?.blurhash.clone(),
272 }
273 .map(Blurhash)
274 }
275
276 pub(crate) async fn thumbnail(
288 &self,
289 client: Client,
290 settings: ThumbnailSettings,
291 priority: ImageRequestPriority,
292 ) -> Result<Option<Image>, ImageError> {
293 let downloader = match &self {
294 Self::Image(c) => {
295 let image_info = c.info.as_deref();
296 ThumbnailDownloader {
297 main: ImageSource {
298 source: (&c.source).into(),
299 info: image_info.map(Into::into),
300 },
301 alt: image_info.and_then(|i| {
302 i.thumbnail_source.as_ref().map(|s| ImageSource {
303 source: s.into(),
304 info: i.thumbnail_info.as_deref().map(Into::into),
305 })
306 }),
307 }
308 }
309 Self::Video(c) => {
310 let Some(video_info) = c.info.as_deref() else {
311 return Ok(None);
312 };
313 let Some(thumbnail_source) = video_info.thumbnail_source.as_ref() else {
314 return Ok(None);
315 };
316
317 ThumbnailDownloader {
318 main: ImageSource {
319 source: thumbnail_source.into(),
320 info: video_info.thumbnail_info.as_deref().map(Into::into),
321 },
322 alt: None,
323 }
324 }
325 Self::Sticker(c) => {
326 let image_info = &c.info;
327 ThumbnailDownloader {
328 main: ImageSource {
329 source: (&c.source).into(),
330 info: Some(image_info.into()),
331 },
332 alt: image_info.thumbnail_source.as_ref().map(|s| ImageSource {
333 source: s.into(),
334 info: image_info.thumbnail_info.as_deref().map(Into::into),
335 }),
336 }
337 }
338 };
339
340 downloader
341 .download(client, settings, priority)
342 .await
343 .map(Some)
344 }
345
346 pub(crate) async fn into_tmp_file(self, client: &Client) -> Result<File, MediaFileError> {
352 MediaMessage::from(self).into_tmp_file(client).await
353 }
354
355 pub(crate) async fn save_to_file(self, client: &Client, parent: &impl IsA<gtk::Widget>) {
359 MediaMessage::from(self).save_to_file(client, parent).await;
360 }
361}
362
363impl From<ImageMessageEventContent> for VisualMediaMessage {
364 fn from(value: ImageMessageEventContent) -> Self {
365 Self::Image(value)
366 }
367}
368
369impl From<VideoMessageEventContent> for VisualMediaMessage {
370 fn from(value: VideoMessageEventContent) -> Self {
371 Self::Video(value)
372 }
373}
374
375impl From<StickerEventContent> for VisualMediaMessage {
376 fn from(value: StickerEventContent) -> Self {
377 Self::Sticker(value.into())
378 }
379}
380
381impl From<Box<StickerEventContent>> for VisualMediaMessage {
382 fn from(value: Box<StickerEventContent>) -> Self {
383 Self::Sticker(value)
384 }
385}
386
387impl From<VisualMediaMessage> for MediaMessage {
388 fn from(value: VisualMediaMessage) -> Self {
389 match value {
390 VisualMediaMessage::Image(c) => Self::Image(c),
391 VisualMediaMessage::Video(c) => Self::Video(c),
392 VisualMediaMessage::Sticker(c) => Self::Sticker(c),
393 }
394 }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub(crate) enum VisualMediaType {
400 Image,
402 Video,
404 Sticker,
406}