diff --git a/src/components/media/audio_player/mod.blp b/src/components/media/audio_player/mod.blp index 5bb6b249..afc4d0ac 100644 --- a/src/components/media/audio_player/mod.blp +++ b/src/components/media/audio_player/mod.blp @@ -65,7 +65,7 @@ template $AudioPlayer: Adw.BreakpointBin { }; } - Gtk.Label filename_label { + Gtk.Label name_label { hexpand: true; xalign: 0.0; ellipsize: end; diff --git a/src/components/media/audio_player/mod.rs b/src/components/media/audio_player/mod.rs index be6fdfe8..85485c94 100644 --- a/src/components/media/audio_player/mod.rs +++ b/src/components/media/audio_player/mod.rs @@ -49,7 +49,7 @@ mod imp { #[template_child] play_button: TemplateChild, #[template_child] - filename_label: TemplateChild, + name_label: TemplateChild, #[template_child] position_label_narrow: TemplateChild, /// The source to play. @@ -172,7 +172,7 @@ mod imp { } )); - self.update_source_filename(); + self.update_source_name(); } self.update_play_button(); @@ -206,7 +206,7 @@ mod imp { self.position_label.set_visible(!narrow); self.remaining_label.set_visible(!narrow); - self.filename_label.set_visible(!standalone); + self.name_label.set_visible(!standalone); self.position_label_narrow .set_visible(narrow && !standalone); @@ -284,15 +284,15 @@ mod imp { } /// Update the name of the source. - fn update_source_filename(&self) { - let filename = self + fn update_source_name(&self) { + let name = self .source .borrow() .as_ref() - .map(AudioPlayerSource::filename) + .map(AudioPlayerSource::name) .unwrap_or_default(); - self.filename_label.set_label(&filename); + self.name_label.set_label(&name); } /// Update the labels displaying the position in the audio stream. @@ -542,14 +542,14 @@ pub(crate) enum AudioPlayerSource { } impl AudioPlayerSource { - /// Get the filename of the source. - fn filename(&self) -> String { + /// Get the name of the source. + fn name(&self) -> String { match self { Self::File(file) => file .path() .and_then(|path| path.file_name().map(|s| s.to_string_lossy().into_owned())) .unwrap_or_default(), - Self::Message(message) => message.message.filename(), + Self::Message(message) => message.message.display_name(), } } diff --git a/src/session/view/content/room_details/history_viewer/event.rs b/src/session/view/content/room_details/history_viewer/event.rs index e2f879ed..26591fb6 100644 --- a/src/session/view/content/room_details/history_viewer/event.rs +++ b/src/session/view/content/room_details/history_viewer/event.rs @@ -10,7 +10,7 @@ use ruma::{ use crate::{ session::model::Room, - utils::matrix::{MediaMessage, VisualMediaMessage}, + utils::matrix::{MediaMessage, VisualMediaMessage, timestamp_to_date}, }; /// The types of events that can be displayed in the history viewers. @@ -141,6 +141,11 @@ impl HistoryViewerEvent { self.matrix_event().event_id.clone() } + /// The timestamp of this event, as a `GDateTime`. + pub(crate) fn timestamp(&self) -> glib::DateTime { + timestamp_to_date(self.matrix_event().origin_server_ts) + } + /// The media message content of this event. pub(crate) fn media_message(&self) -> MediaMessage { MediaMessage::from_message(&self.matrix_event().content.msgtype) diff --git a/src/session/view/content/room_details/history_viewer/file_row.rs b/src/session/view/content/room_details/history_viewer/file_row.rs index 1424f4a5..6aaed6d0 100644 --- a/src/session/view/content/room_details/history_viewer/file_row.rs +++ b/src/session/view/content/room_details/history_viewer/file_row.rs @@ -64,7 +64,7 @@ mod imp { if let Some(event) = &event { let media_message = event.media_message(); if let MediaMessage::File(file) = &media_message { - let filename = media_message.filename(); + let filename = media_message.filename(&event.timestamp()); self.title_label.set_label(&filename); self.button @@ -136,7 +136,7 @@ mod imp { return; } }; - let filename = event.media_message().filename(); + let filename = event.media_message().filename(&event.timestamp()); let parent_window = obj.root().and_downcast::(); let dialog = gtk::FileDialog::builder() diff --git a/src/session/view/content/room_history/event_actions/group.rs b/src/session/view/content/room_history/event_actions/group.rs index 9d914661..d2f2ca3a 100644 --- a/src/session/view/content/room_history/event_actions/group.rs +++ b/src/session/view/content/room_history/event_actions/group.rs @@ -553,7 +553,9 @@ pub(crate) trait EventActionsGroup: ObjectSubclass { }; let client = session.client(); - media_message.save_to_file(&client, &*self.obj()).await; + media_message + .save_to_file(&event.timestamp(), &client, &*self.obj()) + .await; } /// Redact the event of this row. diff --git a/src/session/view/content/room_history/message_row/audio.blp b/src/session/view/content/room_history/message_row/audio.blp index cd1e8c8f..bbc1eb97 100644 --- a/src/session/view/content/room_history/message_row/audio.blp +++ b/src/session/view/content/room_history/message_row/audio.blp @@ -16,7 +16,7 @@ template $ContentMessageAudio: Gtk.Box { ellipsize: end; xalign: 0.0; hexpand: true; - label: bind template.filename; + label: bind template.name; } } diff --git a/src/session/view/content/room_history/message_row/audio.rs b/src/session/view/content/room_history/message_row/audio.rs index 228c946f..2abf40da 100644 --- a/src/session/view/content/room_history/message_row/audio.rs +++ b/src/session/view/content/room_history/message_row/audio.rs @@ -22,9 +22,9 @@ mod imp { pub struct MessageAudio { #[template_child] player: TemplateChild, - /// The filename of the audio file. + /// The name of the audio file. #[property(get)] - filename: RefCell, + name: RefCell, /// Whether to display this audio message in a compact format. #[property(get)] compact: Cell, @@ -52,24 +52,24 @@ mod imp { impl BoxImpl for MessageAudio {} impl MessageAudio { - /// Set the filename of the audio file. - fn set_filename(&self, filename: Option) { - let filename = filename.unwrap_or_default(); + /// Set the name of the audio file. + fn set_name(&self, name: Option) { + let name = name.unwrap_or_default(); - if *self.filename.borrow() == filename { + if *self.name.borrow() == name { return; } let obj = self.obj(); - let accessible_label = if filename.is_empty() { + let accessible_label = if name.is_empty() { gettext("Audio") } else { - gettext_f("Audio: {filename}", &[("filename", &filename)]) + gettext_f("Audio: {filename}", &[("filename", &name)]) }; obj.update_property(&[gtk::accessible::Property::Label(&accessible_label)]); - self.filename.replace(filename); - obj.notify_filename(); + self.name.replace(name); + obj.notify_name(); } /// Set the compact format of this audio message. @@ -82,7 +82,7 @@ mod imp { /// Display the given `audio` message. pub(super) fn set_audio_message(&self, message: AudioPlayerMessage, format: ContentFormat) { - self.set_filename(Some(message.message.filename())); + self.set_name(Some(message.message.display_name())); let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized); self.set_compact(compact); diff --git a/src/session/view/content/room_history/message_row/content.rs b/src/session/view/content/room_history/message_row/content.rs index 53166c1f..605b1a4c 100644 --- a/src/session/view/content/room_history/message_row/content.rs +++ b/src/session/view/content/room_history/message_row/content.rs @@ -449,7 +449,7 @@ trait MessageContentContainer: ChildPropertyExt { let widget = self.child_or_default::(); let media_message = MediaMessage::from(file); - widget.set_filename(Some(media_message.filename())); + widget.set_filename(Some(media_message.display_name())); widget.set_format(format); } MediaMessage::Image(image) => { diff --git a/src/session/view/media_viewer.rs b/src/session/view/media_viewer.rs index 732f9aa5..19a4f4d8 100644 --- a/src/session/view/media_viewer.rs +++ b/src/session/view/media_viewer.rs @@ -491,7 +491,14 @@ mod imp { }; let client = session.client(); - media_message.save_to_file(&client, &*self.obj()).await; + media_message + .save_to_file( + // The timestamp should be unused for visual media messages. + &glib::DateTime::now_local().expect("Getting local time should work"), + &client, + &*self.obj(), + ) + .await; } /// Copy the permalink of the event of the media message to the diff --git a/src/utils/matrix/media_message.rs b/src/utils/matrix/media_message.rs index 9b8f7679..32fab185 100644 --- a/src/utils/matrix/media_message.rs +++ b/src/utils/matrix/media_message.rs @@ -1,5 +1,5 @@ use gettextrs::gettext; -use gtk::{gio, prelude::*}; +use gtk::{gio, glib, prelude::*}; use matrix_sdk::Client; use ruma::events::{ room::message::{ @@ -12,6 +12,7 @@ use tracing::{debug, error}; use crate::{ components::ContentType, + gettext_f, prelude::*, toast, utils::{ @@ -74,12 +75,63 @@ impl MediaMessage { } } - /// The filename of the media. + /// The name of the media, as displayed in the interface. /// - /// For a sticker, this returns the description of the sticker. - pub(crate) fn filename(&self) -> String { + /// This is usually the filename in the message, except: + /// + /// - For a voice message, it's a placeholder string because file names are + /// usually generated randomly. + /// - For a sticker, this returns the description of the sticker, because + /// they do not have a filename. + pub(crate) fn display_name(&self) -> String { + match self { + Self::Audio(c) => { + if c.voice.is_some() { + gettext("Voice Message") + } else { + filename!(c, Some(mime::AUDIO)) + } + } + Self::File(c) => filename!(c, None), + Self::Image(c) => filename!(c, Some(mime::IMAGE)), + Self::Video(c) => filename!(c, Some(mime::VIDEO)), + Self::Sticker(c) => c.body.clone(), + } + } + + /// The filename of the media, used when saving the file. + /// + /// This is usually the filename in the message, except: + /// + /// - For a voice message, it's a generated name that uses the timestamp of + /// the message. + /// - For a sticker, this returns the description of the sticker, because + /// they do not have a filename. + pub(crate) fn filename(&self, timestamp: &glib::DateTime) -> String { match self { - Self::Audio(c) => filename!(c, Some(mime::AUDIO)), + Self::Audio(c) => { + let mut filename = filename!(c, Some(mime::AUDIO)); + + if c.voice.is_some() { + let datetime = timestamp + .to_local() + .and_then(|local_timestamp| local_timestamp.format("%Y-%m-%d %H-%M-%S")) + // Fallback to the timestamp in seconds. + .map_or_else(|_| timestamp.second().to_string(), String::from); + // Translators: this is the name of the file that the voice message is saved as. + // Do NOT translate the content between '{' and '}', this is a variable name + // corresponding to a date and time, e.g. "2017-05-21 12-24-03". + let name = + gettext_f("Voice Message From {datetime}", &[("datetime", &datetime)]); + + filename = filename + .rsplit_once('.') + .map(|(_, extension)| format!("{name}.{extension}")) + .unwrap_or(name); + } + + filename + } Self::File(c) => filename!(c, None), Self::Image(c) => filename!(c, Some(mime::IMAGE)), Self::Video(c) => filename!(c, Some(mime::VIDEO)), @@ -157,8 +209,13 @@ impl MediaMessage { /// Save the content of the media to a file selected by the user. /// /// Shows a dialog to the user to select a file on the system. - pub(crate) async fn save_to_file(self, client: &Client, parent: &impl IsA) { - let filename = self.filename(); + pub(crate) async fn save_to_file( + self, + timestamp: &glib::DateTime, + client: &Client, + parent: &impl IsA, + ) { + let filename = self.filename(timestamp); let data = match self.into_content(client).await { Ok(data) => data, @@ -374,8 +431,15 @@ impl VisualMediaMessage { /// Save the content of the media to a file selected by the user. /// /// Shows a dialog to the user to select a file on the system. - pub(crate) async fn save_to_file(self, client: &Client, parent: &impl IsA) { - MediaMessage::from(self).save_to_file(client, parent).await; + pub(crate) async fn save_to_file( + self, + timestamp: &glib::DateTime, + client: &Client, + parent: &impl IsA, + ) { + MediaMessage::from(self) + .save_to_file(timestamp, client, parent) + .await; } }