From 8bc0d107e740be468ee0c9dcd253de36355088d3 Mon Sep 17 00:00:00 2001
From: fiaxh <git@mx.ax.lt>
Date: Sun, 27 Aug 2017 23:55:49 +0200
Subject: Plugins providing conversation items for ConversationView

---
 main/CMakeLists.txt                                |  12 +-
 main/data/conversation_summary/message_item.ui     |   9 +-
 main/data/conversation_summary/view.ui             |   2 +-
 .../conversation_summary/chat_state_populator.vala | 116 +++++++++++
 .../ui/conversation_summary/conversation_item.vala |  32 ---
 .../conversation_item_skeleton.vala                | 142 ++++++++++++++
 .../ui/conversation_summary/conversation_view.vala | 173 +++++++++++++++++
 .../default_message_display.vala                   |  53 +++++
 .../conversation_summary/merged_message_item.vala  |  59 ------
 main/src/ui/conversation_summary/message_item.vala | 122 ------------
 .../ui/conversation_summary/message_populator.vala |  66 +++++++
 .../ui/conversation_summary/message_textview.vala  |   8 +-
 main/src/ui/conversation_summary/slashme_item.vala |  44 -----
 .../slashme_message_display.vala                   |  75 +++++++
 main/src/ui/conversation_summary/status_item.vala  |  30 ---
 main/src/ui/conversation_summary/view.vala         | 216 ---------------------
 main/src/ui/unified_window.vala                    |   4 +-
 main/src/ui/util/helper.vala                       |   4 +-
 18 files changed, 648 insertions(+), 519 deletions(-)
 create mode 100644 main/src/ui/conversation_summary/chat_state_populator.vala
 delete mode 100644 main/src/ui/conversation_summary/conversation_item.vala
 create mode 100644 main/src/ui/conversation_summary/conversation_item_skeleton.vala
 create mode 100644 main/src/ui/conversation_summary/conversation_view.vala
 create mode 100644 main/src/ui/conversation_summary/default_message_display.vala
 delete mode 100644 main/src/ui/conversation_summary/merged_message_item.vala
 delete mode 100644 main/src/ui/conversation_summary/message_item.vala
 create mode 100644 main/src/ui/conversation_summary/message_populator.vala
 delete mode 100644 main/src/ui/conversation_summary/slashme_item.vala
 create mode 100644 main/src/ui/conversation_summary/slashme_message_display.vala
 delete mode 100644 main/src/ui/conversation_summary/status_item.vala
 delete mode 100644 main/src/ui/conversation_summary/view.vala

(limited to 'main')

diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt
index 83c7a6d5..cebd6d4c 100644
--- a/main/CMakeLists.txt
+++ b/main/CMakeLists.txt
@@ -96,13 +96,13 @@ SOURCES
     src/ui/conversation_selector/groupchat_row.vala
     src/ui/conversation_selector/list.vala
     src/ui/conversation_selector/view.vala
-    src/ui/conversation_summary/conversation_item.vala
-    src/ui/conversation_summary/merged_message_item.vala
-    src/ui/conversation_summary/message_item.vala
+    src/ui/conversation_summary/chat_state_populator.vala
+    src/ui/conversation_summary/conversation_item_skeleton.vala
+    src/ui/conversation_summary/conversation_view.vala
+    src/ui/conversation_summary/default_message_display.vala
+    src/ui/conversation_summary/message_populator.vala
     src/ui/conversation_summary/message_textview.vala
-    src/ui/conversation_summary/slashme_item.vala
-    src/ui/conversation_summary/status_item.vala
-    src/ui/conversation_summary/view.vala
+    src/ui/conversation_summary/slashme_message_display.vala
     src/ui/conversation_titlebar/encryption_entry.vala
     src/ui/conversation_titlebar/menu_entry.vala
     src/ui/conversation_titlebar/occupants_entry.vala
diff --git a/main/data/conversation_summary/message_item.ui b/main/data/conversation_summary/message_item.ui
index e7b4f46f..8d53a691 100644
--- a/main/data/conversation_summary/message_item.ui
+++ b/main/data/conversation_summary/message_item.ui
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-    <template class="DinoUiConversationSummaryMessageItem">
+    <template class="DinoUiConversationSummaryConversationItemSkeleton">
         <property name="hexpand">True</property>
         <property name="column-spacing">7</property>
         <property name="orientation">horizontal</property>
@@ -23,8 +23,9 @@
         </child>
         <child>
             <object class="GtkLabel" id="time_label">
-                <property name="visible">True</property>
                 <property name="xalign">1</property>
+                <property name="valign">start</property>
+                <property name="visible">True</property>
                 <style>
                     <class name="dim-label"/>
                 </style>
@@ -40,6 +41,7 @@
             <object class="GtkImage" id="encryption_image">
                 <property name="visible">False</property>
                 <property name="xalign">1</property>
+                <property name="valign">start</property>
                 <style>
                     <class name="dim-label"/>
                 </style>
@@ -55,6 +57,7 @@
             <object class="GtkImage" id="received_image">
                 <property name="visible">False</property>
                 <property name="xalign">1</property>
+                <property name="valign">start</property>
                 <style>
                     <class name="dim-label"/>
                 </style>
@@ -67,4 +70,4 @@
             </packing>
         </child>
     </template>
-</interface>
\ No newline at end of file
+</interface>
diff --git a/main/data/conversation_summary/view.ui b/main/data/conversation_summary/view.ui
index 07fb7b71..9139da56 100644
--- a/main/data/conversation_summary/view.ui
+++ b/main/data/conversation_summary/view.ui
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-    <template class="DinoUiConversationSummaryView">
+    <template class="DinoUiConversationSummaryConversationView">
         <property name="expand">True</property>
         <property name="homogeneous">False</property>
         <property name="spacing">0</property>
