[fractal/fractal-next] content: Implement room history



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(&gtk::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: &gtk::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]