diff --git a/briar-gtk/briar_gtk/presenters/main_window.py b/briar-gtk/briar_gtk/presenters/main_window.py
index cb81cd1c3771d3369fecc1042098fa38040645e7..4bafce689ef4051c09b4bbee3c19e7716b35c573 100644
--- a/briar-gtk/briar_gtk/presenters/main_window.py
+++ b/briar-gtk/briar_gtk/presenters/main_window.py
@@ -4,7 +4,6 @@
 
 from briar_gtk.handlers.notification import NotificationHandler
 from briar_gtk.presenters.private_chat import PrivateChatPresenter
-from briar_gtk.presenters.sidebar import SidebarPresenter
 from briar_gtk.define import APP
 from briar_gtk.views.private_chat import PrivateChatView
 from briar_gtk.views.sidebar import SidebarView
@@ -13,12 +12,13 @@ from briar_gtk.widgets.about_dialog import AboutDialogWidget
 
 class MainWindowPresenter:
 
-    def __init__(self, main_window_view, builder):
-        self._main_window_view = main_window_view
-        self._builder = builder
+    def __init__(self, view):
+        self._notification_handler = NotificationHandler()
+        self._private_chat_presenter = None
+        self._sidebar_presenter = SidebarView(view.builder).presenter
         self._signals = list()
+        self._view = view
 
-        self._setup_children()
         self._setup_destroy_listener()
 
     @staticmethod
@@ -27,45 +27,30 @@ class MainWindowPresenter:
         about_dialog.show()
 
     def open_change_contact_alias_dialog(self):
-        if self._private_chat_presenter is not None:
+        if isinstance(self._private_chat_presenter, PrivateChatPresenter):
             self._private_chat_presenter.open_change_contact_alias_dialog()
 
     def open_delete_all_messages_dialog(self):
-        if self._private_chat_presenter is not None:
+        if isinstance(self._private_chat_presenter, PrivateChatPresenter):
             self._private_chat_presenter.open_delete_all_messages_dialog()
 
     def open_delete_contact_dialog(self):
-        if self._private_chat_presenter is not None:
+        if isinstance(self._private_chat_presenter, PrivateChatPresenter):
             self._private_chat_presenter.open_delete_contact_dialog()
 
     def close_private_chat(self):
-        if self._private_chat_presenter is not None:
+        if isinstance(self._private_chat_presenter, PrivateChatPresenter):
             self._private_chat_presenter.close_private_chat()
             self._private_chat_presenter = None
 
     def open_private_chat(self, contact_id):
-        private_chat_view = PrivateChatView(self._builder)
-        self._private_chat_presenter = PrivateChatPresenter(
-            contact_id, private_chat_view, self._sidebar_presenter,
-            self._builder, APP().api)
-
-    def _setup_children(self):
-        self._setup_notification_handler()
-        self._setup_sidebar_presenter()
-        self._private_chat_presenter = None
-        contact_name_label = self._builder.get_object("contact_name")
-        contact_name_label.set_text("")
-
-    def _setup_notification_handler(self):
-        self._notification_handler = NotificationHandler()
-
-    def _setup_sidebar_presenter(self):
-        sidebar_view = SidebarView(self._builder)
-        self._sidebar_presenter = SidebarPresenter(
-            sidebar_view, APP().api)
+        self.close_private_chat()
+        private_chat_view = PrivateChatView(
+            self._view.builder, contact_id, self._sidebar_presenter)
+        self._private_chat_presenter = private_chat_view.presenter
 
     def _setup_destroy_listener(self):
-        self._main_window_view.connect("destroy", self._on_destroy)
+        self._view.connect("destroy", self._on_destroy)
 
     # pylint: disable=unused-argument
     def _on_destroy(self, widget):
diff --git a/briar-gtk/briar_gtk/presenters/private_chat.py b/briar-gtk/briar_gtk/presenters/private_chat.py
index 6af1ca55abce12bed2cbcf57d0ebb254222293c5..dc9f4d1dd8cd907fb4b9dd8a84c336c467ee0924 100644
--- a/briar-gtk/briar_gtk/presenters/private_chat.py
+++ b/briar-gtk/briar_gtk/presenters/private_chat.py
@@ -2,8 +2,10 @@
 # SPDX-License-Identifier: AGPL-3.0-only
 # License-Filename: LICENSE.md
 