diff --git a/main/src/ui/conversation_summary/chat_state_populator.vala b/main/src/ui/conversation_summary/chat_state_populator.vala
new file mode 100644
index 00000000..06d0cf87
--- /dev/null
+++ b/main/src/ui/conversation_summary/chat_state_populator.vala
@@ -0,0 +1,116 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+using Xmpp;
+
+namespace Dino.Ui.ConversationSummary {
+
+class ChatStatePopulator : Plugins.ConversationItemPopulator, Object {
+
+    public string id { get { return "chat_state"; } }
+
+    private StreamInteractor? stream_interactor;
+    private Conversation? current_conversation;
+    private Plugins.ConversationItemCollection? item_collection;
+
+    private MetaChatStateItem? meta_item;
+
+    public ChatStatePopulator(StreamInteractor stream_interactor) {
+        this.stream_interactor = stream_interactor;
+
+        stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((account, jid, state) => {
+            if (current_conversation != null && current_conversation.account.equals(account) && current_conversation.counterpart.equals_bare(jid)) {
+                Idle.add(() => { update_chat_state(account, jid, state); return false; });
+            }
+        });
+        stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
+            if (conversation.equals(current_conversation)) {
+                Idle.add(() => { update_chat_state(conversation.account, conversation.counterpart); return false; });
+            }
+        });
+    }
+
+    public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection, Plugins.WidgetType type) {
+        current_conversation = conversation;
+        this.item_collection = item_collection;
+        this.meta_item = null;
+
+        update_chat_state(conversation.account, conversation.counterpart);
+    }
+
+    public void close(Conversation conversation) { }
+
+    public void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
+
+    public void populate_between_widgets(Conversation conversation, DateTime from, DateTime to) { }
+
+    private void update_chat_state(Account account, Jid jid, string? state = null) {
+        string? state_ = state;
+        if (state_ == null) {
+            state_ = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_state(current_conversation.account, current_conversation.counterpart);
+        }
+        string? new_text = null;
+        if (state_ != null) {
+            if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
+                string display_name = Util.get_display_name(stream_interactor, jid, account);
+                if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) {
+                    new_text = _("is typing...");
+                } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
+                    new_text = _("has stopped typing");
+                }
+            }
+        }
+        if (meta_item != null && new_text == null) {
+            item_collection.remove_item(meta_item);
+            meta_item = null;
+        } else if (meta_item != null && new_text != null) {
+            meta_item.set_text(new_text);
+        } else if (new_text != null) {
+            meta_item = new MetaChatStateItem(stream_interactor, current_conversation, jid, new_text);
+            item_collection.insert_item(meta_item);
+        }
+
+    }
+}
+
+public class MetaChatStateItem : Plugins.MetaConversationItem {
+    public override Jid? jid { get; set; }
+    public override bool dim { get; set; default=true; }
+    public override DateTime? sort_time { get; set; default=new DateTime.now_utc().add_years(10); }
+
+    public override bool can_merge { get; set; default=false; }
+    public override bool requires_avatar { get; set; default=true; }
+    public override bool requires_header { get; set; default=false; }
+
+    private StreamInteractor stream_interactor;
+    private Conversation conversation;
+    private string text;
+    private Label label;
+
+    public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string text) {
+        this.stream_interactor = stream_interactor;
+        this.conversation = conversation;
+        this.jid = jid;
+        this.text = text;
+    }
+
+    public override Object get_widget(Plugins.WidgetType widget_type) {
+        label = new Label("") { xalign=0, vexpand=true, visible=true };
+        label.get_style_context().add_class("dim-label");
+        update_text();
+        return label;
+    }
+
+    public void set_text(string text) {
+        this.text = text;
+        update_text();
+    }
+
+    private void update_text() {
+        string display_name = Util.get_display_name(stream_interactor, jid, conversation.account);
+        label.label = display_name + " " + text;
+    }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/conversation_item.vala b/main/src/ui/conversation_summary/conversation_item.vala
deleted file mode 100644
index a99025ab..00000000
--- a/main/src/ui/conversation_summary/conversation_item.vala
+++ /dev/null
@@ -1,32 +0,0 @@
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-public enum MessageKind {
-    TEXT,
-    ME_COMMAND
-}
-
-public MessageKind get_message_kind(Message message) {
-    if (message.body.has_prefix("/me ")) {
-        return MessageKind.ME_COMMAND;
-    } else {
-        return MessageKind.TEXT;
-    }
-}
-
-public interface ConversationItem : Gtk.Widget {
-    public abstract bool merge(Entities.Message message);
-
-    public static ConversationItem create_for_message(StreamInteractor stream_interactor, Conversation conversation, Message message) {
-        switch (get_message_kind(message)) {
-            case MessageKind.TEXT:
-                return new MergedMessageItem(stream_interactor, conversation, message);
-            case MessageKind.ME_COMMAND:
-                return new SlashMeItem(stream_interactor, conversation, message);
-        }
-        assert_not_reached();
-    }
-}
-
-}
\ No newline at end of file
diff --git a/main/src/ui/conversation_summary/conversation_item_skeleton.vala b/main/src/ui/conversation_summary/conversation_item_skeleton.vala
new file mode 100644
index 00000000..b30d45d3
--- /dev/null
+++ b/main/src/ui/conversation_summary/conversation_item_skeleton.vala
@@ -0,0 +1,142 @@
+using Gee;
+using Gdk;
+using Gtk;
+using Markup;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+[GtkTemplate (ui = "/im/dino/conversation_summary/message_item.ui")]
+public class ConversationItemSkeleton : Grid {
+
+    [GtkChild] private Image image;
+    [GtkChild] private Label time_label;
+    [GtkChild] private Image encryption_image;
+    [GtkChild] private Image received_image;
+
+    public StreamInteractor stream_interactor;
+    public Conversation conversation { get; set; }
+    public Gee.List<Plugins.MetaConversationItem> items = new ArrayList<Plugins.MetaConversationItem>();
+
+    private Box box = new Box(Orientation.VERTICAL, 2) { visible=true };
+
+    public ConversationItemSkeleton(StreamInteractor stream_interactor, Conversation conversation) {
+        this.conversation = conversation;
+        this.stream_interactor = stream_interactor;
+
+        set_main_widget(box);
+    }
+
+    public void add_meta_item(Plugins.MetaConversationItem item) {
+        items.add(item);
+        if (items.size == 1) {
+            setup(item);
+        }
+        Widget widget = (Widget) item.get_widget(Plugins.WidgetType.GTK);
+        if (item.requires_header) {
+            box.add(widget);
+        } else {
+            set_title_widget(widget);
+        }
+        item.notify["mark"].connect_after(update_received);
+        update_received();
+    }
+
+    public void set_title_widget(Widget w) {
+        attach(w, 1, 0, 1, 1);
+    }
+
+    public void set_main_widget(Widget w) {
+        attach(w, 1, 1, 2, 1);
+    }
+
+    public void update_time() {
+        if (items.size > 0 && items[0].display_time != null) {
+            time_label.label = get_relative_time(items[0].display_time.to_local());
+        }
+    }
+
+    private void setup(Plugins.MetaConversationItem item) {
+        update_time();
+        Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).set_greyscale(item.dim).draw_jid(stream_interactor, item.jid, conversation.account));
+        if (item.requires_header) {
+            set_default_title_widget(item.jid);
+        }
+        if (item.encryption != null && item.encryption != Encryption.NONE) {
+            encryption_image.visible = true;
+            encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
+        }
+    }
+
+    private void set_default_title_widget(Jid jid) {
+        Label name_label = new Label("") { use_markup=true, xalign=0, hexpand=true, visible=true };
+        string display_name = Util.get_display_name(stream_interactor, jid, conversation.account);
+        string color = Util.get_name_hex_color(stream_interactor, conversation.account, jid, Util.is_dark_theme(name_label));
+        name_label.label = @"<span foreground=\"#$color\">$display_name</span>";
+        name_label.style_updated.connect(() => {
+            string new_color = Util.get_name_hex_color(stream_interactor, conversation.account, jid, Util.is_dark_theme(name_label));
+            name_label.set_markup(@"<span foreground=\"#$new_color\">$display_name</span>");
+        });
+        set_title_widget(name_label);
+    }
+
+    private void update_received() {
+        bool all_received = true;
+        bool all_read = true;
+        foreach (Plugins.MetaConversationItem item in items) {
+            if (item.mark == Message.Marked.WONTSEND) {
+                received_image.visible = true;
+                received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR);
+                Util.force_error_color(received_image);
+                Util.force_error_color(encryption_image);
+                Util.force_error_color(time_label);
+                return;
+            } else if (item.mark != Message.Marked.READ) {
+                all_read = false;
+                if (item.mark != Message.Marked.RECEIVED) {
+                    all_received = false;
+                }
+            }
+        }
+        if (all_read) {
+            received_image.visible = true;
+            received_image.set_from_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR);
+        } else if (all_received) {
+            received_image.visible = true;
+            received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR);
+        } else if (received_image.visible) {
+            received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR);
+        }
+    }
+
+    private static string get_relative_time(DateTime datetime) {
+        DateTime now = new DateTime.now_local();
+        TimeSpan timespan = now.difference(datetime);
+        if (timespan > 365 * TimeSpan.DAY) {
+        return datetime.format(Util.is_24h_format() ?
+            /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H\u2236%M") :
+                /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l\u2236%M %p"));
+        } else if (timespan > 7 * TimeSpan.DAY) {
+        return datetime.format(Util.is_24h_format() ?
+            /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H\u2236%M") :
+                /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l\u2236%M %p"));
+        } else if (datetime.get_day_of_month() != new DateTime.now_utc().get_day_of_month()) {
+        return datetime.format(Util.is_24h_format() ?
+            /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */ _("%a, %H\u2236%M") :
+                /* xgettext:no-c-format */ _("%a, %l\u2236%M %p"));
+        } else if (timespan > 9 * TimeSpan.MINUTE) {
+        return datetime.format(Util.is_24h_format() ?
+            /* xgettext:no-c-format */  /* Time in 24h format (w/o seconds) */ _("%H\u2236%M") :
+                /* xgettext:no-c-format */  /* Time in 12h format (w/o seconds) */ _("%l\u2236%M %p"));
+        } else if (timespan > TimeSpan.MINUTE) {
+            ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE);
+            /* xgettext:this is the beginning of a sentence. */
+            return n("%i min ago", "%i mins ago", mins).printf(mins);
+        } else {
+            return _("Just now");
+        }
+    }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/conversation_view.vala b/main/src/ui/conversation_summary/conversation_view.vala
new file mode 100644
index 00000000..b090e5d7
--- /dev/null
+++ b/main/src/ui/conversation_summary/conversation_view.vala
@@ -0,0 +1,173 @@
+using Gee;
+using Gtk;
+using Pango;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+[GtkTemplate (ui = "/im/dino/conversation_summary/view.ui")]
+public class ConversationView : Box, Plugins.ConversationItemCollection {
+
+    public Conversation? conversation { get; private set; }
+
+    [GtkChild] private ScrolledWindow scrolled;
+    [GtkChild] private Box main;
+    [GtkChild] private Stack stack;
+
+    private StreamInteractor stream_interactor;
+    private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>((a, b) => { return a.sort_time.compare(b.sort_time); });
+    private Gee.Map<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>> meta_after_items = new Gee.HashMap<Plugins.MetaConversationItem, Gee.List<Plugins.MetaConversationItem>>();
+    private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
+    private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
+    private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
+    private MessagePopulator message_item_populator;
+
+    private double? was_value;
+    private double? was_upper;
+    private double? was_page_size;
+
+    private Mutex reloading_mutex = new Mutex();
+    private bool animate = false;
+
+    public ConversationView(StreamInteractor stream_interactor) {
+        this.stream_interactor = stream_interactor;
+        scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
+        scrolled.vadjustment.notify["value"].connect(on_value_notify);
+
+        message_item_populator = new MessagePopulator(stream_interactor);
+
+        Application app = GLib.Application.get_default() as Application;
+        app.plugin_registry.register_conversation_item_populator(new ChatStatePopulator(stream_interactor));
+
+        Timeout.add_seconds(60, () => {
+            foreach (ConversationItemSkeleton item_skeleton in item_skeletons) {
+                item_skeleton.update_time();
+            }
+            return true;
+        });
+
+        Util.force_base_background(this);
+    }
+
+    public void initialize_for_conversation(Conversation? conversation) {
+        this.conversation = conversation;
+        stack.set_visible_child_name("void");
+        clear();
+        was_upper = null;
+        was_page_size = null;
+        animate = false;
+        Timeout.add(20, () => { animate = true; return false; });
+
+        message_item_populator.init(conversation, this);
+        message_item_populator.populate_number(conversation, new DateTime.now_utc(), 50);
+
+        Dino.Application app = Dino.Application.get_default();
+        foreach (Plugins.ConversationItemPopulator populator in app.plugin_registry.conversation_item_populators) {
+            populator.init(conversation, this, Plugins.WidgetType.GTK);
+        }
+
+        stack.set_visible_child_name("main");
+    }
+
+    public void insert_item(Plugins.MetaConversationItem item) {
+        meta_items.add(item);
+        if (!item.can_merge || !merge_back(item)) {
+            insert_new(item);
+        }
+    }
+
+    public void remove_item(Plugins.MetaConversationItem item) {
+        main.remove(widgets[item]);
+        widgets.unset(item);
+        meta_items.remove(item);
+        item_skeletons.remove(item_item_skeletons[item]);
+        item_item_skeletons.unset(item);
+    }
+
+    private bool merge_back(Plugins.MetaConversationItem item) {
+        Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
+        if (lower_item != null) {
+            ConversationItemSkeleton lower_skeleton = item_item_skeletons[lower_item];
+            Plugins.MetaConversationItem lower_start_item = lower_skeleton.items[0];
+            if (lower_start_item.can_merge &&
+                    item.display_time.difference(lower_start_item.display_time) < TimeSpan.MINUTE &&
+                    lower_start_item.jid.equals(item.jid) &&
+                    lower_start_item.encryption == item.encryption &&
+                    item.mark != Message.Marked.WONTSEND) {
+                lower_skeleton.add_meta_item(item);
+                force_alloc_width(lower_skeleton, main.get_allocated_width());
+                item_item_skeletons[item] = lower_skeleton;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void insert_new(Plugins.MetaConversationItem item) {
+        ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation);
+        item_skeleton.add_meta_item(item);
+        item_item_skeletons[item] = item_skeleton;
+        Plugins.MetaConversationItem? lower_item = meta_items.lower(item);
+        int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
+        item_skeletons.insert(index, item_skeleton);
+
+        Widget insert = item_skeleton;
+        if (animate) {
+            Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
+            revealer.add(item_skeleton);
+            insert = revealer;
+            main.add(insert);
+            revealer.reveal_child = true;
+        } else {
+            main.add(insert);
+        }
+        widgets[item] = insert;
+        force_alloc_width(insert, main.get_allocated_width());
+        main.reorder_child(insert, index);
+    }
+
+    private void on_upper_notify() {
+        if (was_upper == null || scrolled.vadjustment.value >  was_upper - was_page_size - 1 ||
+                scrolled.vadjustment.value >  was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
+            scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
+        } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
+            scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
+        }
+        was_upper = scrolled.vadjustment.upper;
+        was_page_size = scrolled.vadjustment.page_size;
+        reloading_mutex.trylock();
+        reloading_mutex.unlock();
+    }
+
+    private void on_value_notify() {
+        if (scrolled.vadjustment.value < 200) {
+            load_earlier_messages();
+        }
+    }
+
+    private void load_earlier_messages() {
+        was_value = scrolled.vadjustment.value;
+        if (!reloading_mutex.trylock()) return;
+        if (meta_items.size > 0) message_item_populator.populate_number(conversation, meta_items.first().sort_time, 20);
+    }
+
+    // Workaround GTK TextView issues
+    private void force_alloc_width(Widget widget, int width) {
+        Allocation alloc = Allocation();
+        widget.get_preferred_width(out alloc.width, null);
+        widget.get_preferred_height(out alloc.height, null);
+        alloc.width = width;
+        widget.size_allocate(alloc);
+    }
+
+    private void clear() {
+        meta_items.clear();
+        meta_after_items.clear();
+        item_skeletons.clear();
+        item_item_skeletons.clear();
+        main.@foreach((widget) => { main.remove(widget); });
+    }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/default_message_display.vala b/main/src/ui/conversation_summary/default_message_display.vala
new file mode 100644
index 00000000..6082253d
--- /dev/null
+++ b/main/src/ui/conversation_summary/default_message_display.vala
@@ -0,0 +1,53 @@
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class DefaultMessageDisplay : Plugins.MessageDisplayProvider, Object {
+    public string id { get; set; default="default"; }
+    public double priority { get; set; default=0; }
+
+    public StreamInteractor stream_interactor;
+
+    public DefaultMessageDisplay(StreamInteractor stream_interactor) {
+        this.stream_interactor = stream_interactor;
+    }
+
+    public bool can_display(Entities.Message? message) { return true; }
+
+    public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
+        return new MetaMessageItem(stream_interactor, message, conversation);
+    }
+}
+
+public class MetaMessageItem : Plugins.MetaConversationItem {
+    public override Jid? jid { get; set; }
+    public override DateTime? sort_time { get; set; }
+    public override DateTime? display_time { get; set; }
+    public override Encryption? encryption { get; set; }
+
+    private StreamInteractor stream_interactor;
+    private Conversation conversation;
+    private Message message;
+
+    public MetaMessageItem(StreamInteractor stream_interactor, Message message, Conversation conversation) {
+        this.stream_interactor = stream_interactor;
+        this.conversation = conversation;
+        this.message = message;
+        this.jid = message.from;
+        this.sort_time = message.local_time;
+        this.display_time = message.time;
+        this.encryption = message.encryption;
+    }
+
+    public override bool can_merge { get; set; default=true; }
+    public override bool requires_avatar { get; set; default=true; }
+    public override bool requires_header { get; set; default=true; }
+
+    public override Object get_widget(Plugins.WidgetType widget_type) {
+        MessageTextView text_view = new MessageTextView() { visible = true };
+        text_view.add_text(message.body);
+        return text_view;
+    }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/merged_message_item.vala b/main/src/ui/conversation_summary/merged_message_item.vala
deleted file mode 100644
index 4cabebac..00000000
--- a/main/src/ui/conversation_summary/merged_message_item.vala
+++ /dev/null
@@ -1,59 +0,0 @@
-using Gee;
-using Gdk;
-using Gtk;
-using Markup;
-
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class MergedMessageItem : MessageItem {
-
-    private Label name_label = new Label("") { xalign=0, visible=true, hexpand=true };
-    private MessageTextView textview = new MessageTextView() { visible=true };
-
-    public MergedMessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
-        base(stream_interactor, conversation, message);
-        set_main_widget(textview);
-        set_title_widget(name_label);
-        add_message(message);
-
-        string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
-        string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.from, false);
-        name_label.set_markup(@"<span foreground=\"#$color\">$display_name</span>");
-
-        textview.style_updated.connect(update_display_style);
-        update_display_style();
-    }
-
-    public override void add_message(Message message) {
-        base.add_message(message);
-        if (messages.size > 1) textview.add_text("\n");
-        string text = message.body;
-        if (text.length > 10000) {
-            text = text.slice(0, 10000) + " [" + _("Message too long") + "]";
-        }
-        textview.add_text(text);
-    }
-
-    public override bool merge(Message message) {
-        if (get_message_kind(message) == MessageKind.TEXT &&
-                this.from.equals(message.from) &&
-                this.messages[0].encryption == message.encryption &&
-                message.time.difference(initial_time) < TimeSpan.MINUTE &&
-                this.messages[0].marked != Entities.Message.Marked.WONTSEND) {
-            add_message(message);
-            return true;
-        }
-        return false;
-
-    }
-
-    private void update_display_style() {
-        string display_name = Util.get_message_display_name(stream_interactor, messages[0], conversation.account);
-        string color = Util.get_name_hex_color(stream_interactor, conversation.account, messages[0].real_jid ?? messages[0].from, Util.is_dark_theme(textview));
-        name_label.set_markup(@"<span foreground=\"#$color\">$display_name</span>");
-    }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/message_item.vala b/main/src/ui/conversation_summary/message_item.vala
deleted file mode 100644
index f669c021..00000000
--- a/main/src/ui/conversation_summary/message_item.vala
+++ /dev/null
@@ -1,122 +0,0 @@
-using Gee;
-using Gdk;
-using Gtk;
-using Markup;
-
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-[GtkTemplate (ui = "/im/dino/conversation_summary/message_item.ui")]
-public class MessageItem : Grid, ConversationItem {
-
-    [GtkChild] private Image image;
-    [GtkChild] private Label time_label;
-    [GtkChild] private Image encryption_image;
-    [GtkChild] private Image received_image;
-
-    public StreamInteractor stream_interactor;
-    public Conversation conversation { get; set; }
-    public Jid from { get; private set; }
-    public DateTime initial_time { get; private set; }
-    public ArrayList<Message> messages = new ArrayList<Message>(Message.equals_func);
-
-    public MessageItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
-        this.conversation = conversation;
-        this.stream_interactor = stream_interactor;
-        this.initial_time = message.time;
-        this.from = message.from;
-
-        if (message.encryption != Encryption.NONE) {
-            encryption_image.visible = true;
-            encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
-        }
-
-        time_label.label = get_relative_time(initial_time.to_local());
-        Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message));
-    }
-
-    public void set_title_widget(Widget w) {
-        attach(w, 1, 0, 1, 1);
-    }
-
-    public void set_main_widget(Widget w) {
-        attach(w, 1, 1, 2, 1);
-    }
-
-    public void update() {
-        time_label.label = get_relative_time(initial_time.to_local());
-    }
-
-    public virtual void add_message(Message message) {
-        messages.add(message);
-
-        message.notify["marked"].connect_after(() => {
-            Idle.add(() => { update_received(); return false; });
-        });
-        update_received();
-    }
-
-    public virtual bool merge(Message message) {
-        return false;
-    }
-
-    private void update_received() {
-        bool all_received = true;
-        bool all_read = true;
-        foreach (Message message in messages) {
-            if (message.marked == Message.Marked.WONTSEND) {
-                received_image.visible = true;
-                received_image.set_from_icon_name("dialog-warning-symbolic", IconSize.SMALL_TOOLBAR);
-                Util.force_error_color(received_image);
-                Util.force_error_color(encryption_image);
-                Util.force_error_color(time_label);
-                return;
-            } else if (message.marked != Message.Marked.READ) {
-                all_read = false;
-                if (message.marked != Message.Marked.RECEIVED) {
-                    all_received = false;
-                }
-            }
-        }
-        if (all_read) {
-            received_image.visible = true;
-            received_image.set_from_icon_name("dino-double-tick-symbolic", IconSize.SMALL_TOOLBAR);
-        } else if (all_received) {
-            received_image.visible = true;
-            received_image.set_from_icon_name("dino-tick-symbolic", IconSize.SMALL_TOOLBAR);
-        } else if (received_image.visible) {
-            received_image.set_from_icon_name("image-loading-symbolic", IconSize.SMALL_TOOLBAR);
-        }
-    }
-
-    private static string get_relative_time(DateTime datetime) {
-        DateTime now = new DateTime.now_local();
-        TimeSpan timespan = now.difference(datetime);
-        if (timespan > 365 * TimeSpan.DAY) {
-            return datetime.format(Util.is_24h_format() ?
-                /* xgettext:no-c-format */ /* Date + time in 24h format (w/o seconds) */ _("%x, %H\u2236%M") :
-                /* xgettext:no-c-format */ /* Date + time in 12h format (w/o seconds)*/ _("%x, %l\u2236%M %p"));
-        } else if (timespan > 7 * TimeSpan.DAY) {
-            return datetime.format(Util.is_24h_format() ?
-                /* xgettext:no-c-format */ /* Month, day and time in 24h format (w/o seconds) */ _("%b %d, %H\u2236%M") :
-                /* xgettext:no-c-format */ /* Month, day and time in 12h format (w/o seconds) */ _("%b %d, %l\u2236%M %p"));
-        } else if (timespan > 1 * TimeSpan.DAY) {
-            return datetime.format(Util.is_24h_format() ?
-                /* xgettext:no-c-format */ /* Day of week and time in 12h format (w/o seconds) */ _("%a, %H\u2236%M") :
-                /* xgettext:no-c-format */ _("%a, %l\u2236%M %p"));
-        } else if (timespan > 9 * TimeSpan.MINUTE) {
-            return datetime.format(Util.is_24h_format() ?
-                /* xgettext:no-c-format */  /* Time in 24h format (w/o seconds) */ _("%H\u2236%M") :
-                /* xgettext:no-c-format */  /* Time in 12h format (w/o seconds) */ _("%l\u2236%M %p"));
-        } else if (timespan > TimeSpan.MINUTE) {
-            ulong mins = (ulong) (timespan.abs() / TimeSpan.MINUTE);
-            /* xgettext:this is the beginning of a sentence. */
-            return n("%i min ago", "%i mins ago", mins).printf(mins);
-        } else {
-            return _("Just now");
-        }
-    }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/message_populator.vala b/main/src/ui/conversation_summary/message_populator.vala
new file mode 100644
index 00000000..2c3eccd2
--- /dev/null
+++ b/main/src/ui/conversation_summary/message_populator.vala
@@ -0,0 +1,66 @@
+using Gee;
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class MessagePopulator : Object {
+
+    private StreamInteractor? stream_interactor;
+    private Conversation? current_conversation;
+    private Plugins.ConversationItemCollection? item_collection;
+
+    public MessagePopulator(StreamInteractor stream_interactor) {
+        this.stream_interactor = stream_interactor;
+
+        Application app = GLib.Application.get_default() as Application;
+        app.plugin_registry.register_message_display(new DefaultMessageDisplay(stream_interactor));
+        app.plugin_registry.register_message_display(new SlashmeMessageDisplay(stream_interactor));
+
+
+        stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => {
+            Idle.add(() => { handle_message(message, conversation); return false; });
+        });
+        stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
+            Idle.add(() => { handle_message(message, conversation); return false; });
+        });
+    }
+
+    public void init(Conversation conversation, Plugins.ConversationItemCollection item_collection) {
+        current_conversation = conversation;
+        this.item_collection = item_collection;
+    }
+
+    public void close(Conversation conversation) { }
+
+    public void populate_number(Conversation conversation, DateTime from, int n) {
+        Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before(conversation, from, n);
+        if (messages != null) {
+            foreach (Entities.Message message in messages) {
+                handle_message(message, conversation);
+            }
+        }
+    }
+
+    private void handle_message(Message message, Conversation conversation) {
+        if (!conversation.equals(current_conversation)) return;
+
+        Plugins.MessageDisplayProvider? best_provider = null;
+        int priority = -1;
+        Application app = GLib.Application.get_default() as Application;
+        foreach (Plugins.MessageDisplayProvider provider in app.plugin_registry.message_displays) {
+            if (provider.can_display(message) && provider.priority > priority) {
+                best_provider = provider;
+            }
+        }
+        Plugins.MetaConversationItem meta_item = best_provider.get_item(message, conversation);
+        meta_item.mark = message.marked;
+        message.notify["marked"].connect(() => {
+            meta_item.mark = message.marked;
+        });
+        item_collection.insert_item(meta_item);
+    }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/message_textview.vala b/main/src/ui/conversation_summary/message_textview.vala
index 80759207..f2a4ca22 100644
--- a/main/src/ui/conversation_summary/message_textview.vala
+++ b/main/src/ui/conversation_summary/message_textview.vala
@@ -27,7 +27,11 @@ public class MessageTextView : TextView {
         minimum_width = 0;
     }
 
