fractal/utils/matrix/
media_message.rs

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
30/// Get the filename of a media message.
31macro_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/// A media message.
49#[derive(Debug, Clone)]
50pub(crate) enum MediaMessage {
51    /// An audio.
52    Audio(AudioMessageEventContent),
53    /// A file.
54    File(FileMessageEventContent),
55    /// An image.
56    Image(ImageMessageEventContent),
57    /// A video.
58    Video(VideoMessageEventContent),
59    /// A sticker.
60    Sticker(Box<StickerEventContent>),
61}
62
63impl MediaMessage {
64    /// Construct a `MediaMessage` from the given message.
65    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    /// The filename of the media.
76    ///
77    /// For a sticker, this returns the description of the sticker.
78    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    /// The caption of the media, if any.
89    ///
90    /// Returns `Some((body, formatted_body))` if the media includes a caption.
91    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    /// Fetch the content of the media with the given client.
102    ///
103    /// Returns an error if something occurred while fetching the content.
104    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    /// Fetch the content of the media with the given client and write it to a
130    /// temporary file.
131    ///
132    /// Returns an error if something occurred while fetching the content.
133    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    /// Save the content of the media to a file selected by the user.
139    ///
140    /// Shows a dialog to the user to select a file on the system.
141    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/// A visual media message.
208#[derive(Debug, Clone)]
209pub(crate) enum VisualMediaMessage {
210    /// An image.
211    Image(ImageMessageEventContent),
212    /// A video.
213    Video(VideoMessageEventContent),
214    /// A sticker.
215    Sticker(Box<StickerEventContent>),
216}
217
218impl VisualMediaMessage {
219    /// Construct a `VisualMediaMessage` from the given message.
220    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    /// The filename of the media.
229    ///
230    /// For a sticker, this returns the description of the sticker.
231    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    /// The dimensions of the media, if any.
240    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    /// The type of the media.
250    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    /// The content type of the media.
259    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    /// Get the Blurhash of the media, if any.
267    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    /// Fetch a thumbnail of the media with the given client and thumbnail
277    /// settings.
278    ///
279    /// This might not return a thumbnail at the requested size, depending on
280    /// the message and the homeserver.
281    ///
282    /// Returns `Ok(None)` if no thumbnail could be retrieved and no fallback
283    /// could be downloaded. This only applies to video messages.
284    ///
285    /// Returns an error if something occurred while fetching the content or
286    /// loading it.
287    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    /// Fetch the content of the media with the given client and write it to a
347    /// temporary file.
348    ///
349    /// Returns an error if something occurred while fetching the content or
350    /// saving the content to a file.
351    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    /// Save the content of the media to a file selected by the user.
356    ///
357    /// Shows a dialog to the user to select a file on the system.
358    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/// The type of a visual media message.
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub(crate) enum VisualMediaType {
400    /// An image.
401    Image,
402    /// A video.
403    Video,
404    /// A sticker.
405    Sticker,
406}