room-history: Show audio messages in timeline

This commit is contained in:
Kévin Commaille 2022-02-03 11:36:50 +01:00
parent 687fcb51d0
commit 3079b7faca
No known key found for this signature in database
GPG key ID: DD507DAE96E8245C
8 changed files with 459 additions and 3 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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()
}
}

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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>())