diff --git a/app.briar.gtk.json b/app.briar.gtk.json
index 93954c4fb696ddc41d9748a205cf7d9330fcf77a..e7c2cf6c3c9b947eae68d587a328cc46049f4a9b 100644
--- a/app.briar.gtk.json
+++ b/app.briar.gtk.json
@@ -45,10 +45,10 @@
             ]
         },
         {
-            "name": "python3-requests2",
+            "name": "python-dependencies",
             "buildsystem": "simple",
             "build-commands": [
-                "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} requests2"
+                "pip3 install --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} requests2 websockets"
             ],
             "sources": [
                 {
@@ -75,7 +75,12 @@
                     "type": "file",
                     "url": "https://files.pythonhosted.org/packages/09/0f/9dacaa075f8e46d463248c28dbb9770445d2e04cca5c4aa23d431c9ba861/requests2-2.16.0.tar.gz",
                     "sha256": "f7685b2d620a172053cced288dddf12d4b3441a382cc382821c0f36c02488516"
-                }
+                },
+		{
+		    "type": "file",
+		    "url": "https://files.pythonhosted.org/packages/ba/60/59844a5cef2428cb752bd4f446b72095b1edee404a58c27e87cd12a141e2/websockets-7.0.tar.gz",
+		    "sha256": "08e3c3e0535befa4f0c4443824496c03ecc25062debbcf895874f8a0b4c97c9f"
+		}
             ]
         },
         {
diff --git a/src/briar/api/models/private_chat.py b/src/briar/api/models/private_chat.py
index 07bc9ddefdcf9e3dfda25ed2aa89df0feaeb9932..4332c49274443cef75731b1d754d88e5ec62bfd4 100644
--- a/src/briar/api/models/private_chat.py
+++ b/src/briar/api/models/private_chat.py
@@ -3,6 +3,7 @@
 # License-Filename: LICENSE.md
 
 from briar.api.models.model import Model
+from briar.api.models.socket_listener import SocketListener
 
 from requests import get as _get
 from urllib.parse import urljoin
@@ -15,3 +16,8 @@ class PrivateChat(Model):
         url = urljoin(self._constants.get_base_url(), 'messages/' + contact_id)
         r = _get(url, headers=headers)
         return r.json()
+
+    def watch_messages(self, contact_id, callback):
+        socket_listener = SocketListener(self._api)
+        socket_listener.watch(callback, "ConversationMessageReceivedEvent",
+                              contact_id=contact_id)
diff --git a/src/briar/api/models/socket_listener.py b/src/briar/api/models/socket_listener.py
new file mode 100644
index 0000000000000000000000000000000000000000..93646f4a75b0de4cc2fef4c5d13c853799c89a28
--- /dev/null
+++ b/src/briar/api/models/socket_listener.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2019 Nico Alt
+# SPDX-License-Identifier: AGPL-3.0-only
+# License-Filename: LICENSE.md
+
+from briar.api.models.model import Model
+
+import asyncio
+import json
+from threading import Thread
+import websockets
+
+
+class SocketListener(Model):
+
+    def watch(self, callback, event, contact_id="0"):
+        websocket_thread = Thread(target=self._start_watch_loop,
+                                  args=(callback, event, contact_id),
+                                  daemon=True)
+        websocket_thread.start()
+
+    def _start_watch_loop(self, callback, event, contact_id="0"):
+        loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(loop)
+        loop.create_task(self._start_websocket(callback, event, contact_id))
+        loop.run_forever()
+        loop.close()
+
+    async def _start_websocket(self, callback, event, contact_id="0"):
+        async with websockets.connect('ws://localhost:7000/v1/ws') as websocket:
+            await websocket.send(self._api.auth_token)
+            await self._watch_messages(websocket, callback)
+
+    async def _watch_messages(self, websocket, callback):
+        while not websocket.closed and not asyncio.get_event_loop().is_closed():
+            message = await websocket.recv()
+            m = json.loads(message)
+            if m['name'] == 'ConversationMessageReceivedEvent':
+                callback(m['data'])
+        if not asyncio.get_event_loop().is_closed():
+            asyncio.get_event_loop().create_task(
+                self._watch_messages(websocket, callback))
diff --git a/src/briar/gtk/containers/chat.py b/src/briar/gtk/containers/chat.py
index 21d8d121e647355aa7a86e0c6136a97e7e5e9d13..803bd82f2dbdfc19b3e4adfdfec210ca2155986a 100644
--- a/src/briar/gtk/containers/chat.py
+++ b/src/briar/gtk/containers/chat.py
@@ -8,7 +8,7 @@ from briar.gtk.define import App
 
 import gi
 gi.require_version('Gtk', '3.0')
-from gi.repository import Gtk
+from gi.repository import GLib, Gtk
 
 
 class ChatContainer(Container):
@@ -27,8 +27,15 @@ class ChatContainer(Container):
     def _load_content(self, contact_id):
         private_chat = PrivateChat(self._api)
         messages_list = private_chat.get(contact_id)
-        messages_list_box = self.builder.get_object("contacts_list")
+        self._messages_list_box = self.builder.get_object("contacts_list")
         for message in messages_list:
-            message_label = Gtk.Label(message["text"])
-            message_label.show()
-            messages_list_box.add(message_label)
+            self._add_message(message)
+        private_chat.watch_messages(contact_id, self._add_message_async)
+
+    def _add_message(self, message):
+        message_label = Gtk.Label(message["text"])
+        message_label.show()
+        self._messages_list_box.add(message_label)
+
+    def _add_message_async(self, message):
+        GLib.idle_add(self._add_message, message)