+import time
+
 from gettext import gettext as _
-from gi.repository import Gtk
+from gi.repository import Gtk, GLib
 
 from briar_wrapper.models.contacts import Contacts
 from briar_wrapper.models.private_chat import PrivateChat
@@ -14,48 +16,26 @@ from briar_gtk.widgets.edit_dialog import EditDialog
 
 # pylint: disable=too-many-arguments
 class PrivateChatPresenter:
-    _current_contact_id = 0
-
-    def __init__(self, contact_id, private_chat_view, sidebar_presenter,
-                 builder, api):
-        self._private_chat_view = private_chat_view
-        self._sidebar_presenter = sidebar_presenter
-        self._builder = builder
-        self._api = api
-        self.open_private_chat(contact_id)
-
-    def close_private_chat(self):  # formerly `show_sidebar`
-        main_window_leaflet = self._builder.get_object("main_window_leaflet")
-        main_window_leaflet.set_visible_child(
-            self._builder.get_object("sidebar_box"))
-
-        main_content_stack = self._builder.get_object("main_content_stack")
-        chat_view = main_content_stack.get_child_by_name("chat_view")
-        chat_view.hide()
-
-        chat_placeholder = main_content_stack.get_child_by_name(
-            "chat_placeholder")
-        chat_placeholder.show()
 
-        self._clear_history_container()
+    def __init__(self, private_chat_view, sidebar_presenter):
+        self._signals = list()
 
-        contacts_list_box = self._builder.get_object("contacts_list_box")
-        contacts_list_box.unselect_all()
+        # TODO: Move whole sidebar presenter logic into briar_wrapper by
+        # notifying sidebar about changes to private chat from model
+        self._sidebar_presenter = sidebar_presenter
 
-        contact_name_label = self._builder.get_object("contact_name")
-        contact_name_label.set_text("")
+        self._view = private_chat_view
 
-        self._current_contact_id = 0
-        self._builder.get_object("chat_menu_button").hide()
+        self._open_private_chat()
 
     def open_change_contact_alias_dialog(self):
-        if self._current_contact_id == 0:
+        if self._view.contact_id == 0:
             raise Exception("Can't change contact alias with ID 0")
 
         confirmation_dialog = EditDialog(
             parent=APP().window,
             flags=Gtk.DialogFlags.MODAL,
-            placeholder=self._get_contact_name(self._current_contact_id)
+            placeholder=self._get_contact_name(self._view.contact_id)
         )
 
         confirmation_dialog.set_title(_("Change contact name"))
@@ -64,14 +44,13 @@ class PrivateChatPresenter:
         user_alias = confirmation_dialog.get_entry().get_text()
         confirmation_dialog.destroy()
         if (response == Gtk.ResponseType.OK) and (user_alias != ''):
-            Contacts(APP().api).set_alias(self._current_contact_id, user_alias)
-            contact_name_label = self._builder.get_object("contact_name")
-            contact_name_label.set_text(user_alias)
+            Contacts(APP().api).set_alias(self._view.contact_id, user_alias)
+            self._set_contact_name_label(user_alias)
             self._sidebar_presenter.refresh_contacts()
             # TODO: Update name in chat history
 
     def open_delete_all_messages_dialog(self):
