[fractal/fractal-next] room-history: Show audio messages in timeline
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] room-history: Show audio messages in timeline
- Date: Mon, 7 Feb 2022 10:32:46 +0000 (UTC)
commit 3079b7faca97ec41d54b07af380421fee2a31562
Author: Kévin Commaille <zecakeh tedomum fr>
Date: Thu Feb 3 11:36:50 2022 +0100
room-history: Show audio messages in timeline
data/resources/resources.gresource.xml | 2 +
data/resources/ui/components-audio-player.ui | 10 +
data/resources/ui/content-message-audio.ui | 45 ++++
po/POTFILES.in | 1 +
src/components/audio_player.rs | 109 ++++++++
src/components/mod.rs | 2 +
.../content/room_history/message_row/audio.rs | 275 +++++++++++++++++++++
.../content/room_history/message_row/mod.rs | 18 +-
8 files changed, 459 insertions(+), 3 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index c8d2868df..9ccb23230 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -16,6 +16,7 @@
<file compressed="true" preprocess="xml-stripblanks"
alias="account-settings.ui">ui/account-settings.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="add-account-row.ui">ui/add-account-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="avatar-with-selection.ui">ui/avatar-with-selection.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="components-audio-player.ui">ui/components-audio-player.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="components-auth-dialog.ui">ui/components-auth-dialog.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="components-avatar.ui">ui/components-avatar.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="components-loading-listbox-row.ui">ui/components-loading-listbox-row.ui</file>
@@ -33,6 +34,7 @@
<file compressed="true" preprocess="xml-stripblanks"
alias="content-member-item.ui">ui/content-member-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-member-page.ui">ui/content-member-page.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-member-row.ui">ui/content-member-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-message-audio.ui">ui/content-message-audio.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-message-file.ui">ui/content-message-file.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-message-media.ui">ui/content-message-media.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="content-message-reaction-list.ui">ui/content-message-reaction-list.ui</file>
diff --git a/data/resources/ui/components-audio-player.ui b/data/resources/ui/components-audio-player.ui
new file mode 100644
index 000000000..f28d91233
--- /dev/null
+++ b/data/resources/ui/components-audio-player.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ComponentsAudioPlayer" parent="AdwBin">
+ <child>
+ <object class="GtkMediaControls">
+ <property name="media-stream" bind-source="ComponentsAudioPlayer" bind-property="media-file"
bind-flags="sync-create"/>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-message-audio.ui b/data/resources/ui/content-message-audio.ui
new file mode 100644
index 000000000..55108544a
--- /dev/null
+++ b/data/resources/ui/content-message-audio.ui
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentMessageAudio" parent="AdwBin">
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="margin-top">6</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible" bind-source="ContentMessageAudio" bind-property="compact"
bind-flags="sync-create"/>
+ <property name="icon-name">audio-x-generic-symbolic</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="ellipsize">end</property>
+ <property name="xalign">0.0</property>
+ <property name="hexpand">true</property>
+ <property name="label" bind-source="ContentMessageAudio" bind-property="body"
bind-flags="sync-create"/>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkSpinner" id="state_spinner">
+ <property name="spinning">true</property>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkImage" id="state_error">
+ <property name="icon-name">dialog-error-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="ComponentsAudioPlayer" id="player">
+ <property name="visible" bind-source="ContentMessageAudio" bind-property="compact"
bind-flags="sync-create|invert-boolean"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a32604cbd..fcb9078a5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -45,6 +45,7 @@ src/session/content/explore/public_room_row.rs
src/session/content/room_details/member_page/mod.rs
src/session/content/room_details/mod.rs
src/session/content/room_history/item_row.rs
+src/session/content/room_history/message_row/audio.rs
src/session/content/room_history/message_row/media.rs
src/session/content/room_history/message_row/mod.rs
src/session/content/room_history/state_row/creation.rs
diff --git a/src/components/audio_player.rs b/src/components/audio_player.rs
new file mode 100644
index 000000000..ff67d0110
--- /dev/null
+++ b/src/components/audio_player.rs
@@ -0,0 +1,109 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+mod imp {
+ use std::cell::RefCell;
+
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/components-audio-player.ui")]
+ pub struct AudioPlayer {
+ /// The media file to play.
+ pub media_file: RefCell<Option<gtk::MediaFile>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for AudioPlayer {
+ const NAME: &'static str = "ComponentsAudioPlayer";
+ type Type = super::AudioPlayer;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for AudioPlayer {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpecObject::new(
+ "media-file",
+ "Media File",
+ "The media file to play",
+ gtk::MediaFile::static_type(),
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "media-file" => {
+ obj.set_media_file(value.get().unwrap());
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "media-file" => obj.media_file().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl WidgetImpl for AudioPlayer {}
+
+ impl BinImpl for AudioPlayer {}
+}
+
+glib::wrapper! {
+ /// A widget displaying a video media file.
+ pub struct AudioPlayer(ObjectSubclass<imp::AudioPlayer>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl AudioPlayer {
+ /// Create a new audio player.
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create AudioPlayer")
+ }
+
+ /// The media file that is playing.
+ pub fn media_file(&self) -> Option<gtk::MediaFile> {
+ self.imp().media_file.borrow().clone()
+ }
+
+ /// Set the media_file to play.
+ pub fn set_media_file(&self, media_file: Option<gtk::MediaFile>) {
+ if self.media_file() == media_file {
+ return;
+ }
+
+ self.imp().media_file.replace(media_file);
+ self.notify("media-file");
+ }
+}
+
+impl Default for AudioPlayer {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/components/mod.rs b/src/components/mod.rs
index ba7c9f5e5..8e7df87cf 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -1,3 +1,4 @@
+mod audio_player;
mod auth_dialog;
mod avatar;
mod badge;
@@ -14,6 +15,7 @@ mod video_player;
mod video_player_renderer;
pub use self::{
+ audio_player::AudioPlayer,
auth_dialog::{AuthData, AuthDialog},
avatar::Avatar,
badge::Badge,
diff --git a/src/session/content/room_history/message_row/audio.rs
b/src/session/content/room_history/message_row/audio.rs
new file mode 100644
index 000000000..e2ed8e87e
--- /dev/null
+++ b/src/session/content/room_history/message_row/audio.rs
@@ -0,0 +1,275 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{
+ gio,
+ glib::{self, clone},
+ subclass::prelude::*,
+ CompositeTemplate,
+};
+use log::warn;
+use matrix_sdk::{media::MediaEventContent, ruma::events::room::message::AudioMessageEventContent};
+
+use super::media::MediaState;
+use crate::{components::AudioPlayer, session::Session, spawn, spawn_tokio, utils::media_type_uid};
+
+mod imp {
+ use std::cell::{Cell, RefCell};
+
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+
+ use super::*;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-message-audio.ui")]
+ pub struct MessageAudio {
+ /// The body of the audio message.
+ pub body: RefCell<Option<String>>,
+ /// The state of the audio file.
+ pub state: Cell<MediaState>,
+ /// Whether to display this audio message in a compact format.
+ pub compact: Cell<bool>,
+ #[template_child]
+ pub player: TemplateChild<AudioPlayer>,
+ #[template_child]
+ pub state_spinner: TemplateChild<gtk::Spinner>,
+ #[template_child]
+ pub state_error: TemplateChild<gtk::Image>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MessageAudio {
+ const NAME: &'static str = "ContentMessageAudio";
+ type Type = super::MessageAudio;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for MessageAudio {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpecString::new(
+ "body",
+ "Body",
+ "The body of the audio message",
+ None,
+ glib::ParamFlags::READABLE,
+ ),
+ glib::ParamSpecEnum::new(
+ "state",
+ "State",
+ "The state of the audio file",
+ MediaState::static_type(),
+ MediaState::default() as i32,
+ glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY,
+ ),
+ glib::ParamSpecBoolean::new(
+ "compact",
+ "Compact",
+ "Whether to display this audio message in a compact format",
+ false,
+ glib::ParamFlags::READABLE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "state" => obj.set_state(value.get().unwrap()),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "body" => obj.body().to_value(),
+ "state" => obj.state().to_value(),
+ "compact" => obj.compact().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+
+ impl WidgetImpl for MessageAudio {}
+
+ impl BinImpl for MessageAudio {}
+}
+
+glib::wrapper! {
+ /// A widget displaying an audio message in the timeline.
+ pub struct MessageAudio(ObjectSubclass<imp::MessageAudio>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl MessageAudio {
+ /// Create a new audio message.
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create MessageAudio")
+ }
+
+ /// The body of the audio message.
+ pub fn body(&self) -> Option<String> {
+ self.imp().body.borrow().to_owned()
+ }
+
+ /// Set the body of the audio message.
+ fn set_body(&self, body: Option<String>) {
+ if self.body() == body {
+ return;
+ }
+
+ self.imp().body.replace(body);
+ self.notify("body");
+ }
+
+ /// Whether to display this audio message in a compact format.
+ pub fn compact(&self) -> bool {
+ self.imp().compact.get()
+ }
+
+ /// Set the compact format of this audio message.
+ fn set_compact(&self, compact: bool) {
+ self.imp().compact.set(compact);
+
+ if compact {
+ self.remove_css_class("osd");
+ self.remove_css_class("toolbar");
+ } else {
+ self.add_css_class("osd");
+ self.add_css_class("toolbar");
+ }
+
+ self.notify("compact");
+ }
+
+ /// The state of the audio file.
+ pub fn state(&self) -> MediaState {
+ self.imp().state.get()
+ }
+
+ /// Set the state of the audio file.
+ fn set_state(&self, state: MediaState) {
+ let priv_ = self.imp();
+
+ if self.state() == state {
+ return;
+ }
+
+ match state {
+ MediaState::Loading | MediaState::Initial => {
+ priv_.state_spinner.set_visible(true);
+ priv_.state_error.set_visible(false);
+ }
+ MediaState::Ready => {
+ priv_.state_spinner.set_visible(false);
+ priv_.state_error.set_visible(false);
+ }
+ MediaState::Error => {
+ priv_.state_spinner.set_visible(false);
+ priv_.state_error.set_visible(true);
+ }
+ }
+
+ priv_.state.set(state);
+ self.notify("state");
+ }
+
+ /// Convenience method to set the state to `Error` with the given error
+ /// message.
+ fn set_error(&self, error: String) {
+ self.set_state(MediaState::Error);
+ self.imp().state_error.set_tooltip_text(Some(&error));
+ }
+
+ /// Display the given `audio` message.
+ pub fn audio(&self, audio: AudioMessageEventContent, session: &Session, compact: bool) {
+ self.set_body(Some(audio.body.clone()));
+
+ self.set_compact(compact);
+ if compact {
+ self.set_state(MediaState::Ready);
+ return;
+ }
+
+ self.set_state(MediaState::Loading);
+
+ let mut path = glib::tmp_dir();
+ path.push(media_type_uid(audio.file()));
+ let file = gio::File::for_path(path);
+
+ if file.query_exists(gio::Cancellable::NONE) {
+ self.display_file(file);
+ return;
+ }
+
+ let client = session.client();
+ let handle = spawn_tokio!(async move { client.get_file(audio, true).await });
+
+ spawn!(
+ glib::PRIORITY_LOW,
+ clone!(@weak self as obj => async move {
+ match handle.await.unwrap() {
+ Ok(Some(data)) => {
+ // The GStreamer backend doesn't work with input streams so
+ // we need to store the file.
+ // See: https://gitlab.gnome.org/GNOME/gtk/-/issues/4062
+ file.replace_contents(
+ &data,
+ None,
+ false,
+ gio::FileCreateFlags::REPLACE_DESTINATION,
+ gio::Cancellable::NONE,
+ )
+ .unwrap();
+ obj.display_file(file);
+ }
+ Ok(None) => {
+ warn!("Could not retrieve invalid audio file");
+ obj.set_error(gettext("Could not retrieve audio file"));
+ }
+ Err(error) => {
+ warn!("Could not retrieve audio file: {}", error);
+ obj.set_error(gettext("Could not retrieve audio file"));
+ }
+ }
+ })
+ );
+ }
+
+ fn display_file(&self, file: gio::File) {
+ let media_file = gtk::MediaFile::for_file(&file);
+
+ media_file.connect_error_notify(clone!(@weak self as obj => move |media_file| {
+ if let Some(error) = media_file.error() {
+ warn!("Error reading audio file: {}", error);
+ obj.set_error(gettext("Error reading audio file"));
+ }
+ }));
+
+ self.imp().player.set_media_file(Some(media_file));
+ self.set_state(MediaState::Ready);
+ }
+}
+
+impl Default for MessageAudio {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/src/session/content/room_history/message_row/mod.rs
b/src/session/content/room_history/message_row/mod.rs
index ca3848e75..b08d0c112 100644
--- a/src/session/content/room_history/message_row/mod.rs
+++ b/src/session/content/room_history/message_row/mod.rs
@@ -1,3 +1,4 @@
+mod audio;
mod file;
mod media;
mod reaction;
@@ -20,8 +21,8 @@ use matrix_sdk::ruma::events::{
};
use self::{
- file::MessageFile, media::MessageMedia, reaction_list::MessageReactionList,
- reply::MessageReply, text::MessageText,
+ audio::MessageAudio, file::MessageFile, media::MessageMedia,
+ reaction_list::MessageReactionList, reply::MessageReply, text::MessageText,
};
use crate::{
components::Avatar, prelude::*, session::room::Event, spawn, utils::filename_for_mime,
@@ -245,7 +246,18 @@ fn build_content(parent: &adw::Bin, event: &Event, compact: bool) {
message.msgtype
};
match msgtype {
- MessageType::Audio(_message) => {}
+ MessageType::Audio(message) => {
+ let child = if let Some(Ok(child)) =
+ parent.child().map(|w| w.downcast::<MessageAudio>())
+ {
+ child
+ } else {
+ let child = MessageAudio::new();
+ parent.set_child(Some(&child));
+ child
+ };
+ child.audio(message, &event.room().session(), compact);
+ }
MessageType::Emote(message) => {
let child = if let Some(Ok(child)) =
parent.child().map(|w| w.downcast::<MessageText>())
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]