room-history: Show audio messages in timeline
This commit is contained in:
parent
687fcb51d0
commit
3079b7faca
8 changed files with 459 additions and 3 deletions
|
@ -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>
|
||||
|
|
10
data/resources/ui/components-audio-player.ui
Normal file
10
data/resources/ui/components-audio-player.ui
Normal 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>
|
45
data/resources/ui/content-message-audio.ui
Normal file
45
data/resources/ui/content-message-audio.ui
Normal 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>
|
|
@ -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
|
||||
|
|
109
src/components/audio_player.rs
Normal file
109
src/components/audio_player.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
275
src/session/content/room_history/message_row/audio.rs
Normal file
275
src/session/content/room_history/message_row/audio.rs
Normal 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()
|
||||
}
|
||||
}
|
|
@ -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>())
|
||||
|
|
Loading…
Reference in a new issue