-        if self._current_contact_id == 0:
+        if self._view.contact_id == 0:
             raise Exception("Can't delete all messages with contact ID 0")
 
         confirmation_dialog = Gtk.MessageDialog(
@@ -89,7 +68,7 @@ class PrivateChatPresenter:
         confirmation_dialog.show_all()
 
     def open_delete_contact_dialog(self):
-        if self._current_contact_id == 0:
+        if self._view.contact_id == 0:
             raise Exception("Can't delete contact with ID 0")
 
         confirmation_dialog = Gtk.MessageDialog(
@@ -107,14 +86,30 @@ class PrivateChatPresenter:
         confirmation_dialog.connect("response", self._delete_contact)
         confirmation_dialog.show_all()
 
-    def open_private_chat(self, contact_id):
-        contact_name = self._get_contact_name(contact_id)
+    def close_private_chat(self):  # formerly `show_sidebar`
+        main_content_stack = self._view.builder.get_object(
+            "main_content_stack")
+        self._hide_chat_view(main_content_stack)
+        self._show_chat_placeholder(main_content_stack)
+        self._show_sidebar_box()
+        self._clear_history_container()
+        self._unselect_contact()
+        self._set_contact_name_label("")
+        self._view.contact_id = 0
+        self._hide_chat_menu_button()
+        self._disconnect_chat_entry_signals()
+
+    def disconnect_signals(self):
+        for signal in self._signals:
+            APP().api.socket_listener.disconnect(signal)
+
+    def _open_private_chat(self):
+        contact_name = self._get_contact_name(self._view.contact_id)
         self._prepare_chat_view(contact_name)
-        self._setup_private_chat_widget(contact_name, contact_id)
-        self._current_contact_id = contact_id
+        self._load_content(contact_name)
 
     @staticmethod
-    def _get_contact_name(contact_id):
+    def _get_contact_name(contact_id):  # TODO: Move into briar_wrapper
         name = ""
         for contact in Contacts(APP().api).get():
             if contact["contactId"] == contact_id:
@@ -124,75 +119,154 @@ class PrivateChatPresenter:
                 break
         return name
 
-    def _delete_all_messages(self, widget, response_id):
-        if response_id == Gtk.ResponseType.OK:
-            private_chat = PrivateChat(APP().api, self._current_contact_id)
-            private_chat.delete_all_messages()
-            self._sidebar_presenter.refresh_contacts()
-            self.close_private_chat()
-        widget.destroy()
+    def _prepare_chat_view(self, contact_name):
+        main_content_stack = self._view.builder.get_object(
+            "main_content_stack")
 
-    def _delete_contact(self, widget, response_id):
-        if response_id == Gtk.ResponseType.OK:
-            Contacts(APP().api).delete(self._current_contact_id)
-            self._sidebar_presenter.refresh_contacts()
-            self.close_private_chat()
-        widget.destroy()
+        self._hide_chat_placeholder(main_content_stack)
+        self._show_chat_view(main_content_stack)
+        self._show_main_content_container()
+        self._set_contact_name_label(contact_name)
+        self._show_chat_menu_button()
 
-    def _prepare_chat_view(self, contact_name):
-        main_content_stack = self._builder.get_object("main_content_stack")
+    @staticmethod
+    def _hide_chat_placeholder(main_content_stack):
         chat_placeholder = main_content_stack.get_child_by_name(
             "chat_placeholder")
-        if self._no_chat_opened():
-            chat_placeholder.hide()
-        else:
-            self._clear_history_container()
+        chat_placeholder.hide()
 
+    @staticmethod
+    def _show_chat_view(main_content_stack):
         chat_view = main_content_stack.get_child_by_name("chat_view")
         chat_view.show()
-        main_window_leaflet = self._builder.get_object("main_window_leaflet")
-        main_content_container = self._builder.get_object(
+
+    def _show_main_content_container(self):
+        main_window_leaflet = self._view.builder.get_object(
+            "main_window_leaflet")
+        main_content_container = self._view.builder.get_object(
             "main_content_container")
         main_window_leaflet.set_visible_child(main_content_container)
-        contact_name_label = self._builder.get_object("contact_name")
+
+    def _set_contact_name_label(self, contact_name):
+        contact_name_label = self._view.builder.get_object("contact_name")
         contact_name_label.set_text(contact_name)
-        self._builder.get_object("chat_menu_button").show()
 
-    def _no_chat_opened(self):
-        main_content_stack = self._builder.get_object("main_content_stack")
-        chat_placeholder = main_content_stack.get_child_by_name(
-            "chat_placeholder")
-        return chat_placeholder.get_visible()
+    def _show_chat_menu_button(self):
+        chat_menu_button = self._view.builder.get_object("chat_menu_button")
+        chat_menu_button.show()
 
-    def _clear_history_container(self):
-        history_container = self._builder.get_object("history_container")
-        children = history_container.get_children()
-        for child in children:
-            child.destroy()
+    def _load_content(self, contact_name):
+        private_chat = PrivateChat(APP().api, self._view.contact_id)
+        messages = private_chat.get()
+
+        self._view.setup_view(contact_name)
+        self._view.show_messages(messages)
+        self._setup_message_listener()
+        self._mark_messages_read(messages, private_chat)
+        self._setup_history_container()
+        self._setup_chat_entry()
+
+    def _setup_message_listener(self):
+        # TODO: Move into briar_wrapper by adding function to PrivateChatModel
+        socket_listener = APP().api.socket_listener
+        signal_id = socket_listener.connect("ConversationMessageReceivedEvent",
+                                            self._view.add_message_async)
+        self._signals.append(signal_id)
 
-    def _setup_private_chat_widget(self, contact_name, contact_id):
-        self._private_chat_view.setup_view(contact_name, contact_id)
-        self._private_chat_view.load_content()
-        history_container = self._builder.get_object("history_container")
-        history_container.add(self._private_chat_view)
+    @staticmethod
+    def _mark_messages_read(messages, private_chat):
+        for message in messages:
+            if message.get("read", True) is False:
+                GLib.idle_add(private_chat.mark_read, message["id"])
+
+    def _setup_history_container(self):
+        history_container = self._view.builder.get_object("history_container")
+        history_container.add(self._view)
         history_container.show_all()
 
-        self._disconnect_chat_entry_signals()
-        chat_entry = self._builder.get_object("chat_entry")
+    def _setup_chat_entry(self):
+        chat_entry = self._view.builder.get_object("chat_entry")
         self._chat_entry_signal_id = chat_entry.connect(
             "activate", self._on_chat_entry_activate
         )
         chat_entry.grab_focus()
 
+    @staticmethod
+    def _hide_chat_view(main_content_stack):
+        chat_view = main_content_stack.get_child_by_name("chat_view")
+        chat_view.hide()
+
+    @staticmethod
+    def _show_chat_placeholder(main_content_stack):
+        chat_placeholder = main_content_stack.get_child_by_name(
+            "chat_placeholder")
+        chat_placeholder.show()
+
+    def _unselect_contact(self):
+        contacts_list_box = self._view.builder.get_object("contacts_list_box")
+        contacts_list_box.unselect_all()
+
+    def _hide_chat_menu_button(self):
+        chat_menu_button = self._view.builder.get_object("chat_menu_button")
+        chat_menu_button.hide()
+
+    def _show_sidebar_box(self):
+        main_window_leaflet = self._view.builder.get_object(
+            "main_window_leaflet")
+        sidebar_box = self._view.builder.get_object("sidebar_box")
+        main_window_leaflet.set_visible_child(sidebar_box)
+
+    def _delete_all_messages(self, widget, response_id):
+        if response_id == Gtk.ResponseType.OK:
+            private_chat = PrivateChat(APP().api, self._view.contact_id)
+            private_chat.delete_all_messages()
+            self._sidebar_presenter.refresh_contacts()
+            self.close_private_chat()
+        widget.destroy()
+
+    def _delete_contact(self, widget, response_id):
+        if response_id == Gtk.ResponseType.OK:
+            Contacts(APP().api).delete(self._view.contact_id)
+            self._sidebar_presenter.refresh_contacts()
+            self.close_private_chat()
+        widget.destroy()
+
+    def _clear_history_container(self):
+        history_container = self._view.builder.get_object("history_container")
+        children = history_container.get_children()
+        for child in children:
+            child.destroy()
+
     def _disconnect_chat_entry_signals(self):
         if not hasattr(self, "_chat_entry_signal_id"):
             return
-        chat_entry = self._builder.get_object("chat_entry")
+        chat_entry = self._view.builder.get_object("chat_entry")
         chat_entry.disconnect(self._chat_entry_signal_id)
         del self._chat_entry_signal_id
 
     def _on_chat_entry_activate(self, widget):
         if len(widget.get_text()) == 0:
             return
-        self._private_chat_view.send_message(widget)
+        self._send_message(widget)
         self._sidebar_presenter.refresh_contacts()
+
+    def _send_message(self, widget):
+        message = widget.get_text()
+        private_chat = PrivateChat(APP().api, self._view.contact_id)
+        private_chat.send(message)
+
+        # TODO: Move into briar_wrapper by emitting event on private_chat.send
+        self._view.add_message(
+            {
+                "text": message,
+                "local": True,
+                "sent": False,
+                "seen": False,
+
+                # TODO: Remove once web events updating is implemented
+                "no_stored_indicator": True,
+
+                "timestamp": int(round(time.time() * 1000))
+            })
+        widget.set_text("")
+        GLib.idle_add(self._view.scroll_to_bottom)
diff --git a/briar-gtk/briar_gtk/views/main_window.py b/briar-gtk/briar_gtk/views/main_window.py
index 3a46d6ddc08109073c5e8fa0ae64cf9cf9217467..d063f2740609f28a8afda1829d519ffdcbe9793b 100644
--- a/briar-gtk/briar_gtk/views/main_window.py
+++ b/briar-gtk/briar_gtk/views/main_window.py
@@ -17,11 +17,9 @@ class MainWindowView(Gtk.Overlay):
 
     def __init__(self, window):
         super().__init__()
-        builder = self._setup_builder()
-        self.presenter = MainWindowPresenter(self, builder)
-        self._setup_view(builder, window)
-        self.show_all()
-        builder.get_object("chat_menu_button").hide()  # TODO: Make default
+        self.builder = self._setup_builder()
+        self.presenter = MainWindowPresenter(self)
+        self._setup_view(window)
 
     def _setup_builder(self):
         builder = Gtk.Builder.new()
@@ -37,18 +35,19 @@ class MainWindowView(Gtk.Overlay):
         builder.connect_signals(self)
         return builder
 
-    def _setup_view(self, builder, window):
-        self._setup_main_window_stack(builder)
-        self._setup_headerbar_stack_holder(builder, window)
+    def _setup_view(self, window):
+        self._setup_main_window_stack()
+        self._setup_header_bar_stack_holder(window)
 
-    def _setup_main_window_stack(self, builder):
-        main_window_stack = builder.get_object("main_window_stack")
+    def _setup_main_window_stack(self):
+        main_window_stack = self.builder.get_object("main_window_stack")
         main_window_stack.show_all()
         self.add(main_window_stack)
-        builder.get_object("chat_menu_button").hide()
+        self.show_all()
 
-    @staticmethod
-    def _setup_headerbar_stack_holder(builder, window):
-        headerbar_stack_holder = builder.get_object("headerbar_stack_holder")
-        headerbar_stack_holder.show_all()
-        window.set_titlebar(headerbar_stack_holder)
+    def _setup_header_bar_stack_holder(self, window):
+        header_bar_stack_holder = self.builder.get_object(
+            "headerbar_stack_holder")
+        header_bar_stack_holder.show_all()
+        self.builder.get_object("chat_menu_button").hide()
+        window.set_titlebar(header_bar_stack_holder)
diff --git a/briar-gtk/briar_gtk/views/private_chat.py b/briar-gtk/briar_gtk/views/private_chat.py
index 5833b26739802c497cd84fe3e465773a460e5208..560e0da01377f578525eb550a72258355a8fbabd 100644
--- a/briar-gtk/briar_gtk/views/private_chat.py
+++ b/briar-gtk/briar_gtk/views/private_chat.py
@@ -5,62 +5,89 @@
 # Initial version based on GNOME Fractal
 # https://gitlab.gnome.org/GNOME/fractal/-/tags/4.2.2
 import os
-import time
 
 from gi.repository import GLib, Gtk, Handy
 
 from briar_wrapper.models.private_chat import PrivateChat
 
 from briar_gtk.define import APP, RESOURCES_DIR
+from briar_gtk.presenters.private_chat import PrivateChatPresenter
 from briar_gtk.widgets.private_message import PrivateMessageWidget
 
 
 # pylint: disable=too-many-instance-attributes
 class PrivateChatView(Gtk.Overlay):
 
-    # TODO: Move more logic into PrivateChatPresenter
-
     CONTAINER_UI = "private_chat.ui"
 
-    def __init__(self, builder):
+    def __init__(self, builder, contact_id, sidebar_presenter):
         super().__init__()
-        self.builder = builder
-
-        self._signals = list()
         self._contact_name = ""
-        self._contact_id = -1
+        self._draw_signal_id = -1
+        self._messages_box = None
+        self._messages_count = 0
         self._previous_message = dict()
 
-    def send_message(self, widget):
-        message = widget.get_text()
-        private_chat = PrivateChat(APP().api, self._contact_id)
-        private_chat.send(message)
+        self.builder = builder
+        self.contact_id = contact_id
+        self.presenter = PrivateChatPresenter(self, sidebar_presenter)
 
-        # TODO: Doesn't work after Ctrl + W
-        self._add_message(
-            {
-                "text": message,
-                "local": True,
-                "sent": False,
-                "seen": False,
+    def setup_view(self, contact_name):
+        self._contact_name = contact_name
+        self._setup_builder()
 
-                # TODO: Remove once web events updating is implemented
-                "no_stored_indicator": True,
+        self._setup_messages_box()
+        clamp = self._setup_clamp()
+        self._setup_messages_column(clamp)
+        self._setup_messages_scroll()
 
-                "timestamp": int(round(time.time() * 1000))
-            })
-        widget.set_text("")
-        GLib.idle_add(self._scroll_to_bottom)
+        self.builder.connect_signals(self)
+        self._setup_destroy_listener()
 
-    def setup_view(self, contact_name, contact_id):
-        self._contact_name = contact_name
-        self._contact_id = contact_id
-        self._add_from_resource(self.CONTAINER_UI)
+    def show_messages(self, messages):
+        self._messages_count = len(messages)
+        for message in messages:
+            # Abusing idle_add function here because otherwise the message box
+            # is too small and scrolling cuts out messages
+            GLib.idle_add(self.add_message, message)
+
+    def add_message(self, message):
+        if self._is_not_message(message):
+            return
+        message_widget = PrivateMessageWidget(
+            self._contact_name,
+            message,
+            self._previous_message
+        )
+        self._previous_message = message
+        self._messages_box.add(message_widget)
+
+    def scroll_to_bottom(self):
+        messages_scroll = self.builder.get_object("messages_scroll")
+        adjustment = messages_scroll.get_vadjustment()
+        adjustment.set_value(
+            adjustment.get_upper() - adjustment.get_page_size()
+        )
+
+    def add_message_async(self, message):
+        if message["data"]["contactId"] != self.contact_id:
+            return
+        GLib.idle_add(self._add_message_and_scroll, message["data"])
+        if message["data"].get("read", True) is False:
+            private_chat = PrivateChat(APP().api, self.contact_id)
+            GLib.idle_add(private_chat.mark_read, message["data"]["id"])
+
+    def _setup_builder(self):
+        self.builder.add_from_resource(
+            os.path.join(RESOURCES_DIR, self.CONTAINER_UI)
+        )
 
+    def _setup_messages_box(self):
         self._messages_box = Gtk.ListBox()
         self._messages_box.get_style_context().add_class("messages-history")
         self._messages_box.show()
 
+    def _setup_clamp(self):
         clamp = Handy.Clamp.new()
         clamp.set_maximum_size(800)
         clamp.set_tightening_threshold(600)
@@ -68,92 +95,42 @@ class PrivateChatView(Gtk.Overlay):
         clamp.set_vexpand(True)
         clamp.add(self._messages_box)
         clamp.show()
+        return clamp
 
+    def _setup_messages_column(self, clamp):
         messages_column = self.builder.get_object("messages_column")
         messages_column.get_style_context().add_class("messages-box")
         messages_column.add(clamp)
         messages_column.show()
 
+    def _setup_messages_scroll(self):  # TODO: (Hopefully) remove hack in GTK 4
         messages_scroll = self.builder.get_object("messages_scroll")
         self._draw_signal_id = messages_scroll.connect(
             "draw", self._on_message_scroll_draw
         )
         self.add(messages_scroll)
 
-        self.builder.connect_signals(self)
-        self._setup_destroy_listener()
-
-    def _add_from_resource(self, ui_filename):
-        self.builder.add_from_resource(
-            os.path.join(RESOURCES_DIR, ui_filename)
-        )
-
     def _setup_destroy_listener(self):
         self.connect("destroy", self._on_destroy)
 
     # pylint: disable=unused-argument
     def _on_destroy(self, widget):
-        self._disconnect_signals()
-
-    def _disconnect_signals(self):
-        for signal in self._signals:
-            APP().api.socket_listener.disconnect(signal)
-
-    def load_content(self):
-        private_chat = PrivateChat(APP().api, self._contact_id)
-        messages_list = private_chat.get()
-        self._messages_count = len(messages_list)
-        for message in messages_list:
-            # Abusing idle_add function here because otherwise the message box
-            # is too small and scrolling cuts out messages
-            GLib.idle_add(self._add_message, message)
-            if message.get("read", True) is False:
-                GLib.idle_add(private_chat.mark_read, message["id"])
-        socket_listener = APP().api.socket_listener
-        signal_id = socket_listener.connect("ConversationMessageReceivedEvent",
-                                            self._add_message_async)
-        self._signals.append(signal_id)
-
-    def _add_message(self, message):
-        if self._is_not_message(message):
-            return
-        message_widget = PrivateMessageWidget(
-            self._contact_name,
-            message,
-            self._previous_message
-        )
-        self._previous_message = message
-        self._messages_box.add(message_widget)
+        self.presenter.disconnect_signals()
 
     @staticmethod
     def _is_not_message(message):
         return "text" not in message
 
-    def _add_message_async(self, message):
-        if message["data"]["contactId"] != self._contact_id:
-            return
-        GLib.idle_add(self._add_message_and_scroll, message["data"])
-        if message["data"].get("read", True) is False:
-            private_chat = PrivateChat(APP().api, self._contact_id)
-            GLib.idle_add(private_chat.mark_read, message["data"]["id"])
-
     def _add_message_and_scroll(self, message):
-        self._add_message(message)
-        GLib.idle_add(self._scroll_to_bottom)
+        self.add_message(message)
+        GLib.idle_add(self.scroll_to_bottom)
 
     # pylint: disable=unused-argument
     def _on_message_scroll_draw(self, widget, cairo_context):
-        self._scroll_to_bottom()
+        self.scroll_to_bottom()
         if self._draw_signal_is_not_needed():
             widget.disconnect(self._draw_signal_id)
 
-    def _scroll_to_bottom(self):
-        messages_scroll = self.builder.get_object("messages_scroll")
-        adjustment = messages_scroll.get_vadjustment()
-        adjustment.set_value(
-            adjustment.get_upper() - adjustment.get_page_size()
-        )
-
     def _draw_signal_is_not_needed(self):
         if self._messages_count == 0:
             return True
diff --git a/briar-gtk/briar_gtk/views/sidebar.py b/briar-gtk/briar_gtk/views/sidebar.py
index 710a7faed03b36d5ecca9e82a3a4fd5e749d5841..d425300c6c039b2f5bb632c11b6e86ea5697aa1e 100644
--- a/briar-gtk/briar_gtk/views/sidebar.py
+++ b/briar-gtk/briar_gtk/views/sidebar.py
@@ -1,16 +1,16 @@
 # Copyright (c) 2020 Nico Alt
 # SPDX-License-Identifier: AGPL-3.0-only
 # License-Filename: LICENSE.md
-
+from briar_gtk.define import APP
+from briar_gtk.presenters.sidebar import SidebarPresenter
 from briar_gtk.widgets.contact_row import ContactRowWidget
 
 
 class SidebarView:
 
-    # TODO: Move more logic into SidebarPresenter
-
     def __init__(self, builder):
         self._builder = builder
+        self.presenter = SidebarPresenter(self, APP().api)
 
     def show_contacts(self, contact_list, selected_contact):
         self._clear_contact_list()