-    public void add_text(string text) {
+    public void add_text(string text_) {
+        string text = text_;
+        if (text.length > 10000) {
+            text = text.slice(0, 10000) + " [" + _("Message too long") + "]";
+        }
         TextIter end;
         buffer.get_end_iter(out end);
         buffer.insert(ref end, text, -1);
@@ -90,4 +94,4 @@ public class MessageTextView : TextView {
     }
 }
 
-}
\ No newline at end of file
+}
diff --git a/main/src/ui/conversation_summary/slashme_item.vala b/main/src/ui/conversation_summary/slashme_item.vala
deleted file mode 100644
index 2056d2d1..00000000
--- a/main/src/ui/conversation_summary/slashme_item.vala
+++ /dev/null
@@ -1,44 +0,0 @@
-using Gdk;
-using Gtk;
-
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-public class SlashMeItem : MessageItem {
-
-    private Box box = new Box(Orientation.VERTICAL, 0) { visible=true, vexpand=true };
-    private MessageTextView textview = new MessageTextView() { visible=true };
-    private string text;
-    private TextTag nick_tag;
-
-    public SlashMeItem(StreamInteractor stream_interactor, Conversation conversation, Message message) {
-        base(stream_interactor, conversation, message);
-        box.set_center_widget(textview);
-        set_title_widget(box);
-        text = message.body.substring(3);
-
-        string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
-        string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, false);
-        nick_tag = textview.buffer.create_tag("nick", foreground: "#" + color);
-        TextIter iter;
-        textview.buffer.get_start_iter(out iter);
-        textview.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
-        textview.add_text(text);
-        add_message(message);
-
-        textview.style_updated.connect(update_display_style);
-        update_display_style();
-    }
-
-    public override bool merge(Message message) {
-        return false;
-    }
-
-    private void update_display_style() {
-        string color = Util.get_name_hex_color(stream_interactor, conversation.account, messages[0].real_jid ?? messages[0].from, Util.is_dark_theme(textview));
-        nick_tag.foreground = "#" + color;
-    }
-}
-
-}
diff --git a/main/src/ui/conversation_summary/slashme_message_display.vala b/main/src/ui/conversation_summary/slashme_message_display.vala
new file mode 100644
index 00000000..58d93142
--- /dev/null
+++ b/main/src/ui/conversation_summary/slashme_message_display.vala
@@ -0,0 +1,75 @@
+using Gtk;
+
+using Dino.Entities;
+
+namespace Dino.Ui.ConversationSummary {
+
+public class SlashmeMessageDisplay : Plugins.MessageDisplayProvider, Object {
+    public string id { get; set; default="slashme"; }
+    public double priority { get; set; default=1; }
+
+    public StreamInteractor stream_interactor;
+
+    public SlashmeMessageDisplay(StreamInteractor stream_interactor) {
+        this.stream_interactor = stream_interactor;
+    }
+
+    public bool can_display(Entities.Message? message) {
+        return message.body.has_prefix("/me");
+    }
+
+    public Plugins.MetaConversationItem? get_item(Entities.Message message, Conversation conversation) {
+        return new MetaSlashmeItem(stream_interactor, message, conversation);
+    }
+}
+
+public class MetaSlashmeItem : Plugins.MetaConversationItem {
+    public override Jid? jid { get; set; }
+    public override DateTime? sort_time { get; set; }
+    public override DateTime? display_time { get; set; }
+    public override Encryption? encryption { get; set; }
+
+    private StreamInteractor stream_interactor;
+    private Conversation conversation;
+    private Message message;
+    private TextTag nick_tag;
+    private MessageTextView text_view;
+
+    public MetaSlashmeItem(StreamInteractor stream_interactor, Message message, Conversation conversation) {
+        this.stream_interactor = stream_interactor;
+        this.conversation = conversation;
+        this.message = message;
+        this.jid = message.from;
+        this.sort_time = message.local_time;
+        this.display_time = message.time;
+        this.encryption = message.encryption;
+    }
+
+    public override bool can_merge { get; set; default=false; }
+    public override bool requires_avatar { get; set; default=true; }
+    public override bool requires_header { get; set; default=false; }
+
+    public override Object get_widget(Plugins.WidgetType widget_type) {
+        text_view = new MessageTextView() { valign=Align.CENTER, vexpand=true, visible = true };
+
+        string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
+        string color = Util.get_name_hex_color(stream_interactor, conversation.account, conversation.counterpart, Util.is_dark_theme(text_view));
+        nick_tag = text_view.buffer.create_tag("nick", foreground: "#" + color);
+        TextIter iter;
+        text_view.buffer.get_start_iter(out iter);
+        text_view.buffer.insert_with_tags(ref iter, display_name, display_name.length, nick_tag);
+
+        text_view.add_text(message.body.substring(3));
+        text_view.style_updated.connect(update_style);
+        text_view.realize.connect(update_style);
+        return text_view;
+    }
+
+    private void update_style() {
+        string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
+        string color = Util.get_name_hex_color(stream_interactor, conversation.account, message.real_jid ?? message.from, Util.is_dark_theme(text_view));
+        nick_tag.foreground = "#" + color;
+    }
+}
+
+}
diff --git a/main/src/ui/conversation_summary/status_item.vala b/main/src/ui/conversation_summary/status_item.vala
deleted file mode 100644
index 1704356c..00000000
--- a/main/src/ui/conversation_summary/status_item.vala
+++ /dev/null
@@ -1,30 +0,0 @@
-using Gtk;
-using Markup;
-
-using Dino.Entities;
-
-namespace Dino.Ui.ConversationSummary {
-
-private class StatusItem : Grid {
-
-    private Image image = new Image();
-    private Label label = new Label("");
-
-    private StreamInteractor stream_interactor;
-    private Conversation conversation;
-
-    public StatusItem(StreamInteractor stream_interactor, Conversation conversation, string? text) {
-        Object(column_spacing : 7);
-        set_hexpand(true);
-        this.stream_interactor = stream_interactor;
-        this.conversation = conversation;
-        image.set_from_pixbuf((new AvatarGenerator(30, 30)).set_greyscale(true).draw_conversation(stream_interactor, conversation));
-        attach(image, 0, 0, 1, 1);
-        attach(label, 1, 0, 1, 1);
-        string display_name = Util.get_display_name(stream_interactor, conversation.counterpart, conversation.account);
-        label.set_markup(@"<span foreground=\"#B1B1B1\">$(escape_text(display_name)) $text</span>");
-        show_all();
-    }
-}
-
-}
\ No newline at end of file
diff --git a/main/src/ui/conversation_summary/view.vala b/main/src/ui/conversation_summary/view.vala
deleted file mode 100644
index 693f7164..00000000
--- a/main/src/ui/conversation_summary/view.vala
+++ /dev/null
@@ -1,216 +0,0 @@
-using Gee;
-using Gtk;
-using Pango;
-
-using Dino.Entities;
-using Xmpp;
-
-namespace Dino.Ui.ConversationSummary {
-
-[GtkTemplate (ui = "/im/dino/conversation_summary/view.ui")]
-public class View : Box {
-
-    public Conversation? conversation { get; private set; }
-    public HashMap<Entities.Message, ConversationItem> conversation_items = new HashMap<Entities.Message, ConversationItem>(Entities.Message.hash_func, Entities.Message.equals_func);
-
-    [GtkChild] private ScrolledWindow scrolled;
-    [GtkChild] private Box main;
-    [GtkChild] private Stack stack;
-
-    private StreamInteractor stream_interactor;
-    private ConversationItem? last_conversation_item;
-    private StatusItem typing_status;
-    private Entities.Message? earliest_message;
-    double? was_value;
-    double? was_upper;
-    double? was_page_size;
-    Object reloading_lock = new Object();
-    bool reloading = false;
-
-    public View(StreamInteractor stream_interactor) {
-        this.stream_interactor = stream_interactor;
-        scrolled.vadjustment.notify["upper"].connect_after(on_upper_notify);
-        scrolled.vadjustment.notify["value"].connect(on_value_notify);
-
-        stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).received_state.connect((account, jid, state) => {
-            Idle.add(() => { on_received_state(account, jid, state); return false; });
-        });
-        stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect((message, conversation) => {
-            Idle.add(() => { show_message(message, conversation, true); return false; });
-        });
-        stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect((message, conversation) => {
-            Idle.add(() => { show_message(message, conversation, true); return false; });
-        });
-        stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect((show, jid, account) => {
-            Idle.add(() => { on_show_received(show, jid, account); return false; });
-        });
-        Timeout.add_seconds(60, () => {
-            foreach (ConversationItem conversation_item in conversation_items.values) {
-                MessageItem message_item = conversation_item as MessageItem;
-                if (message_item != null) message_item.update();
-            }
-            return true;
-        });
-
-        Util.force_base_background(this);
-    }
-
-    public void initialize_for_conversation(Conversation? conversation) {
-        this.conversation = conversation;
-        stack.set_visible_child_name("void");
-        clear();
-        conversation_items.clear();
-        was_upper = null;
-        was_page_size = null;
-        last_conversation_item = null;
-
-        ArrayList<Object> objects = new ArrayList<Object>();
-        Gee.List<Entities.Message> messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages(conversation);
-        if (messages.size > 0) {
-            earliest_message = messages[messages.size -1];
-            objects.add_all(messages);
-        }
-        HashMap<Jid, ArrayList<Show>>? shows = stream_interactor.get_module(PresenceManager.IDENTITY).get_shows(conversation.counterpart, conversation.account);
-        if (shows != null) {
-            foreach (Jid jid in shows.keys) objects.add_all(shows[jid]);
-        }
-        objects.sort((a, b) => {
-            DateTime? dt1 = null;
-            DateTime? dt2 = null;
-            Entities.Message m1 = a as Entities.Message;
-            if (m1 != null) dt1 = m1.time;
-            Show s1 = a as Show;
-            if (s1 != null) dt1 = s1.datetime;
-            Entities.Message m2 = b as Entities.Message;
-            if (m2 != null) dt2 = m2.time;
-            Show s2 = b as Show;
-            if (s2 != null) dt2 = s2.datetime;
-            return dt1.compare(dt2);
-        });
-        foreach (Object o in objects) {
-            Entities.Message message = o as Entities.Message;
-            Show show = o as Show;
-            if (message != null) {
-                show_message(message, conversation);
-            } else if (show != null) {
-                on_show_received(show, conversation.counterpart, conversation.account);
-            }
-        }
-        update_chat_state();
-        stack.set_visible_child_name("main");
-    }
-
-    private void on_received_state(Account account, Jid jid, string state) {
-        if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals_bare(jid)) {
-            update_chat_state(state);
-        }
-    }
-
-    private void update_chat_state(string? state = null) {
-        string? state_ = state;
-        if (state_ == null) {
-            state_ = stream_interactor.get_module(CounterpartInteractionManager.IDENTITY).get_chat_state(conversation.account, conversation.counterpart);
-        }
-        if (typing_status != null) {
-            main.remove(typing_status);
-        }
-        if (state_ != null) {
-            if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING || state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
-                if (state_ == Xep.ChatStateNotifications.STATE_COMPOSING) {
-                    typing_status = new StatusItem(stream_interactor, conversation, _("is typing…"));
-                } else if (state_ == Xep.ChatStateNotifications.STATE_PAUSED) {
-                    typing_status = new StatusItem(stream_interactor, conversation, _("has stopped typing"));
-                }
-                main.add(typing_status);
-            }
-        }
-    }
-
-    private void on_show_received(Show show, Jid jid, Account account) {
-
-    }
-
-    private void on_upper_notify() {
-        if (was_upper == null || scrolled.vadjustment.value >  was_upper - was_page_size - 1 ||
-                scrolled.vadjustment.value >  was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
-            scrolled.vadjustment.value = scrolled.vadjustment.upper - scrolled.vadjustment.page_size; // scroll down
-        } else if (scrolled.vadjustment.value < scrolled.vadjustment.upper - scrolled.vadjustment.page_size - 1) {
-            scrolled.vadjustment.value = scrolled.vadjustment.upper - was_upper + scrolled.vadjustment.value; // stay at same content
-        }
-        was_upper = scrolled.vadjustment.upper;
-        was_page_size = scrolled.vadjustment.page_size;
-        lock(reloading_lock) {
-            reloading = false;
-        }
-    }
-
-    private void on_value_notify() {
-        if (scrolled.vadjustment.value < 200) {
-            load_earlier_messages();
-        }
-    }
-
-    private void load_earlier_messages() {
-        if (earliest_message == null) return;
-
-        was_value = scrolled.vadjustment.value;
-        lock(reloading_lock) {
-            if(reloading) return;
-            reloading = true;
-        }
-        Gee.List<Entities.Message>? messages = stream_interactor.get_module(MessageStorage.IDENTITY).get_messages_before(conversation, earliest_message);
-        if (messages != null && messages.size > 0) {
-            earliest_message = messages[0];
-            MergedMessageItem? current_item = null;
-            int items_added = 0;
-            for (int i = 0; i < messages.size; i++) {
-                if (current_item == null || !current_item.merge(messages[i])) {
-                    current_item = new MergedMessageItem(stream_interactor, conversation, messages[i]);
-                    force_alloc_width(current_item, main.get_allocated_width());
-                    main.add(current_item);
-                    conversation_items[messages[i]] = current_item;
-                    main.reorder_child(current_item, items_added);
-                    items_added++;
-                }
-            }
-            return;
-        }
-        reloading = false;
-    }
-
-    private void show_message(Entities.Message message, Conversation conversation, bool animate = false) {
-        if (this.conversation != null && this.conversation.equals(conversation)) {
-            if (last_conversation_item == null || !last_conversation_item.merge(message)) {
-                ConversationItem conversation_item = ConversationItem.create_for_message(stream_interactor, conversation, message);
-                if (animate) {
-                    Revealer revealer = new Revealer() {transition_duration = 200, transition_type = RevealerTransitionType.SLIDE_UP, visible = true};
-                    revealer.add(conversation_item);
-                    force_alloc_width(revealer, main.get_allocated_width());
-                    main.add(revealer);
-                    revealer.set_reveal_child(true);
-                } else {
-                    force_alloc_width(conversation_item, main.get_allocated_width());
-                    main.add(conversation_item);
-                }
-                last_conversation_item = conversation_item;
-            }
-            conversation_items[message] = last_conversation_item;
-            update_chat_state();
-        }
-    }
-
-    // Workaround GTK TextView issues
-    private void force_alloc_width(Widget widget, int width) {
-        Allocation alloc = Allocation();
-        widget.get_preferred_width(out alloc.width, null);
-        widget.get_preferred_height(out alloc.height, null);
-        alloc.width = width;
-        widget.size_allocate(alloc);
-    }
-
-    private void clear() {
-        main.@foreach((widget) => { main.remove(widget); });
-    }
-}
-
-}
diff --git a/main/src/ui/unified_window.vala b/main/src/ui/unified_window.vala
index 8244c67a..3a419161 100644
--- a/main/src/ui/unified_window.vala
+++ b/main/src/ui/unified_window.vala
@@ -12,7 +12,7 @@ public class UnifiedWindow : Window {
     private ChatInput.View chat_input;
     private ConversationListTitlebar conversation_list_titlebar;
     private ConversationSelector.View filterable_conversation_list;
-    private ConversationSummary.View conversation_frame;
+    private ConversationSummary.ConversationView conversation_frame;
     private ConversationTitlebar conversation_titlebar;
     private HeaderBar placeholder_headerbar = new HeaderBar() { title="Dino", show_close_button=true, visible=true };
     private Paned headerbar_paned = new Paned(Orientation.HORIZONTAL) { visible=true };
@@ -69,7 +69,7 @@ public class UnifiedWindow : Window {
 
     private void setup_unified() {
         chat_input = new ChatInput.View(stream_interactor) { visible=true };
-        conversation_frame = new ConversationSummary.View(stream_interactor) { visible=true };
+        conversation_frame = new ConversationSummary.ConversationView(stream_interactor) { visible=true };
         filterable_conversation_list = new ConversationSelector.View(stream_interactor) { visible=true };
 
         Grid grid = new Grid() { orientation=Orientation.VERTICAL, visible=true };
diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala
index f1355b39..a2dde504 100644
--- a/main/src/ui/util/helper.vala
+++ b/main/src/ui/util/helper.vala
@@ -119,8 +119,8 @@ public static void force_error_color(Gtk.Widget widget, string selector = "*") {
 }
 
 public static bool is_dark_theme(Gtk.Widget widget) {
-    Gdk.RGBA bg = widget.get_style_context().get_background_color(StateFlags.NORMAL);
-    return (bg.red < 0.5 && bg.green < 0.5 && bg.blue < 0.5);
+    Gdk.RGBA bg = widget.get_style_context().get_color(StateFlags.NORMAL);
+    return (bg.red > 0.5 && bg.green > 0.5 && bg.blue > 0.5);
 }
 
 public static bool is_24h_format() {
-- 
cgit v1.2.3-70-g09d2