[fractal/fractal-next] content: Implement room history
- From: Julian Sparber <jsparber src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [fractal/fractal-next] content: Implement room history
- Date: Fri, 30 Apr 2021 18:58:09 +0000 (UTC)
commit 16bba4dc445f5ef62298fc2b647a5835cc666cc5
Author: Julian Sparber <julian sparber net>
Date: Tue Apr 27 13:03:58 2021 +0200
content: Implement room history
data/resources/resources.gresource.xml | 6 +
data/resources/style.css | 9 +-
data/resources/ui/content-divider-row.ui | 35 +++
data/resources/ui/content-item-row-menu.ui | 102 +++++++
data/resources/ui/content-item.ui | 18 ++
data/resources/ui/content-message-row.ui | 50 ++++
data/resources/ui/content-state-row.ui | 21 ++
data/resources/ui/content.ui | 32 ++-
data/resources/ui/context-menu-bin.ui | 18 ++
src/components/context_menu_bin.rs | 188 +++++++++++++
src/components/mod.rs | 3 +
src/main.rs | 1 +
src/meson.build | 7 +-
src/session/{ => content}/content.rs | 75 ++++--
src/session/content/divider_row.rs | 92 +++++++
src/session/content/item_row.rs | 180 +++++++++++++
src/session/content/message_row.rs | 416 +++++++++++++++++++++++++++++
src/session/content/mod.rs | 11 +
src/session/content/state_row.rs | 80 ++++++
src/session/mod.rs | 8 +-
20 files changed, 1314 insertions(+), 38 deletions(-)
---
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index aae78ef5..499c665b 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -3,14 +3,20 @@
<gresource prefix="/org/gnome/FractalNext/">
<file compressed="true" preprocess="xml-stripblanks" alias="shortcuts.ui">ui/shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="content.ui">ui/content.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks" alias="content-item.ui">ui/content-item.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-message-row.ui">ui/content-message-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-divider-row.ui">ui/content-divider-row.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="content-state-row.ui">ui/content-state-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="login.ui">ui/login.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="session.ui">ui/session.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar.ui">ui/sidebar.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="sidebar-item.ui">ui/sidebar-item.ui</file>
<file compressed="true" preprocess="xml-stripblanks"
alias="sidebar-room-row.ui">ui/sidebar-room-row.ui</file>
<file compressed="true" preprocess="xml-stripblanks" alias="window.ui">ui/window.ui</file>
+ <file compressed="true" preprocess="xml-stripblanks"
alias="context-menu-bin.ui">ui/context-menu-bin.ui</file>
<file compressed="true">style.css</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/send-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/status/welcome.svg</file>
</gresource>
</gresources>
+
diff --git a/data/resources/style.css b/data/resources/style.css
index 0360bab4..650883e3 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -51,4 +51,11 @@
background-color: @theme_selected_bg_color;
}
-
+/* Content */
+.codeview {
+ border-radius: 5px;
+ padding: 6px;
+ font-family: monospace;
+ background-color: @text_view_bg;
+ color: @theme_text_color;
+}
diff --git a/data/resources/ui/content-divider-row.ui b/data/resources/ui/content-divider-row.ui
new file mode 100644
index 00000000..540b7e3c
--- /dev/null
+++ b/data/resources/ui/content-divider-row.ui
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentDividerRow" parent="AdwBin">
+ <property name="can-focus">False</property>
+ <style>
+ <class name="divider-row"/>
+ </style>
+ <property name="child">
+ <object class="GtkBox">
+ <property name="spacing">12</property>
+ <property name="margin-start">24</property>
+ <property name="margin-end">24</property>
+ <child>
+ <object class="GtkSeparator">
+ <property name="valign">center</property>
+ <property name="hexpand">true</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="label">
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="valign">center</property>
+ <property name="hexpand">true</property>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-item-row-menu.ui b/data/resources/ui/content-item-row-menu.ui
new file mode 100644
index 00000000..5e4fe940
--- /dev/null
+++ b/data/resources/ui/content-item-row-menu.ui
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <object class="GtkPopoverMenu" id="message_menu_popover">
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">6</property>
+ <property name="margin_end">6</property>
+ <property name="margin_top">6</property>
+ <property name="margin_bottom">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkModelButton" id="reply_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="action_name">message.reply</property>
+ <property name="text" translatable="yes">Reply</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="open_with_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="text" translatable="yes">Open With…</property>
+ <property name="action_name">message.open_with</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="save_image_as_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="text" translatable="yes">Save Image As…</property>
+ <property name="action_name">message.save_as</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="save_video_as_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="text" translatable="yes">Save Video As…</property>
+ <property name="action_name">message.save_as</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="copy_image_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="text" translatable="yes">Copy Image</property>
+ <property name="action_name">message.copy_image</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="copy_selected_text_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="text" translatable="yes">Copy Selection</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="copy_text_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="text" translatable="yes">Copy Text</property>
+ <property name="action_name">message.copy_text</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="view_source_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="action_name">message.show_source</property>
+ <property name="text" translatable="yes">View Source</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator" id="message_menu_separator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkModelButton" id="delete_message_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="action_name">message.delete</property>
+ <property name="text" translatable="yes">Delete Message</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="submenu">main</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+</interface>
diff --git a/data/resources/ui/content-item.ui b/data/resources/ui/content-item.ui
new file mode 100644
index 00000000..3dbf6b97
--- /dev/null
+++ b/data/resources/ui/content-item.ui
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="GtkListItem">
+ <property name="activatable">False</property>
+ <binding name="selectable">
+ <lookup type="RoomItem" name="selectable">
+ <lookup name="item">GtkListItem</lookup>
+ </lookup>
+ </binding>
+ <property name="child">
+ <object class="ContentItemRow">
+ <binding name="item">
+ <lookup name="item">GtkListItem</lookup>
+ </binding>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-message-row.ui b/data/resources/ui/content-message-row.ui
new file mode 100644
index 00000000..37c74018
--- /dev/null
+++ b/data/resources/ui/content-message-row.ui
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentMessageRow" parent="AdwBin">
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <child>
+ <object class="AdwAvatar" id="avatar">
+ <property name="show-initials">True</property>
+ <property name="size">24</property>
+ <property name="text" bind-source="display_name" bind-property="label" bind-flags="sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="header">
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="display_name">
+ <property name="ellipsize">end</property>
+ <property name="selectable">True</property>
+ <style>
+ <class name="displayname"/>
+ </style>
+ </object>
+ </child>
+ <child type="end">
+ <object class="GtkLabel" id="timestamp">
+ <style>
+ <class name="timestamp"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwBin" id="content">
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/resources/ui/content-state-row.ui b/data/resources/ui/content-state-row.ui
new file mode 100644
index 00000000..8fbf5983
--- /dev/null
+++ b/data/resources/ui/content-state-row.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContentStateRow" parent="AdwBin">
+ <property name="child">
+ <object class="GtkBox">
+ <property name="spacing">6</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="AdwBin" id="content" />
+ </child>
+ <child type="end">
+ <object class="GtkLabel" id="timestamp">
+ <style>
+ <class name="timestamp"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </property>
+ </template>
+</interface>
diff --git a/data/resources/ui/content.ui b/data/resources/ui/content.ui
index d59ed88b..298c1fd1 100644
--- a/data/resources/ui/content.ui
+++ b/data/resources/ui/content.ui
@@ -23,7 +23,7 @@
</child>
<child>
<object class="GtkSearchBar" id="room_search">
- <property name="search-mode-enabled" bind-source="search_content_button" bind-property="active"
/>
+ <property name="search-mode-enabled" bind-source="search_content_button" bind-property="active"/>
<property name="child">
<object class="AdwClamp">
<property name="hexpand">True</property>
@@ -35,20 +35,37 @@
</object>
</child>
<child>
- <object class="AdwClamp">
+ <object class="GtkScrolledWindow" id="scrolled_window">
<property name="vexpand">True</property>
- <property name="hexpand">True</property>
+ <property name="hscrollbar-policy">never</property>
<style>
<class name="content"/>
</style>
- <child>
- <object class="GtkListView" id="room_history">
+ <property name="child">
+ <object class="AdwClampScrollable">
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="child">
+ <object class="GtkListView" id="listview">
+ <style>
+ <class name="navigation-sidebar"/>
+ </style>
+ <property name="factory">
+ <object class="GtkBuilderListItemFactory">
+ <property name="resource">/org/gnome/FractalNext/content-item.ui</property>
+ </object>
+ </property>
+ <accessibility>
+ <property name="label" translatable="yes">Room History</property>
+ </accessibility>
+ </object>
+ </property>
</object>
- </child>
+ </property>
</object>
</child>
<child>
- <object class="GtkSeparator" />
+ <object class="GtkSeparator"/>
</child>
<child>
<object class="AdwClamp">
@@ -86,3 +103,4 @@
</child>
</template>
</interface>
+
diff --git a/data/resources/ui/context-menu-bin.ui b/data/resources/ui/context-menu-bin.ui
new file mode 100644
index 00000000..51b296c5
--- /dev/null
+++ b/data/resources/ui/context-menu-bin.ui
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="ContextMenuBin" parent="AdwBin">
+ <property name="focusable">True</property>
+ <child>
+ <object class="GtkGestureClick" id="click_gesture">
+ <property name="button">3</property>
+ <property name="exclusive">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGestureLongPress" id="long_press_gesture">
+ <property name="touch_only">True</property>
+ <property name="exclusive">True</property>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/components/context_menu_bin.rs b/src/components/context_menu_bin.rs
new file mode 100644
index 00000000..420e8d08
--- /dev/null
+++ b/src/components/context_menu_bin.rs
@@ -0,0 +1,188 @@
+use adw::subclass::prelude::*;
+use gtk::prelude::*;
+use gtk::subclass::prelude::*;
+use gtk::{gdk, gio, glib, glib::clone, CompositeTemplate};
+use log::debug;
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+
+ #[derive(Debug, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/context-menu-bin.ui")]
+ pub struct ContextMenuBin {
+ #[template_child]
+ pub click_gesture: TemplateChild<gtk::GestureClick>,
+ #[template_child]
+ pub long_press_gesture: TemplateChild<gtk::GestureLongPress>,
+ pub popover: gtk::PopoverMenu,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ContextMenuBin {
+ const NAME: &'static str = "ContextMenuBin";
+ type Type = super::ContextMenuBin;
+ type ParentType = adw::Bin;
+
+ fn new() -> Self {
+ Self {
+ click_gesture: TemplateChild::default(),
+ long_press_gesture: TemplateChild::default(),
+ // WORKAROUND: there is some issue with creating the popover from the template
+ popover: gtk::PopoverMenuBuilder::new()
+ .position(gtk::PositionType::Bottom)
+ .has_arrow(false)
+ .halign(gtk::Align::Start)
+ .build(),
+ }
+ }
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+
+ klass.install_action("context-menu.activate", None, move |widget, _, _| {
+ widget.open_menu_at(0, 0)
+ });
+ klass.add_binding_action(
+ gdk::keys::constants::F10,
+ gdk::ModifierType::SHIFT_MASK,
+ "context-menu.activate",
+ None,
+ );
+ klass.add_binding_action(
+ gdk::keys::constants::Menu,
+ gdk::ModifierType::empty(),
+ "context-menu.activate",
+ None,
+ );
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ impl ObjectImpl for ContextMenuBin {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_object(
+ "context-menu",
+ "Context Menu",
+ "The context menu",
+ gio::MenuModel::static_type(),
+ glib::ParamFlags::READWRITE,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "context-menu" => {
+ let context_menu = value
+ .get::<Option<gio::MenuModel>>()
+ .expect("type conformity checked by `Object::set_property`");
+ obj.set_context_menu(context_menu);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "context-menu" => obj.context_menu().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.popover.set_parent(obj);
+ self.long_press_gesture
+ .connect_pressed(clone!(@weak obj => move |gesture, x, y| {
+ gesture.set_state(gtk::EventSequenceState::Claimed);
+ gesture.reset();
+ obj.open_menu_at(x as i32, y as i32);
+ }));
+
+ self.click_gesture.connect_released(
+ clone!(@weak obj => move |gesture, n_press, x, y| {
+ if n_press > 1 {
+ return;
+ }
+
+ gesture.set_state(gtk::EventSequenceState::Claimed);
+ obj.open_menu_at(x as i32, y as i32);
+ }),
+ );
+ self.parent_constructed(obj);
+ }
+
+ fn dispose(&self, _obj: &Self::Type) {
+ self.popover.unparent();
+ }
+ }
+
+ impl WidgetImpl for ContextMenuBin {}
+
+ impl BinImpl for ContextMenuBin {}
+}
+
+glib::wrapper! {
+ pub struct ContextMenuBin(ObjectSubclass<imp::ContextMenuBin>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+/// A Bin widget that adds a conext menu
+impl ContextMenuBin {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create ContextMenuBin")
+ }
+
+ pub fn set_context_menu(&self, menu: Option<gio::MenuModel>) {
+ let priv_ = imp::ContextMenuBin::from_instance(self);
+ priv_.popover.set_menu_model(menu.as_ref());
+ }
+
+ pub fn context_menu(&self) -> Option<gio::MenuModel> {
+ let priv_ = imp::ContextMenuBin::from_instance(self);
+ priv_.popover.menu_model()
+ }
+
+ fn open_menu_at(&self, x: i32, y: i32) {
+ let priv_ = imp::ContextMenuBin::from_instance(self);
+ let popover = &priv_.popover;
+
+ debug!("Context menu was activated");
+
+ if popover.menu_model().is_none() {
+ return;
+ }
+
+ popover.set_pointing_to(&gdk::Rectangle {
+ x,
+ y,
+ width: 0,
+ height: 0,
+ });
+ popover.popup();
+ }
+}
+
+unsafe impl<T: ContextMenuBinImpl> IsSubclassable<T> for ContextMenuBin {
+ fn class_init(class: &mut glib::Class<Self>) {
+ <glib::Object as IsSubclassable<T>>::class_init(class);
+ }
+ fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
+ <glib::Object as IsSubclassable<T>>::instance_init(instance);
+ }
+}
+
+pub trait ContextMenuBinImpl: BinImpl {}
diff --git a/src/components/mod.rs b/src/components/mod.rs
new file mode 100644
index 00000000..8ea2852e
--- /dev/null
+++ b/src/components/mod.rs
@@ -0,0 +1,3 @@
+mod context_menu_bin;
+
+pub use self::context_menu_bin::{ContextMenuBin, ContextMenuBinImpl};
diff --git a/src/main.rs b/src/main.rs
index bba4ce57..ea741574 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -6,6 +6,7 @@ mod application;
#[rustfmt::skip]
mod config;
+mod components;
mod login;
mod secret;
mod session;
diff --git a/src/meson.build b/src/meson.build
index 1e192571..692a9999 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -32,7 +32,12 @@ sources = files(
'session/categories/category.rs',
'session/categories/category_type.rs',
'session/categories/mod.rs',
- 'session/content.rs',
+ 'session/content/content.rs',
+ 'session/content/divider_row.rs',
+ 'session/content/item_row.rs',
+ 'session/content/message_row.rs',
+ 'session/content/mod.rs',
+ 'session/content/state_row.rs',
'session/room/event.rs',
'session/room/highlight_flags.rs',
'session/room/item.rs',
diff --git a/src/session/content.rs b/src/session/content/content.rs
similarity index 53%
rename from src/session/content.rs
rename to src/session/content/content.rs
index b3a8c61b..66ad426f 100644
--- a/src/session/content.rs
+++ b/src/session/content/content.rs
@@ -1,23 +1,26 @@
-use adw;
-use adw::subclass::prelude::BinImpl;
-use gtk::subclass::prelude::*;
-use gtk::{self, prelude::*};
-use gtk::{glib, glib::SyncSender, CompositeTemplate};
-use matrix_sdk::identifiers::RoomId;
+use adw::subclass::prelude::*;
+use gtk::{glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+use crate::session::{
+ content::ItemRow,
+ room::{Room, Timeline},
+};
mod imp {
use super::*;
use glib::subclass::InitializingObject;
use std::cell::Cell;
- #[derive(Debug, CompositeTemplate)]
+ #[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/org/gnome/FractalNext/content.ui")]
pub struct Content {
pub compact: Cell<bool>,
#[template_child]
pub headerbar: TemplateChild<adw::HeaderBar>,
#[template_child]
- pub room_history: TemplateChild<gtk::ListView>,
+ pub listview: TemplateChild<gtk::ListView>,
+ #[template_child]
+ pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
}
#[glib::object_subclass]
@@ -26,16 +29,10 @@ mod imp {
type Type = super::Content;
type ParentType = adw::Bin;
- fn new() -> Self {
- Self {
- compact: Cell::new(false),
- headerbar: TemplateChild::default(),
- room_history: TemplateChild::default(),
- }
- }
-
fn class_init(klass: &mut Self::Class) {
+ ItemRow::static_type();
Self::bind_template(klass);
+ klass.set_accessible_role(gtk::AccessibleRole::Group);
}
fn instance_init(obj: &InitializingObject<Self>) {
@@ -68,9 +65,7 @@ mod imp {
) {
match pspec.name() {
"compact" => {
- let compact = value
- .get()
- .expect("type conformity checked by `Object::set_property`");
+ let compact = value.get().unwrap();
self.compact.set(compact);
}
_ => unimplemented!(),
@@ -83,6 +78,23 @@ mod imp {
_ => unimplemented!(),
}
}
+
+ fn constructed(&self, obj: &Self::Type) {
+ let adj = self.scrolled_window.vadjustment().unwrap();
+ // TODO: make sure that we have enough messages to fill at least to scroll pages, if the room
history is long enough
+
+ adj.connect_value_changed(clone!(@weak obj => move |adj| {
+ // Load more message when the user gets close to the end of the known room history
+ // Use the page size twice to detect if the user gets close the end
+ if adj.value() < adj.page_size() * 2.0 {
+ if let Some(room) = obj.room() {
+ room.load_previous_events();
+ }
+ }
+ }));
+
+ self.parent_constructed(obj);
+ }
}
impl WidgetImpl for Content {}
@@ -99,13 +111,22 @@ impl Content {
glib::Object::new(&[]).expect("Failed to create Content")
}
- /// Sets up the required channel to recive async updates from the `Client`
- pub fn setup_channel(&self) -> SyncSender<RoomId> {
- let (sender, receiver) = glib::MainContext::sync_channel::<RoomId>(Default::default(), 100);
- receiver.attach(None, move |_room_id| {
- //TODO: actually do something: update the message GListModel
- glib::Continue(true)
- });
- sender
+ pub fn set_room(&self, room: &Room) {
+ let priv_ = imp::Content::from_instance(self);
+ // TODO: use gtk::MultiSelection to allow selection
+ priv_
+ .listview
+ .set_model(Some(>k::NoSelection::new(Some(room.timeline()))));
+ }
+
+ fn room(&self) -> Option<Room> {
+ let priv_ = imp::Content::from_instance(self);
+ priv_
+ .listview
+ .model()
+ .and_then(|model| model.downcast::<gtk::NoSelection>().ok())
+ .and_then(|model| model.model())
+ .and_then(|model| model.downcast::<Timeline>().ok())
+ .map(|timeline| timeline.room().to_owned())
}
}
diff --git a/src/session/content/divider_row.rs b/src/session/content/divider_row.rs
new file mode 100644
index 00000000..a27ba5fb
--- /dev/null
+++ b/src/session/content/divider_row.rs
@@ -0,0 +1,92 @@
+use adw::subclass::prelude::*;
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-divider-row.ui")]
+ pub struct DividerRow {
+ #[template_child]
+ pub label: TemplateChild<gtk::Label>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for DividerRow {
+ const NAME: &'static str = "ContentDividerRow";
+ type Type = super::DividerRow;
+ 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 DividerRow {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_string(
+ "label",
+ "Label",
+ "The label for this divider",
+ None,
+ glib::ParamFlags::READWRITE,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "label" => {
+ let label = value.get().unwrap();
+ obj.set_label(label);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "label" => obj.label().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+ impl WidgetImpl for DividerRow {}
+ impl BinImpl for DividerRow {}
+}
+
+glib::wrapper! {
+ pub struct DividerRow(ObjectSubclass<imp::DividerRow>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+impl DividerRow {
+ pub fn new(label: String) -> Self {
+ glib::Object::new(&[("label", &label)]).expect("Failed to create DividerRow")
+ }
+
+ pub fn set_label(&self, label: &str) {
+ let priv_ = imp::DividerRow::from_instance(self);
+ priv_.label.set_text(label);
+ }
+
+ pub fn label(&self) -> String {
+ let priv_ = imp::DividerRow::from_instance(self);
+ priv_.label.text().as_str().to_owned()
+ }
+}
diff --git a/src/session/content/item_row.rs b/src/session/content/item_row.rs
new file mode 100644
index 00000000..cfb86606
--- /dev/null
+++ b/src/session/content/item_row.rs
@@ -0,0 +1,180 @@
+use adw::{prelude::*, subclass::prelude::*};
+use chrono::{offset::Local, Datelike};
+use gettextrs::gettext;
+use gtk::{glib, prelude::*, subclass::prelude::*};
+
+use crate::components::{ContextMenuBin, ContextMenuBinImpl};
+use crate::session::content::{DividerRow, MessageRow, StateRow};
+use crate::session::room::{Item, ItemType};
+use matrix_sdk::events::AnyRoomEvent;
+
+mod imp {
+ use super::*;
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default)]
+ pub struct ItemRow {
+ pub item: RefCell<Option<Item>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for ItemRow {
+ const NAME: &'static str = "ContentItemRow";
+ type Type = super::ItemRow;
+ type ParentType = ContextMenuBin;
+ }
+
+ impl ObjectImpl for ItemRow {
+ fn properties() -> &'static [glib::ParamSpec] {
+ use once_cell::sync::Lazy;
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![glib::ParamSpec::new_object(
+ "item",
+ "item",
+ "The item represented by this row",
+ Item::static_type(),
+ glib::ParamFlags::READWRITE,
+ )]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "item" => {
+ let item = value.get::<Option<Item>>().unwrap();
+ obj.set_item(item);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, _obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "item" => self.item.borrow().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+
+ fn constructed(&self, obj: &Self::Type) {
+ self.parent_constructed(obj);
+ }
+ }
+
+ impl WidgetImpl for ItemRow {}
+ impl BinImpl for ItemRow {}
+ impl ContextMenuBinImpl for ItemRow {}
+}
+
+glib::wrapper! {
+ pub struct ItemRow(ObjectSubclass<imp::ItemRow>)
+ @extends gtk::Widget, ContextMenuBin, adw::Bin, @implements gtk::Accessible;
+}
+
+// TODO:
+// - [ ] Add context menu for operations
+// - [ ] Don't show rows for items that don't have a visible UI
+impl ItemRow {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create ItemRow")
+ }
+
+ /// This method sets this row to a new `Item`.
+ ///
+ /// It tries to reuse the widget and only update the content whenever possible, but it will
+ /// create a new widget and drop the old one if it has to.
+ fn set_item(&self, item: Option<Item>) {
+ let priv_ = imp::ItemRow::from_instance(&self);
+
+ if let Some(ref item) = item {
+ match item.type_() {
+ ItemType::Event(event) => match event.matrix_event() {
+ AnyRoomEvent::Message(_message) => {
+ let child = if let Some(Ok(child)) =
+ self.child().map(|w| w.downcast::<MessageRow>())
+ {
+ child
+ } else {
+ let child = MessageRow::new();
+ self.set_child(Some(&child));
+ child
+ };
+ child.set_event(event.clone());
+ }
+ AnyRoomEvent::State(state) => {
+ let child = if let Some(Ok(child)) =
+ self.child().map(|w| w.downcast::<StateRow>())
+ {
+ child
+ } else {
+ let child = StateRow::new();
+ self.set_child(Some(&child));
+ child
+ };
+
+ child.update(&state);
+ }
+ AnyRoomEvent::RedactedMessage(_) => {
+ let child = if let Some(Ok(child)) =
+ self.child().map(|w| w.downcast::<MessageRow>())
+ {
+ child
+ } else {
+ let child = MessageRow::new();
+ self.set_child(Some(&child));
+ child
+ };
+ child.set_event(event.clone());
+ }
+ AnyRoomEvent::RedactedState(_) => {
+ let child = if let Some(Ok(child)) =
+ self.child().map(|w| w.downcast::<MessageRow>())
+ {
+ child
+ } else {
+ let child = MessageRow::new();
+ self.set_child(Some(&child));
+ child
+ };
+ child.set_event(event.clone());
+ }
+ },
+ ItemType::DayDivider(date) => {
+ let fmt = if date.year() == Local::today().year() {
+ // Translators: This is a date format in the day divider without the year
+ gettext("%A, %B %e")
+ } else {
+ // Translators: This is a date format in the day divider with the year
+ gettext("%A, %B %e, %Y")
+ };
+ let date = date.format(&fmt).to_string();
+
+ if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
+ child.set_label(&date);
+ } else {
+ let child = DividerRow::new(date);
+ self.set_child(Some(&child));
+ };
+ }
+ ItemType::NewMessageDivider => {
+ let label = gettext("New Messages");
+
+ if let Some(Ok(child)) = self.child().map(|w| w.downcast::<DividerRow>()) {
+ child.set_label(&label);
+ } else {
+ let child = DividerRow::new(label);
+ self.set_child(Some(&child));
+ };
+ }
+ }
+ }
+ priv_.item.replace(item);
+ }
+}
diff --git a/src/session/content/message_row.rs b/src/session/content/message_row.rs
new file mode 100644
index 00000000..9247f526
--- /dev/null
+++ b/src/session/content/message_row.rs
@@ -0,0 +1,416 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{
+ glib, glib::clone, glib::signal::SignalHandlerId, prelude::*, subclass::prelude::*,
+ CompositeTemplate,
+};
+use html2pango::{
+ block::{markup_html, HtmlBlock},
+ html_escape, markup_links,
+};
+use log::warn;
+use matrix_sdk::events::{
+ room::message::MessageFormat,
+ room::message::{FormattedBody, MessageType},
+ room::redaction::RedactionEventContent,
+ AnyMessageEvent, AnyMessageEventContent, AnyRoomEvent,
+};
+
+use crate::session::room::Event;
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+ use once_cell::sync::Lazy;
+ use std::cell::RefCell;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-message-row.ui")]
+ pub struct MessageRow {
+ #[template_child]
+ pub avatar: TemplateChild<adw::Avatar>,
+ #[template_child]
+ pub header: TemplateChild<gtk::Box>,
+ #[template_child]
+ pub display_name: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub timestamp: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub content: TemplateChild<adw::Bin>,
+ pub relates_to_changed_handler: RefCell<Option<SignalHandlerId>>,
+ pub bindings: RefCell<Vec<glib::Binding>>,
+ pub event: RefCell<Option<Event>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for MessageRow {
+ const NAME: &'static str = "ContentMessageRow";
+ type Type = super::MessageRow;
+ 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 MessageRow {
+ fn properties() -> &'static [glib::ParamSpec] {
+ static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
+ vec![
+ glib::ParamSpec::new_boolean(
+ "show-header",
+ "Show Header",
+ "Whether this item should show a header or not. This does do nothing if this event
doesn't have a header. ",
+ false,
+ glib::ParamFlags::READWRITE,
+ ),
+ ]
+ });
+
+ PROPERTIES.as_ref()
+ }
+
+ fn set_property(
+ &self,
+ obj: &Self::Type,
+ _id: usize,
+ value: &glib::Value,
+ pspec: &glib::ParamSpec,
+ ) {
+ match pspec.name() {
+ "show-header" => {
+ let show_header = value.get().unwrap();
+ let _ = obj.set_show_header(show_header);
+ }
+ _ => unimplemented!(),
+ }
+ }
+
+ fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
+ match pspec.name() {
+ "show-header" => obj.show_header().to_value(),
+ _ => unimplemented!(),
+ }
+ }
+ }
+ impl WidgetImpl for MessageRow {}
+ impl BinImpl for MessageRow {}
+}
+
+glib::wrapper! {
+ pub struct MessageRow(ObjectSubclass<imp::MessageRow>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+//TODO
+// - [] Implement widgets to show message events
+impl MessageRow {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create MessageRow")
+ }
+
+ pub fn show_header(&self) -> bool {
+ let priv_ = imp::MessageRow::from_instance(self);
+ priv_.avatar.is_visible() && priv_.header.is_visible()
+ }
+
+ pub fn set_show_header(&self, visible: bool) {
+ let priv_ = imp::MessageRow::from_instance(self);
+ priv_.avatar.set_visible(visible);
+ priv_.header.set_visible(visible);
+ self.notify("show-header");
+ }
+
+ pub fn set_event(&self, event: Event) {
+ let priv_ = imp::MessageRow::from_instance(self);
+ // Remove signals and bindings from the previous event
+ if let Some(event) = priv_.event.take() {
+ if let Some(relates_to_changed_handler) = priv_.relates_to_changed_handler.take() {
+ event.disconnect(relates_to_changed_handler);
+ }
+
+ while let Some(binding) = priv_.bindings.borrow_mut().pop() {
+ binding.unbind();
+ }
+ }
+
+ //TODO: bind the user's avatar to the message row
+ let display_name_binding = event
+ .sender()
+ .bind_property("display-name", &priv_.display_name.get(), "label")
+ .flags(glib::BindingFlags::SYNC_CREATE)
+ .build()
+ .unwrap();
+
+ let show_header_binding = event
+ .bind_property("show-header", self, "show-header")
+ .flags(glib::BindingFlags::SYNC_CREATE)
+ .build()
+ .unwrap();
+
+ priv_
+ .bindings
+ .borrow_mut()
+ .append(&mut vec![display_name_binding, show_header_binding]);
+
+ priv_
+ .relates_to_changed_handler
+ .replace(Some(event.connect_relates_to_changed(
+ clone!(@weak self as obj => move |event| {
+ obj.update_content(&event);
+ }),
+ )));
+ self.update_content(&event);
+ priv_.event.replace(Some(event));
+ }
+
+ fn find_last_event(&self, event: &Event) -> Event {
+ if let Some(replacement_event) = event.relates_to().iter().rev().find(|event| {
+ let matrix_event = event.matrix_event();
+ match matrix_event {
+ AnyRoomEvent::Message(AnyMessageEvent::RoomMessage(message)) => {
+ message.content.new_content.is_some()
+ }
+ AnyRoomEvent::Message(AnyMessageEvent::RoomRedaction(_)) => true,
+ _ => false,
+ }
+ }) {
+ if !replacement_event.relates_to().is_empty() {
+ self.find_last_event(replacement_event)
+ } else {
+ replacement_event.clone()
+ }
+ } else {
+ event.clone()
+ }
+ }
+ /// Find the content we need to display
+ fn find_content(&self, event: &Event) -> AnyMessageEventContent {
+ match self.find_last_event(event).matrix_event() {
+ AnyRoomEvent::Message(message) => message.content(),
+ AnyRoomEvent::RedactedMessage(message) => {
+ if let Some(ref redaction_event) = message.unsigned().redacted_because {
+ AnyMessageEvent::RoomRedaction(*redaction_event.clone()).content()
+ } else {
+ AnyMessageEventContent::RoomRedaction(RedactionEventContent { reason: None })
+ }
+ }
+ AnyRoomEvent::RedactedState(state) => {
+ if let Some(ref redaction_event) = state.unsigned().redacted_because {
+ AnyMessageEvent::RoomRedaction(*redaction_event.clone()).content()
+ } else {
+ AnyMessageEventContent::RoomRedaction(RedactionEventContent { reason: None })
+ }
+ }
+ _ => panic!("This event isn't a room message event or redacted event"),
+ }
+ }
+
+ fn update_content(&self, event: &Event) {
+ let priv_ = imp::MessageRow::from_instance(self);
+ let content = self.find_content(event);
+
+ // TODO: create widgets for all event types
+ // TODO: display reaction events from event.relates_to()
+ match content {
+ AnyMessageEventContent::RoomMessage(message) => {
+ let msgtype = if let Some(new_message) = message.new_content {
+ new_message.msgtype
+ } else {
+ message.msgtype
+ };
+ match msgtype {
+ MessageType::Audio(_message) => {}
+ MessageType::Emote(message) => {
+ let text = if let Some(formatted) = message
+ .formatted
+ .filter(|m| m.format == MessageFormat::Html)
+ {
+ markup_links(&html_escape(&formatted.body))
+ } else {
+ message.body
+ };
+ // TODO we need to bind the display name to the sender
+ self.show_label_with_markup(&format!(
+ "<b>{}</b> {}",
+ event.sender().display_name(),
+ text
+ ));
+ }
+ MessageType::File(_message) => {}
+ MessageType::Image(_message) => {}
+ MessageType::Location(_message) => {}
+ MessageType::Notice(message) => {
+ // TODO: we should reuse the already present child widgets when possible
+ let child = if let Some(html_blocks) =
+ parse_formatted_body(message.formatted.as_ref())
+ {
+ create_widget_for_html_message(html_blocks)
+ } else {
+ let child = gtk::Label::new(Some(&message.body));
+ set_label_styles(&child);
+ child.upcast::<gtk::Widget>()
+ };
+
+ priv_.content.set_child(Some(&child));
+ }
+ MessageType::ServerNotice(message) => {
+ self.show_label_with_text(&message.body);
+ }
+ MessageType::Text(message) => {
+ // TODO: we should reuse the already present child widgets when possible
+ let child = if let Some(html_blocks) =
+ parse_formatted_body(message.formatted.as_ref())
+ {
+ create_widget_for_html_message(html_blocks)
+ } else {
+ let child = gtk::Label::new(Some(&message.body));
+ set_label_styles(&child);
+ child.upcast::<gtk::Widget>()
+ };
+
+ priv_.content.set_child(Some(&child));
+ }
+ MessageType::Video(_message) => {}
+ MessageType::VerificationRequest(_message) => {}
+ _ => {
+ warn!("Event not supported: {:?}", msgtype)
+ }
+ }
+ }
+ AnyMessageEventContent::RoomRedaction(_) => {
+ self.show_label_with_text("This message was removed.");
+ }
+ _ => warn!("Event not supported: {:?}", content),
+ }
+ }
+
+ fn show_label_with_text(&self, text: &str) {
+ let priv_ = imp::MessageRow::from_instance(self);
+ if let Some(Ok(child)) = priv_.content.child().map(|w| w.downcast::<gtk::Label>()) {
+ child.set_text(&text);
+ } else {
+ let child = gtk::Label::new(Some(&text));
+ set_label_styles(&child);
+ priv_.content.set_child(Some(&child));
+ }
+ }
+
+ fn show_label_with_markup(&self, text: &str) {
+ let priv_ = imp::MessageRow::from_instance(self);
+ if let Some(Ok(child)) = priv_.content.child().map(|w| w.downcast::<gtk::Label>()) {
+ child.set_markup(&text);
+ } else {
+ let child = gtk::Label::new(None);
+ child.set_markup(&text);
+ set_label_styles(&child);
+ priv_.content.set_child(Some(&child));
+ }
+ }
+}
+
+fn parse_formatted_body(formatted: Option<&FormattedBody>) -> Option<Vec<HtmlBlock>> {
+ formatted
+ .filter(|m| m.format == MessageFormat::Html)
+ .filter(|formatted| !formatted.body.contains("<!-- raw HTML omitted -->"))
+ .and_then(|formatted| markup_html(&formatted.body).ok())
+}
+
+fn create_widget_for_html_message(blocks: Vec<HtmlBlock>) -> gtk::Widget {
+ let container = gtk::Box::new(gtk::Orientation::Vertical, 6);
+ for block in blocks {
+ let widget = create_widget_for_html_block(&block);
+ container.append(&widget);
+ }
+ container.upcast::<gtk::Widget>()
+}
+
+fn set_label_styles(w: >k::Label) {
+ w.set_wrap(true);
+ w.set_justify(gtk::Justification::Left);
+ w.set_xalign(0.0);
+ w.set_valign(gtk::Align::Start);
+ w.set_halign(gtk::Align::Fill);
+ w.set_selectable(true);
+}
+
+fn create_widget_for_html_block(block: &HtmlBlock) -> gtk::Widget {
+ match block {
+ HtmlBlock::Heading(n, s) => {
+ let w = gtk::Label::new(None);
+ set_label_styles(&w);
+ w.set_markup(&s);
+ w.add_css_class(&format!("h{}", n));
+ w.upcast::<gtk::Widget>()
+ }
+ HtmlBlock::UList(elements) => {
+ let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
+ bx.set_margin_end(6);
+ bx.set_margin_start(6);
+
+ for li in elements.iter() {
+ let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
+ let bullet = gtk::Label::new(Some("•"));
+ bullet.set_valign(gtk::Align::Start);
+ let w = gtk::Label::new(None);
+ set_label_styles(&w);
+ h_box.append(&bullet);
+ h_box.append(&w);
+ w.set_markup(&li);
+ bx.append(&h_box);
+ }
+
+ bx.upcast::<gtk::Widget>()
+ }
+ HtmlBlock::OList(elements) => {
+ let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
+ bx.set_margin_end(6);
+ bx.set_margin_start(6);
+
+ for (i, ol) in elements.iter().enumerate() {
+ let h_box = gtk::Box::new(gtk::Orientation::Horizontal, 6);
+ let bullet = gtk::Label::new(Some(&format!("{}.", i + 1)));
+ bullet.set_valign(gtk::Align::Start);
+ let w = gtk::Label::new(None);
+ set_label_styles(&w);
+ h_box.append(&bullet);
+ h_box.append(&w);
+ w.set_markup(&ol);
+ bx.append(&h_box);
+ }
+
+ bx.upcast::<gtk::Widget>()
+ }
+ HtmlBlock::Code(s) => {
+ use sourceview::BufferExt;
+ let scrolled = gtk::ScrolledWindow::new();
+ scrolled.set_policy(gtk::PolicyType::Automatic, gtk::PolicyType::Never);
+ let buffer = sourceview::Buffer::new(None);
+ buffer.set_highlight_matching_brackets(false);
+ buffer.set_text(&s);
+ let view = sourceview::View::with_buffer(&buffer);
+ view.set_editable(false);
+ view.add_css_class("codeview");
+ scrolled.set_child(Some(&view));
+ scrolled.upcast::<gtk::Widget>()
+ }
+ HtmlBlock::Quote(blocks) => {
+ let bx = gtk::Box::new(gtk::Orientation::Vertical, 6);
+ bx.add_css_class("quote");
+ for block in blocks.iter() {
+ let w = create_widget_for_html_block(block);
+ bx.append(&w);
+ }
+ bx.upcast::<gtk::Widget>()
+ }
+ HtmlBlock::Text(s) => {
+ let w = gtk::Label::new(None);
+ set_label_styles(&w);
+ w.set_markup(&s);
+ w.upcast::<gtk::Widget>()
+ }
+ }
+}
diff --git a/src/session/content/mod.rs b/src/session/content/mod.rs
new file mode 100644
index 00000000..6831a481
--- /dev/null
+++ b/src/session/content/mod.rs
@@ -0,0 +1,11 @@
+mod content;
+mod divider_row;
+mod item_row;
+mod message_row;
+mod state_row;
+
+pub use self::content::Content;
+use self::divider_row::DividerRow;
+use self::item_row::ItemRow;
+use self::message_row::MessageRow;
+use self::state_row::StateRow;
diff --git a/src/session/content/state_row.rs b/src/session/content/state_row.rs
new file mode 100644
index 00000000..a027637b
--- /dev/null
+++ b/src/session/content/state_row.rs
@@ -0,0 +1,80 @@
+use adw::{prelude::*, subclass::prelude::*};
+use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
+use matrix_sdk::events::{AnyStateEvent, AnyStateEventContent};
+
+mod imp {
+ use super::*;
+ use glib::subclass::InitializingObject;
+
+ #[derive(Debug, Default, CompositeTemplate)]
+ #[template(resource = "/org/gnome/FractalNext/content-state-row.ui")]
+ pub struct StateRow {
+ #[template_child]
+ pub timestamp: TemplateChild<gtk::Label>,
+ #[template_child]
+ pub content: TemplateChild<adw::Bin>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for StateRow {
+ const NAME: &'static str = "ContentStateRow";
+ type Type = super::StateRow;
+ 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 StateRow {}
+ impl WidgetImpl for StateRow {}
+ impl BinImpl for StateRow {}
+}
+
+glib::wrapper! {
+ pub struct StateRow(ObjectSubclass<imp::StateRow>)
+ @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
+}
+
+//TODO
+// - [] Implement widgets to show state events
+impl StateRow {
+ pub fn new() -> Self {
+ glib::Object::new(&[]).expect("Failed to create StateRow")
+ }
+
+ pub fn update(&self, state: &AnyStateEvent) {
+ let _priv_ = imp::StateRow::from_instance(self);
+ // We may want to show more state events in the future
+ // For a full list of state events see:
+ // https://matrix-org.github.io/matrix-rust-sdk/matrix_sdk/events/enum.AnyStateEventContent.html
+ let message = match state.content() {
+ AnyStateEventContent::RoomCreate(_event) => format!("The beginning of this room."),
+ AnyStateEventContent::RoomEncryption(_event) => format!("This room is now encrypted."),
+ AnyStateEventContent::RoomMember(_event) => {
+ // TODO: fully implement this state event
+ format!("A member did change something: state, avatar, name ...")
+ }
+ AnyStateEventContent::RoomThirdPartyInvite(event) => {
+ format!("{} was invited.", event.display_name)
+ }
+ AnyStateEventContent::RoomTombstone(event) => {
+ format!("The room was upgraded: {}", event.body)
+ // Todo: add button for new room with acction session.show_room::room_id
+ }
+ _ => {
+ format!("Unsupported Event: this shouldn't be shown.")
+ }
+ };
+ if let Some(Ok(child)) = self.child().map(|w| w.downcast::<gtk::Label>()) {
+ child.set_text(&message);
+ } else {
+ let child = gtk::Label::new(Some(&message));
+ self.set_child(Some(&child));
+ };
+ }
+}
diff --git a/src/session/mod.rs b/src/session/mod.rs
index c19ca8aa..78c595da 100644
--- a/src/session/mod.rs
+++ b/src/session/mod.rs
@@ -326,9 +326,13 @@ impl Session {
secret::store_session(homeserver, session)
}
- // TODO: handle show room
fn handle_show_room_action(&self, room_id: RoomId) {
- warn!("TODO: implement room action: {:?}", room_id);
+ let priv_ = imp::Session::from_instance(self);
+ if let Some(room) = priv_.rooms.borrow().get(&room_id) {
+ priv_.content.set_room(room);
+ } else {
+ warn!("No room with {} was found", room_id);
+ }
}
fn handle_sync_reposne(&self, response: SyncResponse) {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]