diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2af1724a6eeae9f6171b27ccb4595f42b76a1995..4c0193b940f2c455ea2380d10ddc65a6b55c2174 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -25,6 +25,7 @@ build-pureos-gcc-byzantium-no-purple:
   stage: build
   script:
     - apt-get -y build-dep .
+    - apt-get -y install git
     - export LC_ALL=C.UTF-8
     - meson . _build -Dpurple=disabled
     - ninja -C _build
@@ -35,7 +36,7 @@ build-pureos-gcc-byzantium:
   stage: build
   script:
     - apt-get -y build-dep .
-    - apt-get -y install libgtk-3-bin xvfb
+    - apt-get -y install libgtk-3-bin xvfb git
     - export LC_ALL=C.UTF-8
     - meson . _build -Db_coverage=true --werror
     - ninja -C _build
@@ -51,7 +52,7 @@ test:debian-gcc:
     - build-pureos-gcc-byzantium
   script:
     - apt-get -y build-dep .
-    - apt-get -y install libgtk-3-bin xvfb lcov
+    - apt-get -y install libgtk-3-bin xvfb lcov git
     - export G_DEBUG=fatal-warnings
     - export LC_ALL=C.UTF-8
     - xvfb-run -a -s "-screen 0 1024x768x24" ninja -C _build test
@@ -66,7 +67,7 @@ check-po:
     - build-pureos-gcc-byzantium
   before_script:
     - apt-get -y update
-    - apt-get -y install intltool
+    - apt-get -y install intltool git
   script:
     # barf on untranslated C files. Seems intltool
     # can't be told to exit with non-zero exit status
diff --git a/.gitmodules b/.gitmodules
index fa136361fc689a5cf44a753279a18c5d524de098..6c4ab7bc691c7567f4d89900fa827606be8301a0 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
 [submodule "subprojects/libgd"]
 	path = subprojects/libgd
 	url = https://gitlab.gnome.org/GNOME/libgd.git
+[submodule "subprojects/libcmatrix"]
+	path = subprojects/libcmatrix
+	url = https://source.puri.sm/Librem5/libcmatrix.git
diff --git a/NEWS b/NEWS
index b62c97c16fef1c3382540a7d64137279438e0f5f..132a08daa9cd9f3e96d00a24c703c9911c1f39ac 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,15 @@
+Overview of Changes in Chats 0.7.0~rc0. Mon, 17 October 2022
+============================================================
+* Move all matrix code to libcmatrix
+* Matrix support is no longer experimental
+* Fix build with GNOME desktop 43
+* Update Translations
+  - Swedish
+  - Ukrainian
+  - Hebrew
+  - Romanian
+  - Spanish
+
 Overview of Changes in Chats 0.6.7. Tue, 25 June 2022
 =====================================================
 * Scale down user avatar to speed up UI
diff --git a/data/sm.puri.Chatty.desktop.in b/data/sm.puri.Chatty.desktop.in
index bc0cb41fc957023fe9e7f9c93695912932b8726e..1e3f7aecdcf6b2a2f1cb28474a65b45af2469064 100644
--- a/data/sm.puri.Chatty.desktop.in
+++ b/data/sm.puri.Chatty.desktop.in
@@ -11,3 +11,4 @@ StartupNotify=true
 MimeType=x-scheme-handler/sms
 # Translators: Do NOT translate or transliterate this text (these are enum types)!
 X-Purism-FormFactor=Workstation;Mobile;
+X-Phosh-UsesFeedback=true
diff --git a/debian/changelog b/debian/changelog
index c38abc14775396e9491c661173da9bd2cad10f2c..d20f91ff4ae243e961ca621d35f79361d8d26234 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,128 @@
+chatty (0.7.0~rc0) byzantium; urgency=medium
+
+  [ Mohammed Sadiq ]
+  * tests: Use memory backend for gsettings
+  * matrix: Port to use libcmatrix
+  * mm-chat-info: Fix possible invalid free
+  * log: Sync with upstream
+  * application: Fix nologin commandline argument
+  * build: Bump meson version to 0.56
+  * build: Fix using deprecated meson options
+  * build: Improve meson build summary
+  * log: Fix a thinko
+  * build: Fix validating ui files
+  * po: Add Hebrew translation
+  * mm: Fix removing duplicate items from array
+  * log: Anonymize unicode strings properly
+  * message: Add support for CmEvent messages
+  * tests: Remove matrix-utils test
+  * matrix: Update libcmatrix and adapt to changes
+  * image-item: Don't update image on dispose
+  * tests: Create matrix account from cm_client
+  * matrix: Remove unused code
+  * ma-chat: Remove unused code
+  * secret-store: Remove unused code
+  * matrix-utils: Remove unused code
+  * ma-account: Remove unused code
+  * subprojects: Update cmatrix
+  * Update cmatrix
+  * subprojects: Update cmatrix
+  * build: Point libcmatrix URL to official source
+  * ma-account: Fix a crash when accessing avatar url
+  * message: Use the same API to get file for all protocols
+  * Update cmatrix
+  * ma-chat: Fix unread count
+  * tests: Remove chat history tests
+  * history: Remove matrix support
+  * ma-chat: Remove no longer used code
+  * ma-account: Remove no longer used code
+  * ma-chat: Fix setting room id
+  * application: Add an action to show about dialog
+  * Add chatty-header-bar
+  * window: Use chatty-header-bar for header bar
+  * Add chatty-main-view
+  * window: Use chatty-main-view
+  * chat-view: Remove empty view
+  * enums: Add ChattyChatState
+  * Update cmatrix
+  * chat: Add API to get chat state
+  * ma-chat: Implement get_state()
+  * chat: Add API to accept/reject invites
+  * ma-chat: Implement accept/reject invites
+  * manager: List invited rooms in the top of chat list
+  * ma-account: Add invited chats to chat list
+  * list-row: Mark invited room as such
+  * Add chatty-invite-view
+  * main-view: Use chatty-invite-view
+  * history: Use sqlite transactions
+  * history: Improve setting user version
+  * history: More sqlite transactions
+  * message: Change status atomically
+  * chat-view: Create a new draft to save in history
+  * invite-view: Add a translator comment
+  * Update cmatrix
+  * enums: Add CHATTY_CHAT_VERIFICATION
+  * Add chatty-ma-key-chat
+  * ma-account: Add chat verifications to chat list
+  * avatar: Use system-lock icon for matrix verification
+  * list-row: Hide chat details for key verifications
+  * manager: Place chat verifications at the top of chat list
+  * Add chatty-verification-view
+  * main-view: Use verification-view for verifications
+  * log: Improve abort() on fatal errors
+  * main: Log backtrace on fatal errors
+
+  [ Chris Talbot ]
+  * Add function to call out transmit or receieve issue for MMS
+
+  [ Anders Jonsson ]
+  * po: Update Swedish translation
+  * po: Update Swedish translation
+
+  [ Yuri Chornoivan ]
+  * po: Update Ukrainian translation
+  * po: Update Ukrainian translation
+  * po: Update Ukrainian translation
+  * po: Update Ukrainian translation
+
+  [ Vittorio Monti ]
+  * Update Italian translation
+  * Update Italian translation
+
+  [ Guido Günther ]
+  * data: Indicate that chatty provides LED/haptic feedback
+
+  [ Jeremy Bicha ]
+  * utils: update for GNOME Desktop 43 thumbnail API changes
+
+  [ Марко М. Костић (Marko M. Kostić) ]
+  * Update Serbian translation
+
+  [ Rafael Fontenelle ]
+  * Update Brazilian Portuguese translation
+
+  [ Emin Tufan Çetin ]
+  * Update Turkish translation
+
+  [ Evangelos Ribeiro Tzaras ]
+  * submodule: Point libcmatrix URL to official sources
+  * settings-dialog: Mark "add" button in account page as suggested action
+  * settings-dialog: Default to https if no scheme given in homeserver entry
+
+  [ Daniel Șerbănescu ]
+  * po: Update Romanian translation
+
+  [ Pablo Correa Gómez ]
+  * po: Update Spanish translation
+
+  [ carlosgonz ]
+  * po: Update Spanish translation
+
+  [ Yosef Or Boczko ]
+  * po: Update Hebrew Translation
+
+ -- Mohammed Sadiq <sadiq@sadiqpk.org>  Mon, 17 Oct 2022 11:16:33 +0530
+
 chatty (0.6.7) byzantium; urgency=medium
 
   [ Andrey Skvortsov ]
diff --git a/meson.build b/meson.build
index 66df1e80087f8fe6b41d8fb7e568e66b0a27ba64..9d3a35cbef7de2fee2f84055d34a4ebdac766be6 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,7 @@
 project(
   'chatty', 'c', 'cpp',
-  version: '0.6.7',
-  meson_version: '>= 0.53.0',
+  version: '0.7.0.rc0',
+  meson_version: '>= 0.56.0',
 )
 
 i18n = import('i18n')
@@ -21,27 +21,12 @@ config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('lo
 config_h.set_quoted('PACKAGE_NAME', meson.project_name())
 config_h.set_quoted('PACKAGE_VERSION', meson.project_version())
 
-# TODO: Use has_headers: 'olm/olm.h' when we bump meson requirement
-libolm_dep = cc.find_library('olm', required: true)
-
-if (cc.has_function('olm_account_unpublished_fallback_key', dependencies: libolm_dep))
-  config_h.set('OLM_ACCOUNT_PICKLE_V4', true)
-else
-  config_h.set('OLM_ACCOUNT_PICKLE_V4', false)
-endif
-
-if (cc.has_function('olm_pk_key_from_private', dependencies: libolm_dep))
-  config_h.set('HAVE_OLM3', true)
-else
-  config_h.set('HAVE_OLM2', true)
-endif
-
 configure_file(
   output: 'config.h',
   configuration: config_h,
 )
 add_project_arguments([
-  '-I' + meson.build_root(),
+  '-I' + meson.project_build_root(),
   '-DHAVE_CONFIG_H',
   '-DGLIB_DISABLE_DEPRECATION_WARNINGS',
   '-DG_LOG_USE_STRUCTURED',
@@ -118,6 +103,13 @@ libgd = subproject('libgd',
 
 libgd_dep = libgd.get_variable('libgd_dep')
 
+libcmatrix = subproject('libcmatrix',
+                        default_options: [
+                          'build-examples=false',
+                        ])
+libcmatrix_dep = libcmatrix.get_variable('libcmatrix_dep')
+
+
 subdir('completion')
 subdir('data')
 subdir('help')
@@ -125,8 +117,20 @@ subdir('src')
 subdir('tests')
 subdir('po')
 
-summary({
-  'Enable libpurple': purple_dep.found(),
-})
+system = target_machine.system()
+if system == 'linux'
+  system = 'GNU/Linux'
+endif
+
+summary({'Target': system,
+         'Target arch': target_machine.cpu(),
+         'Compiler': cc.get_id(),
+         'Version': cc.version(),
+         'Linker': cc.get_linker_id(),
+        }, section: 'Toolchain')
+
+summary({'Build type': get_option('buildtype'),
+         'libpurple': purple_dep.found(),
+        }, section: 'Configuration')
 
 meson.add_install_script('build-aux/meson/postinstall.py')
diff --git a/po/LINGUAS b/po/LINGUAS
index 4b483e709be4904885352a3453b11c1fcb8963f4..601220786174957ccff8d236374a93f65afea73f 100644
--- a/po/LINGUAS
+++ b/po/LINGUAS
@@ -7,6 +7,7 @@ es
 fi
 fr
 fur
+he
 ht
 hu
 id
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 62abc7088239b58e50a778dec9a153000e946328..3a4bf969fb33a385d8cd8070202b76d050bd0617 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -13,10 +13,12 @@ src/chatty-contact-list.c
 src/chatty-clock.c
 src/chatty-contact-provider.c
 src/chatty-contact-provider.h
+src/chatty-header-bar.c
 src/chatty-history.c
 src/chatty-history.h
 src/chatty-fp-row.c
 src/chatty-fp-row.h
+src/chatty-invite-view.c
 src/chatty-list-row.c
 src/chatty-list-row.h
 src/chatty-manager.c
@@ -35,6 +37,7 @@ src/chatty-secret-store.c
 src/chatty-secret-store.h
 src/chatty-settings.c
 src/chatty-settings.h
+src/chatty-verification-view.c
 src/chatty-window.c
 src/chatty-window.h
 src/dialogs/chatty-info-dialog.c
@@ -60,6 +63,7 @@ src/matrix/chatty-ma-account.c
 src/matrix/chatty-ma-account.h
 src/matrix/chatty-ma-chat.c
 src/matrix/chatty-ma-chat.h
+src/matrix/chatty-ma-key-chat.c
 src/matrix/matrix-utils.c
 src/matrix/matrix-utils.h
 src/mm/chatty-mmsd.c
@@ -69,7 +73,9 @@ src/ui/chatty-contact-row.ui
 src/ui/chatty-dialog-join-muc.ui
 src/ui/chatty-dialog-new-chat.ui
 src/ui/chatty-file-item.ui
+src/ui/chatty-header-bar.ui
 src/ui/chatty-info-dialog.ui
+src/ui/chatty-invite-view.ui
 src/ui/chatty-ma-chat-info.ui
 src/ui/chatty-pp-chat-info.ui
 src/ui/chatty-mm-chat-info.ui
@@ -78,6 +84,7 @@ src/ui/chatty-list-row.ui
 src/ui/chatty-ma-account-details.ui
 src/ui/chatty-pp-account-details.ui
 src/ui/chatty-settings-dialog.ui
+src/ui/chatty-verification-view.ui
 src/ui/chatty-window.ui
 src/ui/help-overlay.ui
 src/users/chatty-account.c
diff --git a/po/es.po b/po/es.po
index c943c4089c98225493bdc93857c8ee8a5ade9bf6..2fc9d099046f8e2696e077cca798db7856205539 100644
--- a/po/es.po
+++ b/po/es.po
@@ -9,8 +9,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: purism-chatty\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-04-25 03:24+0000\n"
-"PO-Revision-Date: 2022-04-25 17:14+0200\n"
+"POT-Creation-Date: 2022-09-13 03:24+0000\n"
+"PO-Revision-Date: 2022-09-13 10:10+0200\n"
 "Last-Translator: Daniel Mustieles García <daniel.mustieles@gmail.com>\n"
 "Language-Team: Spanish - Spain <gnome-es-list@gnome.org>\n"
 "Language: es_ES\n"
@@ -21,17 +21,14 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=(n != 1)\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/ui/chatty-window.ui:226
+#: src/chatty-application.c:338 src/chatty-window.c:339
+#: src/ui/chatty-window.ui:265
 msgid "Chats"
-msgstr "Chats"
-
-#: data/sm.puri.Chatty.desktop.in:5
-msgid "sm.puri.Chatty"
-msgstr "sm.puri.Chatty"
+msgstr "Chatty"
 
 #: data/sm.puri.Chatty.desktop.in:6
 msgid "SMS and XMPP chat application"
-msgstr "Aplicación de mensajería por SMS y XMPP"
+msgstr "Una Aplicación de mensajería"
 
 #: data/sm.puri.Chatty.desktop.in:7
 msgid "XMPP;SMS;chat;jabber;messaging;modem"
@@ -75,13 +72,11 @@ msgstr "Habilitar sincronización de archivo MAM del servidor"
 
 #: data/sm.puri.Chatty.gschema.xml:37
 msgid "Enable purple"
-msgstr ""
+msgstr "Activar purple"
 
 #: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511
-#, fuzzy
-#| msgid "Disable all accounts"
 msgid "Enable purple accounts"
-msgstr "Desactivar todas las cuentas"
+msgstr "Activar las cuentas de purple"
 
 #: data/sm.puri.Chatty.gschema.xml:43
 msgid "Send typing notifications"
@@ -105,31 +100,31 @@ msgstr "Reemplazar texto a emoticonos"
 
 #: data/sm.puri.Chatty.gschema.xml:62
 msgid "Convert text matching emoticons as real emoticons"
-msgstr "Reemplazar emoticonos textuales que coincidan"
+msgstr "Reemplazar texto por Emoticonos reales"
 
 #: data/sm.puri.Chatty.gschema.xml:67
 msgid "Enter key sends the message"
-msgstr "La tecla intro envía el mensaje"
+msgstr "La tecla Intro envía el mensaje"
 
 #: data/sm.puri.Chatty.gschema.xml:68
 msgid "Whether pressing Enter key sends the message"
-msgstr "Si presionar la tecla Intro enviá el mensaje"
+msgstr "Si presionar la tecla Intro envía el mensaje"
 
 #: data/sm.puri.Chatty.gschema.xml:73
 msgid "Request SMS delivery reports"
-msgstr ""
+msgstr "Solicitar entrega de envío de SMS"
 
 #: data/sm.puri.Chatty.gschema.xml:74
 msgid "Whether to request delivery reports for outgoing SMS"
-msgstr ""
+msgstr "Ya sea para solicitar entregas de SMS salientes"
 
 #: data/sm.puri.Chatty.gschema.xml:79
 msgid "Enable experimental features"
-msgstr ""
+msgstr "Habilitar funciones experimentales"
 
 #: data/sm.puri.Chatty.gschema.xml:80
 msgid "Whether to enable experimental features"
-msgstr ""
+msgstr "Ya sea para habilitar funciones experimentales"
 
 #: data/sm.puri.Chatty.gschema.xml:85
 msgid "Window maximized"
@@ -153,33 +148,33 @@ msgstr "Una aplicación de mensajería"
 
 #: data/sm.puri.Chatty.metainfo.xml.in:9
 msgid "Chats is a messaging application supporting XMPP and SMS."
-msgstr "Chats es una aplicación de mensajería que soporte SMS y XMPP"
+msgstr "Chatty es una aplicación de mensajería que soporta SMS y XMPP"
 
 #: data/sm.puri.Chatty.metainfo.xml.in:16
 msgid "Chats message window"
-msgstr "Ventana de mensajes de Chats"
+msgstr "Ventana de mensajes de Chatty"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Mostrar versión de lanzamiento"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
-msgstr "Iniciar en modo de daemon"
+msgstr "Iniciar en modo demonio"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Desactivar todas las cuentas"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Activar mensajes de depuración de libpurple"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Activar mensajes detallados de depuración de libpurple"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:144
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -190,37 +185,29 @@ msgstr ""
 msgid "Send To"
 msgstr "Enviar a"
 
-#: src/chatty-chat-list.c:152
-#, fuzzy
-#| msgid ""
-#| "Select an <b>Instant Message</b> contact with the \"+\" button in the "
-#| "titlebar."
+#: src/chatty-chat-list.c:175
 msgid "Select a contact with the <b>“+”</b> button in the titlebar."
-msgstr ""
-"Seleccionar un contacto de <b>Mensaje instantáneo</b> con el botón \"+\" en "
-"la barra de título."
-
-#: src/chatty-chat-list.c:156
-#, fuzzy
-#| msgid ""
-#| "For <b>Instant Messaging</b> add or activate an account in <i>"
-#| "\"preferences\"</i>."
+msgstr "Seleccionar un contacto con el botón <b>«+»</b> en la barra de título."
+
+#: src/chatty-chat-list.c:179
 msgid "Add instant messaging accounts in Preferences."
-msgstr ""
-"Para usar <b>Mensajería instantánea</b> agregar o activar una cuenta en <i>"
-"\"Preferencias\"</i>."
+msgstr "Agregar una cuenta de mensajería instantánea en Preferencias."
 
-#: src/chatty-chat-list.c:192 src/chatty-contact-list.c:282
+#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282
 msgid "No Search Results"
-msgstr ""
+msgstr "Sin resultados de búsqueda"
 
-#: src/chatty-chat-list.c:193
+#: src/chatty-chat-list.c:216
 msgid "Try different search"
-msgstr ""
+msgstr "Intentar búsqueda diferente"
+
+#: src/chatty-chat-list.c:220
+msgid "No archived chats"
+msgstr "No hay Conversaciones archivadas"
 
-#: src/chatty-chat-list.c:196
+#: src/chatty-chat-list.c:222
 msgid "Start Chatting"
-msgstr "Empezar a chatear"
+msgstr "Iniciar a Conversar"
 
 #: src/chatty-chat-view.c:212
 msgid "This is an SMS conversation"
@@ -236,18 +223,18 @@ msgstr "Esta es una conversación de mensajería instantánea"
 
 #: src/chatty-chat-view.c:224
 msgid "Your messages are encrypted"
-msgstr "Sus mensajes están cifrados"
+msgstr "Los mensajes están cifrados"
 
 #: src/chatty-chat-view.c:227
 msgid "Your messages are not encrypted"
-msgstr "Sus mensajes no están cifrados"
+msgstr "Los mensajes no están cifrados"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:420
 msgid "Select File..."
 msgstr "Seleccionar archivo..."
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:352 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -255,7 +242,7 @@ msgstr "Seleccionar archivo..."
 msgid "Cancel"
 msgstr "Cancelar"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
@@ -289,7 +276,7 @@ msgstr ""
 
 #: src/chatty-contact-list.c:287
 msgid "No Contacts"
-msgstr "Ningún contacto"
+msgstr "No hay contactos"
 
 #: src/chatty-clock.c:86
 msgid "Just Now"
@@ -299,8 +286,6 @@ msgstr "Justo ahora"
 #. * See https://docs.gtk.org/glib/method.DateTime.format.html
 #.
 #: src/chatty-clock.c:95 src/chatty-clock.c:125
-#, fuzzy
-#| msgid "%I:%M %p"
 msgid "%I∶%M %p"
 msgstr "%I:%M %p"
 
@@ -313,25 +298,25 @@ msgstr[1] "Hace %lu minutos"
 
 #: src/chatty-clock.c:112
 msgid "Today %H∶%M"
-msgstr ""
+msgstr "Hoy %H∶%M"
 
 #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PM”.
 #. * See https://docs.gtk.org/glib/method.DateTime.format.html
 #.
 #: src/chatty-clock.c:120
 msgid "Today %I∶%M %p"
-msgstr ""
+msgstr "Hoy %I∶%M %p"
 
 #: src/chatty-clock.c:132
 msgid "Yesterday %H∶%M"
-msgstr ""
+msgstr "Ayer %H∶%M"
 
 #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PM”.
 #. * See https://docs.gtk.org/glib/method.DateTime.format.html
 #.
 #: src/chatty-clock.c:137
 msgid "Yesterday %I∶%M %p"
-msgstr ""
+msgstr "Ayer  %I∶%M %p"
 
 #. TRANSLATORS: Timestamp from more than 7 days ago or future date
 #. * (eg: when the system time is wrong), e.g. “2022-01-01”.
@@ -345,11 +330,11 @@ msgstr "%d-%m-%Y"
 #: src/chatty-fp-row.c:131
 #, c-format
 msgid "Device ID %s fingerprint:"
-msgstr ""
+msgstr "Id. dispositivo %s huella digital:"
 
 #: src/chatty-list-row.c:118
 msgid "Owner"
-msgstr "Dueño"
+msgstr "Propietario"
 
 #: src/chatty-list-row.c:121
 msgid "Moderator"
@@ -359,15 +344,19 @@ msgstr "Moderador"
 msgid "Member"
 msgstr "Miembro"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:724
 #, c-format
 msgid "“%s” is not a valid URI"
-msgstr ""
+msgstr "“%s” no es un URI válido"
 
 #: src/chatty-message-row.c:82
 msgid "Copy"
 msgstr "Copiar"
 
+#: src/chatty-message.c:233
+msgid "Got an encrypted message, but couldn't decrypt due to missing keys"
+msgstr "Recibí un mensaje cifrado, pero no pude descifrarlo debido a falta de claves"
+
 #: src/chatty-notification.c:181
 msgid "Open Message"
 msgstr "Abrir mensaje"
@@ -381,7 +370,7 @@ msgstr "Mensaje nuevo de %s"
 msgid "Message Received"
 msgstr "Mensaje recibido"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44
 msgid "Close"
 msgstr "Cerrar"
 
@@ -411,11 +400,11 @@ msgstr "Usuario %s ha agregado %s a los contactos"
 msgid "Authorize %s?"
 msgstr "¿Autorizar %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264
 msgid "Reject"
 msgstr "Rechazar"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265
 msgid "Accept"
 msgstr "Aceptar"
 
@@ -424,70 +413,69 @@ msgstr "Aceptar"
 msgid "Add %s to contact list"
 msgstr "Agregar %s a la lista de contactos"
 
-#: src/purple/chatty-purple.c:587
+#: src/purple/chatty-purple.c:586
 msgid "Login failed"
 msgstr "Ingreso ha fallado"
 
-#: src/purple/chatty-purple.c:592
+#: src/purple/chatty-purple.c:591
 msgid "Please check ID and password"
 msgstr "Por favor verifique la ID y contraseña"
 
-#: src/chatty-secret-store.c:98
-#, c-format
-msgid "Chatty password for \"%s\""
-msgstr ""
-
-#: src/chatty-window.c:338
+#: src/chatty-window.c:408
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "Borrar chat con «%s»"
 
-#: src/chatty-window.c:339
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Este borra el historial de conversaciones"
 
-#: src/chatty-window.c:341
+#: src/chatty-window.c:411
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "Desconectar del chat en grupo «%s»"
 
-#: src/chatty-window.c:342
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Este quita el chat de la lista de chats"
 
-#: src/chatty-window.c:354
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Borrar"
 
-#: src/chatty-window.c:433
+#: src/chatty-window.c:502
 msgid "You shall no longer be notified for new messages, continue?"
 msgstr ""
 
-#: src/chatty-window.c:529
+#: src/chatty-window.c:577
+msgid "Archived"
+msgstr "Archivado"
+
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "Un cliente de mensajería SMS y XMPP"
 
-#: src/chatty-window.c:536
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr "reconocimiento-de-traductores"
 
-#: src/chatty-window.c:874
+#: src/chatty-window.c:989
 msgid "Any Protocol"
 msgstr "Cualquier protocolo"
 
-#: src/chatty-window.c:875 src/ui/chatty-settings-dialog.ui:627
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:876
+#: src/chatty-window.c:991
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:879 src/ui/chatty-settings-dialog.ui:615
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:882 src/ui/chatty-settings-dialog.ui:641
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -525,44 +513,35 @@ msgid "Encryption not available"
 msgstr "Cifrado no disponible"
 
 #: src/dialogs/chatty-pp-chat-info.c:259
-#, fuzzy, c-format
-#| msgid "Member"
+#, c-format
 msgid "%u Member"
 msgid_plural "%u Members"
-msgstr[0] "Miembro"
-msgstr[1] "Miembro"
+msgstr[0] "%u Miembro"
+msgstr[1] "%u Miembros"
 
 #: src/dialogs/chatty-pp-chat-info.c:293
 msgid "Phone Number:"
 msgstr "Número telefónico"
 
 #: src/dialogs/chatty-pp-chat-info.c:295
-#, fuzzy
-#| msgid "XMPP ID"
 msgid "XMPP ID:"
-msgstr "ID de XMPP"
+msgstr "ID de XMPP:"
 
 #: src/dialogs/chatty-pp-chat-info.c:300
-#, fuzzy
-#| msgid "Matrix"
 msgid "Matrix ID:"
-msgstr "Matrix"
+msgstr "ID de Matrix:"
 
 #: src/dialogs/chatty-pp-chat-info.c:303
-#, fuzzy
-#| msgid "Telegram"
 msgid "Telegram ID:"
-msgstr "Telegram"
+msgstr "ID de Telegram:"
 
 #: src/dialogs/chatty-new-chat-dialog.c:171
-#, fuzzy
-#| msgid "_Add"
 msgid "Add"
-msgstr "_Añadir"
+msgstr "Añadir"
 
 #: src/dialogs/chatty-new-chat-dialog.c:181
 msgid "Create"
-msgstr ""
+msgstr "Crear"
 
 #: src/dialogs/chatty-new-chat-dialog.c:187
 msgid "Add Contact"
@@ -579,7 +558,7 @@ msgstr "SMS"
 
 #: src/dialogs/chatty-new-chat-dialog.c:640
 msgid "You"
-msgstr ""
+msgstr "Usted"
 
 #: src/dialogs/chatty-ma-account-details.c:385
 #: src/dialogs/chatty-pp-account-details.c:178
@@ -589,139 +568,107 @@ msgstr "conectado"
 #: src/dialogs/chatty-ma-account-details.c:387
 #: src/dialogs/chatty-pp-account-details.c:180
 msgid "connecting…"
-msgstr "esta conectando..."
+msgstr "está conectando..."
 
 #: src/dialogs/chatty-ma-account-details.c:389
 #: src/dialogs/chatty-pp-account-details.c:182
 msgid "disconnected"
 msgstr "desconectado"
 
-#: src/dialogs/chatty-settings-dialog.c:255
-#: src/dialogs/chatty-settings-dialog.c:405
+#: src/dialogs/chatty-settings-dialog.c:251
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Falló al verificar el servidor: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:253
 msgid "Failed to verify server"
-msgstr ""
+msgstr "Falló al verificar el servidor"
 
-#: src/dialogs/chatty-settings-dialog.c:306
-#: src/dialogs/chatty-settings-dialog.c:385
+#: src/dialogs/chatty-settings-dialog.c:259
 msgid "Couldn't get Home server address"
 msgstr ""
 
-#: src/dialogs/chatty-settings-dialog.c:510
+#: src/dialogs/chatty-settings-dialog.c:430
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
-msgstr "Borrar cuenta"
+msgstr "Eliminar cuenta"
 
-#: src/dialogs/chatty-settings-dialog.c:513
+#: src/dialogs/chatty-settings-dialog.c:433
 #, c-format
 msgid "Delete account %s?"
-msgstr "¿Borra la cuenta de %s?"
+msgstr "¿Eliminar la cuenta de %s?"
 
-#: src/dialogs/chatty-settings-dialog.c:654
+#: src/dialogs/chatty-settings-dialog.c:584
 msgid "Restart chatty to disable purple"
 msgstr ""
 
-#: src/dialogs/chatty-settings-dialog.c:656
+#: src/dialogs/chatty-settings-dialog.c:586
 #: src/ui/chatty-settings-dialog.ui:512
-#, fuzzy
-#| msgid "Enable libpurple debug messages"
 msgid "Enable purple plugin"
-msgstr "Activar mensajes de depuración de libpurple"
+msgstr "Activar complemento purple"
 
-#: src/dialogs/chatty-settings-dialog.c:670
+#: src/dialogs/chatty-settings-dialog.c:600
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr ""
 
-#: src/dialogs/chatty-settings-dialog.c:672
+#: src/dialogs/chatty-settings-dialog.c:602
 #: src/ui/chatty-settings-dialog.ui:298
-#, fuzzy
-#| msgid "Open Account Settings"
 msgid "Purple Settings"
-msgstr "Abrir configuración de cuenta"
+msgstr "Configuración de purple"
 
-#: src/dialogs/chatty-settings-dialog.c:674
-#, fuzzy
-#| msgid "Accounts"
+#: src/dialogs/chatty-settings-dialog.c:604
 msgid "New Account"
-msgstr "Cuentas"
+msgstr "Nueva cuenta"
 
-#: src/dialogs/chatty-settings-dialog.c:676
+#: src/dialogs/chatty-settings-dialog.c:606
 #: src/ui/chatty-settings-dialog.ui:364
-#, fuzzy
-#| msgid "Add Contact"
 msgid "Blocked Contacts"
-msgstr "Agregar contacto"
+msgstr "Contactos bloqueados"
 
-#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7
-#: src/ui/chatty-window.ui:17
+#: src/dialogs/chatty-settings-dialog.c:608 src/ui/chatty-settings-dialog.ui:7
+#: src/ui/chatty-window.ui:30
 msgid "Preferences"
 msgstr "Preferencias"
 
-#: src/dialogs/chatty-settings-dialog.c:707
+#: src/dialogs/chatty-settings-dialog.c:636
 msgid "Select Protocol"
 msgstr "Seleccionar protocolo"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:948
+#: src/dialogs/chatty-settings-dialog.c:880
 msgid "@user:matrix.org or user@example.com"
 msgstr ""
 
-#: src/dialogs/chatty-settings-dialog.c:1057
+#: src/dialogs/chatty-settings-dialog.c:989
 msgid "Unblock contact"
 msgstr ""
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/matrix/chatty-ma-account.c:106
 msgid "Incorrect password"
 msgstr ""
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:109
 msgid "_OK"
 msgstr ""
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:110 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "_Cancelar"
 
-#: src/matrix/chatty-ma-account.c:306
-#, c-format
-msgid "Please enter password for “%s”"
-msgstr ""
-
-#: src/matrix/matrix-utils.c:474
-#, c-format
-msgid "The certificate for ‘%s’ has unknown CA"
-msgstr ""
-
-#: src/matrix/matrix-utils.c:476
-#, c-format
-msgid "The certificate for ‘%s’ is self-signed"
-msgstr ""
-
-#: src/matrix/matrix-utils.c:480
+#: src/matrix/chatty-ma-account.c:117
 #, c-format
-msgid "The certificate for ‘%s’ has expired"
-msgstr ""
-
-#: src/matrix/matrix-utils.c:484
-#, c-format
-msgid "The certificate for ‘%s’ has been revoked"
-msgstr ""
-
-#: src/matrix/matrix-utils.c:493
-#, c-format
-msgid "Error validating certificate for ‘%s’"
+msgid "Please enter password for “%s”, homeserver: %s"
 msgstr ""
 
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
 #.
 #: src/mm/chatty-mmsd.c:827
-#, fuzzy
-#| msgid "%Y-%m-%d"
 msgid "%Y-%m-%d %H∶%M"
-msgstr "%d-%m-%Y"
+msgstr "%H:%M %d-%m-%Y"
 
 #: src/mm/chatty-mmsd.c:1111
 #, c-format
@@ -749,16 +696,12 @@ msgid "Password (optional)"
 msgstr "Contraseña (opcional)"
 
 #: src/ui/chatty-dialog-new-chat.ui:126
-#, fuzzy
-#| msgid "Group Details"
 msgid "Group Title"
-msgstr "Detalles del grupo"
+msgstr "Título del grupo"
 
 #: src/ui/chatty-dialog-new-chat.ui:177
-#, fuzzy
-#| msgid "Add %s to contact list"
 msgid "Add members from contacts…"
-msgstr "Agregar %s a la lista de contactos"
+msgstr "Agregar miembros desde la lista de contactos"
 
 #: src/ui/chatty-dialog-new-chat.ui:223
 msgid "Send To:"
@@ -776,7 +719,7 @@ msgstr "Añadir a Contactos"
 msgid "Remove Attachment"
 msgstr ""
 
-#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:126
+#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139
 msgid "Chat Details"
 msgstr "Detalles del chat"
 
@@ -793,10 +736,8 @@ msgid "Matrix ID"
 msgstr "ID de Matrix"
 
 #: src/ui/chatty-ma-chat-info.ui:93 src/ui/chatty-pp-chat-info.ui:289
-#, fuzzy
-#| msgid "Chat Details"
 msgid "Chat settings"
-msgstr "Detalles del chat"
+msgstr "Configuración del chat"
 
 #: src/ui/chatty-ma-chat-info.ui:113 src/ui/chatty-pp-chat-info.ui:238
 #: src/ui/chatty-pp-chat-info.ui:344
@@ -846,19 +787,17 @@ msgstr "Huellas digitales"
 
 #: src/ui/chatty-mm-chat-info.ui:29
 msgid "Title"
-msgstr ""
+msgstr "Título"
 
 #: src/ui/chatty-mm-chat-info.ui:33
 msgid "Blank for default"
 msgstr ""
 
 #: src/ui/chatty-mm-chat-info.ui:43
-#, fuzzy
-#| msgid "0 members"
 msgid "Group Members"
-msgstr "0 miembros"
+msgstr "Miembros del grupo"
 
-#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:379
+#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433
 msgid "Call"
 msgstr ""
 
@@ -916,7 +855,7 @@ msgstr "_Guardar"
 
 #: src/ui/chatty-settings-dialog.ui:92
 msgid "_Apply"
-msgstr ""
+msgstr "_Aplicar"
 
 #: src/ui/chatty-settings-dialog.ui:169
 msgid "Accounts"
@@ -956,16 +895,12 @@ msgstr ""
 
 #. TRANSLATORS: Return is the Enter key.
 #: src/ui/chatty-settings-dialog.ui:251
-#, fuzzy
-#| msgid "Send message with return key"
 msgid "Send Messages with Return"
 msgstr "Manda mensaje con la tecla Entrar"
 
 #: src/ui/chatty-settings-dialog.ui:267
-#, fuzzy
-#| msgid "Room settings"
 msgid "Protocol Settings"
-msgstr "Configuración de sala"
+msgstr "Configuración de protocolo"
 
 #: src/ui/chatty-settings-dialog.ui:333
 msgid "Request Delivery Reports"
@@ -1019,71 +954,78 @@ msgstr ""
 msgid "Add _new account…"
 msgstr "Agregar una cuenta nueva…"
 
-#: src/ui/chatty-window.ui:28
+#: src/ui/chatty-window.ui:17
+msgctxt "show archived chat list when clicked"
+msgid "Archived"
+msgstr "Archivado"
+
+#: src/ui/chatty-window.ui:41
 msgid "Keyboard _Shortcuts"
 msgstr ""
 
-#: src/ui/chatty-window.ui:35
+#: src/ui/chatty-window.ui:48
 msgid "Help"
 msgstr "Ayuda"
 
-#: src/ui/chatty-window.ui:44
+#: src/ui/chatty-window.ui:57
 msgid "About Chats"
 msgstr "Acerca de Chats"
 
-#: src/ui/chatty-window.ui:72
+#: src/ui/chatty-window.ui:85
 msgid "New Message…"
 msgstr "Mensaje nuevo…"
 
-#: src/ui/chatty-window.ui:85
+#: src/ui/chatty-window.ui:98
 msgid "New SMS/MMS Message…"
 msgstr "Nuevo SMS/MMS…"
 
-#: src/ui/chatty-window.ui:98
+#: src/ui/chatty-window.ui:111
 msgid "New Group Message…"
 msgstr "Mensaje nuevo en grupo…"
 
-#: src/ui/chatty-window.ui:151
+#: src/ui/chatty-window.ui:164
 msgid "Leave Chat"
 msgstr "Salir del chat"
 
-#: src/ui/chatty-window.ui:164
+#: src/ui/chatty-window.ui:177
 msgid "Block Contact"
 msgstr "Bloquear contacto"
 
-#: src/ui/chatty-window.ui:177
-#, fuzzy
-#| msgid "Invite Contact"
+#: src/ui/chatty-window.ui:190
 msgid "Unblock Contact"
-msgstr "Invitar a contactarse"
+msgstr "Desbloquear contacto"
 
-#: src/ui/chatty-window.ui:190
+#: src/ui/chatty-window.ui:203
+msgid "Archive chat"
+msgstr "Achivar conversación"
+
+#: src/ui/chatty-window.ui:216
+msgid "Unarchive chat"
+msgstr "Desarchivar conversación"
+
+#: src/ui/chatty-window.ui:229
 msgid "Delete Chat"
 msgstr "Eliminar chat"
 
 #: src/ui/help-overlay.ui:14
 msgctxt "shortcut window"
 msgid "General"
-msgstr ""
+msgstr "General"
 
 #: src/ui/help-overlay.ui:19
-#, fuzzy
-#| msgid "Open Message"
 msgctxt "shortcut window"
 msgid "Open Menu"
-msgstr "Abrir mensaje"
+msgstr "Abrir menú"
 
 #: src/ui/help-overlay.ui:27
-#, fuzzy
-#| msgid "Open Message"
 msgctxt "shortcut window"
 msgid "Open Search"
-msgstr "Abrir mensaje"
+msgstr "Abrir búsqueda"
 
 #: src/ui/help-overlay.ui:35
 msgctxt "shortcut window"
 msgid "Show Shortcuts"
-msgstr ""
+msgstr "Mostrar atajos de teclado"
 
 #: src/users/chatty-contact.c:325
 msgid "Mobile: "
@@ -1097,6 +1039,9 @@ msgstr "Trabajo: "
 msgid "Other: "
 msgstr "Otro: "
 
+#~ msgid "sm.puri.Chatty"
+#~ msgstr "sm.puri.Chatty"
+
 #~ msgid "Mark offline users differently"
 #~ msgstr "Marcar a los usuarios desconectados de manera diferente"
 
diff --git a/po/he.po b/po/he.po
new file mode 100644
index 0000000000000000000000000000000000000000..0964e657752aab8b24fbab3432c153aee6fb2fd5
--- /dev/null
+++ b/po/he.po
@@ -0,0 +1,1216 @@
+# Hebrew translation for chatty.
+# Copyright (C) 2021 chatty's COPYRIGHT HOLDER
+# This file is distributed under the same license as the chatty package.
+# Yaron Shahrabani <sh.yaron@gmail.com>, 2021.
+# Yosef Or Boczko <yoseforb@gmail.com>, 2022.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: chatty master\n"
+"Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
+"POT-Creation-Date: 2022-09-08 03:24+0000\n"
+"PO-Revision-Date: 2022-09-11 07:37+0300\n"
+"Last-Translator: Yosef Or Boczko <yoseforb@gmail.com>\n"
+"Language-Team: Hebrew <>\n"
+"Language: he\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : n>10 && n%10==0 ? "
+"2 : 3)\n"
+"X-Generator: Gtranslator 40.0\n"
+
+#: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
+#: src/chatty-application.c:338 src/chatty-window.c:339
+#: src/ui/chatty-window.ui:265
+msgid "Chats"
+msgstr "התכתבויות"
+
+#: data/sm.puri.Chatty.desktop.in:6
+msgid "SMS and XMPP chat application"
+msgstr "יישום התכתבות במסרונים וב־XMPP"
+
+#: data/sm.puri.Chatty.desktop.in:7
+msgid "XMPP;SMS;chat;jabber;messaging;modem"
+msgstr "‫XMPP;מסרון;מסרונים;אס אם אס;SMS;התכתבות;צ׳אט;שיח;מודם;ג׳אבר"
+
+#: data/sm.puri.Chatty.gschema.xml:7 data/sm.puri.Chatty.gschema.xml:8
+msgid "Whether the application is launching the first time"
+msgstr "האם זאת הטעינה הראשונה של היישומון"
+
+#: data/sm.puri.Chatty.gschema.xml:13
+msgid "Country code in ISO 3166-1 alpha-2 format"
+msgstr "קוד מדינה בתצורת ISO 3166-1 alpha-2"
+
+#: data/sm.puri.Chatty.gschema.xml:14
+msgid "Two letter country code of the last available SIM/Network"
+msgstr "קוד מדינה בשתי ספרות של הרשת/SIM האחרונים הזמינים"
+
+#: data/sm.puri.Chatty.gschema.xml:19
+msgid "Send message read receipts"
+msgstr "לשלוח קבלות קריאה על הודעות"
+
+#: data/sm.puri.Chatty.gschema.xml:20
+msgid "Whether to send the status of the message if read"
+msgstr "האם לשלוח את מצב קריאת ההודעה"
+
+#: data/sm.puri.Chatty.gschema.xml:25
+msgid "Message carbon copies"
+msgstr "עותקים של הודעות"
+
+#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554
+msgid "Share chat history among devices"
+msgstr "שיתוף היסטוריית ההתכתבות בין מכשירים"
+
+#: data/sm.puri.Chatty.gschema.xml:31
+msgid "Enable Message Archive Management"
+msgstr "לאפשר ניהול ארכיון הודעות"
+
+#: data/sm.puri.Chatty.gschema.xml:32
+msgid "Enable MAM archive synchronization from the server"
+msgstr "לאפשר סנכרון ארכיון MAM מהשרת"
+
+#: data/sm.puri.Chatty.gschema.xml:37
+msgid "Enable purple"
+msgstr "לאפשר purple"
+
+#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511
+msgid "Enable purple accounts"
+msgstr "לאפשר חשבונות purple"
+
+#: data/sm.puri.Chatty.gschema.xml:43
+msgid "Send typing notifications"
+msgstr "לשלוח התראות הקלדה"
+
+#: data/sm.puri.Chatty.gschema.xml:44
+msgid "Whether to Send typing notifications"
+msgstr "האם לשלוח התראות הקלדה"
+
+#: data/sm.puri.Chatty.gschema.xml:49 data/sm.puri.Chatty.gschema.xml:50
+msgid "Mark Idle users differently"
+msgstr "לסמן משתמשים בלתי פעילים אחרת"
+
+#: data/sm.puri.Chatty.gschema.xml:55 data/sm.puri.Chatty.gschema.xml:56
+msgid "Indicate unknown contacts"
+msgstr "לציין אנשי קשר בלתי ידועים"
+
+#: data/sm.puri.Chatty.gschema.xml:61
+msgid "Convert text to emoticons"
+msgstr "להמיר טקסט לרגשונים"
+
+#: data/sm.puri.Chatty.gschema.xml:62
+msgid "Convert text matching emoticons as real emoticons"
+msgstr "להמיר טקסט שמחקה רגשונים לרגשונים אמתיים"
+
+#: data/sm.puri.Chatty.gschema.xml:67
+msgid "Enter key sends the message"
+msgstr "‫Enter שולח את ההודעה"
+
+#: data/sm.puri.Chatty.gschema.xml:68
+msgid "Whether pressing Enter key sends the message"
+msgstr "האם לחיצה על Enter תשלח את ההודעה"
+
+#: data/sm.puri.Chatty.gschema.xml:73
+msgid "Request SMS delivery reports"
+msgstr "דרישת דיווחי מסירת הודעות SMS"
+
+#: data/sm.puri.Chatty.gschema.xml:74
+msgid "Whether to request delivery reports for outgoing SMS"
+msgstr "האם לדרוש דיווח על שליחה של הודעות SMS יוצאות"
+
+#: data/sm.puri.Chatty.gschema.xml:79
+msgid "Enable experimental features"
+msgstr "להפעיל תכונות ניסיוניות"
+
+#: data/sm.puri.Chatty.gschema.xml:80
+msgid "Whether to enable experimental features"
+msgstr "האם להפעיל תכונות ניסיוניות"
+
+#: data/sm.puri.Chatty.gschema.xml:85
+msgid "Window maximized"
+msgstr "החלון מוגדל לחלוטין"
+
+#: data/sm.puri.Chatty.gschema.xml:86
+msgid "Window maximized state"
+msgstr "מצב הגדלת חלון"
+
+#: data/sm.puri.Chatty.gschema.xml:91
+msgid "Window size"
+msgstr "גודל חלון"
+
+#: data/sm.puri.Chatty.gschema.xml:92
+msgid "Window size (width, height)."
+msgstr "גודל חלון (רוחב, גובה)."
+
+#: data/sm.puri.Chatty.metainfo.xml.in:7
+msgid "A messaging application"
+msgstr "יישומון התכתבות"
+
+#: data/sm.puri.Chatty.metainfo.xml.in:9
+msgid "Chats is a messaging application supporting XMPP and SMS."
+msgstr "התכתבויות הוא יישומון התכתבות שתומך ב־XMPP ובמסרונים."
+
+#: data/sm.puri.Chatty.metainfo.xml.in:16
+msgid "Chats message window"
+msgstr "חלון ההודעות של התכתבויות"
+
+#: src/chatty-application.c:78
+msgid "Show release version"
+msgstr "להציג גרסה"
+
+#: src/chatty-application.c:79
+msgid "Start in daemon mode"
+msgstr "לעלות במצב רקע"
+
+#: src/chatty-application.c:80
+msgid "Disable all accounts"
+msgstr "להשבית את כל החשבונות"
+
+#: src/chatty-application.c:81
+msgid "Enable libpurple debug messages"
+msgstr "לאפשר הודעות ניפוי שגיאות של libpurple"
+
+#: src/chatty-application.c:83
+msgid "Enable verbose libpurple debug messages"
+msgstr "להפעיל הודעות ניפוי שגיאות מפורטות של libpurple"
+
+#: src/chatty-application.c:144
+#, c-format
+msgid ""
+"There was an error displaying help:\n"
+"%s"
+msgstr ""
+"אירעה שגיאה בהצגת העזרה:\n"
+"%s"
+
+#: src/chatty-avatar.c:158 src/chatty-contact-list.c:391
+msgid "Send To"
+msgstr "לשלוח אל"
+
+#: src/chatty-chat-list.c:175
+msgid "Select a contact with the <b>“+”</b> button in the titlebar."
+msgstr "נא לבחור באיש קשר עם הכפתור <b>”+„</b> שבשורת הכותרת."
+
+#: src/chatty-chat-list.c:179
+msgid "Add instant messaging accounts in Preferences."
+msgstr "הוספת חשבונות הודעות מידיות בהעדפות."
+
+#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282
+msgid "No Search Results"
+msgstr "אין תוצאות חיפוש"
+
+#: src/chatty-chat-list.c:216
+msgid "Try different search"
+msgstr "יש לנסות חיפוש שונה"
+
+#: src/chatty-chat-list.c:220
+msgid "No archived chats"
+msgstr "אין שיחות מאורכבות"
+
+#: src/chatty-chat-list.c:222
+msgid "Start Chatting"
+msgstr "התחלת התכתבות"
+
+#: src/chatty-chat-view.c:212
+msgid "This is an SMS conversation"
+msgstr "זו התכתבות במסרונים (התמסררות)"
+
+#: src/chatty-chat-view.c:214 src/chatty-chat-view.c:220
+msgid "Your messages are not encrypted, and carrier rates may apply"
+msgstr "ההודעות שלך אינן מוצפנות, ויתכנו חיובים מהספק"
+
+#: src/chatty-chat-view.c:218
+msgid "This is an IM conversation"
+msgstr "זאת שיחה בהודעות פרטיות"
+
+#: src/chatty-chat-view.c:224
+msgid "Your messages are encrypted"
+msgstr "ההודעות שלך מוצפנות"
+
+#: src/chatty-chat-view.c:227
+msgid "Your messages are not encrypted"
+msgstr "ההודעות שלך אינן מוצפנות"
+
+#: src/chatty-chat-view.c:420
+msgid "Select File..."
+msgstr "בחירת קובץ…"
+
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
+#: src/dialogs/chatty-ma-account-details.c:119
+#: src/dialogs/chatty-pp-account-details.c:91
+#: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
+#: src/ui/chatty-info-dialog.ui:54
+msgid "Cancel"
+msgstr "ביטול"
+
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
+#: src/dialogs/chatty-pp-chat-info.c:89
+#: src/dialogs/chatty-ma-account-details.c:118
+#: src/dialogs/chatty-pp-account-details.c:90
+msgid "Open"
+msgstr "פתיחה"
+
+#: src/chatty-chat.c:635
+msgid "Empty room"
+msgstr "חדר ריק"
+
+#. TRANSLATORS: %s are name/user-id/phone numbers of two users
+#: src/chatty-chat.c:642
+#, c-format
+msgid "%s and %s"
+msgstr "‫%s וגם %s"
+
+#: src/chatty-chat.c:644
+#, c-format
+msgid "%s and %u other"
+msgid_plural "%s and %u others"
+msgstr[0] "‫%s ועוד אחד"
+msgstr[1] "‫%s ועוד שניים"
+msgstr[2] "‫%s ועוד %u נוספים"
+msgstr[3] "‫%s ועוד %u נוספים"
+
+#: src/chatty-contact-list.c:247 src/dialogs/chatty-mm-chat-info.c:121
+msgid "Unknown Contact"
+msgstr "איש קשר לא ידוע"
+
+#: src/chatty-contact-list.c:283
+msgid "Try different search, or type a valid number to create new chat"
+msgstr "כדאי לנסות חיפוש אחר או להקליד מספר טלפון תקני כדי ליצור התכתבות חדשה"
+
+#: src/chatty-contact-list.c:287
+msgid "No Contacts"
+msgstr "אין אנשי קשר"
+
+#: src/chatty-clock.c:86
+msgid "Just Now"
+msgstr "ממש עכשיו"
+
+#. TRANSLATORS: Timestamp with 12 hour time, e.g. “06∶42 PM”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:95 src/chatty-clock.c:125
+msgid "%I∶%M %p"
+msgstr "%I:%M %p"
+
+#: src/chatty-clock.c:104
+#, c-format
+msgid "%lu minute ago"
+msgid_plural "%lu minutes ago"
+msgstr[0] "לפני דקה"
+msgstr[1] "לפני %lu דקות"
+msgstr[2] "לפני %lu דקות"
+msgstr[3] "לפני %lu דקות"
+
+#: src/chatty-clock.c:112
+msgid "Today %H∶%M"
+msgstr "היום, ‎%H∶%M"
+
+#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PM”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:120
+msgid "Today %I∶%M %p"
+msgstr "היום, ‎%H∶%M ‏%p"
+
+#: src/chatty-clock.c:132
+msgid "Yesterday %H∶%M"
+msgstr "אתמול, ‎%H∶%M"
+
+#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PM”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:137
+msgid "Yesterday %I∶%M %p"
+msgstr "אתמול, ‎%H∶%M ‏%p"
+
+#. TRANSLATORS: Timestamp from more than 7 days ago or future date
+#. * (eg: when the system time is wrong), e.g. “2022-01-01”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:153
+msgid "%Y-%m-%d"
+msgstr "%Y-%m-%d"
+
+#. TRANSLATORS: %s is the Device ID
+#: src/chatty-fp-row.c:131
+#, c-format
+msgid "Device ID %s fingerprint:"
+msgstr "טביעת אצבע של מזהה מכשיר %s:"
+
+#: src/chatty-list-row.c:118
+msgid "Owner"
+msgstr "בעלים"
+
+#: src/chatty-list-row.c:121
+msgid "Moderator"
+msgstr "מפקח"
+
+#: src/chatty-list-row.c:124
+msgid "Member"
+msgstr "חבר"
+
+#: src/chatty-manager.c:724
+#, c-format
+msgid "“%s” is not a valid URI"
+msgstr "הכתובת „%s” אינה תקינה"
+
+#: src/chatty-message-row.c:82
+msgid "Copy"
+msgstr "העתקה"
+
+#: src/chatty-message.c:233
+msgid "Got an encrypted message, but couldn't decrypt due to missing keys"
+msgstr "התקבלה הודעה מוצפנת, אך לא ניתן לפענח אותה עקב מחסור במפתחות"
+
+#: src/chatty-notification.c:181
+msgid "Open Message"
+msgstr "לפתוח הודעה"
+
+#: src/chatty-notification.c:214
+#, c-format
+msgid "New message from %s"
+msgstr "הודעה חדשה מאת %s"
+
+#: src/chatty-notification.c:216
+msgid "Message Received"
+msgstr "התקבלה הודעה"
+
+#: src/purple/chatty-purple-notify.c:44
+msgid "Close"
+msgstr "סגירה"
+
+#: src/purple/chatty-purple-request.c:170
+msgid "Save File..."
+msgstr "שמירת קובץ…"
+
+#: src/purple/chatty-purple-request.c:171
+msgid "Open File..."
+msgstr "פתיחת קובץ…"
+
+#: src/purple/chatty-purple-request.c:177
+msgid "Save"
+msgstr "שמירה"
+
+#: src/purple/chatty-purple.c:240
+msgid "Contact added"
+msgstr "איש הקשר נוסף"
+
+#: src/purple/chatty-purple.c:242
+#, c-format
+msgid "User %s has added %s to the contacts"
+msgstr "המשתמש %s הוסיף את %s לאנשי קשר"
+
+#: src/purple/chatty-purple.c:262
+#, c-format
+msgid "Authorize %s?"
+msgstr "לאשר את %s?"
+
+#: src/purple/chatty-purple.c:264
+msgid "Reject"
+msgstr "לדחות"
+
+#: src/purple/chatty-purple.c:265
+msgid "Accept"
+msgstr "לקבל"
+
+#: src/purple/chatty-purple.c:268
+#, c-format
+msgid "Add %s to contact list"
+msgstr "להוסיף את %s לרשימת אנשי הקשר"
+
+#: src/purple/chatty-purple.c:586
+msgid "Login failed"
+msgstr "הכניסה נכשלה"
+
+#: src/purple/chatty-purple.c:591
+msgid "Please check ID and password"
+msgstr "נא לבדוק מזהה וססמה"
+
+#: src/chatty-window.c:408
+#, c-format
+msgid "Delete chat with “%s”"
+msgstr "מחיקת ההתכתבות עם „%s”"
+
+#: src/chatty-window.c:409
+msgid "This deletes the conversation history"
+msgstr "פעולה זו מוחקת את היסטוריית ההתכתבות"
+
+#: src/chatty-window.c:411
+#, c-format
+msgid "Disconnect group chat “%s”"
+msgstr "התנתקות מהתכתבות קבוצתית „%s”"
+
+#: src/chatty-window.c:412
+msgid "This removes chat from chats list"
+msgstr "פעולה זו מסירה את ההתכתבות מרשימת ההתכתבויות"
+
+#: src/chatty-window.c:424
+msgid "Delete"
+msgstr "מחיקה"
+
+#: src/chatty-window.c:502
+msgid "You shall no longer be notified for new messages, continue?"
+msgstr "פעולה זו תשבית את ההתראות על הודעות חדשות, להמשיך?"
+
+#: src/chatty-window.c:577
+msgid "Archived"
+msgstr "ארכיון"
+
+#: src/chatty-window.c:630
+msgid "An SMS and XMPP messaging client"
+msgstr "לקוח התכתבות במסרונים וב־XMPP"
+
+#: src/chatty-window.c:637
+msgid "translator-credits"
+msgstr ""
+"ירון שהרבני <sh.yaron@gmail.com>\n"
+"יוסף אור בוצ׳קו <yoseforb@gmail.com>"
+
+#: src/chatty-window.c:989
+msgid "Any Protocol"
+msgstr "כל פרוטוקול"
+
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
+msgid "Matrix"
+msgstr "‫Matrix"
+
+#: src/chatty-window.c:991
+msgid "SMS/MMS"
+msgstr "SMS/MMS"
+
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
+msgid "XMPP"
+msgstr "‫XMPP"
+
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
+msgid "Telegram"
+msgstr "טלגרם"
+
+#: src/dialogs/chatty-mm-chat-info.c:140
+msgctxt "Refer to self in contact list"
+msgid "You"
+msgstr "אני"
+
+#: src/dialogs/chatty-pp-chat-info.c:86
+#: src/dialogs/chatty-ma-account-details.c:115
+#: src/dialogs/chatty-pp-account-details.c:87
+msgid "Set Avatar"
+msgstr "הגדרת תמונה ייצוגית"
+
+#: src/dialogs/chatty-pp-chat-info.c:96
+#: src/dialogs/chatty-ma-account-details.c:125
+#: src/dialogs/chatty-pp-account-details.c:97
+msgid "Images"
+msgstr "תמונות"
+
+#: src/dialogs/chatty-pp-chat-info.c:206
+msgid "Encryption is not available"
+msgstr "אין הצפנה זמינה"
+
+#: src/dialogs/chatty-pp-chat-info.c:210
+msgid "This chat is encrypted"
+msgstr "התכתבות זו מוצפנת"
+
+#: src/dialogs/chatty-pp-chat-info.c:214
+msgid "This chat is not encrypted"
+msgstr "התכתבות זו אינה מוצפנת"
+
+#: src/dialogs/chatty-pp-chat-info.c:238
+msgid "Encryption not available"
+msgstr "הצפנה אינה זמינה"
+
+#: src/dialogs/chatty-pp-chat-info.c:259
+#, c-format
+msgid "%u Member"
+msgid_plural "%u Members"
+msgstr[0] "חבר אחד"
+msgstr[1] "שני חברים"
+msgstr[2] "‫%u חברים"
+msgstr[3] "‫%u חברים"
+
+#: src/dialogs/chatty-pp-chat-info.c:293
+msgid "Phone Number:"
+msgstr "מספר טלפון:"
+
+#: src/dialogs/chatty-pp-chat-info.c:295
+msgid "XMPP ID:"
+msgstr "מזהה XMPP:"
+
+#: src/dialogs/chatty-pp-chat-info.c:300
+msgid "Matrix ID:"
+msgstr "מזהה Matrix:"
+
+#: src/dialogs/chatty-pp-chat-info.c:303
+msgid "Telegram ID:"
+msgstr "מזהה טלגרם:"
+
+#: src/dialogs/chatty-new-chat-dialog.c:171
+msgid "Add"
+msgstr "הוספה"
+
+#: src/dialogs/chatty-new-chat-dialog.c:181
+msgid "Create"
+msgstr "יצירה"
+
+#: src/dialogs/chatty-new-chat-dialog.c:187
+msgid "Add Contact"
+msgstr "הוספת איש קשר"
+
+#: src/dialogs/chatty-new-chat-dialog.c:224
+#, c-format
+msgid "Error opening GNOME Contacts: %s"
+msgstr "שגיאה בפתיחת אנשי הקשר של GNOME:‏ %s"
+
+#: src/dialogs/chatty-new-chat-dialog.c:452
+msgid "SMS"
+msgstr "SMS"
+
+#: src/dialogs/chatty-new-chat-dialog.c:640
+msgid "You"
+msgstr "אני"
+
+#: src/dialogs/chatty-ma-account-details.c:385
+#: src/dialogs/chatty-pp-account-details.c:178
+msgid "connected"
+msgstr "מחובר"
+
+#: src/dialogs/chatty-ma-account-details.c:387
+#: src/dialogs/chatty-pp-account-details.c:180
+msgid "connecting…"
+msgstr "מתחבר…"
+
+#: src/dialogs/chatty-ma-account-details.c:389
+#: src/dialogs/chatty-pp-account-details.c:182
+msgid "disconnected"
+msgstr "מנותק"
+
+#: src/dialogs/chatty-settings-dialog.c:251
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "ארע כשל אימות השרת: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:253
+msgid "Failed to verify server"
+msgstr "ארע כשל אימות השרת"
+
+#: src/dialogs/chatty-settings-dialog.c:259
+msgid "Couldn't get Home server address"
+msgstr "לא ניתן לקבל את כתובת השרת הביתי"
+
+#: src/dialogs/chatty-settings-dialog.c:428
+#: src/ui/chatty-ma-account-details.ui:182
+#: src/ui/chatty-pp-account-details.ui:199
+msgid "Delete Account"
+msgstr "מחיקת חשבון"
+
+#: src/dialogs/chatty-settings-dialog.c:431
+#, c-format
+msgid "Delete account %s?"
+msgstr "למחוק את החשבון %s?"
+
+#: src/dialogs/chatty-settings-dialog.c:582
+msgid "Restart chatty to disable purple"
+msgstr "הפעלה מחדש של Chatty להשבתת Purple"
+
+#: src/dialogs/chatty-settings-dialog.c:584
+#: src/ui/chatty-settings-dialog.ui:512
+msgid "Enable purple plugin"
+msgstr "לאפשר תוסף purple"
+
+#: src/dialogs/chatty-settings-dialog.c:598
+#: src/ui/chatty-settings-dialog.ui:279
+msgid "SMS and MMS Settings"
+msgstr "הגדרות SMS ו־MMS"
+
+#: src/dialogs/chatty-settings-dialog.c:600
+#: src/ui/chatty-settings-dialog.ui:298
+msgid "Purple Settings"
+msgstr "הגדרות Purple"
+
+#: src/dialogs/chatty-settings-dialog.c:602
+msgid "New Account"
+msgstr "חשבון חדש"
+
+#: src/dialogs/chatty-settings-dialog.c:604
+#: src/ui/chatty-settings-dialog.ui:364
+msgid "Blocked Contacts"
+msgstr "אנשי קשר חסומים"
+
+#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7
+#: src/ui/chatty-window.ui:30
+msgid "Preferences"
+msgstr "העדפות"
+
+#: src/dialogs/chatty-settings-dialog.c:634
+msgid "Select Protocol"
+msgstr "בחירת פרוטוקול"
+
+#. TRANSLATORS: Only translate 'or'
+#: src/dialogs/chatty-settings-dialog.c:878
+msgid "@user:matrix.org or user@example.com"
+msgstr "‏‎@user:matrix.org או ‎user@example.com"
+
+#: src/dialogs/chatty-settings-dialog.c:987
+msgid "Unblock contact"
+msgstr "שחרור איש קשר"
+
+#: src/matrix/chatty-ma-account.c:105
+msgid "Incorrect password"
+msgstr "ססמה שגויה"
+
+#: src/matrix/chatty-ma-account.c:108
+msgid "_OK"
+msgstr "_אישור"
+
+#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74
+#: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
+msgid "_Cancel"
+msgstr "_ביטול"
+
+#: src/matrix/chatty-ma-account.c:115
+#, c-format
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "נא למלא ססמה עבור „%s”, שרת בית: %s"
+
+#. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
+#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
+#.
+#: src/mm/chatty-mmsd.c:827
+msgid "%Y-%m-%d %H∶%M"
+msgstr "%m-%d-%Y %H:%M"
+
+#: src/mm/chatty-mmsd.c:1111
+#, c-format
+msgid "You received an MMS, but it expired on: %s"
+msgstr "קיבלת הודעת MMS, אך תוקפה פג: %s"
+
+#: src/mm/chatty-mmsd.c:1114
+msgid "You received an empty MMS."
+msgstr "קיבלת הודעת MMS ריקה."
+
+#: src/ui/chatty-dialog-join-muc.ui:17
+msgid "New Group Chat"
+msgstr "התכתבות קבוצתית חדשה"
+
+#: src/ui/chatty-dialog-join-muc.ui:34
+msgid "Join Chat"
+msgstr "הצטרפות להתכתבות"
+
+#: src/ui/chatty-dialog-join-muc.ui:82 src/ui/chatty-dialog-new-chat.ui:269
+msgid "Select chat account"
+msgstr "נא לבחור חשבון התכתבות"
+
+#: src/ui/chatty-dialog-join-muc.ui:152
+msgid "Password (optional)"
+msgstr "ססמה (רשות)"
+
+#: src/ui/chatty-dialog-new-chat.ui:126
+msgid "Group Title"
+msgstr "כותרת לקבוצה"
+
+#: src/ui/chatty-dialog-new-chat.ui:177
+#| msgid "Add %s to contact list"
+msgid "Add members from contacts…"
+msgstr "הוספת חברים מאנשי הקשר…"
+
+#: src/ui/chatty-dialog-new-chat.ui:223
+msgid "Send To:"
+msgstr "לשלוח אל:"
+
+#: src/ui/chatty-dialog-new-chat.ui:308
+msgid "Name (optional)"
+msgstr "שם (רשות)"
+
+#: src/ui/chatty-dialog-new-chat.ui:351
+msgid "Add to Contacts"
+msgstr "הוספה לאנשי קשר"
+
+#: src/ui/chatty-file-item.ui:28
+msgid "Remove Attachment"
+msgstr "הסרת צרופה"
+
+#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139
+msgid "Chat Details"
+msgstr "פרטי התכתבות"
+
+#: src/ui/chatty-info-dialog.ui:64
+msgid "Apply"
+msgstr "החלה"
+
+#: src/ui/chatty-info-dialog.ui:82
+msgid "Invite"
+msgstr "להזמין"
+
+#: src/ui/chatty-ma-chat-info.ui:67 src/ui/chatty-ma-account-details.ui:233
+#| msgid "Matrix ID:"
+msgid "Matrix ID"
+msgstr "מזהה Matrix"
+
+#: src/ui/chatty-ma-chat-info.ui:93 src/ui/chatty-pp-chat-info.ui:289
+msgid "Chat settings"
+msgstr "הגדרות התכתבות"
+
+#: src/ui/chatty-ma-chat-info.ui:113 src/ui/chatty-pp-chat-info.ui:238
+#: src/ui/chatty-pp-chat-info.ui:344
+msgid "Encryption"
+msgstr "הצפנה"
+
+#: src/ui/chatty-ma-chat-info.ui:114 src/ui/chatty-pp-chat-info.ui:345
+msgid "Encrypt Messages"
+msgstr "הצפנת הודעות"
+
+#: src/ui/chatty-pp-chat-info.ui:43 src/ui/chatty-pp-account-details.ui:60
+#| msgid "Set Avatar"
+msgid "Change Avatar"
+msgstr "שינוי תמונה ייצוגית"
+
+#: src/ui/chatty-pp-chat-info.ui:69 src/ui/chatty-pp-account-details.ui:34
+#| msgid "Set Avatar"
+msgid "Delete Avatar"
+msgstr "מחיקת תמונה ייצוגית"
+
+#: src/ui/chatty-pp-chat-info.ui:108
+msgid "Room topic"
+msgstr "נושא החדר"
+
+#: src/ui/chatty-pp-chat-info.ui:211
+msgid "XMPP ID"
+msgstr "מזהה XMPP"
+
+#: src/ui/chatty-pp-chat-info.ui:264 src/ui/chatty-ma-account-details.ui:67
+#: src/ui/chatty-pp-account-details.ui:142
+msgid "Status"
+msgstr "מצב"
+
+#: src/ui/chatty-pp-chat-info.ui:310
+msgid "Notifications"
+msgstr "התראות"
+
+#: src/ui/chatty-pp-chat-info.ui:327
+msgid "Status Messages"
+msgstr "הודעות מצב"
+
+#: src/ui/chatty-pp-chat-info.ui:328
+msgid "Show status messages in chat"
+msgstr "להציג הודעות מצב בהתכתבות"
+
+#: src/ui/chatty-pp-chat-info.ui:367
+msgid "Fingerprints"
+msgstr "טביעות אצבע"
+
+#: src/ui/chatty-mm-chat-info.ui:29
+msgid "Title"
+msgstr "כותרת"
+
+#: src/ui/chatty-mm-chat-info.ui:33
+msgid "Blank for default"
+msgstr "ריק בברירת מחדל"
+
+#: src/ui/chatty-mm-chat-info.ui:43
+#| msgid "%u Member"
+#| msgid_plural "%u Members"
+msgid "Group Members"
+msgstr "חברי הקבוצה"
+
+#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433
+msgid "Call"
+msgstr "שיחה"
+
+#: src/ui/chatty-ma-account-details.ui:93
+msgid "Name"
+msgstr "שם"
+
+#: src/ui/chatty-ma-account-details.ui:123
+msgid "Email"
+msgstr "דוא״ל"
+
+#: src/ui/chatty-ma-account-details.ui:152
+msgid "Phone"
+msgstr "טלפון"
+
+#: src/ui/chatty-ma-account-details.ui:196
+msgid "Advanced information"
+msgstr "מידע מתקדם"
+
+#: src/ui/chatty-ma-account-details.ui:207
+#| msgid "Matrix Home Server"
+msgid "Homeserver"
+msgstr "שם שרת"
+
+#: src/ui/chatty-ma-account-details.ui:259
+msgid "Device ID"
+msgstr "מזהה התקן"
+
+#: src/ui/chatty-pp-account-details.ui:92
+msgid "Account ID"
+msgstr "מזהה חשבון"
+
+#: src/ui/chatty-pp-account-details.ui:116
+#| msgid "Any Protocol"
+msgid "Protocol"
+msgstr "פרוטוקול"
+
+#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701
+msgid "Password"
+msgstr "ססמה"
+
+#: src/ui/chatty-pp-account-details.ui:213
+msgid "Own Fingerprint"
+msgstr "טביעת אצבע עצמית"
+
+#: src/ui/chatty-settings-dialog.ui:27
+msgid "Back"
+msgstr "חזרה"
+
+#: src/ui/chatty-settings-dialog.ui:60
+msgid "_Add"
+msgstr "_הוספה"
+
+#: src/ui/chatty-settings-dialog.ui:76
+msgid "_Save"
+msgstr "_שמירה"
+
+#: src/ui/chatty-settings-dialog.ui:92
+msgid "_Apply"
+msgstr "ה_חלה"
+
+#: src/ui/chatty-settings-dialog.ui:169
+#| msgid "Account ID"
+msgid "Accounts"
+msgstr "משתמשים"
+
+#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531
+msgid "Privacy"
+msgstr "פרטיות"
+
+#: src/ui/chatty-settings-dialog.ui:195
+msgid "Message Receipts"
+msgstr "קבלות על הודעות"
+
+#: src/ui/chatty-settings-dialog.ui:196
+msgid "Confirm received messages"
+msgstr "לאשר שהודעות התקבלו"
+
+#: src/ui/chatty-settings-dialog.ui:211
+msgid "Typing Notification"
+msgstr "התראות הקלדה"
+
+#: src/ui/chatty-settings-dialog.ui:212
+msgid "Send typing messages"
+msgstr "שליחת הודעות הקלדה"
+
+#: src/ui/chatty-settings-dialog.ui:230
+msgid "Editor"
+msgstr "עורך"
+
+#: src/ui/chatty-settings-dialog.ui:235
+msgid "Graphical Emoticons"
+msgstr "רגשונים גרפיים"
+
+#: src/ui/chatty-settings-dialog.ui:236
+msgid "if you type :) it will be changed to 😃"
+msgstr "הקלדה של :) תשתנה לחייכן 😃"
+
+#. TRANSLATORS: Return is the Enter key.
+#: src/ui/chatty-settings-dialog.ui:251
+#| msgid "Send message with return key"
+msgid "Send Messages with Return"
+msgstr "לשלוח הודעה עם מקש Return"
+
+#: src/ui/chatty-settings-dialog.ui:267
+#| msgid "Purple Settings"
+msgid "Protocol Settings"
+msgstr "הגדרות פרוטוקול"
+
+#: src/ui/chatty-settings-dialog.ui:333
+#| msgid "Request SMS delivery reports"
+msgid "Request Delivery Reports"
+msgstr "דרישת דיווחי מסירת הודעות"
+
+#: src/ui/chatty-settings-dialog.ui:348
+msgid "SMIL for MMS"
+msgstr "‏SMIL עבור MMS"
+
+#: src/ui/chatty-settings-dialog.ui:384
+#| msgid "SMS and MMS Settings"
+msgid "MMS Carrier Settings"
+msgstr "הגדרות חברה ל־MMS"
+
+#: src/ui/chatty-settings-dialog.ui:388
+msgid "MMSC"
+msgstr "MMSC"
+
+#: src/ui/chatty-settings-dialog.ui:403
+msgid "APN"
+msgstr "APN"
+
+#: src/ui/chatty-settings-dialog.ui:418
+msgid "Proxy"
+msgstr "מתווך"
+
+#: src/ui/chatty-settings-dialog.ui:447
+#| msgid "You shall no longer be notified for new messages, continue?"
+msgid "You shall not be notified for the messages from blocked contacts"
+msgstr "לא תתקבלנה התראות על הודעות מאנשי קשר חסומים"
+
+#: src/ui/chatty-settings-dialog.ui:481
+msgid "Blocked chat list empty"
+msgstr "רשימת שיחות חסומות ריקה"
+
+#: src/ui/chatty-settings-dialog.ui:537
+msgid "Message Archive Management"
+msgstr "ניהול ארכיון הודעות"
+
+#: src/ui/chatty-settings-dialog.ui:538
+msgid "Sync conversations with chat server"
+msgstr "לסנכרן התכתבויות עם שרת התכתבויות"
+
+#: src/ui/chatty-settings-dialog.ui:553
+msgid "Message Carbon Copies"
+msgstr "עותקים של הודעות"
+
+#: src/ui/chatty-settings-dialog.ui:737
+#| msgid "Matrix Home Server"
+msgid "Home server"
+msgstr "שרת ביתי"
+
+#: src/ui/chatty-settings-dialog.ui:773
+#| msgid "Add new account…"
+msgid "Add _new account…"
+msgstr "הוספת חשבון _חדש…"
+
+#: src/ui/chatty-window.ui:17
+#| msgid "Archived"
+msgctxt "show archived chat list when clicked"
+msgid "Archived"
+msgstr "ארכיון"
+
+#: src/ui/chatty-window.ui:41
+msgid "Keyboard _Shortcuts"
+msgstr "_צירופי מקשים"
+
+#: src/ui/chatty-window.ui:48
+msgid "Help"
+msgstr "עזרה"
+
+#: src/ui/chatty-window.ui:57
+msgid "About Chats"
+msgstr "על התכתבויות"
+
+#: src/ui/chatty-window.ui:85
+msgid "New Message…"
+msgstr "הודעה חדשה…"
+
+#: src/ui/chatty-window.ui:98
+#| msgid "New Message…"
+msgid "New SMS/MMS Message…"
+msgstr "הודעת SMS/MMS חדשה…"
+
+#: src/ui/chatty-window.ui:111
+msgid "New Group Message…"
+msgstr "הודעה קבוצתית חדשה…"
+
+#: src/ui/chatty-window.ui:164
+msgid "Leave Chat"
+msgstr "לצאת מההתכתבות"
+
+#: src/ui/chatty-window.ui:177
+#| msgid "Blocked Contacts"
+msgid "Block Contact"
+msgstr "חסימת איש קשר"
+
+#: src/ui/chatty-window.ui:190
+#| msgid "Unblock contact"
+msgid "Unblock Contact"
+msgstr "שחרור איש קשר"
+
+#: src/ui/chatty-window.ui:203
+#| msgid "No archived chats"
+msgid "Archive chat"
+msgstr "ארכוב שיחה"
+
+#: src/ui/chatty-window.ui:216
+#| msgid "No archived chats"
+msgid "Unarchive chat"
+msgstr "הוצאת שיחה מארכיון"
+
+#: src/ui/chatty-window.ui:229
+msgid "Delete Chat"
+msgstr "למחוק התכתבות"
+
+#: src/ui/help-overlay.ui:14
+msgctxt "shortcut window"
+msgid "General"
+msgstr "כללי"
+
+#: src/ui/help-overlay.ui:19
+#| msgid "Open Message"
+msgctxt "shortcut window"
+msgid "Open Menu"
+msgstr "פתיחת תפריט"
+
+#: src/ui/help-overlay.ui:27
+#| msgid "Open Message"
+msgctxt "shortcut window"
+msgid "Open Search"
+msgstr "פתיחת חיפוש"
+
+#: src/ui/help-overlay.ui:35
+msgctxt "shortcut window"
+msgid "Show Shortcuts"
+msgstr "הצגת צירופי מקשים"
+
+#: src/users/chatty-contact.c:325
+msgid "Mobile: "
+msgstr "נייד: "
+
+#: src/users/chatty-contact.c:327
+msgid "Work: "
+msgstr "עבודה: "
+
+#: src/users/chatty-contact.c:329
+msgid "Other: "
+msgstr "אחר: "
+
+#, c-format
+#~ msgid "Chatty password for \"%s\""
+#~ msgstr "ססמה של Chatty עבור „%s”"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ has unknown CA"
+#~ msgstr "לאישור עבור ‚%s’ יש רשות אישורים בלתי מוכרת"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ is self-signed"
+#~ msgstr "האישור הזה עבור ‚%s’ נחתם עצמית"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ has expired"
+#~ msgstr "האישור של ‚%s’ פג"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ has been revoked"
+#~ msgstr "האישור עבור ‚%s’ נשלל"
+
+#, c-format
+#~ msgid "Error validating certificate for ‘%s’"
+#~ msgstr "שגיאה באימות אישור עבור ‚%s’"
+
+#~ msgid "sm.puri.Chatty"
+#~ msgstr "sm.puri.Chatty"
+
+#~ msgid "Mark offline users differently"
+#~ msgstr "לסמן משתמשים בלתי־מקוונים אחרת"
+
+#~ msgid "ask your counterpart to use E2EE."
+#~ msgstr "ניתן לבקש מהצד השני בשיחה להפעיל E2EE (הצפנה מקצה לקצה)."
+
+#~ msgid "Your messages are secured"
+#~ msgstr "ההודעות שלך מאובטחות"
+
+#~ msgid "by end-to-end encryption."
+#~ msgstr "בהצפנה מקצה לקצה."
+
+#~ msgid "and carrier rates may apply."
+#~ msgstr "יכול להיות שהשיחה תעלה כסף."
+
+#~ msgctxt "timestamp-suffix-seconds"
+#~ msgid "s"
+#~ msgstr "שנ׳"
+
+#~ msgctxt "timestamp-suffix-minute"
+#~ msgid "m"
+#~ msgstr "ח׳"
+
+#~ msgctxt "timestamp-suffix-minutes"
+#~ msgid "m"
+#~ msgstr "ח׳"
+
+#~ msgctxt "timestamp-suffix-hour"
+#~ msgid "h"
+#~ msgstr "שע׳"
+
+#~ msgctxt "timestamp-suffix-hours"
+#~ msgid "h"
+#~ msgstr "שע׳"
+
+#~ msgctxt "timestamp-suffix-day"
+#~ msgid "d"
+#~ msgstr "י׳"
+
+#~ msgctxt "timestamp-suffix-days"
+#~ msgid "d"
+#~ msgstr "י׳"
+
+#~ msgctxt "timestamp-suffix-month"
+#~ msgid "mo"
+#~ msgstr "חודש"
+
+#~ msgctxt "timestamp-suffix-months"
+#~ msgid "mos"
+#~ msgstr "חודשים"
+
+#~ msgctxt "timestamp-suffix-year"
+#~ msgid "y"
+#~ msgstr "שנה"
+
+#~ msgctxt "timestamp-suffix-years"
+#~ msgid "y"
+#~ msgstr "שנים"
+
+#~ msgid "Over"
+#~ msgstr "למעלה מ־"
+
+#~ msgid "Almost"
+#~ msgstr "כמעט"
+
+#~ msgid "Choose a contact"
+#~ msgstr "נא לבחור איש קשר"
+
+#~ msgid ""
+#~ "Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</"
+#~ "b> button in the titlebar."
+#~ msgstr ""
+#~ "ניתן לבחור איש קשר ל<b>מסרונים</b> או ל<b>הודעה מיידית</b> עם הכפתור "
+#~ "<b>„+”</b> בשורת הכותרת."
+
+#~ msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar."
+#~ msgstr "ניתן ליצור התכתבות ב<b>מסרונים</b> עם הכפתור „+” שבשורת הכותרת."
+
+#, c-format
+#~ msgid "Error saving contact: %s"
+#~ msgstr "שגיאה בשמירת איש קשר: %s"
+
+#~ msgid "Start Chat"
+#~ msgstr "להתחיל התכתבות"
+
+#~ msgid "Chats List"
+#~ msgstr "רשימת התכתבויות"
+
+#~ msgid "Indicate Offline Contacts"
+#~ msgstr "לציין אנשי קשר בלתי מקוונים"
+
+#~ msgid "Grey out avatars from offline contacts"
+#~ msgstr "להאפיר תמונות ייצוגיות של אנשי קשר בלתי מקוונים"
+
+#~ msgid "Indicate Idle Contacts"
+#~ msgstr "לציין אנשי קשר בלתי פעילים"
+
+#~ msgid "Blur avatars from idle contacts"
+#~ msgstr "לטשטש תמונות ייצוגיות של אנשי קשר בלתי פעילים"
+
+#~ msgid "Color unknown contact ID red"
+#~ msgstr "לצבוע מזהי אנשי קשר בלתי מוכרים באדום"
+
+#~ msgid "Convert ASCII emoticons"
+#~ msgstr "להמיר ASCII רגשונים"
+
+#~ msgid "Return = Send Message"
+#~ msgstr "‫Return = לשלוח הודעה"
+
+#~ msgid "_Accept"
+#~ msgstr "ל_קבל"
+
+#~ msgid "New Bulk SMS…"
+#~ msgstr "מסרון מרוכז חדש…"
diff --git a/po/it.po b/po/it.po
index e4fcaa77cd71308e535612cb10d535e37e52262b..1eb114cc3251ec33d479014680f2c7579e2b4c5d 100644
--- a/po/it.po
+++ b/po/it.po
@@ -5,8 +5,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: purism-chatty\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-04-28 15:25+0000\n"
-"PO-Revision-Date: 2022-05-03 15:18+0200\n"
+"POT-Creation-Date: 2022-09-09 15:24+0000\n"
+"PO-Revision-Date: 2022-09-12 15:42+0200\n"
 "Last-Translator: antpanlinux <ant.pandolfo@gmail.com>\n"
 "Language-Team: Italian\n"
 "Language: it\n"
@@ -17,7 +17,7 @@ msgstr ""
 "Plural-Forms: nplurals=2; plural=(n != 1)\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/chatty-window.c:330
+#: src/chatty-application.c:338 src/chatty-window.c:339
 #: src/ui/chatty-window.ui:265
 msgid "Chats"
 msgstr "Chats"
@@ -150,27 +150,27 @@ msgstr "Chats è un'applicazione di messaggistica che supporta XMPP e SMS."
 msgid "Chats message window"
 msgstr "Finestra di messaggio Chats"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Mostra versione"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Avvia in modalità demone"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Disattiva tutti gli account"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Attiva i messaggi di debug libpurple"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Attiva i messaggi dettagliati di debug libpurple"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:144
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -230,12 +230,12 @@ msgstr "Il tuo messaggio è cifrato"
 msgid "Your messages are not encrypted"
 msgstr "Il tuo messaggio non è cifrato"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:420
 msgid "Select File..."
 msgstr "Seleziona file..."
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -243,7 +243,7 @@ msgstr "Seleziona file..."
 msgid "Cancel"
 msgstr "Annulla"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
@@ -347,7 +347,7 @@ msgstr "Moderatore"
 msgid "Member"
 msgstr "Membro"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:724
 #, c-format
 msgid "“%s” is not a valid URI"
 msgstr "“%s” non è un URI valido"
@@ -356,6 +356,12 @@ msgstr "“%s” non è un URI valido"
 msgid "Copy"
 msgstr "Copia"
 
+#: src/chatty-message.c:233
+msgid "Got an encrypted message, but couldn't decrypt due to missing keys"
+msgstr ""
+"È stato ricevuto un messaggio cifrato, ma non è stato possibile decifrarlo a "
+"causa di chiavi mancanti"
+
 #: src/chatty-notification.c:181
 msgid "Open Message"
 msgstr "Apri messaggio"
@@ -369,7 +375,7 @@ msgstr "Nuovo messaggio da %s"
 msgid "Message Received"
 msgstr "Messaggio ricevuto"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44
 msgid "Close"
 msgstr "Chiudi"
 
@@ -399,11 +405,11 @@ msgstr "L'utente %s ha aggiunto %s ai contatti"
 msgid "Authorize %s?"
 msgstr "Autorizzi %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264
 msgid "Reject"
 msgstr "Rifiuta"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265
 msgid "Accept"
 msgstr "Accetta"
 
@@ -412,76 +418,71 @@ msgstr "Accetta"
 msgid "Add %s to contact list"
 msgstr "Aggiungi %s alla lista contatti"
 
-#: src/purple/chatty-purple.c:587
+#: src/purple/chatty-purple.c:586
 msgid "Login failed"
 msgstr "Login fallito"
 
-#: src/purple/chatty-purple.c:592
+#: src/purple/chatty-purple.c:591
 msgid "Please check ID and password"
 msgstr "Per favore controlla ID e password"
 
-#: src/chatty-secret-store.c:98
-#, c-format
-msgid "Chatty password for \"%s\""
-msgstr "Password Chatty per \"%s\""
-
-#: src/chatty-window.c:399
+#: src/chatty-window.c:408
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "Elimina chat con “%s”"
 
-#: src/chatty-window.c:400
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Questo elimina la cronologia delle conversazioni"
 
-#: src/chatty-window.c:402
+#: src/chatty-window.c:411
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "Disconnetti la chat di gruppo “%s”"
 
-#: src/chatty-window.c:403
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Questo rimuove la chat dalla lista delle chat"
 
-#: src/chatty-window.c:415
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Elimina"
 
-#: src/chatty-window.c:493
+#: src/chatty-window.c:502
 msgid "You shall no longer be notified for new messages, continue?"
 msgstr "Non sarai più notificato se riceverai nuovi messaggi. Vuoi continuare?"
 
-#: src/chatty-window.c:568
+#: src/chatty-window.c:577
 msgid "Archived"
 msgstr "Archiviata"
 
-#: src/chatty-window.c:621
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "Un client di messaggistica SMS e XMPP"
 
-#: src/chatty-window.c:628
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr ""
 "Pandolfo Antonio <ant.pandolfo@gmail.com>\n"
 "Vittorio <postav@pm.me>, 2021"
 
-#: src/chatty-window.c:978
+#: src/chatty-window.c:989
 msgid "Any Protocol"
 msgstr "Qualsiasi Protocollo"
 
-#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:980
+#: src/chatty-window.c:991
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -581,115 +582,93 @@ msgstr "connessione in corso…"
 msgid "disconnected"
 msgstr "disconnesso"
 
-#: src/dialogs/chatty-settings-dialog.c:255
-#: src/dialogs/chatty-settings-dialog.c:405
+#: src/dialogs/chatty-settings-dialog.c:251
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Impossibile verificare il server: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:253
 msgid "Failed to verify server"
 msgstr "Impossibile verificare il server"
 
-#: src/dialogs/chatty-settings-dialog.c:306
-#: src/dialogs/chatty-settings-dialog.c:385
+#: src/dialogs/chatty-settings-dialog.c:259
 msgid "Couldn't get Home server address"
 msgstr "Impossibile ottenere l'indirizzo del server personale"
 
-#: src/dialogs/chatty-settings-dialog.c:510
+#: src/dialogs/chatty-settings-dialog.c:428
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Elimina l'account"
 
-#: src/dialogs/chatty-settings-dialog.c:513
+#: src/dialogs/chatty-settings-dialog.c:431
 #, c-format
 msgid "Delete account %s?"
 msgstr "Eliminare l'account %s?"
 
-#: src/dialogs/chatty-settings-dialog.c:654
+#: src/dialogs/chatty-settings-dialog.c:582
 msgid "Restart chatty to disable purple"
 msgstr "Riavvia Chatty per disattivare purple"
 
-#: src/dialogs/chatty-settings-dialog.c:656
+#: src/dialogs/chatty-settings-dialog.c:584
 #: src/ui/chatty-settings-dialog.ui:512
 msgid "Enable purple plugin"
 msgstr "Attiva il plugin purple"
 
-#: src/dialogs/chatty-settings-dialog.c:670
+#: src/dialogs/chatty-settings-dialog.c:598
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr "Impostazioni SMS e MMS"
 
-#: src/dialogs/chatty-settings-dialog.c:672
+#: src/dialogs/chatty-settings-dialog.c:600
 #: src/ui/chatty-settings-dialog.ui:298
 msgid "Purple Settings"
 msgstr "Impostazioni purple"
 
-#: src/dialogs/chatty-settings-dialog.c:674
+#: src/dialogs/chatty-settings-dialog.c:602
 msgid "New Account"
 msgstr "Nuovo account"
 
-#: src/dialogs/chatty-settings-dialog.c:676
+#: src/dialogs/chatty-settings-dialog.c:604
 #: src/ui/chatty-settings-dialog.ui:364
 msgid "Blocked Contacts"
 msgstr "Contatti bloccati"
 
-#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7
+#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7
 #: src/ui/chatty-window.ui:30
 msgid "Preferences"
 msgstr "Preferenze"
 
-#: src/dialogs/chatty-settings-dialog.c:707
+#: src/dialogs/chatty-settings-dialog.c:634
 msgid "Select Protocol"
 msgstr "Seleziona Protocollo"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:948
+#: src/dialogs/chatty-settings-dialog.c:878
 msgid "@user:matrix.org or user@example.com"
 msgstr "@user:matrix.org oppure user@example.com"
 
-#: src/dialogs/chatty-settings-dialog.c:1057
+#: src/dialogs/chatty-settings-dialog.c:987
 msgid "Unblock contact"
 msgstr "Sblocca contatto"
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/matrix/chatty-ma-account.c:105
 msgid "Incorrect password"
 msgstr "Password errata"
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:108
 msgid "_OK"
 msgstr "_OK"
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "_Annulla"
 
-#: src/matrix/chatty-ma-account.c:306
-#, c-format
-msgid "Please enter password for “%s”"
-msgstr "Inserire la password per “%s”"
-
-#: src/matrix/matrix-utils.c:474
-#, c-format
-msgid "The certificate for ‘%s’ has unknown CA"
-msgstr "Il certificato per ‘%s’ ha una CA sconosciuta"
-
-#: src/matrix/matrix-utils.c:476
-#, c-format
-msgid "The certificate for ‘%s’ is self-signed"
-msgstr "Il certificato per ‘%s’ è autofirmato"
-
-#: src/matrix/matrix-utils.c:480
-#, c-format
-msgid "The certificate for ‘%s’ has expired"
-msgstr "Il certificato per ‘%s’ è scaduto"
-
-#: src/matrix/matrix-utils.c:484
-#, c-format
-msgid "The certificate for ‘%s’ has been revoked"
-msgstr "Il certificato per ‘%s’ è stato revocato"
-
-#: src/matrix/matrix-utils.c:493
+#: src/matrix/chatty-ma-account.c:115
 #, c-format
-msgid "Error validating certificate for ‘%s’"
-msgstr "Errore durante la convalida del certificato per ‘%s’"
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Inserire la password per “%s”, server personale: %s"
 
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
diff --git a/po/pt_BR.po b/po/pt_BR.po
index 885d798dbf121edf2c2fbd01c9051724bc8733ef..b2e8d307a69ab292ec863513e124c76e4dd2bad8 100644
--- a/po/pt_BR.po
+++ b/po/pt_BR.po
@@ -3,33 +3,30 @@
 # This file is distributed under the same license as the chatty package.
 # Luís Fernando Stürmer da Rosa <luisfsr@dismail.de>, 2018-2020.
 # Bruno Lopes <brunolopesdsilv@gmail.com>, 2020.
-# Rafael Fontenelle <rafaelff@gnome.org>, 2020-2021.
 # Matheus Barbosa <mdpb.matheus@gmail.com>, 2022.
+# Rafael Fontenelle <rafaelff@gnome.org>, 2020-2022.
 #
 msgid ""
 msgstr ""
 "Project-Id-Version: purism-chatty\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-02-01 15:25+0000\n"
-"PO-Revision-Date: 2022-01-31 14:22-0300\n"
-"Last-Translator: Matheus Barbosa <mdpb.matheus@gmail.com>\n"
+"POT-Creation-Date: 2022-08-08 03:24+0000\n"
+"PO-Revision-Date: 2022-08-30 09:25-0300\n"
+"Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n"
 "Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n"
 "Language: pt_BR\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Gtranslator 40.0\n"
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"X-Generator: Gtranslator 40.0\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/ui/chatty-window.ui:200
+#: src/chatty-application.c:336 src/chatty-window.c:339
+#: src/ui/chatty-window.ui:265
 msgid "Chats"
 msgstr "Conversas"
 
-#: data/sm.puri.Chatty.desktop.in:5
-msgid "sm.puri.Chatty"
-msgstr "sm.puri.Chatty"
-
 #: data/sm.puri.Chatty.desktop.in:6
 msgid "SMS and XMPP chat application"
 msgstr "Aplicativo de conversa via XMPP e SMS"
@@ -62,7 +59,7 @@ msgstr "Se é para enviar o status da mensagem lida"
 msgid "Message carbon copies"
 msgstr "Cópias de mensagens"
 
-#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:479
+#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554
 msgid "Share chat history among devices"
 msgstr "Sincronizar histórico entre dispositivos"
 
@@ -78,7 +75,7 @@ msgstr "Habilitar a sincronia de mensagens arquivadas pelo servidor"
 msgid "Enable purple"
 msgstr "Habilitar purple"
 
-#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:436
+#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511
 msgid "Enable purple accounts"
 msgstr "Habilitar contas purple"
 
@@ -158,27 +155,27 @@ msgstr "Conversas é um aplicativo compatível com XMPP e SMS."
 msgid "Chats message window"
 msgstr "Janela de mensagens"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Exibir versão"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Inicializar em segundo plano"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Desabilitar contas"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Habilitar mensagens de depuração do libpurple"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Habilitar mensagens de depuração detalhada do libpurple"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:144
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -191,23 +188,27 @@ msgstr ""
 msgid "Send To"
 msgstr "Enviar para"
 
-#: src/chatty-chat-list.c:152
+#: src/chatty-chat-list.c:175
 msgid "Select a contact with the <b>“+”</b> button in the titlebar."
 msgstr "Escolha um contato com o botão <b>“+”</b> na barra de título."
 
-#: src/chatty-chat-list.c:156
+#: src/chatty-chat-list.c:179
 msgid "Add instant messaging accounts in Preferences."
 msgstr "Adicionar contas de mensagens instantâneas nas Preferências."
 
-#: src/chatty-chat-list.c:192 src/chatty-contact-list.c:282
+#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282
 msgid "No Search Results"
 msgstr "Sem resultados da pesquisa"
 
-#: src/chatty-chat-list.c:193
+#: src/chatty-chat-list.c:216
 msgid "Try different search"
 msgstr "Tente uma pesquisa diferente"
 
-#: src/chatty-chat-list.c:196
+#: src/chatty-chat-list.c:220
+msgid "No archived chats"
+msgstr "Nenhuma conversa arquivada"
+
+#: src/chatty-chat-list.c:222
 msgid "Start Chatting"
 msgstr "Começar a conversar"
 
@@ -233,12 +234,12 @@ msgstr "Suas mensagens estão criptografadas"
 msgid "Your messages are not encrypted"
 msgstr "Suas mensagens não estão criptografadas"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:420
 msgid "Select File..."
 msgstr "Selecionar arquivo…"
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:347 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -246,24 +247,24 @@ msgstr "Selecionar arquivo…"
 msgid "Cancel"
 msgstr "Cancelar"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
 msgid "Open"
 msgstr "Abrir"
 
-#: src/chatty-chat.c:632
+#: src/chatty-chat.c:635
 msgid "Empty room"
 msgstr "Sala vazia"
 
 #. TRANSLATORS: %s are name/user-id/phone numbers of two users
-#: src/chatty-chat.c:639
+#: src/chatty-chat.c:642
 #, c-format
 msgid "%s and %s"
 msgstr "%s e %s"
 
-#: src/chatty-chat.c:641
+#: src/chatty-chat.c:644
 #, c-format
 msgid "%s and %u other"
 msgid_plural "%s and %u others"
@@ -350,7 +351,7 @@ msgstr "Moderador"
 msgid "Member"
 msgstr "Participante"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:729
 #, c-format
 msgid "“%s” is not a valid URI"
 msgstr "“%s” não é um URI válido"
@@ -372,7 +373,7 @@ msgstr "Nova mensagem de %s"
 msgid "Message Received"
 msgstr "Mensagem recebida"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:504
 msgid "Close"
 msgstr "Fechar"
 
@@ -402,11 +403,11 @@ msgstr "O usuário %s adicionou %s aos contatos"
 msgid "Authorize %s?"
 msgstr "Autorizar %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:508
 msgid "Reject"
 msgstr "Rejeitar"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:509
 msgid "Accept"
 msgstr "Aceitar"
 
@@ -423,38 +424,46 @@ msgstr "O login falhou"
 msgid "Please check ID and password"
 msgstr "Verifique o ID e a senha"
 
-#: src/chatty-secret-store.c:98
+#: src/chatty-secret-store.c:99
 #, c-format
 msgid "Chatty password for \"%s\""
 msgstr "Senha do Chatty para “%s”"
 
-#: src/chatty-window.c:333
+#: src/chatty-window.c:408
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "Apagar conversa com “%s”"
 
-#: src/chatty-window.c:334
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Isto fará com que o histórico de conversas seja apagado"
 
-#: src/chatty-window.c:336
+#: src/chatty-window.c:411
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "Sair da conversa em grupo “%s”"
 
-#: src/chatty-window.c:337
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Isto removerá a conversa da lista"
 
-#: src/chatty-window.c:349
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Apagar"
 
-#: src/chatty-window.c:473
+#: src/chatty-window.c:502
+msgid "You shall no longer be notified for new messages, continue?"
+msgstr "Você não receberá mais notificação sobre novas mensagens, continuar?"
+
+#: src/chatty-window.c:577
+msgid "Archived"
+msgstr "Arquivada"
+
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "Um aplicativo de conversa via XMPP e SMS"
 
-#: src/chatty-window.c:480
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr ""
 "Luís Fernando Stürmer da Rosa <luisfsr@dismail.de>\n"
@@ -462,23 +471,23 @@ msgstr ""
 "Bruno Lopes <brunolopesdsilv@gmail.com>\n"
 "Matheus Barbosa <mdpb.matheus@gmail.com>"
 
-#: src/chatty-window.c:814
+#: src/chatty-window.c:989
 msgid "Any Protocol"
 msgstr "Qualquer protocolo"
 
-#: src/chatty-window.c:815 src/ui/chatty-settings-dialog.ui:552
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:816
+#: src/chatty-window.c:991
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:819 src/ui/chatty-settings-dialog.ui:540
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:822 src/ui/chatty-settings-dialog.ui:566
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -578,103 +587,117 @@ msgstr "conectando…"
 msgid "disconnected"
 msgstr "desconectado"
 
-#: src/dialogs/chatty-settings-dialog.c:210
-#: src/dialogs/chatty-settings-dialog.c:374
+#: src/dialogs/chatty-settings-dialog.c:267
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Falha ao verificar o servidor: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:269
+#: src/dialogs/chatty-settings-dialog.c:479
 msgid "Failed to verify server"
 msgstr "Erro ao verificar o servidor"
 
-#: src/dialogs/chatty-settings-dialog.c:261
-#: src/dialogs/chatty-settings-dialog.c:354
+#: src/dialogs/chatty-settings-dialog.c:275
+#: src/dialogs/chatty-settings-dialog.c:459
 msgid "Couldn't get Home server address"
 msgstr "Não foi possível obter o endereço do servidor Home"
 
-#: src/dialogs/chatty-settings-dialog.c:479
+#: src/dialogs/chatty-settings-dialog.c:584
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Apagar conta"
 
-#: src/dialogs/chatty-settings-dialog.c:482
+#: src/dialogs/chatty-settings-dialog.c:587
 #, c-format
 msgid "Delete account %s?"
 msgstr "Apagar conta %s?"
 
-#: src/dialogs/chatty-settings-dialog.c:618
+#: src/dialogs/chatty-settings-dialog.c:728
 msgid "Restart chatty to disable purple"
 msgstr "Reinicie o Chatty para desabilitar o purple"
 
-#: src/dialogs/chatty-settings-dialog.c:620
-#: src/ui/chatty-settings-dialog.ui:437
+#: src/dialogs/chatty-settings-dialog.c:730
+#: src/ui/chatty-settings-dialog.ui:512
 msgid "Enable purple plugin"
 msgstr "Habilite plugin do purple"
 
-#: src/dialogs/chatty-settings-dialog.c:634
+#: src/dialogs/chatty-settings-dialog.c:744
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr "Configurações de SMS e MMS"
 
-#: src/dialogs/chatty-settings-dialog.c:636
+#: src/dialogs/chatty-settings-dialog.c:746
 #: src/ui/chatty-settings-dialog.ui:298
 msgid "Purple Settings"
 msgstr "Configurações do purple"
 
-#: src/dialogs/chatty-settings-dialog.c:638
+#: src/dialogs/chatty-settings-dialog.c:748
 msgid "New Account"
 msgstr "Nova conta"
 
-#: src/dialogs/chatty-settings-dialog.c:640 src/ui/chatty-settings-dialog.ui:7
-#: src/ui/chatty-window.ui:17
+#: src/dialogs/chatty-settings-dialog.c:750
+#: src/ui/chatty-settings-dialog.ui:364
+msgid "Blocked Contacts"
+msgstr "Contatos bloqueados"
+
+#: src/dialogs/chatty-settings-dialog.c:752 src/ui/chatty-settings-dialog.ui:7
+#: src/ui/chatty-window.ui:30
 msgid "Preferences"
 msgstr "Preferências"
 
-#: src/dialogs/chatty-settings-dialog.c:669
+#: src/dialogs/chatty-settings-dialog.c:781
 msgid "Select Protocol"
 msgstr "Selecione o protocolo"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:865
+#: src/dialogs/chatty-settings-dialog.c:1022
 msgid "@user:matrix.org or user@example.com"
 msgstr "@usuário:matrix.org ou usuário@example.com"
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/dialogs/chatty-settings-dialog.c:1131
+msgid "Unblock contact"
+msgstr "Desbloquear contato"
+
+#: src/matrix/chatty-ma-account.c:231
 msgid "Incorrect password"
 msgstr "Senha incorreta"
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:234
 msgid "_OK"
 msgstr "_OK"
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:235 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "_Cancelar"
 
-#: src/matrix/chatty-ma-account.c:306
+#: src/matrix/chatty-ma-account.c:241
 #, c-format
-msgid "Please enter password for “%s”"
-msgstr "Por favor, insira a senha para “%s”"
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Por favor, insira a senha para “%s”, servidor: %s"
 
-#: src/matrix/matrix-utils.c:474
+#: src/matrix/matrix-utils.c:473
 #, c-format
 msgid "The certificate for ‘%s’ has unknown CA"
 msgstr "O certificado para ‘%s’ é de uma CA desconhecida"
 
-#: src/matrix/matrix-utils.c:476
+#: src/matrix/matrix-utils.c:475
 #, c-format
 msgid "The certificate for ‘%s’ is self-signed"
 msgstr "O certificado para ‘%s’ é auto-assinado"
 
-#: src/matrix/matrix-utils.c:480
+#: src/matrix/matrix-utils.c:479
 #, c-format
 msgid "The certificate for ‘%s’ has expired"
 msgstr "O certificado para ‘%s’ expirou"
 
-#: src/matrix/matrix-utils.c:484
+#: src/matrix/matrix-utils.c:483
 #, c-format
 msgid "The certificate for ‘%s’ has been revoked"
 msgstr "O certificado para ‘%s’ foi revogado"
 
-#: src/matrix/matrix-utils.c:493
+#: src/matrix/matrix-utils.c:492
 #, c-format
 msgid "Error validating certificate for ‘%s’"
 msgstr "Erro ao validar o certificado para ‘%s’"
@@ -682,16 +705,16 @@ msgstr "Erro ao validar o certificado para ‘%s’"
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
 #.
-#: src/mm/chatty-mmsd.c:826
+#: src/mm/chatty-mmsd.c:827
 msgid "%Y-%m-%d %H∶%M"
 msgstr "%Y-%m-%d %H∶%M"
 
-#: src/mm/chatty-mmsd.c:1107
+#: src/mm/chatty-mmsd.c:1111
 #, c-format
 msgid "You received an MMS, but it expired on: %s"
 msgstr "Você recebeu um MMS, mas ele expirou em: %s"
 
-#: src/mm/chatty-mmsd.c:1110
+#: src/mm/chatty-mmsd.c:1114
 msgid "You received an empty MMS."
 msgstr "Você recebeu um MMS vazio."
 
@@ -735,7 +758,7 @@ msgstr "Adicionar aos contatos"
 msgid "Remove Attachment"
 msgstr "Remover anexo"
 
-#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:126
+#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139
 msgid "Chat Details"
 msgstr "Detalhes da conversa"
 
@@ -813,7 +836,7 @@ msgstr "Em branco por padrão"
 msgid "Group Members"
 msgstr "Membros do grupo"
 
-#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:353
+#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433
 msgid "Call"
 msgstr "Ligação"
 
@@ -835,7 +858,7 @@ msgstr "Informações avançadas"
 
 #: src/ui/chatty-ma-account-details.ui:207
 msgid "Homeserver"
-msgstr "Servidor doméstico"
+msgstr "Servidor"
 
 #: src/ui/chatty-ma-account-details.ui:259
 msgid "Device ID"
@@ -849,7 +872,7 @@ msgstr "ID da conta"
 msgid "Protocol"
 msgstr "Protocolo"
 
-#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:626
+#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701
 msgid "Password"
 msgstr "Senha"
 
@@ -877,7 +900,7 @@ msgstr "_Aplicar"
 msgid "Accounts"
 msgstr "Contas"
 
-#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:456
+#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531
 msgid "Privacy"
 msgstr "Privacidade"
 
@@ -926,71 +949,100 @@ msgstr "Solicitar relatórios de entrega"
 msgid "SMIL for MMS"
 msgstr "SMIL para MMS"
 
-#: src/ui/chatty-settings-dialog.ui:366
+#: src/ui/chatty-settings-dialog.ui:384
 msgid "MMS Carrier Settings"
 msgstr "Configurações de MMS da operadora"
 
-#: src/ui/chatty-settings-dialog.ui:370
+#: src/ui/chatty-settings-dialog.ui:388
 msgid "MMSC"
 msgstr "MMSC"
 
-#: src/ui/chatty-settings-dialog.ui:385
+#: src/ui/chatty-settings-dialog.ui:403
 msgid "APN"
 msgstr "Pontos de acesso"
 
-#: src/ui/chatty-settings-dialog.ui:400
+#: src/ui/chatty-settings-dialog.ui:418
 msgid "Proxy"
 msgstr "Proxy"
 
-#: src/ui/chatty-settings-dialog.ui:462
+#: src/ui/chatty-settings-dialog.ui:447
+msgid "You shall not be notified for the messages from blocked contacts"
+msgstr "Você não será notificado sobre as mensagens de contatos bloqueados"
+
+#: src/ui/chatty-settings-dialog.ui:481
+msgid "Blocked chat list empty"
+msgstr "Lista de conversas bloqueadas vazia"
+
+#: src/ui/chatty-settings-dialog.ui:537
 msgid "Message Archive Management"
 msgstr "Gerenciador de mensagens arquivadas"
 
-#: src/ui/chatty-settings-dialog.ui:463
+#: src/ui/chatty-settings-dialog.ui:538
 msgid "Sync conversations with chat server"
 msgstr "Sincronizar conversas com o servidor"
 
-#: src/ui/chatty-settings-dialog.ui:478
+#: src/ui/chatty-settings-dialog.ui:553
 msgid "Message Carbon Copies"
 msgstr "Copiar as mensagens"
 
-#: src/ui/chatty-settings-dialog.ui:662
+#: src/ui/chatty-settings-dialog.ui:737
 msgid "Home server"
 msgstr "Servidor doméstico"
 
-#: src/ui/chatty-settings-dialog.ui:698
+#: src/ui/chatty-settings-dialog.ui:773
 msgid "Add _new account…"
 msgstr "Adicionar _nova conta…"
 
-#: src/ui/chatty-window.ui:28
+#: src/ui/chatty-window.ui:17
+msgctxt "show archived chat list when clicked"
+msgid "Archived"
+msgstr "Arquivada"
+
+#: src/ui/chatty-window.ui:41
 msgid "Keyboard _Shortcuts"
 msgstr "Atalhos de _teclado"
 
-#: src/ui/chatty-window.ui:35
+#: src/ui/chatty-window.ui:48
 msgid "Help"
 msgstr "Ajuda"
 
-#: src/ui/chatty-window.ui:44
+#: src/ui/chatty-window.ui:57
 msgid "About Chats"
 msgstr "Sobre o Conversas"
 
-#: src/ui/chatty-window.ui:72
+#: src/ui/chatty-window.ui:85
 msgid "New Message…"
 msgstr "Nova mensagem…"
 
-#: src/ui/chatty-window.ui:85
+#: src/ui/chatty-window.ui:98
 msgid "New SMS/MMS Message…"
 msgstr "Nova mensagem SMS/MMS…"
 
-#: src/ui/chatty-window.ui:98
+#: src/ui/chatty-window.ui:111
 msgid "New Group Message…"
 msgstr "Nova mensagem de grupo…"
 
-#: src/ui/chatty-window.ui:151
+#: src/ui/chatty-window.ui:164
 msgid "Leave Chat"
 msgstr "Sair da conversa"
 
-#: src/ui/chatty-window.ui:164
+#: src/ui/chatty-window.ui:177
+msgid "Block Contact"
+msgstr "Bloquear contato"
+
+#: src/ui/chatty-window.ui:190
+msgid "Unblock Contact"
+msgstr "Desbloquear contato"
+
+#: src/ui/chatty-window.ui:203
+msgid "Archive chat"
+msgstr "Conversa arquivada"
+
+#: src/ui/chatty-window.ui:216
+msgid "Unarchive chat"
+msgstr "Conversa desarquivada"
+
+#: src/ui/chatty-window.ui:229
 msgid "Delete Chat"
 msgstr "Apagar conversa"
 
@@ -1026,6 +1078,9 @@ msgstr "No trabalho: "
 msgid "Other: "
 msgstr "Outro: "
 
+#~ msgid "sm.puri.Chatty"
+#~ msgstr "sm.puri.Chatty"
+
 #~ msgctxt "timestamp-suffix-seconds"
 #~ msgid "s"
 #~ msgstr "s"
diff --git a/po/ro.po b/po/ro.po
index b0f74220ce1f575c2f642ad6bd22f35cf380631d..8f37e6d1970b4800e54cc94cd078cf808bdb781c 100644
--- a/po/ro.po
+++ b/po/ro.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: chatty master\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-04-28 15:25+0000\n"
-"PO-Revision-Date: 2022-04-28 18:29+0200\n"
+"POT-Creation-Date: 2022-09-08 03:24+0000\n"
+"PO-Revision-Date: 2022-09-08 16:54+0200\n"
 "Last-Translator: Florentina Mușat <florentina.musat.28@gmail.com>\n"
 "Language-Team: Romanian <gnomero-list@lists.sourceforge.net>\n"
 "Language: ro\n"
@@ -21,7 +21,7 @@ msgstr ""
 "X-Poedit-SourceCharset: UTF-8\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/chatty-window.c:330
+#: src/chatty-application.c:338 src/chatty-window.c:339
 #: src/ui/chatty-window.ui:265
 msgid "Chats"
 msgstr "Discuții"
@@ -155,27 +155,27 @@ msgstr "Discuții este o aplicație de mesagerie XMPP și SMS."
 msgid "Chats message window"
 msgstr "Fereastra de mesaj pentru Discuții"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Arată versiunea"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Pornește în modul serviciu"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Dezactivează toate conturile"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Activează mesajele de depanare libpurple"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Activează mesajele detaliate de depanare libpurple"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:144
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -232,12 +232,12 @@ msgstr "Mesajele sunt cifrate"
 msgid "Your messages are not encrypted"
 msgstr "Mesajele nu sunt cifrate"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:420
 msgid "Select File..."
 msgstr "Alege fișierul..."
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -245,7 +245,7 @@ msgstr "Alege fișierul..."
 msgid "Cancel"
 msgstr "Anulează"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
@@ -349,7 +349,7 @@ msgstr "Moderator"
 msgid "Member"
 msgstr "Membru"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:724
 #, c-format
 msgid "“%s” is not a valid URI"
 msgstr "„%s” nu este un URI valabil"
@@ -358,6 +358,12 @@ msgstr "„%s” nu este un URI valabil"
 msgid "Copy"
 msgstr "Copiază"
 
+#: src/chatty-message.c:233
+msgid "Got an encrypted message, but couldn't decrypt due to missing keys"
+msgstr ""
+"S-a primit un mesaj cifrat, dar nu a putut fi descifrat din cauza lipsei "
+"cheilor"
+
 #: src/chatty-notification.c:181
 msgid "Open Message"
 msgstr "Deschide mesajul"
@@ -371,7 +377,7 @@ msgstr "Mesaj nou de la %s"
 msgid "Message Received"
 msgstr "Mesaj primit"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44
 msgid "Close"
 msgstr "ÃŽnchide"
 
@@ -401,11 +407,11 @@ msgstr "Utilizatorul %s a adăugat %s la contacte"
 msgid "Authorize %s?"
 msgstr "Autorizezi %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264
 msgid "Reject"
 msgstr "Respinge"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265
 msgid "Accept"
 msgstr "Acceptă"
 
@@ -414,76 +420,71 @@ msgstr "Acceptă"
 msgid "Add %s to contact list"
 msgstr "Adaugă %s la lista de contacte"
 
-#: src/purple/chatty-purple.c:587
+#: src/purple/chatty-purple.c:586
 msgid "Login failed"
 msgstr "Autentificarea a eșuat"
 
-#: src/purple/chatty-purple.c:592
+#: src/purple/chatty-purple.c:591
 msgid "Please check ID and password"
 msgstr "Verifică ID-ul și parola"
 
-#: src/chatty-secret-store.c:98
-#, c-format
-msgid "Chatty password for \"%s\""
-msgstr "Parola Chatty pentru „%s”"
-
-#: src/chatty-window.c:399
+#: src/chatty-window.c:408
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "Șterge discuția cu „%s”"
 
-#: src/chatty-window.c:400
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Aceasta șterge istoricul conversației"
 
-#: src/chatty-window.c:402
+#: src/chatty-window.c:411
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "Deconectează discuția în grup „%s”"
 
-#: src/chatty-window.c:403
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Aceasta elimină discuția din lista discuțiilor"
 
-#: src/chatty-window.c:415
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Șterge"
 
-#: src/chatty-window.c:493
+#: src/chatty-window.c:502
 msgid "You shall no longer be notified for new messages, continue?"
 msgstr "Nu vei mai fi înștiințat dacă vei primi mesaje noi. Continui?"
 
-#: src/chatty-window.c:568
+#: src/chatty-window.c:577
 msgid "Archived"
 msgstr "Arhivată"
 
-#: src/chatty-window.c:621
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "Un client de mesagerie SMS și XMPP"
 
-#: src/chatty-window.c:628
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr ""
 "Florentina Mușat <florentina [dot] musat [dot] 28 [at] gmail [dot] com>, "
 "2020-2021"
 
-#: src/chatty-window.c:978
+#: src/chatty-window.c:989
 msgid "Any Protocol"
 msgstr "Orice Protocol"
 
-#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:980
+#: src/chatty-window.c:991
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -584,115 +585,93 @@ msgstr "se conectează…"
 msgid "disconnected"
 msgstr "deconectat"
 
-#: src/dialogs/chatty-settings-dialog.c:255
-#: src/dialogs/chatty-settings-dialog.c:405
+#: src/dialogs/chatty-settings-dialog.c:251
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Nu s-a putut verifica serverul: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:253
 msgid "Failed to verify server"
 msgstr "Serverul nu a putut fi verificat"
 
-#: src/dialogs/chatty-settings-dialog.c:306
-#: src/dialogs/chatty-settings-dialog.c:385
+#: src/dialogs/chatty-settings-dialog.c:259
 msgid "Couldn't get Home server address"
 msgstr "Nu s-a putut obține adresa serverului de acasă"
 
-#: src/dialogs/chatty-settings-dialog.c:510
+#: src/dialogs/chatty-settings-dialog.c:428
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Șterge contul"
 
-#: src/dialogs/chatty-settings-dialog.c:513
+#: src/dialogs/chatty-settings-dialog.c:431
 #, c-format
 msgid "Delete account %s?"
 msgstr "Ștergi contul %s?"
 
-#: src/dialogs/chatty-settings-dialog.c:654
+#: src/dialogs/chatty-settings-dialog.c:582
 msgid "Restart chatty to disable purple"
 msgstr "Repornește Chatty pentru a dezactiva purple"
 
-#: src/dialogs/chatty-settings-dialog.c:656
+#: src/dialogs/chatty-settings-dialog.c:584
 #: src/ui/chatty-settings-dialog.ui:512
 msgid "Enable purple plugin"
 msgstr "Activează modulul purple"
 
-#: src/dialogs/chatty-settings-dialog.c:670
+#: src/dialogs/chatty-settings-dialog.c:598
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr "Opțiuni SMS și MMS"
 
-#: src/dialogs/chatty-settings-dialog.c:672
+#: src/dialogs/chatty-settings-dialog.c:600
 #: src/ui/chatty-settings-dialog.ui:298
 msgid "Purple Settings"
 msgstr "Opțiuni purple"
 
-#: src/dialogs/chatty-settings-dialog.c:674
+#: src/dialogs/chatty-settings-dialog.c:602
 msgid "New Account"
 msgstr "Cont nou"
 
-#: src/dialogs/chatty-settings-dialog.c:676
+#: src/dialogs/chatty-settings-dialog.c:604
 #: src/ui/chatty-settings-dialog.ui:364
 msgid "Blocked Contacts"
 msgstr "Contacte blocate"
 
-#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7
+#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7
 #: src/ui/chatty-window.ui:30
 msgid "Preferences"
 msgstr "Preferințe"
 
-#: src/dialogs/chatty-settings-dialog.c:707
+#: src/dialogs/chatty-settings-dialog.c:634
 msgid "Select Protocol"
 msgstr "Alege protocolul"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:948
+#: src/dialogs/chatty-settings-dialog.c:878
 msgid "@user:matrix.org or user@example.com"
 msgstr "@user:matrix.org sau user@example.com"
 
-#: src/dialogs/chatty-settings-dialog.c:1057
+#: src/dialogs/chatty-settings-dialog.c:987
 msgid "Unblock contact"
 msgstr "Deblochează contactul"
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/matrix/chatty-ma-account.c:105
 msgid "Incorrect password"
 msgstr "Parolă greșită"
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:108
 msgid "_OK"
 msgstr "_OK"
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "_Anulează"
 
-#: src/matrix/chatty-ma-account.c:306
-#, c-format
-msgid "Please enter password for “%s”"
-msgstr "Introdu parola pentru „%s”"
-
-#: src/matrix/matrix-utils.c:474
-#, c-format
-msgid "The certificate for ‘%s’ has unknown CA"
-msgstr "Certificatul pentru „%s” are CA necunoscută"
-
-#: src/matrix/matrix-utils.c:476
-#, c-format
-msgid "The certificate for ‘%s’ is self-signed"
-msgstr "Certificatul pentru „%s” este autosemnat"
-
-#: src/matrix/matrix-utils.c:480
-#, c-format
-msgid "The certificate for ‘%s’ has expired"
-msgstr "Certificatul pentru „%s” a expirat"
-
-#: src/matrix/matrix-utils.c:484
-#, c-format
-msgid "The certificate for ‘%s’ has been revoked"
-msgstr "Certificatul pentru „%s” a fost anulat"
-
-#: src/matrix/matrix-utils.c:493
+#: src/matrix/chatty-ma-account.c:115
 #, c-format
-msgid "Error validating certificate for ‘%s’"
-msgstr "Eroare la validarea certificatului pentru „%s”"
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Introdu parola pentru „%s”, server de acasă: %s"
 
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
@@ -1070,5 +1049,3 @@ msgstr "Serviciu: "
 msgid "Other: "
 msgstr "Altele: "
 
-#~ msgid "sm.puri.Chatty"
-#~ msgstr "sm.puri.Chatty"
diff --git a/po/sr.po b/po/sr.po
index 9766363f55bcba573bf8306628ab97dd6ebc9081..36c3784c457df800239c8c380d30d47c5605f1af 100644
--- a/po/sr.po
+++ b/po/sr.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: chatty master\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2020-09-15 15:25+0000\n"
-"PO-Revision-Date: 2020-09-15 23:52+0200\n"
+"POT-Creation-Date: 2022-08-08 03:24+0000\n"
+"PO-Revision-Date: 2022-08-20 09:11+0200\n"
 "Last-Translator: Марко М. Костић <marko.m.kostic@gmail.com>\n"
 "Language-Team: Serbian <gnome-sr@googlegroups.com>\n"
 "Language: sr\n"
@@ -17,17 +17,14 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=4; plural=n==1? 3 : n%10==1 && n%100!=11 ? 0 : n"
 "%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
-"X-Generator: Poedit 2.4.1\n"
+"X-Generator: Poedit 3.1.1\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:304 src/ui/chatty-window.ui:225
+#: src/chatty-application.c:336 src/chatty-window.c:339
+#: src/ui/chatty-window.ui:265
 msgid "Chats"
 msgstr "Разговори"
 
-#: data/sm.puri.Chatty.desktop.in:5
-msgid "sm.puri.Chatty"
-msgstr "sm.puri.Chatty"
-
 #: data/sm.puri.Chatty.desktop.in:6
 msgid "SMS and XMPP chat application"
 msgstr "СМС и Икс-МПП апликација за разговоре"
@@ -62,7 +59,7 @@ msgstr "Да ли треба слати стање поруке ако је пр
 msgid "Message carbon copies"
 msgstr "Индиго копије поруке"
 
-#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:153
+#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554
 msgid "Share chat history among devices"
 msgstr "Дели историјат разговора између уређаја"
 
@@ -75,17 +72,21 @@ msgid "Enable MAM archive synchronization from the server"
 msgstr "Омогући усклађивање МАМ архива са сервера"
 
 #: data/sm.puri.Chatty.gschema.xml:37
+msgid "Enable purple"
+msgstr "Омогући библиотеку purple"
+
+#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511
+msgid "Enable purple accounts"
+msgstr "Омогући purple налоге"
+
+#: data/sm.puri.Chatty.gschema.xml:43
 msgid "Send typing notifications"
 msgstr "Шаљи обавештења о куцању"
 
-#: data/sm.puri.Chatty.gschema.xml:38
+#: data/sm.puri.Chatty.gschema.xml:44
 msgid "Whether to Send typing notifications"
 msgstr "Да ли треба слати обавештења о куцању"
 
-#: data/sm.puri.Chatty.gschema.xml:43 data/sm.puri.Chatty.gschema.xml:44
-msgid "Mark offline users differently"
-msgstr "Означи кориснике ван мреже другачије"
-
 #: data/sm.puri.Chatty.gschema.xml:49 data/sm.puri.Chatty.gschema.xml:50
 msgid "Mark Idle users differently"
 msgstr "Означи кориснике у мировању другачије"
@@ -111,18 +112,34 @@ msgid "Whether pressing Enter key sends the message"
 msgstr "Да ли се порука шаље притиском на тастер Enter"
 
 #: data/sm.puri.Chatty.gschema.xml:73
+msgid "Request SMS delivery reports"
+msgstr "Затражи извештаје о достави СМС-а"
+
+#: data/sm.puri.Chatty.gschema.xml:74
+msgid "Whether to request delivery reports for outgoing SMS"
+msgstr "Да ли треба тражити извештаје о достави одлазних СМС-ова"
+
+#: data/sm.puri.Chatty.gschema.xml:79
+msgid "Enable experimental features"
+msgstr "Омогући пробне функције"
+
+#: data/sm.puri.Chatty.gschema.xml:80
+msgid "Whether to enable experimental features"
+msgstr "Да ли треба омогућити експерименталне функције"
+
+#: data/sm.puri.Chatty.gschema.xml:85
 msgid "Window maximized"
 msgstr "Увећан прозор"
 
-#: data/sm.puri.Chatty.gschema.xml:74
+#: data/sm.puri.Chatty.gschema.xml:86
 msgid "Window maximized state"
 msgstr "Стање увећаног прозора"
 
-#: data/sm.puri.Chatty.gschema.xml:79
+#: data/sm.puri.Chatty.gschema.xml:91
 msgid "Window size"
 msgstr "Величина прозора"
 
-#: data/sm.puri.Chatty.gschema.xml:80
+#: data/sm.puri.Chatty.gschema.xml:92
 msgid "Window size (width, height)."
 msgstr "Величина прозора (ширина, висина)."
 
@@ -138,674 +155,1084 @@ msgstr "Разговори је апликација за поруке која
 msgid "Chats message window"
 msgstr "Прозор са порукама разговора"
 
-#: src/chatty-application.c:68
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Прикажи издање програма"
 
-#: src/chatty-application.c:69
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Покрени у режиму демона"
 
-#: src/chatty-application.c:70
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Онемогући све налоге"
 
-#: src/chatty-application.c:71
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Омогући libpurple поруке са грешкама"
 
-#: src/chatty-application.c:72
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Омогући причљиве libpurple поруке са грешкама"
 
-#: src/chatty-application.c:108
+#: src/chatty-application.c:144
 #, c-format
-msgid "Authorize %s?"
-msgstr "Овластити „%s“?"
+msgid ""
+"There was an error displaying help:\n"
+"%s"
+msgstr ""
+"Догодила се грешка приликом приказа помоћи:\n"
+"%s"
 
-#: src/chatty-application.c:112
-msgid "Reject"
-msgstr "Одбаци"
+#: src/chatty-avatar.c:158 src/chatty-contact-list.c:391
+msgid "Send To"
+msgstr "Пошаљи на"
 
-#: src/chatty-application.c:114
-msgid "Accept"
-msgstr "Прихвати"
+#: src/chatty-chat-list.c:175
+msgid "Select a contact with the <b>“+”</b> button in the titlebar."
+msgstr ""
+"Изабери контакт са дугметом <b>\"+\"</b> које се налази у насловној траци."
+
+#: src/chatty-chat-list.c:179
+msgid "Add instant messaging accounts in Preferences."
+msgstr "Додајте налоге брзог ћаскања у Поставкама."
+
+#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282
+msgid "No Search Results"
+msgstr "Нема резултата претраге"
+
+#: src/chatty-chat-list.c:216
+msgid "Try different search"
+msgstr "Пробајте другачију претрагу"
+
+#: src/chatty-chat-list.c:220
+msgid "No archived chats"
+msgstr "Нема архивираних ћаскања"
+
+#: src/chatty-chat-list.c:222
+msgid "Start Chatting"
+msgstr "Започни разговор"
+
+#: src/chatty-chat-view.c:212
+msgid "This is an SMS conversation"
+msgstr "Ово је СМС разговор"
+
+#: src/chatty-chat-view.c:214 src/chatty-chat-view.c:220
+msgid "Your messages are not encrypted, and carrier rates may apply"
+msgstr ""
+"Ваше поруке нису шифроване, и накнаде достављача услуге могу бити примењене"
+
+#: src/chatty-chat-view.c:218
+msgid "This is an IM conversation"
+msgstr "Ово је разговор преко брзих порука"
+
+#: src/chatty-chat-view.c:224
+msgid "Your messages are encrypted"
+msgstr "Ваше поруке су шифроване"
+
+#: src/chatty-chat-view.c:227
+msgid "Your messages are not encrypted"
+msgstr "Ваше поруке нису шифроване"
+
+#: src/chatty-chat-view.c:420
+msgid "Select File..."
+msgstr "Изабери датотеку…"
+
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
+#: src/dialogs/chatty-ma-account-details.c:119
+#: src/dialogs/chatty-pp-account-details.c:91
+#: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
+#: src/ui/chatty-info-dialog.ui:54
+msgid "Cancel"
+msgstr "Откажи"
+
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
+#: src/dialogs/chatty-pp-chat-info.c:89
+#: src/dialogs/chatty-ma-account-details.c:118
+#: src/dialogs/chatty-pp-account-details.c:90
+msgid "Open"
+msgstr "Отвори"
+
+#: src/chatty-chat.c:635
+msgid "Empty room"
+msgstr "Празна соба"
 
-#: src/chatty-application.c:119
+#. TRANSLATORS: %s are name/user-id/phone numbers of two users
+#: src/chatty-chat.c:642
 #, c-format
-msgid "Add %s to contact list"
-msgstr "Додај контакт „%s“ у списак контаката"
+msgid "%s and %s"
+msgstr "%s и %s"
 
-#: src/chatty-application.c:142
-msgid "Contact added"
-msgstr "Контакт је додат"
+#: src/chatty-chat.c:644
+#, c-format
+msgid "%s and %u other"
+msgid_plural "%s and %u others"
+msgstr[0] "%s и %u других"
+msgstr[1] "%s и %u других"
+msgstr[2] "%s и %u других"
+msgstr[3] "%s и %u других"
+
+#: src/chatty-contact-list.c:247 src/dialogs/chatty-mm-chat-info.c:121
+msgid "Unknown Contact"
+msgstr "Непознати контакт"
+
+#: src/chatty-contact-list.c:283
+msgid "Try different search, or type a valid number to create new chat"
+msgstr "Пробајте другачију претрагу или укуцајте исправан број за ново ћаскање"
+
+#: src/chatty-contact-list.c:287
+msgid "No Contacts"
+msgstr "Нема контаката"
+
+#: src/chatty-clock.c:86
+msgid "Just Now"
+msgstr "Управо сада"
+
+#. TRANSLATORS: Timestamp with 12 hour time, e.g. “06∶42 PM”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:95 src/chatty-clock.c:125
+msgid "%I∶%M %p"
+msgstr "%I∶%M %p"
 
-#: src/chatty-application.c:145
+#: src/chatty-clock.c:104
 #, c-format
-msgid "User %s has added %s to the contacts"
-msgstr "Корисник „%s“ је додао контакт „%s“"
+msgid "%lu minute ago"
+msgid_plural "%lu minutes ago"
+msgstr[0] "пре %lu минута"
+msgstr[1] "пре %lu минута"
+msgstr[2] "пре %lu минута"
+msgstr[3] "пре %lu минута"
+
+#: src/chatty-clock.c:112
+msgid "Today %H∶%M"
+msgstr "Данас у %H:%M"
+
+#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PM”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:120
+msgid "Today %I∶%M %p"
+msgstr "Данас у %I:%M %p"
 
-#: src/chatty-application.c:165
-msgid "Login failed"
-msgstr "Пријава није успела"
+#: src/chatty-clock.c:132
+msgid "Yesterday %H∶%M"
+msgstr "Јуче у %H:%M"
 
-#: src/chatty-application.c:171
-msgid "Please check ID and password"
-msgstr "Проверите ИБ и лозинку"
+#. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PM”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:137
+msgid "Yesterday %I∶%M %p"
+msgstr "Јуче у %I:%M %p"
 
-#: src/chatty-chat-view.c:70 src/chatty-chat-view.c:75
-msgid "This is an IM conversation."
-msgstr "Ово је разговор преко брзих порука."
+#. TRANSLATORS: Timestamp from more than 7 days ago or future date
+#. * (eg: when the system time is wrong), e.g. “2022-01-01”.
+#. * See https://docs.gtk.org/glib/method.DateTime.format.html
+#.
+#: src/chatty-clock.c:153
+msgid "%Y-%m-%d"
+msgstr "%d.%m.%Y"
 
-#: src/chatty-chat-view.c:71 src/chatty-chat-view.c:81
-msgid "Your messages are not encrypted,"
-msgstr "Ваше поруке нису шифроване,"
+#. TRANSLATORS: %s is the Device ID
+#: src/chatty-fp-row.c:131
+#, c-format
+msgid "Device ID %s fingerprint:"
+msgstr "Уређајског ИБ-ја %s отисак:"
 
-#: src/chatty-chat-view.c:72
-msgid "ask your counterpart to use E2EE."
-msgstr ""
-"питајте саговорника да искористи \n"
-"шифровање с-краја-на-крај."
-
-#: src/chatty-chat-view.c:76
-msgid "Your messages are secured"
-msgstr "Ваше поруке су безбедне"
-
-#: src/chatty-chat-view.c:77
-msgid "by end-to-end encryption."
-msgstr "због шифровања с-краја-на-крај."
-
-#: src/chatty-chat-view.c:80
-msgid "This is an SMS conversation."
-msgstr "Ово је СМС разговор."
-
-#: src/chatty-chat-view.c:82
-msgid "and carrier rates may apply."
-msgstr "и наплата трошкова оператера је могућа."
-
-#. Translators: Timestamp seconds suffix
-#: src/chatty-list-row.c:73
-msgctxt "timestamp-suffix-seconds"
-msgid "s"
-msgstr "с"
-
-#. Translators: Timestamp minute suffix
-#: src/chatty-list-row.c:75
-msgctxt "timestamp-suffix-minute"
-msgid "m"
-msgstr "м"
-
-#. Translators: Timestamp minutes suffix
-#: src/chatty-list-row.c:77
-msgctxt "timestamp-suffix-minutes"
-msgid "m"
-msgstr "м"
-
-#. Translators: Timestamp hour suffix
-#: src/chatty-list-row.c:79
-msgctxt "timestamp-suffix-hour"
-msgid "h"
-msgstr "ч"
-
-#. Translators: Timestamp hours suffix
-#: src/chatty-list-row.c:81
-msgctxt "timestamp-suffix-hours"
-msgid "h"
-msgstr "ч"
-
-#. Translators: Timestamp day suffix
-#: src/chatty-list-row.c:83
-msgctxt "timestamp-suffix-day"
-msgid "d"
-msgstr "д"
-
-#. Translators: Timestamp days suffix
-#: src/chatty-list-row.c:85
-msgctxt "timestamp-suffix-days"
-msgid "d"
-msgstr "д"
-
-#. Translators: Timestamp month suffix
-#: src/chatty-list-row.c:87
-msgctxt "timestamp-suffix-month"
-msgid "mo"
-msgstr "мес."
-
-#. Translators: Timestamp months suffix
-#: src/chatty-list-row.c:89
-msgctxt "timestamp-suffix-months"
-msgid "mos"
-msgstr "мес."
-
-#. Translators: Timestamp year suffix
-#: src/chatty-list-row.c:91
-msgctxt "timestamp-suffix-year"
-msgid "y"
-msgstr "г"
-
-#. Translators: Timestamp years suffix
-#: src/chatty-list-row.c:93
-msgctxt "timestamp-suffix-years"
-msgid "y"
-msgstr "г"
-
-#. Translators: Timestamp prefix (e.g. Over 5h)
-#: src/chatty-list-row.c:195
-msgid "Over"
-msgstr "Више од"
-
-#. Translators: Timestamp prefix (e.g. Almost 5h)
-#: src/chatty-list-row.c:200
-msgid "Almost"
-msgstr "Скоро"
-
-#: src/chatty-list-row.c:217
+#: src/chatty-list-row.c:118
 msgid "Owner"
 msgstr "Власник"
 
-#: src/chatty-list-row.c:220
+#: src/chatty-list-row.c:121
 msgid "Moderator"
 msgstr "Модератор"
 
-#: src/chatty-list-row.c:223
+#: src/chatty-list-row.c:124
 msgid "Member"
 msgstr "Члан"
 
-#: src/chatty-manager.c:891
+#: src/chatty-manager.c:729
 #, c-format
-msgid "New message from %s"
-msgstr "Нова порука од корисника „%s“"
+msgid "“%s” is not a valid URI"
+msgstr "%s није исправан УРИ"
 
-#: src/chatty-message-row.c:89
+#: src/chatty-message-row.c:82
 msgid "Copy"
 msgstr "Копирај"
 
-#: src/chatty-notify.c:67
+#: src/chatty-notification.c:181
 msgid "Open Message"
 msgstr "Отвори поруку"
 
-#: src/chatty-notify.c:72
+#: src/chatty-notification.c:214
+#, c-format
+msgid "New message from %s"
+msgstr "Нова порука од корисника „%s“"
+
+#: src/chatty-notification.c:216
 msgid "Message Received"
 msgstr "Порука је примљена"
 
-#: src/chatty-notify.c:78
-msgid "Message Error"
-msgstr "Грешка у поруци"
-
-#: src/chatty-notify.c:84
-msgid "Account Info"
-msgstr "Подаци налога"
-
-#: src/chatty-notify.c:90
-msgid "Account Connected"
-msgstr "Налог је повезан"
-
-#: src/chatty-purple-notify.c:44
+#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:504
 msgid "Close"
 msgstr "Затвори"
 
-#: src/chatty-purple-request.c:186
+#: src/purple/chatty-purple-request.c:170
 msgid "Save File..."
 msgstr "Сачувај датотеку..."
 
-#: src/chatty-purple-request.c:187
+#: src/purple/chatty-purple-request.c:171
 msgid "Open File..."
 msgstr "Отвори датотеку..."
 
-#: src/chatty-purple-request.c:191 src/chatty-window.c:587
-#: src/dialogs/chatty-settings-dialog.c:443
-#: src/dialogs/chatty-user-info-dialog.c:66 src/ui/chatty-dialog-join-muc.ui:16
-#: src/ui/chatty-dialog-muc-info.ui:56
-msgid "Cancel"
-msgstr "Откажи"
-
-#: src/chatty-purple-request.c:193
+#: src/purple/chatty-purple-request.c:177
 msgid "Save"
 msgstr "Сачувај"
 
-#: src/chatty-purple-request.c:193 src/dialogs/chatty-settings-dialog.c:442
-#: src/dialogs/chatty-user-info-dialog.c:65
-msgid "Open"
-msgstr "Отвори"
-
-#. TRANSLATORS: Timestamp from the last week with 24 hour time, e.g. “Tuesday 18∶42”.
-#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
-#.
-#: src/chatty-utils.c:291
-msgid "%A %H∶%M"
-msgstr "%A %H∶%M"
+#: src/purple/chatty-purple.c:240
+msgid "Contact added"
+msgstr "Контакт је додат"
 
-#. TRANSLATORS: Timestamp from the last week with 12 hour time, e.g. “Tuesday 06∶42 PM”.
-#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
-#.
-#: src/chatty-utils.c:296
-msgid "%A %I∶%M %p"
-msgstr "%A %I∶%M %p"
+#: src/purple/chatty-purple.c:242
+#, c-format
+msgid "User %s has added %s to the contacts"
+msgstr "Корисник „%s“ је додао контакт „%s“"
 
-#. TRANSLATORS: Timestamp from more than 7 days ago, e.g. “2020-08-11”.
-#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
-#.
-#: src/chatty-utils.c:303
-msgid "%Y-%m-%d"
-msgstr "%d.%m.%Y"
+#: src/purple/chatty-purple.c:262
+#, c-format
+msgid "Authorize %s?"
+msgstr "Овластити „%s“?"
 
-#: src/chatty-window.c:104 src/chatty-window.c:109 src/chatty-window.c:114
-msgid "Choose a contact"
-msgstr "Изабери контакт"
+#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:508
+msgid "Reject"
+msgstr "Одбаци"
 
-#: src/chatty-window.c:105
-msgid ""
-"Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</b> "
-"button in the titlebar."
-msgstr ""
-"Изабери опцију <b>СМС</b> или <b>Брза порука</b> за контакт уз помоћ дугмета "
-"<b>\"+\"</b> које се налази у насловној траци."
+#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:509
+msgid "Accept"
+msgstr "Прихвати"
 
-#: src/chatty-window.c:110
-msgid ""
-"Select an <b>Instant Message</b> contact with the \"+\" button in the "
-"titlebar."
-msgstr ""
-"Изабери опцију <b>Брза порука</b> за контакт уз помоћ дугмета <b>\"+\"</b> "
-"које се налази у насловној траци."
+#: src/purple/chatty-purple.c:268
+#, c-format
+msgid "Add %s to contact list"
+msgstr "Додај контакт „%s“ у списак контаката"
 
-#: src/chatty-window.c:115
-msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar."
-msgstr ""
-"Започни <b>СМС</b> разговор уз помоћ дугмета <b>\"+\"</b> које се налази у "
-"насловној траци."
+#: src/purple/chatty-purple.c:587
+msgid "Login failed"
+msgstr "Пријава није успела"
 
-#: src/chatty-window.c:116 src/chatty-window.c:120
-msgid ""
-"For <b>Instant Messaging</b> add or activate an account in <i>\"preferences"
-"\"</i>."
-msgstr ""
-"За опцију <b>Брза порука</b> додајте или активирајте налог у одељку "
-"<i>„поставке“</i>."
+#: src/purple/chatty-purple.c:592
+msgid "Please check ID and password"
+msgstr "Проверите ИБ и лозинку"
 
-#: src/chatty-window.c:119
-msgid "Start chatting"
-msgstr "Започни разговор"
+#: src/chatty-secret-store.c:99
+#, c-format
+msgid "Chatty password for \"%s\""
+msgstr "Лозинка Разговора за „%s“"
 
-#: src/chatty-window.c:572
-msgid "Delete chat with"
-msgstr "Обриши разговор са"
+#: src/chatty-window.c:408
+#, c-format
+msgid "Delete chat with “%s”"
+msgstr "Обриши разговор са „%s“"
 
-#: src/chatty-window.c:573
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Ово брише историјат разговора"
 
-#: src/chatty-window.c:575
-msgid "Disconnect group chat"
-msgstr "Откачи групно разговор"
+#: src/chatty-window.c:411
+#, c-format
+msgid "Disconnect group chat “%s”"
+msgstr "Откачи групно разговор „%s“"
 
-#: src/chatty-window.c:576
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Ово уклања разговор са списка разговора"
 
-#: src/chatty-window.c:589
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Обриши"
 
-#: src/chatty-window.c:799
+#: src/chatty-window.c:502
+msgid "You shall no longer be notified for new messages, continue?"
+msgstr "Више нећете примати обавештења о новим порукама, наставити?"
+
+#: src/chatty-window.c:577
+msgid "Archived"
+msgstr "Архивирано"
+
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "Клијент за СМС и Икс-МПП разговор"
 
-#: src/chatty-window.c:806
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr ""
 "Марко М. Костић <marko.m.kostic@gmail.com>\n"
 "\n"
 "https://гном.срб — превод Гнома на српски језик"
 
-#: src/dialogs/chatty-muc-info-dialog.c:319
-msgid "members"
-msgstr "чланови"
+#: src/chatty-window.c:989
+msgid "Any Protocol"
+msgstr "Било који протокол"
 
-#: src/dialogs/chatty-new-chat-dialog.c:148
-msgid "Send To"
-msgstr "Пошаљи на"
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
+msgid "Matrix"
+msgstr "Матрикс"
+
+#: src/chatty-window.c:991
+msgid "SMS/MMS"
+msgstr "СМС/ММС"
+
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
+msgid "XMPP"
+msgstr "Икс-МПП"
+
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
+msgid "Telegram"
+msgstr "Телеграм"
+
+#: src/dialogs/chatty-mm-chat-info.c:140
+msgctxt "Refer to self in contact list"
+msgid "You"
+msgstr "Ја"
+
+#: src/dialogs/chatty-pp-chat-info.c:86
+#: src/dialogs/chatty-ma-account-details.c:115
+#: src/dialogs/chatty-pp-account-details.c:87
+msgid "Set Avatar"
+msgstr "Постави аватар"
+
+#: src/dialogs/chatty-pp-chat-info.c:96
+#: src/dialogs/chatty-ma-account-details.c:125
+#: src/dialogs/chatty-pp-account-details.c:97
+msgid "Images"
+msgstr "Слике"
 
-#: src/dialogs/chatty-new-chat-dialog.c:192
+#: src/dialogs/chatty-pp-chat-info.c:206
+msgid "Encryption is not available"
+msgstr "Шифровање није доступно"
+
+#: src/dialogs/chatty-pp-chat-info.c:210
+msgid "This chat is encrypted"
+msgstr "Овај разговор је шифрован"
+
+#: src/dialogs/chatty-pp-chat-info.c:214
+msgid "This chat is not encrypted"
+msgstr "Овај разговор није шифрован"
+
+#: src/dialogs/chatty-pp-chat-info.c:238
+msgid "Encryption not available"
+msgstr "Шифровање недоступно"
+
+#: src/dialogs/chatty-pp-chat-info.c:259
+#, c-format
+msgid "%u Member"
+msgid_plural "%u Members"
+msgstr[0] "%u члан"
+msgstr[1] "%u члана"
+msgstr[2] "%u чланова"
+msgstr[3] "%u члан"
+
+#: src/dialogs/chatty-pp-chat-info.c:293
+msgid "Phone Number:"
+msgstr "Број телефона:"
+
+#: src/dialogs/chatty-pp-chat-info.c:295
+msgid "XMPP ID:"
+msgstr "Икс-МПП ИБ:"
+
+#: src/dialogs/chatty-pp-chat-info.c:300
+msgid "Matrix ID:"
+msgstr "Матрикс ИБ:"
+
+#: src/dialogs/chatty-pp-chat-info.c:303
+msgid "Telegram ID:"
+msgstr "Телеграм ИБ:"
+
+#: src/dialogs/chatty-new-chat-dialog.c:171
+msgid "Add"
+msgstr "Додај"
+
+#: src/dialogs/chatty-new-chat-dialog.c:181
+msgid "Create"
+msgstr "Направи"
+
+#: src/dialogs/chatty-new-chat-dialog.c:187
+msgid "Add Contact"
+msgstr "Додај контакт"
+
+#: src/dialogs/chatty-new-chat-dialog.c:224
 #, c-format
 msgid "Error opening GNOME Contacts: %s"
 msgstr "Грешка при отварању Гномових Контаката: %s"
 
-#: src/dialogs/chatty-settings-dialog.c:319
-msgid "Select Protocol"
-msgstr "Изабери протокол"
+#: src/dialogs/chatty-new-chat-dialog.c:452
+msgid "SMS"
+msgstr "СМС"
 
-#: src/dialogs/chatty-settings-dialog.c:324
-msgid "Add XMPP account"
-msgstr "Додај Икс-МПП налог"
+#: src/dialogs/chatty-new-chat-dialog.c:640
+msgid "You"
+msgstr "Ви"
 
-#: src/dialogs/chatty-settings-dialog.c:355
+#: src/dialogs/chatty-ma-account-details.c:385
+#: src/dialogs/chatty-pp-account-details.c:178
 msgid "connected"
 msgstr "повезан"
 
-#: src/dialogs/chatty-settings-dialog.c:357
+#: src/dialogs/chatty-ma-account-details.c:387
+#: src/dialogs/chatty-pp-account-details.c:180
 msgid "connecting…"
 msgstr "повезујем се…"
 
-#: src/dialogs/chatty-settings-dialog.c:359
+#: src/dialogs/chatty-ma-account-details.c:389
+#: src/dialogs/chatty-pp-account-details.c:182
 msgid "disconnected"
 msgstr "откачен"
 
-#: src/dialogs/chatty-settings-dialog.c:439
-#: src/dialogs/chatty-user-info-dialog.c:62
-msgid "Set Avatar"
-msgstr "Постави аватар"
-
-#: src/dialogs/chatty-settings-dialog.c:509
-#: src/ui/chatty-settings-dialog.ui:477
+#: src/dialogs/chatty-settings-dialog.c:267
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Нисам успео да потврдим сервер: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:269
+#: src/dialogs/chatty-settings-dialog.c:479
+msgid "Failed to verify server"
+msgstr "Нисам успео да потврдим сервер"
+
+#: src/dialogs/chatty-settings-dialog.c:275
+#: src/dialogs/chatty-settings-dialog.c:459
+msgid "Couldn't get Home server address"
+msgstr "Не могу добавити адресу кућног сервера"
+
+#: src/dialogs/chatty-settings-dialog.c:584
+#: src/ui/chatty-ma-account-details.ui:182
+#: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Обриши налог"
 
-#: src/dialogs/chatty-settings-dialog.c:512
+#: src/dialogs/chatty-settings-dialog.c:587
 #, c-format
 msgid "Delete account %s?"
 msgstr "Обрисати налог „%s“?"
 
-#: src/dialogs/chatty-user-info-dialog.c:136
-msgid "Encryption not available"
-msgstr "Шифровање недоступно"
+#: src/dialogs/chatty-settings-dialog.c:728
+msgid "Restart chatty to disable purple"
+msgstr "Поново покрените ћаскање за онемогућавање библиотеке purple"
 
-#: src/dialogs/chatty-user-info-dialog.c:174
-msgid "Encryption is not available"
-msgstr "Шифровање није доступно"
+#: src/dialogs/chatty-settings-dialog.c:730
+#: src/ui/chatty-settings-dialog.ui:512
+msgid "Enable purple plugin"
+msgstr "Омогући purple прикључак"
 
-#: src/dialogs/chatty-user-info-dialog.c:176
-msgid "This chat is encrypted"
-msgstr "Овај разговор је шифрован"
+#: src/dialogs/chatty-settings-dialog.c:744
+#: src/ui/chatty-settings-dialog.ui:279
+msgid "SMS and MMS Settings"
+msgstr "СМС/ММС подешавања"
 
-#: src/dialogs/chatty-user-info-dialog.c:178
-msgid "This chat is not encrypted"
-msgstr "Овај разговор није шифрован"
+#: src/dialogs/chatty-settings-dialog.c:746
+#: src/ui/chatty-settings-dialog.ui:298
+msgid "Purple Settings"
+msgstr "Purple подешавања"
 
-#: src/dialogs/chatty-user-info-dialog.c:246
-msgid "Phone Number:"
-msgstr "Број телефона:"
+#: src/dialogs/chatty-settings-dialog.c:748
+msgid "New Account"
+msgstr "Нови налог"
+
+#: src/dialogs/chatty-settings-dialog.c:750
+#: src/ui/chatty-settings-dialog.ui:364
+msgid "Blocked Contacts"
+msgstr "Блокирани контакти"
+
+#: src/dialogs/chatty-settings-dialog.c:752 src/ui/chatty-settings-dialog.ui:7
+#: src/ui/chatty-window.ui:30
+msgid "Preferences"
+msgstr "Поставке"
+
+#: src/dialogs/chatty-settings-dialog.c:781
+msgid "Select Protocol"
+msgstr "Изабери протокол"
+
+#. TRANSLATORS: Only translate 'or'
+#: src/dialogs/chatty-settings-dialog.c:1022
+msgid "@user:matrix.org or user@example.com"
+msgstr "@korisnik:matrix.org или korisnik@primer.rs"
+
+#: src/dialogs/chatty-settings-dialog.c:1131
+msgid "Unblock contact"
+msgstr "Деблокирај контакт"
 
-#: src/ui/chatty-dialog-join-muc.ui:12
+#: src/matrix/chatty-ma-account.c:231
+msgid "Incorrect password"
+msgstr "Нетачна лозинка"
+
+#: src/matrix/chatty-ma-account.c:234
+msgid "_OK"
+msgstr "У _реду"
+
+#: src/matrix/chatty-ma-account.c:235 src/ui/chatty-dialog-new-chat.ui:74
+#: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
+msgid "_Cancel"
+msgstr "_Откажи"
+
+#: src/matrix/chatty-ma-account.c:241
+#, c-format
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Унесите лозинку за „%s“, кућни сервер: %s"
+
+#: src/matrix/matrix-utils.c:473
+#, c-format
+msgid "The certificate for ‘%s’ has unknown CA"
+msgstr "Сертификат за „%s“ садржи непознато сертификационо тело"
+
+#: src/matrix/matrix-utils.c:475
+#, c-format
+msgid "The certificate for ‘%s’ is self-signed"
+msgstr "Сертификат за „%s“ је самопотписан"
+
+#: src/matrix/matrix-utils.c:479
+#, c-format
+msgid "The certificate for ‘%s’ has expired"
+msgstr "Сертификат за „%s“ је истекао"
+
+#: src/matrix/matrix-utils.c:483
+#, c-format
+msgid "The certificate for ‘%s’ has been revoked"
+msgstr "Сертификат за „%s“ је опозван"
+
+#: src/matrix/matrix-utils.c:492
+#, c-format
+msgid "Error validating certificate for ‘%s’"
+msgstr "Грешка приликом потврђивања сертификата за „%s“"
+
+#. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
+#. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
+#.
+#: src/mm/chatty-mmsd.c:827
+msgid "%Y-%m-%d %H∶%M"
+msgstr "%d.%m.%Y %H∶%M"
+
+#: src/mm/chatty-mmsd.c:1111
+#, c-format
+msgid "You received an MMS, but it expired on: %s"
+msgstr "Добили сте ММС али је он истекао у: %s"
+
+#: src/mm/chatty-mmsd.c:1114
+msgid "You received an empty MMS."
+msgstr "Добили сте празан ММС."
+
+#: src/ui/chatty-dialog-join-muc.ui:17
 msgid "New Group Chat"
 msgstr "Нови групни разговор"
 
-#: src/ui/chatty-dialog-join-muc.ui:28
+#: src/ui/chatty-dialog-join-muc.ui:34
 msgid "Join Chat"
 msgstr "Придружи се разговору"
 
-#: src/ui/chatty-dialog-join-muc.ui:81 src/ui/chatty-dialog-new-chat.ui:237
+#: src/ui/chatty-dialog-join-muc.ui:82 src/ui/chatty-dialog-new-chat.ui:269
 msgid "Select chat account"
 msgstr "Изабери налог ћаскања"
 
-#: src/ui/chatty-dialog-join-muc.ui:155
+#: src/ui/chatty-dialog-join-muc.ui:152
 msgid "Password (optional)"
 msgstr "Лозинка (изборно)"
 
-#: src/ui/chatty-dialog-muc-info.ui:26
-msgid "Group Details"
-msgstr "Појединости групе"
+#: src/ui/chatty-dialog-new-chat.ui:126
+msgid "Group Title"
+msgstr "Назив групе"
 
-#: src/ui/chatty-dialog-muc-info.ui:52
-msgid "Invite Contact"
-msgstr "Позови контакт"
+#: src/ui/chatty-dialog-new-chat.ui:177
+msgid "Add members from contacts…"
+msgstr "Додај чланове из контаката…"
 
-#: src/ui/chatty-dialog-muc-info.ui:69
+#: src/ui/chatty-dialog-new-chat.ui:223
+msgid "Send To:"
+msgstr "Пошаљи на:"
+
+#: src/ui/chatty-dialog-new-chat.ui:308
+msgid "Name (optional)"
+msgstr "Име (изборно)"
+
+#: src/ui/chatty-dialog-new-chat.ui:351
+msgid "Add to Contacts"
+msgstr "Додај у контакте"
+
+#: src/ui/chatty-file-item.ui:28
+msgid "Remove Attachment"
+msgstr "Уклони прилог"
+
+#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139
+msgid "Chat Details"
+msgstr "Појединости разговора"
+
+#: src/ui/chatty-info-dialog.ui:64
+msgid "Apply"
+msgstr "Примени"
+
+#: src/ui/chatty-info-dialog.ui:82
 msgid "Invite"
 msgstr "Позови"
 
-#: src/ui/chatty-dialog-muc-info.ui:166
+#: src/ui/chatty-ma-chat-info.ui:67 src/ui/chatty-ma-account-details.ui:233
+msgid "Matrix ID"
+msgstr "Матрикс ИБ"
+
+#: src/ui/chatty-ma-chat-info.ui:93 src/ui/chatty-pp-chat-info.ui:289
+msgid "Chat settings"
+msgstr "Подешавања разговора"
+
+#: src/ui/chatty-ma-chat-info.ui:113 src/ui/chatty-pp-chat-info.ui:238
+#: src/ui/chatty-pp-chat-info.ui:344
+msgid "Encryption"
+msgstr "Шифровање"
+
+#: src/ui/chatty-ma-chat-info.ui:114 src/ui/chatty-pp-chat-info.ui:345
+msgid "Encrypt Messages"
+msgstr "Шифроване поруке"
+
+#: src/ui/chatty-pp-chat-info.ui:43 src/ui/chatty-pp-account-details.ui:60
+msgid "Change Avatar"
+msgstr "Измени аватар"
+
+#: src/ui/chatty-pp-chat-info.ui:69 src/ui/chatty-pp-account-details.ui:34
+msgid "Delete Avatar"
+msgstr "Обриши аватар"
+
+#: src/ui/chatty-pp-chat-info.ui:108
 msgid "Room topic"
 msgstr "Тема собе"
 
-#: src/ui/chatty-dialog-muc-info.ui:225
-msgid "Room settings"
-msgstr "Подешавања собе"
+#: src/ui/chatty-pp-chat-info.ui:211
+msgid "XMPP ID"
+msgstr "Икс-МПП ИБ"
+
+#: src/ui/chatty-pp-chat-info.ui:264 src/ui/chatty-ma-account-details.ui:67
+#: src/ui/chatty-pp-account-details.ui:142
+msgid "Status"
+msgstr "Стање"
 
-#: src/ui/chatty-dialog-muc-info.ui:248 src/ui/chatty-dialog-user-info.ui:209
+#: src/ui/chatty-pp-chat-info.ui:310
 msgid "Notifications"
 msgstr "Обавештења"
 
-#: src/ui/chatty-dialog-muc-info.ui:249
-msgid "Show notification badge"
-msgstr "Прикажи врпцу обавештења"
-
-#: src/ui/chatty-dialog-muc-info.ui:265
+#: src/ui/chatty-pp-chat-info.ui:327
 msgid "Status Messages"
 msgstr "Поруке стања"
 
-#: src/ui/chatty-dialog-muc-info.ui:266
+#: src/ui/chatty-pp-chat-info.ui:328
 msgid "Show status messages in chat"
 msgstr "Прикажи поруке стања у разговору"
 
-#: src/ui/chatty-dialog-muc-info.ui:289
-msgid "0 members"
-msgstr "0 чланова"
+#: src/ui/chatty-pp-chat-info.ui:367
+msgid "Fingerprints"
+msgstr "Отисци прста"
 
-#: src/ui/chatty-dialog-muc-info.ui:377
-msgid "Invite Message"
-msgstr "Позивница"
+#: src/ui/chatty-mm-chat-info.ui:29
+msgid "Title"
+msgstr "Наслов"
 
-#: src/ui/chatty-dialog-new-chat.ui:26
-msgid "Start Chat"
-msgstr "Започни разговор"
+#: src/ui/chatty-mm-chat-info.ui:33
+msgid "Blank for default"
+msgstr "Подразумевано празно"
 
-#: src/ui/chatty-dialog-new-chat.ui:55
-msgid "New Contact"
-msgstr "Нови контакт"
+#: src/ui/chatty-mm-chat-info.ui:43
+msgid "Group Members"
+msgstr "Чланови групе"
 
-#: src/ui/chatty-dialog-new-chat.ui:83 src/ui/chatty-window.ui:126
-msgid "Add Contact"
-msgstr "Додај контакт"
+#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433
+msgid "Call"
+msgstr "Позив"
 
-#: src/ui/chatty-dialog-new-chat.ui:149
-msgid "Send To:"
-msgstr "Пошаљи на:"
+#: src/ui/chatty-ma-account-details.ui:93
+msgid "Name"
+msgstr "Име"
 
-#: src/ui/chatty-dialog-new-chat.ui:288
-msgid "Name (optional)"
-msgstr "Име (изборно)"
+#: src/ui/chatty-ma-account-details.ui:123
+msgid "Email"
+msgstr "Е-адреса"
 
-#: src/ui/chatty-dialog-new-chat.ui:328 src/ui/chatty-window.ui:152
-msgid "Add to Contacts"
-msgstr "Додај у контакте"
+#: src/ui/chatty-ma-account-details.ui:152
+msgid "Phone"
+msgstr "Телефон"
 
-#: src/ui/chatty-dialog-user-info.ui:12 src/ui/chatty-window.ui:112
-msgid "Chat Details"
-msgstr "Појединости разговора"
+#: src/ui/chatty-ma-account-details.ui:196
+msgid "Advanced information"
+msgstr "Напредни подаци"
 
-#: src/ui/chatty-dialog-user-info.ui:96
-msgid "XMPP ID"
-msgstr "Икс-МПП ИБ"
+#: src/ui/chatty-ma-account-details.ui:207
+msgid "Homeserver"
+msgstr "Кућни сервер"
 
-#: src/ui/chatty-dialog-user-info.ui:111 src/ui/chatty-dialog-user-info.ui:225
-msgid "Encryption"
-msgstr "Шифровање"
+#: src/ui/chatty-ma-account-details.ui:259
+msgid "Device ID"
+msgstr "ИБ уређаја"
 
-#: src/ui/chatty-dialog-user-info.ui:126 src/ui/chatty-settings-dialog.ui:390
-msgid "Status"
-msgstr "Стање"
+#: src/ui/chatty-pp-account-details.ui:92
+msgid "Account ID"
+msgstr "ИБ налога"
 
-#: src/ui/chatty-dialog-user-info.ui:226
-msgid "Secure messaging using OMEMO"
-msgstr "Безбедно дописивање уз ОМЕМО"
+#: src/ui/chatty-pp-account-details.ui:116
+msgid "Protocol"
+msgstr "Протокол"
 
-#: src/ui/chatty-dialog-user-info.ui:247
-msgid "Fingerprints"
-msgstr "Отисци прста"
+#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701
+msgid "Password"
+msgstr "Лозинка"
 
-#: src/ui/chatty-settings-dialog.ui:12 src/ui/chatty-window.ui:18
-msgid "Preferences"
-msgstr "Поставке"
+#: src/ui/chatty-pp-account-details.ui:213
+msgid "Own Fingerprint"
+msgstr "Сопствени отисак прста"
 
-#: src/ui/chatty-settings-dialog.ui:21
+#: src/ui/chatty-settings-dialog.ui:27
 msgid "Back"
 msgstr "Назад"
 
-#: src/ui/chatty-settings-dialog.ui:41
+#: src/ui/chatty-settings-dialog.ui:60
 msgid "_Add"
 msgstr "Дод_ај"
 
-#: src/ui/chatty-settings-dialog.ui:57
+#: src/ui/chatty-settings-dialog.ui:76
 msgid "_Save"
 msgstr "_Сачувај"
 
-#: src/ui/chatty-settings-dialog.ui:91
+#: src/ui/chatty-settings-dialog.ui:92
+msgid "_Apply"
+msgstr "_Примени"
+
+#: src/ui/chatty-settings-dialog.ui:169
 msgid "Accounts"
 msgstr "Налози"
 
-#: src/ui/chatty-settings-dialog.ui:105
-msgid "Add new account…"
-msgstr "Додај нови налог…"
-
-#: src/ui/chatty-settings-dialog.ui:117
+#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531
 msgid "Privacy"
 msgstr "Приватност"
 
-#: src/ui/chatty-settings-dialog.ui:122
+#: src/ui/chatty-settings-dialog.ui:195
 msgid "Message Receipts"
 msgstr "Повратнице порука"
 
-#: src/ui/chatty-settings-dialog.ui:123
+#: src/ui/chatty-settings-dialog.ui:196
 msgid "Confirm received messages"
 msgstr "Потврди примљене поруке"
 
-#: src/ui/chatty-settings-dialog.ui:137
-msgid "Message Archive Management"
-msgstr "Управљање архивама порука"
-
-#: src/ui/chatty-settings-dialog.ui:138
-msgid "Sync conversations with chat server"
-msgstr "Усклади разговоре са сервером разговора"
-
-#: src/ui/chatty-settings-dialog.ui:152
-msgid "Message Carbon Copies"
-msgstr "Индиго копије поруке"
-
-#: src/ui/chatty-settings-dialog.ui:167
+#: src/ui/chatty-settings-dialog.ui:211
 msgid "Typing Notification"
 msgstr "Обавештења куцања"
 
-#: src/ui/chatty-settings-dialog.ui:168
+#: src/ui/chatty-settings-dialog.ui:212
 msgid "Send typing messages"
 msgstr "Шаљи поруке о куцању"
 
-#: src/ui/chatty-settings-dialog.ui:185
-msgid "Chats List"
-msgstr "Списак разговора"
+#: src/ui/chatty-settings-dialog.ui:230
+msgid "Editor"
+msgstr "Уређивач"
 
-#: src/ui/chatty-settings-dialog.ui:190
-msgid "Indicate Offline Contacts"
-msgstr "Истакни контакте ван мреже"
+#: src/ui/chatty-settings-dialog.ui:235
+msgid "Graphical Emoticons"
+msgstr "Графички емотикони"
 
-#: src/ui/chatty-settings-dialog.ui:191
-msgid "Grey out avatars from offline contacts"
-msgstr "Засиви аватаре контаката ван мреже"
+#: src/ui/chatty-settings-dialog.ui:236
+msgid "if you type :) it will be changed to 😃"
+msgstr "Ако укуцате :) промениће се у 😃"
 
-#: src/ui/chatty-settings-dialog.ui:205
-msgid "Indicate Idle Contacts"
-msgstr "Истакни контакте у мировању"
+#. TRANSLATORS: Return is the Enter key.
+#: src/ui/chatty-settings-dialog.ui:251
+msgid "Send Messages with Return"
+msgstr "Шаљи поруке са Унеси тастером"
 
-#: src/ui/chatty-settings-dialog.ui:206
-msgid "Blur avatars from idle contacts"
-msgstr "Замути аватаре контаката у мировању"
+#: src/ui/chatty-settings-dialog.ui:267
+msgid "Protocol Settings"
+msgstr "Подешавања протокола"
 
-#: src/ui/chatty-settings-dialog.ui:220
-msgid "Indicate Unknown Contacts"
-msgstr "Истакни непознате контакте"
+#: src/ui/chatty-settings-dialog.ui:333
+msgid "Request Delivery Reports"
+msgstr "Захтевај извештаје о достави"
 
-#: src/ui/chatty-settings-dialog.ui:221
-msgid "Color unknown contact ID red"
-msgstr "Обоји црвеном ИБ-јеве непознатих контаката"
+#: src/ui/chatty-settings-dialog.ui:348
+msgid "SMIL for MMS"
+msgstr "СМИЛ за ММС"
 
-#: src/ui/chatty-settings-dialog.ui:238
-msgid "Editor"
-msgstr "Уређивач"
+#: src/ui/chatty-settings-dialog.ui:384
+msgid "MMS Carrier Settings"
+msgstr "Подешавања ММС достављача"
 
-#: src/ui/chatty-settings-dialog.ui:243
-msgid "Graphical Emoticons"
-msgstr "Графички емотикони"
+#: src/ui/chatty-settings-dialog.ui:388
+msgid "MMSC"
+msgstr "ММСЦ"
 
-#: src/ui/chatty-settings-dialog.ui:244
-msgid "Convert ASCII emoticons"
-msgstr "Претвори текстуалне емотиконе"
+#: src/ui/chatty-settings-dialog.ui:403
+msgid "APN"
+msgstr "АПН"
 
-#: src/ui/chatty-settings-dialog.ui:258
-msgid "Return = Send Message"
-msgstr "Enter = пошаљи поруку"
+#: src/ui/chatty-settings-dialog.ui:418
+msgid "Proxy"
+msgstr "Посредник"
 
-#: src/ui/chatty-settings-dialog.ui:259
-msgid "Send message with return key"
-msgstr "Шаљи поруку преко тастера Enter"
+#: src/ui/chatty-settings-dialog.ui:447
+msgid "You shall not be notified for the messages from blocked contacts"
+msgstr "Нећете добијати обавештења о порукама блокираних контаката"
 
-#: src/ui/chatty-settings-dialog.ui:328
-msgid "Account ID"
-msgstr "ИБ налога"
+#: src/ui/chatty-settings-dialog.ui:481
+msgid "Blocked chat list empty"
+msgstr "Списак блокираних ћаскања је празан"
 
-#: src/ui/chatty-settings-dialog.ui:361
-msgid "Protocol"
-msgstr "Протокол"
+#: src/ui/chatty-settings-dialog.ui:537
+msgid "Message Archive Management"
+msgstr "Управљање архивама порука"
 
-#: src/ui/chatty-settings-dialog.ui:419 src/ui/chatty-settings-dialog.ui:716
-msgid "Password"
-msgstr "Лозинка"
+#: src/ui/chatty-settings-dialog.ui:538
+msgid "Sync conversations with chat server"
+msgstr "Усклади разговоре са сервером разговора"
 
-#: src/ui/chatty-settings-dialog.ui:494
-msgid "Own Fingerprint"
-msgstr "Сопствени отисак прста"
+#: src/ui/chatty-settings-dialog.ui:553
+msgid "Message Carbon Copies"
+msgstr "Индиго копије поруке"
 
-#: src/ui/chatty-settings-dialog.ui:520
-msgid "Other Devices"
-msgstr "Други уређаји"
+#: src/ui/chatty-settings-dialog.ui:737
+msgid "Home server"
+msgstr "Кућни сервер"
 
-#: src/ui/chatty-settings-dialog.ui:604
-msgid "XMPP"
-msgstr "Икс-МПП"
+#: src/ui/chatty-settings-dialog.ui:773
+msgid "Add _new account…"
+msgstr "Додај _нови налог…"
 
-#: src/ui/chatty-settings-dialog.ui:618
-msgid "Matrix"
-msgstr "Матрикс"
+#: src/ui/chatty-window.ui:17
+msgctxt "show archived chat list when clicked"
+msgid "Archived"
+msgstr "Архивирано"
 
-#: src/ui/chatty-settings-dialog.ui:633
-msgid "Telegram"
-msgstr "Телеграм"
+#: src/ui/chatty-window.ui:41
+msgid "Keyboard _Shortcuts"
+msgstr "Пр_ечице на тастатури"
 
-#: src/ui/chatty-settings-dialog.ui:691
-msgid "Provider"
-msgstr "Достављач"
+#: src/ui/chatty-window.ui:48
+msgid "Help"
+msgstr "Помоћ"
 
-#: src/ui/chatty-window.ui:31
+#: src/ui/chatty-window.ui:57
 msgid "About Chats"
 msgstr "О Разговорима"
 
-#: src/ui/chatty-window.ui:59
+#: src/ui/chatty-window.ui:85
 msgid "New Message…"
 msgstr "Нова порука…"
 
-#: src/ui/chatty-window.ui:72
+#: src/ui/chatty-window.ui:98
+msgid "New SMS/MMS Message…"
+msgstr "Нова СМС/ММС порука…"
+
+#: src/ui/chatty-window.ui:111
 msgid "New Group Message…"
 msgstr "Нова групна порука…"
 
-#: src/ui/chatty-window.ui:85
-msgid "New Bulk SMS…"
-msgstr "Нови вишеструки СМС…"
-
-#: src/ui/chatty-window.ui:177
+#: src/ui/chatty-window.ui:164
 msgid "Leave Chat"
 msgstr "Напусти разговор"
 
+#: src/ui/chatty-window.ui:177
+msgid "Block Contact"
+msgstr "Блокирај контакт"
+
 #: src/ui/chatty-window.ui:190
+msgid "Unblock Contact"
+msgstr "Деблокирај контакт"
+
+#: src/ui/chatty-window.ui:203
+msgid "Archive chat"
+msgstr "Архивирај ћаскање"
+
+#: src/ui/chatty-window.ui:216
+msgid "Unarchive chat"
+msgstr "Деархивирај ћаскање"
+
+#: src/ui/chatty-window.ui:229
 msgid "Delete Chat"
 msgstr "Обриши разговор"
 
-#: src/users/chatty-contact.c:313
+#: src/ui/help-overlay.ui:14
+msgctxt "shortcut window"
+msgid "General"
+msgstr "Опште"
+
+#: src/ui/help-overlay.ui:19
+msgctxt "shortcut window"
+msgid "Open Menu"
+msgstr "Отвори мени"
+
+#: src/ui/help-overlay.ui:27
+msgctxt "shortcut window"
+msgid "Open Search"
+msgstr "Отворите претрагу"
+
+#: src/ui/help-overlay.ui:35
+msgctxt "shortcut window"
+msgid "Show Shortcuts"
+msgstr "Прикажи пречице"
+
+#: src/users/chatty-contact.c:325
 msgid "Mobile: "
-msgstr "Мобилни:"
+msgstr "Мобилни: "
 
-#: src/users/chatty-contact.c:315
+#: src/users/chatty-contact.c:327
 msgid "Work: "
-msgstr "Посао:"
+msgstr "Посао: "
 
-#: src/users/chatty-contact.c:317
+#: src/users/chatty-contact.c:329
 msgid "Other: "
-msgstr "Друго:"
+msgstr "Друго: "
+
+#~ msgid "sm.puri.Chatty"
+#~ msgstr "sm.puri.Chatty"
+
+#~ msgctxt "timestamp-suffix-seconds"
+#~ msgid "s"
+#~ msgstr "с"
+
+#~ msgctxt "timestamp-suffix-minute"
+#~ msgid "m"
+#~ msgstr "м"
+
+#~ msgctxt "timestamp-suffix-minutes"
+#~ msgid "m"
+#~ msgstr "м"
+
+#~ msgctxt "timestamp-suffix-hour"
+#~ msgid "h"
+#~ msgstr "ч"
+
+#~ msgctxt "timestamp-suffix-hours"
+#~ msgid "h"
+#~ msgstr "ч"
+
+#~ msgctxt "timestamp-suffix-day"
+#~ msgid "d"
+#~ msgstr "д"
+
+#~ msgctxt "timestamp-suffix-days"
+#~ msgid "d"
+#~ msgstr "д"
+
+#~ msgctxt "timestamp-suffix-month"
+#~ msgid "mo"
+#~ msgstr "мес."
+
+#~ msgctxt "timestamp-suffix-months"
+#~ msgid "mos"
+#~ msgstr "мес."
+
+#~ msgctxt "timestamp-suffix-year"
+#~ msgid "y"
+#~ msgstr "г"
+
+#~ msgctxt "timestamp-suffix-years"
+#~ msgid "y"
+#~ msgstr "г"
+
+#~ msgid "Over"
+#~ msgstr "Више од"
+
+#~ msgid "Almost"
+#~ msgstr "Скоро"
+
+#, fuzzy, c-format
+#~| msgid "Error opening GNOME Contacts: %s"
+#~ msgid "Error saving contact: %s"
+#~ msgstr "Грешка при отварању Гномових Контаката: %s"
+
+#~ msgid "Start Chat"
+#~ msgstr "Започни разговор"
+
+#~ msgid "Chats List"
+#~ msgstr "Списак разговора"
+
+#~ msgid "Indicate Idle Contacts"
+#~ msgstr "Истакни контакте у мировању"
+
+#~ msgid "Blur avatars from idle contacts"
+#~ msgstr "Замути аватаре контаката у мировању"
+
+#~ msgid "Color unknown contact ID red"
+#~ msgstr "Обоји црвеном ИБ-јеве непознатих контаката"
+
+#~ msgid "Convert ASCII emoticons"
+#~ msgstr "Претвори текстуалне емотиконе"
+
+#~ msgid "Return = Send Message"
+#~ msgstr "Enter = пошаљи поруку"
+
+#~ msgid "Matrix Home Server"
+#~ msgstr "Матрикс кућни сервер"
+
+#~ msgid "_Accept"
+#~ msgstr "Прихв_ати"
+
+#~ msgid "Mark offline users differently"
+#~ msgstr "Означи кориснике ван мреже другачије"
+
+#~ msgid "ask your counterpart to use E2EE."
+#~ msgstr ""
+#~ "питајте саговорника да искористи \n"
+#~ "шифровање с-краја-на-крај."
+
+#~ msgid "Your messages are secured"
+#~ msgstr "Ваше поруке су безбедне"
+
+#~ msgid "by end-to-end encryption."
+#~ msgstr "због шифровања с-краја-на-крај."
+
+#~ msgid "and carrier rates may apply."
+#~ msgstr "и наплата трошкова оператера је могућа."
+
+#~ msgid "Message Error"
+#~ msgstr "Грешка у поруци"
+
+#~ msgid "Account Info"
+#~ msgstr "Подаци налога"
+
+#~ msgid "Account Connected"
+#~ msgstr "Налог је повезан"
+
+#~ msgid "Choose a contact"
+#~ msgstr "Изабери контакт"
+
+#~ msgid ""
+#~ "Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</"
+#~ "b> button in the titlebar."
+#~ msgstr ""
+#~ "Изабери опцију <b>СМС</b> или <b>Брза порука</b> за контакт уз помоћ "
+#~ "дугмета <b>\"+\"</b> које се налази у насловној траци."
+
+#~ msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar."
+#~ msgstr ""
+#~ "Започни <b>СМС</b> разговор уз помоћ дугмета <b>\"+\"</b> које се налази "
+#~ "у насловној траци."
+
+#~ msgid "members"
+#~ msgstr "чланови"
+
+#~ msgid "Add XMPP account"
+#~ msgstr "Додај Икс-МПП налог"
+
+#~ msgid "Show notification badge"
+#~ msgstr "Прикажи врпцу обавештења"
+
+#~ msgid "0 members"
+#~ msgstr "0 чланова"
+
+#~ msgid "Secure messaging using OMEMO"
+#~ msgstr "Безбедно дописивање уз ОМЕМО"
+
+#~ msgid "Indicate Offline Contacts"
+#~ msgstr "Истакни контакте ван мреже"
+
+#~ msgid "Grey out avatars from offline contacts"
+#~ msgstr "Засиви аватаре контаката ван мреже"
+
+#~ msgid "Other Devices"
+#~ msgstr "Други уређаји"
+
+#~ msgid "Provider"
+#~ msgstr "Достављач"
+
+#~ msgid "New Bulk SMS…"
+#~ msgstr "Нови вишеструки СМС…"
diff --git a/po/sv.po b/po/sv.po
index 1ebe8a55ff8f25fbc07afde74074b99070a6ccd3..35380ab6b63410f5396ede9200ba5df3451a010a 100644
--- a/po/sv.po
+++ b/po/sv.po
@@ -8,8 +8,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: chatty master\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-04-26 15:24+0000\n"
-"PO-Revision-Date: 2022-04-26 19:31+0200\n"
+"POT-Creation-Date: 2022-09-08 03:24+0000\n"
+"PO-Revision-Date: 2022-09-07 17:59+0200\n"
 "Last-Translator: Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
 "Language-Team: Swedish <tp-sv@listor.tp-sv.se>\n"
 "Language: sv\n"
@@ -17,18 +17,14 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Generator: Poedit 3.0.1\n"
+"X-Generator: Poedit 3.1.1\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/chatty-window.c:330
+#: src/chatty-application.c:338 src/chatty-window.c:339
 #: src/ui/chatty-window.ui:265
 msgid "Chats"
 msgstr "Chattar"
 
-#: data/sm.puri.Chatty.desktop.in:5
-msgid "sm.puri.Chatty"
-msgstr "sm.puri.Chatty"
-
 #: data/sm.puri.Chatty.desktop.in:6
 msgid "SMS and XMPP chat application"
 msgstr "SMS- och XMPP-chattprogram"
@@ -157,27 +153,27 @@ msgstr "Chattar är ett meddelandeprogram med stöd för XMPP och SMS."
 msgid "Chats message window"
 msgstr "Meddelandefönster för Chattar"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Visa utgåvans version"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Starta i demonläge"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Inaktivera alla konton"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Aktivera libpurple-felsökningsmeddelanden"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Aktivera utförliga libpurple-felsökningsmeddelanden"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:144
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -234,12 +230,12 @@ msgstr "Dina meddelanden krypteras"
 msgid "Your messages are not encrypted"
 msgstr "Dina meddelanden krypteras inte"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:420
 msgid "Select File..."
 msgstr "Välj fil…"
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -247,7 +243,7 @@ msgstr "Välj fil…"
 msgid "Cancel"
 msgstr "Avbryt"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
@@ -305,25 +301,25 @@ msgstr[1] "%lu minuter sedan"
 
 #: src/chatty-clock.c:112
 msgid "Today %H∶%M"
-msgstr "Idag %H∶%M"
+msgstr "I dag %H∶%M"
 
 #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Today 06∶42 PM”.
 #. * See https://docs.gtk.org/glib/method.DateTime.format.html
 #.
 #: src/chatty-clock.c:120
 msgid "Today %I∶%M %p"
-msgstr "Idag %I∶%M %p"
+msgstr "I dag %I∶%M %p"
 
 #: src/chatty-clock.c:132
 msgid "Yesterday %H∶%M"
-msgstr "Igår %H∶%M"
+msgstr "I går %H∶%M"
 
 #. TRANSLATORS: Timestamp with 12 hour time, e.g. “Yesterday 06∶42 PM”.
 #. * See https://docs.gtk.org/glib/method.DateTime.format.html
 #.
 #: src/chatty-clock.c:137
 msgid "Yesterday %I∶%M %p"
-msgstr "Igår %I∶%M %p"
+msgstr "I går %I∶%M %p"
 
 #. TRANSLATORS: Timestamp from more than 7 days ago or future date
 #. * (eg: when the system time is wrong), e.g. “2022-01-01”.
@@ -351,7 +347,7 @@ msgstr "Moderator"
 msgid "Member"
 msgstr "Medlem"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:724
 #, c-format
 msgid "“%s” is not a valid URI"
 msgstr "”%s” är inte en giltig URI"
@@ -360,6 +356,12 @@ msgstr "”%s” är inte en giltig URI"
 msgid "Copy"
 msgstr "Kopiera"
 
+#: src/chatty-message.c:233
+msgid "Got an encrypted message, but couldn't decrypt due to missing keys"
+msgstr ""
+"Fick ett krypterat meddelande, men kunde inte dekryptera på grund av saknade "
+"nycklar"
+
 #: src/chatty-notification.c:181
 msgid "Open Message"
 msgstr "Öppna meddelande"
@@ -373,7 +375,7 @@ msgstr "Nytt meddelande från %s"
 msgid "Message Received"
 msgstr "Meddelande togs emot"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44
 msgid "Close"
 msgstr "Stäng"
 
@@ -403,11 +405,11 @@ msgstr "Användaren %s har lagt till %s till kontakterna"
 msgid "Authorize %s?"
 msgstr "Auktorisera %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264
 msgid "Reject"
 msgstr "Avvisa"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265
 msgid "Accept"
 msgstr "Acceptera"
 
@@ -416,76 +418,71 @@ msgstr "Acceptera"
 msgid "Add %s to contact list"
 msgstr "Lägg till %s till kontaktlista"
 
-#: src/purple/chatty-purple.c:587
+#: src/purple/chatty-purple.c:586
 msgid "Login failed"
 msgstr "Inloggning misslyckades"
 
-#: src/purple/chatty-purple.c:592
+#: src/purple/chatty-purple.c:591
 msgid "Please check ID and password"
 msgstr "Kontrollera ID och lösenord"
 
-#: src/chatty-secret-store.c:98
-#, c-format
-msgid "Chatty password for \"%s\""
-msgstr "Chatty-lösenord för ”%s”"
-
-#: src/chatty-window.c:399
+#: src/chatty-window.c:408
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "Ta bort chatt med ”%s”"
 
-#: src/chatty-window.c:400
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Det här tar bort meddelandehistoriken"
 
-#: src/chatty-window.c:402
+#: src/chatty-window.c:411
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "Lämna gruppchatten ”%s”"
 
-#: src/chatty-window.c:403
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Detta tar bort chatten från chattlistan"
 
-#: src/chatty-window.c:415
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Ta bort"
 
-#: src/chatty-window.c:493
+#: src/chatty-window.c:502
 msgid "You shall no longer be notified for new messages, continue?"
 msgstr "Du kommer inte längre att meddelas om nya meddelanden, fortsätt?"
 
-#: src/chatty-window.c:568
+#: src/chatty-window.c:577
 msgid "Archived"
 msgstr "Arkiverade"
 
-#: src/chatty-window.c:621
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "En SMS- och XMPP-meddelandeklient"
 
-#: src/chatty-window.c:628
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr ""
 "Anders Jonsson <anders.jonsson@norsjovallen.se>\n"
 "Luna Jernberg <droidbittin@gmail.com>"
 
-#: src/chatty-window.c:978
+#: src/chatty-window.c:989
 msgid "Any Protocol"
 msgstr "Alla protokoll"
 
-#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:980
+#: src/chatty-window.c:991
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -585,115 +582,93 @@ msgstr "ansluter…"
 msgid "disconnected"
 msgstr "frånkopplad"
 
-#: src/dialogs/chatty-settings-dialog.c:255
-#: src/dialogs/chatty-settings-dialog.c:405
+#: src/dialogs/chatty-settings-dialog.c:251
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Misslyckades med att verifiera server: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:253
 msgid "Failed to verify server"
 msgstr "Misslyckades med att verifiera server"
 
-#: src/dialogs/chatty-settings-dialog.c:306
-#: src/dialogs/chatty-settings-dialog.c:385
+#: src/dialogs/chatty-settings-dialog.c:259
 msgid "Couldn't get Home server address"
 msgstr "Kunde inte erhålla hemserverns adress"
 
-#: src/dialogs/chatty-settings-dialog.c:510
+#: src/dialogs/chatty-settings-dialog.c:428
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Ta bort konto"
 
-#: src/dialogs/chatty-settings-dialog.c:513
+#: src/dialogs/chatty-settings-dialog.c:431
 #, c-format
 msgid "Delete account %s?"
 msgstr "Ta bort kontot %s?"
 
-#: src/dialogs/chatty-settings-dialog.c:654
+#: src/dialogs/chatty-settings-dialog.c:582
 msgid "Restart chatty to disable purple"
 msgstr "Starta om chatty för att inaktivera purple"
 
-#: src/dialogs/chatty-settings-dialog.c:656
+#: src/dialogs/chatty-settings-dialog.c:584
 #: src/ui/chatty-settings-dialog.ui:512
 msgid "Enable purple plugin"
 msgstr "Aktivera purple-insticksmodul"
 
-#: src/dialogs/chatty-settings-dialog.c:670
+#: src/dialogs/chatty-settings-dialog.c:598
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr "SMS och MMS-inställningar"
 
-#: src/dialogs/chatty-settings-dialog.c:672
+#: src/dialogs/chatty-settings-dialog.c:600
 #: src/ui/chatty-settings-dialog.ui:298
 msgid "Purple Settings"
 msgstr "Purple-inställningar"
 
-#: src/dialogs/chatty-settings-dialog.c:674
+#: src/dialogs/chatty-settings-dialog.c:602
 msgid "New Account"
 msgstr "Nytt konto"
 
-#: src/dialogs/chatty-settings-dialog.c:676
+#: src/dialogs/chatty-settings-dialog.c:604
 #: src/ui/chatty-settings-dialog.ui:364
 msgid "Blocked Contacts"
 msgstr "Blockerade kontakter"
 
-#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7
+#: src/dialogs/chatty-settings-dialog.c:606 src/ui/chatty-settings-dialog.ui:7
 #: src/ui/chatty-window.ui:30
 msgid "Preferences"
 msgstr "Inställningar"
 
-#: src/dialogs/chatty-settings-dialog.c:707
+#: src/dialogs/chatty-settings-dialog.c:634
 msgid "Select Protocol"
 msgstr "Välj protokoll"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:948
+#: src/dialogs/chatty-settings-dialog.c:878
 msgid "@user:matrix.org or user@example.com"
 msgstr "@user:matrix.org eller user@example.com"
 
-#: src/dialogs/chatty-settings-dialog.c:1057
+#: src/dialogs/chatty-settings-dialog.c:987
 msgid "Unblock contact"
 msgstr "Avblockera kontakt"
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/matrix/chatty-ma-account.c:105
 msgid "Incorrect password"
 msgstr "Felaktigt lösenord"
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:108
 msgid "_OK"
 msgstr "_OK"
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:109 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "A_vbryt"
 
-#: src/matrix/chatty-ma-account.c:306
+#: src/matrix/chatty-ma-account.c:115
 #, c-format
-msgid "Please enter password for “%s”"
-msgstr "Ange lösenordet för ”%s”"
-
-#: src/matrix/matrix-utils.c:474
-#, c-format
-msgid "The certificate for ‘%s’ has unknown CA"
-msgstr "Certifikatet för ”%s” har okänd CA"
-
-#: src/matrix/matrix-utils.c:476
-#, c-format
-msgid "The certificate for ‘%s’ is self-signed"
-msgstr "Certifikatet för ”%s” är självsignerat"
-
-#: src/matrix/matrix-utils.c:480
-#, c-format
-msgid "The certificate for ‘%s’ has expired"
-msgstr "Certifikatet för ”%s” har gått ut"
-
-#: src/matrix/matrix-utils.c:484
-#, c-format
-msgid "The certificate for ‘%s’ has been revoked"
-msgstr "Certifikatet för ”%s” har återkallats"
-
-#: src/matrix/matrix-utils.c:493
-#, c-format
-msgid "Error validating certificate for ‘%s’"
-msgstr "Fel vid validering av certifikat för ”%s”"
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Ange lösenordet för ”%s”, hemserver: %s"
 
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
@@ -1070,3 +1045,27 @@ msgstr "Arbete: "
 #: src/users/chatty-contact.c:329
 msgid "Other: "
 msgstr "Annat: "
+
+#, c-format
+#~ msgid "Chatty password for \"%s\""
+#~ msgstr "Chatty-lösenord för ”%s”"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ has unknown CA"
+#~ msgstr "Certifikatet för ”%s” har okänd CA"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ is self-signed"
+#~ msgstr "Certifikatet för ”%s” är självsignerat"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ has expired"
+#~ msgstr "Certifikatet för ”%s” har gått ut"
+
+#, c-format
+#~ msgid "The certificate for ‘%s’ has been revoked"
+#~ msgstr "Certifikatet för ”%s” har återkallats"
+
+#, c-format
+#~ msgid "Error validating certificate for ‘%s’"
+#~ msgstr "Fel vid validering av certifikat för ”%s”"
diff --git a/po/tr.po b/po/tr.po
index 28bb507c43c8feab28d87f18be3df7395364e1ce..f5773a95e5044bdaecc4fe489ba9d2824684821c 100644
--- a/po/tr.po
+++ b/po/tr.po
@@ -5,8 +5,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: purism-chatty\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-02-02 15:33+0000\n"
-"PO-Revision-Date: 2022-03-19 13:04+0300\n"
+"POT-Creation-Date: 2022-07-18 15:24+0000\n"
+"PO-Revision-Date: 2022-09-04 13:31+0300\n"
 "Last-Translator: Emin Tufan Çetin <etcetin@gmail.com>\n"
 "Language-Team: Turkish <gnome-turk@gnome.org>\n"
 "Language: tr\n"
@@ -17,14 +17,11 @@ msgstr ""
 "Plural-Forms: nplurals=1; plural=0;\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/ui/chatty-window.ui:200
+#: src/chatty-application.c:336 src/chatty-window.c:339
+#: src/ui/chatty-window.ui:265
 msgid "Chats"
 msgstr "KonuÅŸmalar"
 
-#: data/sm.puri.Chatty.desktop.in:5
-msgid "sm.puri.Chatty"
-msgstr "sm.puri.Chatty"
-
 #: data/sm.puri.Chatty.desktop.in:6
 msgid "SMS and XMPP chat application"
 msgstr "SMS ve XMPP konuşma uygulaması"
@@ -32,7 +29,8 @@ msgstr "SMS ve XMPP konuşma uygulaması"
 #: data/sm.puri.Chatty.desktop.in:7
 msgid "XMPP;SMS;chat;jabber;messaging;modem"
 msgstr ""
-"XMPP;SMS;chat;jabber;mesajlaşma;iletişim;modem;ileti;kısamesaj;kısaileti;MMS;multimedyamesajı;multimedyailetisi;çokluortammesajı;çokluortamiletisi;"
+"XMPP;SMS;chat;jabber;mesajlaşma;iletişim;modem;ileti;kısamesaj;kısaileti;MMS;"
+"multimedyamesajı;multimedyailetisi;çokluortammesajı;çokluortamiletisi;"
 
 #: data/sm.puri.Chatty.gschema.xml:7 data/sm.puri.Chatty.gschema.xml:8
 msgid "Whether the application is launching the first time"
@@ -58,7 +56,7 @@ msgstr "İletinin okunma durumununun gönderilip gönderilmeyeceği"
 msgid "Message carbon copies"
 msgstr "İleti karbon kopyaları"
 
-#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:479
+#: data/sm.puri.Chatty.gschema.xml:26 src/ui/chatty-settings-dialog.ui:554
 msgid "Share chat history among devices"
 msgstr "Konuşma geçmişini aygıtlar arası paylaş"
 
@@ -74,7 +72,7 @@ msgstr "Sunucudan İAY arşiv eşzamanlamasını etkinleştir"
 msgid "Enable purple"
 msgstr "Purple’ı etkinleştir"
 
-#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:436
+#: data/sm.puri.Chatty.gschema.xml:38 src/ui/chatty-settings-dialog.ui:511
 msgid "Enable purple accounts"
 msgstr "Purple hesaplarını etkinleştir"
 
@@ -154,27 +152,27 @@ msgstr "Konuşmalar, XMPP ve SMS’i destekleyen mesajlaşma uygulamasıdır."
 msgid "Chats message window"
 msgstr "KonuÅŸmalar ileti penceresi"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Dağıtım sürümünü göster"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Art alan kipinde baÅŸlat"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Tüm hesapları devre dışı bırak"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "libpurple hata ayıklama iletilerini etkinleştir"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Ayrıntılı libpurple hata ayıklama iletilerini etkinleştir"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:144
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -187,23 +185,27 @@ msgstr ""
 msgid "Send To"
 msgstr "Şuna Gönder"
 
-#: src/chatty-chat-list.c:152
+#: src/chatty-chat-list.c:175
 msgid "Select a contact with the <b>“+”</b> button in the titlebar."
 msgstr "Başlık çubuğundaki <b>“+”</b> düğmesiyle kişi seçin."
 
-#: src/chatty-chat-list.c:156
+#: src/chatty-chat-list.c:179
 msgid "Add instant messaging accounts in Preferences."
 msgstr "Tercihlerde anında mesajlaşma hesapları ekleyin."
 
-#: src/chatty-chat-list.c:192 src/chatty-contact-list.c:282
+#: src/chatty-chat-list.c:215 src/chatty-contact-list.c:282
 msgid "No Search Results"
 msgstr "Arama Sonucu Yok"
 
-#: src/chatty-chat-list.c:193
+#: src/chatty-chat-list.c:216
 msgid "Try different search"
 msgstr "BaÅŸka arama dene"
 
-#: src/chatty-chat-list.c:196
+#: src/chatty-chat-list.c:220
+msgid "No archived chats"
+msgstr "ArÅŸivli konuÅŸma yok"
+
+#: src/chatty-chat-list.c:222
 msgid "Start Chatting"
 msgstr "KonuÅŸma BaÅŸlat"
 
@@ -227,12 +229,12 @@ msgstr "Ä°letileriniz ÅŸifreleniyor"
 msgid "Your messages are not encrypted"
 msgstr "Ä°letileriniz ÅŸifrelenmiyor"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:420
 msgid "Select File..."
 msgstr "Dosyayı Seç…"
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:347 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:422 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -240,24 +242,24 @@ msgstr "Dosyayı Seç…"
 msgid "Cancel"
 msgstr "Ä°ptal Et"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:424 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
 msgid "Open"
 msgstr "Aç"
 
-#: src/chatty-chat.c:632
+#: src/chatty-chat.c:635
 msgid "Empty room"
 msgstr "BoÅŸ oda"
 
 #. TRANSLATORS: %s are name/user-id/phone numbers of two users
-#: src/chatty-chat.c:639
+#: src/chatty-chat.c:642
 #, c-format
 msgid "%s and %s"
 msgstr "%s ve %s"
 
-#: src/chatty-chat.c:641
+#: src/chatty-chat.c:644
 #, c-format
 msgid "%s and %u other"
 msgid_plural "%s and %u others"
@@ -340,7 +342,7 @@ msgstr "Moderatör"
 msgid "Member"
 msgstr "Ãœye"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:729
 #, c-format
 msgid "“%s” is not a valid URI"
 msgstr "“%s” geçerli URI değil"
@@ -362,7 +364,7 @@ msgstr "%s kiÅŸisinden ileti"
 msgid "Message Received"
 msgstr "İleti Alındı"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:504
 msgid "Close"
 msgstr "Kapat"
 
@@ -392,11 +394,11 @@ msgstr "%s kullanıcısı, %s kişisini kişilerine ekledi"
 msgid "Authorize %s?"
 msgstr "Yetkilendir: %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:508
 msgid "Reject"
 msgstr "Reddet"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:509
 msgid "Accept"
 msgstr "Kabul Et"
 
@@ -413,58 +415,66 @@ msgstr "Giriş başarısız"
 msgid "Please check ID and password"
 msgstr "Lütfen kimliği ve parolayı denetleyin"
 
-#: src/chatty-secret-store.c:98
+#: src/chatty-secret-store.c:99
 #, c-format
 msgid "Chatty password for \"%s\""
 msgstr "“%s” için Chatty parolası"
 
-#: src/chatty-window.c:333
+#: src/chatty-window.c:408
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "“%s” ile konuşmayı sil"
 
-#: src/chatty-window.c:334
+#: src/chatty-window.c:409
 msgid "This deletes the conversation history"
 msgstr "Bu, konuşma geçmişini siler"
 
-#: src/chatty-window.c:336
+#: src/chatty-window.c:411
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "“%s” küme konuşmasından ayrıl"
 
-#: src/chatty-window.c:337
+#: src/chatty-window.c:412
 msgid "This removes chat from chats list"
 msgstr "Bu, konuşmayı konuşma listesinden kaldırır"
 
-#: src/chatty-window.c:349
+#: src/chatty-window.c:424
 msgid "Delete"
 msgstr "Sil"
 
-#: src/chatty-window.c:474
+#: src/chatty-window.c:502
+msgid "You shall no longer be notified for new messages, continue?"
+msgstr "Bundan sonra yeni iletiler bildirilmeyecek, sürdürülsün mü?"
+
+#: src/chatty-window.c:577
+msgid "Archived"
+msgstr "ArÅŸivlendi"
+
+#: src/chatty-window.c:630
 msgid "An SMS and XMPP messaging client"
 msgstr "SMS ve XMPP mesajlaÅŸma istemcisi"
 
-#: src/chatty-window.c:481
+#: src/chatty-window.c:637
 msgid "translator-credits"
 msgstr "Emin Tufan Çetin <etcetin@gmail.com>"
 
-#: src/chatty-window.c:815
+#: src/chatty-window.c:989
 msgid "Any Protocol"
 msgstr "Herhangi Bir İletişim Kuralı"
 
-#: src/chatty-window.c:816 src/ui/chatty-settings-dialog.ui:552
+#: src/chatty-window.c:990 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:817
+#: src/chatty-window.c:991
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:820 src/ui/chatty-settings-dialog.ui:540
+#: src/chatty-window.c:994 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:823 src/ui/chatty-settings-dialog.ui:566
+#: src/chatty-window.c:997 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -563,103 +573,117 @@ msgstr "bağlanıyor…"
 msgid "disconnected"
 msgstr "bağlantı kesildi"
 
-#: src/dialogs/chatty-settings-dialog.c:210
-#: src/dialogs/chatty-settings-dialog.c:374
+#: src/dialogs/chatty-settings-dialog.c:267
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Sunucu doğrulanamadı: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:269
+#: src/dialogs/chatty-settings-dialog.c:479
 msgid "Failed to verify server"
 msgstr "Sunucu doğrulanamadı"
 
-#: src/dialogs/chatty-settings-dialog.c:261
-#: src/dialogs/chatty-settings-dialog.c:354
+#: src/dialogs/chatty-settings-dialog.c:275
+#: src/dialogs/chatty-settings-dialog.c:459
 msgid "Couldn't get Home server address"
 msgstr "Ev sunucusu adresi alınamadı."
 
-#: src/dialogs/chatty-settings-dialog.c:479
+#: src/dialogs/chatty-settings-dialog.c:584
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Hesabı Sil"
 
-#: src/dialogs/chatty-settings-dialog.c:482
+#: src/dialogs/chatty-settings-dialog.c:587
 #, c-format
 msgid "Delete account %s?"
 msgstr "%s hesabını sil?"
 
-#: src/dialogs/chatty-settings-dialog.c:618
+#: src/dialogs/chatty-settings-dialog.c:728
 msgid "Restart chatty to disable purple"
 msgstr "Purple’ı devre dışı bırakmak için chatty’yi yeniden başlat"
 
-#: src/dialogs/chatty-settings-dialog.c:620
-#: src/ui/chatty-settings-dialog.ui:437
+#: src/dialogs/chatty-settings-dialog.c:730
+#: src/ui/chatty-settings-dialog.ui:512
 msgid "Enable purple plugin"
 msgstr "Purple eklentisini etkinleÅŸtir"
 
-#: src/dialogs/chatty-settings-dialog.c:634
+#: src/dialogs/chatty-settings-dialog.c:744
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr "SMS ve MMS Ayarları"
 
-#: src/dialogs/chatty-settings-dialog.c:636
+#: src/dialogs/chatty-settings-dialog.c:746
 #: src/ui/chatty-settings-dialog.ui:298
 msgid "Purple Settings"
 msgstr "Purple Ayarları"
 
-#: src/dialogs/chatty-settings-dialog.c:638
+#: src/dialogs/chatty-settings-dialog.c:748
 msgid "New Account"
 msgstr "Yeni Hesap"
 
-#: src/dialogs/chatty-settings-dialog.c:640 src/ui/chatty-settings-dialog.ui:7
-#: src/ui/chatty-window.ui:17
+#: src/dialogs/chatty-settings-dialog.c:750
+#: src/ui/chatty-settings-dialog.ui:364
+msgid "Blocked Contacts"
+msgstr "Engellenen KiÅŸiler"
+
+#: src/dialogs/chatty-settings-dialog.c:752 src/ui/chatty-settings-dialog.ui:7
+#: src/ui/chatty-window.ui:30
 msgid "Preferences"
 msgstr "Tercihler"
 
-#: src/dialogs/chatty-settings-dialog.c:669
+#: src/dialogs/chatty-settings-dialog.c:781
 msgid "Select Protocol"
 msgstr "İletişim Kuralı Seç"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:865
+#: src/dialogs/chatty-settings-dialog.c:1022
 msgid "@user:matrix.org or user@example.com"
 msgstr "@kullanici:matrix.org veya kullanici@ornek.com"
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/dialogs/chatty-settings-dialog.c:1131
+msgid "Unblock contact"
+msgstr "Engeli kaldır"
+
+#: src/matrix/chatty-ma-account.c:231
 msgid "Incorrect password"
 msgstr "Geçersiz parola"
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:234
 msgid "_OK"
 msgstr "_Tamam"
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:235 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "Ä°ptal _Et"
 
-#: src/matrix/chatty-ma-account.c:306
+#: src/matrix/chatty-ma-account.c:241
 #, c-format
-msgid "Please enter password for “%s”"
-msgstr "Lütfen “%s” için parola girin"
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Lütfen “%s” için parola girin, ana sunucu: %s"
 
-#: src/matrix/matrix-utils.c:474
+#: src/matrix/matrix-utils.c:473
 #, c-format
 msgid "The certificate for ‘%s’ has unknown CA"
 msgstr "‘%s’ için sertifikanın bilinmeyen CAʼsı var"
 
-#: src/matrix/matrix-utils.c:476
+#: src/matrix/matrix-utils.c:475
 #, c-format
 msgid "The certificate for ‘%s’ is self-signed"
 msgstr "‘%s’ için sertifikanın imzacısı kendidir"
 
-#: src/matrix/matrix-utils.c:480
+#: src/matrix/matrix-utils.c:479
 #, c-format
 msgid "The certificate for ‘%s’ has expired"
 msgstr "‘%s’ için sertifikanın süresi dolmuş"
 
-#: src/matrix/matrix-utils.c:484
+#: src/matrix/matrix-utils.c:483
 #, c-format
 msgid "The certificate for ‘%s’ has been revoked"
 msgstr "‘%s’ için sertifika iptal edilmiş"
 
-#: src/matrix/matrix-utils.c:493
+#: src/matrix/matrix-utils.c:492
 #, c-format
 msgid "Error validating certificate for ‘%s’"
 msgstr "‘%s’ için sertifika doğrulanırken hata"
@@ -667,16 +691,16 @@ msgstr "‘%s’ için sertifika doğrulanırken hata"
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
 #.
-#: src/mm/chatty-mmsd.c:826
+#: src/mm/chatty-mmsd.c:827
 msgid "%Y-%m-%d %H∶%M"
 msgstr "%Y-%m-%d %H∶%M"
 
-#: src/mm/chatty-mmsd.c:1107
+#: src/mm/chatty-mmsd.c:1111
 #, c-format
 msgid "You received an MMS, but it expired on: %s"
 msgstr "MMS aldınız ancak şunda süresi doldu: %s"
 
-#: src/mm/chatty-mmsd.c:1110
+#: src/mm/chatty-mmsd.c:1114
 msgid "You received an empty MMS."
 msgstr "Boş MMS aldınız."
 
@@ -720,7 +744,7 @@ msgstr "KiÅŸilere Ekle"
 msgid "Remove Attachment"
 msgstr "Eki Kaldır"
 
-#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:126
+#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139
 msgid "Chat Details"
 msgstr "Konuşma Ayrıntıları"
 
@@ -798,7 +822,7 @@ msgstr "Öntanımlı için boş"
 msgid "Group Members"
 msgstr "Küme Üyeleri"
 
-#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:353
+#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433
 msgid "Call"
 msgstr "Ara"
 
@@ -834,7 +858,7 @@ msgstr "Kullanıcı Kimliği"
 msgid "Protocol"
 msgstr "İletişim Kuralı"
 
-#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:626
+#: src/ui/chatty-pp-account-details.ui:168 src/ui/chatty-settings-dialog.ui:701
 msgid "Password"
 msgstr "Parola"
 
@@ -862,7 +886,7 @@ msgstr "_Uygula"
 msgid "Accounts"
 msgstr "Hesaplar"
 
-#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:456
+#: src/ui/chatty-settings-dialog.ui:189 src/ui/chatty-settings-dialog.ui:531
 msgid "Privacy"
 msgstr "Gizlilik"
 
@@ -911,71 +935,100 @@ msgstr "Teslim Raporları İste"
 msgid "SMIL for MMS"
 msgstr "MMS için SMIL"
 
-#: src/ui/chatty-settings-dialog.ui:366
+#: src/ui/chatty-settings-dialog.ui:384
 msgid "MMS Carrier Settings"
 msgstr "MMS Ulak Ayarları"
 
-#: src/ui/chatty-settings-dialog.ui:370
+#: src/ui/chatty-settings-dialog.ui:388
 msgid "MMSC"
 msgstr "MMSC"
 
-#: src/ui/chatty-settings-dialog.ui:385
+#: src/ui/chatty-settings-dialog.ui:403
 msgid "APN"
 msgstr "APN"
 
-#: src/ui/chatty-settings-dialog.ui:400
+#: src/ui/chatty-settings-dialog.ui:418
 msgid "Proxy"
 msgstr "Vekil"
 
-#: src/ui/chatty-settings-dialog.ui:462
+#: src/ui/chatty-settings-dialog.ui:447
+msgid "You shall not be notified for the messages from blocked contacts"
+msgstr "Bundan sonra engelli kiÅŸilerden iletiler bildirilmeyecek"
+
+#: src/ui/chatty-settings-dialog.ui:481
+msgid "Blocked chat list empty"
+msgstr "Engelli konuÅŸma listesi boÅŸ"
+
+#: src/ui/chatty-settings-dialog.ui:537
 msgid "Message Archive Management"
 msgstr "İleti Arşiv Yönetimi"
 
-#: src/ui/chatty-settings-dialog.ui:463
+#: src/ui/chatty-settings-dialog.ui:538
 msgid "Sync conversations with chat server"
 msgstr "Konuşmaları konuşma sunucusuyla eşzamanlandır"
 
-#: src/ui/chatty-settings-dialog.ui:478
+#: src/ui/chatty-settings-dialog.ui:553
 msgid "Message Carbon Copies"
 msgstr "İleti Karbon Kopyaları"
 
-#: src/ui/chatty-settings-dialog.ui:662
+#: src/ui/chatty-settings-dialog.ui:737
 msgid "Home server"
 msgstr "Ev sunucusu"
 
-#: src/ui/chatty-settings-dialog.ui:698
+#: src/ui/chatty-settings-dialog.ui:773
 msgid "Add _new account…"
 msgstr "_Yeni hesap ekle…"
 
-#: src/ui/chatty-window.ui:28
+#: src/ui/chatty-window.ui:17
+msgctxt "show archived chat list when clicked"
+msgid "Archived"
+msgstr "ArÅŸivli"
+
+#: src/ui/chatty-window.ui:41
 msgid "Keyboard _Shortcuts"
 msgstr "Klavye _Kısayolları"
 
-#: src/ui/chatty-window.ui:35
+#: src/ui/chatty-window.ui:48
 msgid "Help"
 msgstr "Yardım"
 
-#: src/ui/chatty-window.ui:44
+#: src/ui/chatty-window.ui:57
 msgid "About Chats"
 msgstr "Konuşmalar Hakkında"
 
-#: src/ui/chatty-window.ui:72
+#: src/ui/chatty-window.ui:85
 msgid "New Message…"
 msgstr "İletiyi Aç…"
 
-#: src/ui/chatty-window.ui:85
+#: src/ui/chatty-window.ui:98
 msgid "New SMS/MMS Message…"
 msgstr "Yeni SMS/MMS İletisi…"
 
-#: src/ui/chatty-window.ui:98
+#: src/ui/chatty-window.ui:111
 msgid "New Group Message…"
 msgstr "Yeni Küme Konuşması…"
 
-#: src/ui/chatty-window.ui:151
+#: src/ui/chatty-window.ui:164
 msgid "Leave Chat"
 msgstr "Konuşmadan Ayrıl"
 
-#: src/ui/chatty-window.ui:164
+#: src/ui/chatty-window.ui:177
+msgid "Block Contact"
+msgstr "KiÅŸiyi Engelle"
+
+#: src/ui/chatty-window.ui:190
+msgid "Unblock Contact"
+msgstr "Engeli Kaldır"
+
+#: src/ui/chatty-window.ui:203
+msgid "Archive chat"
+msgstr "Konuşmayı Arşivle"
+
+#: src/ui/chatty-window.ui:216
+msgid "Unarchive chat"
+msgstr "Arşivden Çıkar"
+
+#: src/ui/chatty-window.ui:229
 msgid "Delete Chat"
 msgstr "Konuşmayı Sil"
 
@@ -1010,117 +1063,3 @@ msgstr "Ä°ÅŸ: "
 #: src/users/chatty-contact.c:329
 msgid "Other: "
 msgstr "DiÄŸer: "
-
-#~ msgid "Mark offline users differently"
-#~ msgstr "Çevrim dışı kullanıcıları başka imle"
-
-#~ msgid "ask your counterpart to use E2EE."
-#~ msgstr "eşinize E2EE kullanmasını söyleyin."
-
-#~ msgid "Your messages are secured"
-#~ msgstr "İletileriniz uçtan uca"
-
-#~ msgid "by end-to-end encryption."
-#~ msgstr "şifrelemeyle korunmaktadır."
-
-#~ msgid "and carrier rates may apply."
-#~ msgstr "ve operatör tarifeleri uygulanabilir."
-
-#~ msgctxt "timestamp-suffix-seconds"
-#~ msgid "s"
-#~ msgstr "sn"
-
-#~ msgctxt "timestamp-suffix-minute"
-#~ msgid "m"
-#~ msgstr "d"
-
-#~ msgctxt "timestamp-suffix-minutes"
-#~ msgid "m"
-#~ msgstr "d"
-
-#~ msgctxt "timestamp-suffix-hour"
-#~ msgid "h"
-#~ msgstr "s"
-
-#~ msgctxt "timestamp-suffix-hours"
-#~ msgid "h"
-#~ msgstr "s"
-
-#~ msgctxt "timestamp-suffix-day"
-#~ msgid "d"
-#~ msgstr "g"
-
-#~ msgctxt "timestamp-suffix-days"
-#~ msgid "d"
-#~ msgstr "g"
-
-#~ msgctxt "timestamp-suffix-month"
-#~ msgid "mo"
-#~ msgstr "a"
-
-#~ msgctxt "timestamp-suffix-months"
-#~ msgid "mos"
-#~ msgstr "a"
-
-#~ msgctxt "timestamp-suffix-year"
-#~ msgid "y"
-#~ msgstr "y"
-
-#~ msgctxt "timestamp-suffix-years"
-#~ msgid "y"
-#~ msgstr "y"
-
-#~ msgid "Over"
-#~ msgstr "Geçik"
-
-#~ msgid "Almost"
-#~ msgstr "Neredeyse"
-
-#~ msgid "Choose a contact"
-#~ msgstr "Kişi seç"
-
-#~ msgid ""
-#~ "Select an <b>SMS</b> or <b>Instant Message</b> contact with the <b>\"+\"</"
-#~ "b> button in the titlebar."
-#~ msgstr ""
-#~ "Başlık çubuğundaki <b>\"+\"</b> düğmesiyle <b>SMS</b> veya <b>Anında "
-#~ "Mesaj</b> kişisi seçin."
-
-#~ msgid "Start a <b>SMS</b> chat with the \"+\" button in the titlebar."
-#~ msgstr "Başlık çubuğundaki \"+\" düğmesiyle <b>SMS</b> konuşması başlat."
-
-#~ msgid "Error saving contact: %s"
-#~ msgstr "KiÅŸi kaydedilirken hata: %s"
-
-#~ msgid "Start Chat"
-#~ msgstr "KonuÅŸma BaÅŸlat"
-
-#~ msgid "Chats List"
-#~ msgstr "KonuÅŸma Listesi"
-
-#~ msgid "Indicate Offline Contacts"
-#~ msgstr "Çevrim Dışı Kişileri İmle"
-
-#~ msgid "Grey out avatars from offline contacts"
-#~ msgstr "Çevrim dışı kişilerin avatarlarını grile"
-
-#~ msgid "Indicate Idle Contacts"
-#~ msgstr "Eylemsiz KiÅŸileri Ä°mle"
-
-#~ msgid "Blur avatars from idle contacts"
-#~ msgstr "Eylemsiz kişilerin avatarlarını bulanıklaştır"
-
-#~ msgid "Color unknown contact ID red"
-#~ msgstr "Bilinmeyen kişi kimliğini kırmızı renk yap"
-
-#~ msgid "Convert ASCII emoticons"
-#~ msgstr "ASCII yüz ifadelerini dönüştür"
-
-#~ msgid "Return = Send Message"
-#~ msgstr "Dönüş = İletiyi Gönder"
-
-#~ msgid "_Accept"
-#~ msgstr "_Kabul Et"
-
-#~ msgid "New Bulk SMS…"
-#~ msgstr "Yeni Çoklu SMS…"
diff --git a/po/uk.po b/po/uk.po
index fc6889f1d962ab3c3ecdf6ff1348cb9c5ff11a92..9b55a4eb949a60392865a2ce612905a0551ff2d9 100644
--- a/po/uk.po
+++ b/po/uk.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: purism-chatty\n"
 "Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/chatty/issues\n"
-"POT-Creation-Date: 2022-04-26 15:24+0000\n"
-"PO-Revision-Date: 2022-04-26 20:04+0300\n"
+"POT-Creation-Date: 2022-10-07 03:24+0000\n"
+"PO-Revision-Date: 2022-10-07 19:11+0300\n"
 "Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
 "Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
 "Language: uk\n"
@@ -16,19 +16,15 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "X-Generator: Lokalize 20.12.0\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<"
-"=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
 
 #: data/sm.puri.Chatty.desktop.in:3 data/sm.puri.Chatty.metainfo.xml.in:6
-#: src/chatty-application.c:333 src/chatty-window.c:330
-#: src/ui/chatty-window.ui:265
+#: src/chatty-application.c:394 src/chatty-header-bar.c:250
+#: src/ui/chatty-header-bar.ui:15
 msgid "Chats"
 msgstr "Спілкування"
 
-#: data/sm.puri.Chatty.desktop.in:5
-msgid "sm.puri.Chatty"
-msgstr "sm.puri.Chatty"
-
 #: data/sm.puri.Chatty.desktop.in:6
 msgid "SMS and XMPP chat application"
 msgstr "Програма для спілкування за допомогою SMS і XMPP"
@@ -161,27 +157,35 @@ msgstr ""
 msgid "Chats message window"
 msgstr "Вікно повідомлень спілкування"
 
-#: src/chatty-application.c:76
+#: src/chatty-application.c:78
 msgid "Show release version"
 msgstr "Показати версію випуску"
 
-#: src/chatty-application.c:77
+#: src/chatty-application.c:79
 msgid "Start in daemon mode"
 msgstr "Запускати у режимі служби"
 
-#: src/chatty-application.c:78
+#: src/chatty-application.c:80
 msgid "Disable all accounts"
 msgstr "Вимкнути усі облікові записи"
 
-#: src/chatty-application.c:79
+#: src/chatty-application.c:81
 msgid "Enable libpurple debug messages"
 msgstr "Увімкнути діагностичні повідомлення libpurple"
 
-#: src/chatty-application.c:81
+#: src/chatty-application.c:83
 msgid "Enable verbose libpurple debug messages"
 msgstr "Увімкнути докладні діагностичні повідомлення libpurple"
 
-#: src/chatty-application.c:142
+#: src/chatty-application.c:167
+msgid "An SMS and XMPP messaging client"
+msgstr "Клієнт обміну повідомленнями SMS і XMPP"
+
+#: src/chatty-application.c:174
+msgid "translator-credits"
+msgstr "Юрій Чорноіван <yurchor@ukr.net>, 2020"
+
+#: src/chatty-application.c:198
 #, c-format
 msgid ""
 "There was an error displaying help:\n"
@@ -219,34 +223,34 @@ msgstr "Немає архівованих спілкувань"
 msgid "Start Chatting"
 msgstr "Почати спілкування"
 
-#: src/chatty-chat-view.c:212
+#: src/chatty-chat-view.c:211
 msgid "This is an SMS conversation"
 msgstr "Це обмін повідомленнями SMS"
 
-#: src/chatty-chat-view.c:214 src/chatty-chat-view.c:220
+#: src/chatty-chat-view.c:213 src/chatty-chat-view.c:219
 msgid "Your messages are not encrypted, and carrier rates may apply"
 msgstr ""
 "Ваші повідомлення не зашифровано, оператор може стягувати сплату за "
 "передавання даних"
 
-#: src/chatty-chat-view.c:218
+#: src/chatty-chat-view.c:217
 msgid "This is an IM conversation"
 msgstr "Це спілкування у службі обміну повідомленнями"
 
-#: src/chatty-chat-view.c:224
+#: src/chatty-chat-view.c:223
 msgid "Your messages are encrypted"
 msgstr "Ваші повідомлення зашифровано"
 
-#: src/chatty-chat-view.c:227
+#: src/chatty-chat-view.c:226
 msgid "Your messages are not encrypted"
 msgstr "Ваші повідомлення не зашифровано"
 
-#: src/chatty-chat-view.c:414
+#: src/chatty-chat-view.c:419
 msgid "Select File..."
 msgstr "Вибір файла…"
 
-#: src/chatty-chat-view.c:417 src/purple/chatty-purple-request.c:175
-#: src/chatty-window.c:413 src/dialogs/chatty-pp-chat-info.c:90
+#: src/chatty-chat-view.c:422 src/purple/chatty-purple-request.c:175
+#: src/chatty-window.c:529 src/dialogs/chatty-pp-chat-info.c:90
 #: src/dialogs/chatty-ma-account-details.c:119
 #: src/dialogs/chatty-pp-account-details.c:91
 #: src/ui/chatty-dialog-join-muc.ui:21 src/ui/chatty-info-dialog.ui:44
@@ -254,24 +258,24 @@ msgstr "Вибір файла…"
 msgid "Cancel"
 msgstr "Скасувати"
 
-#: src/chatty-chat-view.c:418 src/purple/chatty-purple-request.c:177
+#: src/chatty-chat-view.c:423 src/purple/chatty-purple-request.c:177
 #: src/dialogs/chatty-pp-chat-info.c:89
 #: src/dialogs/chatty-ma-account-details.c:118
 #: src/dialogs/chatty-pp-account-details.c:90
 msgid "Open"
 msgstr "Відкрити"
 
-#: src/chatty-chat.c:635
+#: src/chatty-chat.c:709
 msgid "Empty room"
 msgstr "Порожня кімната"
 
 #. TRANSLATORS: %s are name/user-id/phone numbers of two users
-#: src/chatty-chat.c:642
+#: src/chatty-chat.c:716
 #, c-format
 msgid "%s and %s"
 msgstr "%s Ñ– %s"
 
-#: src/chatty-chat.c:644
+#: src/chatty-chat.c:718
 #, c-format
 msgid "%s and %u other"
 msgid_plural "%s and %u others"
@@ -342,12 +346,21 @@ msgstr "Вчора %H:%M"
 msgid "%Y-%m-%d"
 msgstr "%d-%m-%Y"
 
+#: src/chatty-header-bar.c:246
+msgid "Archived"
+msgstr "Архівовано"
+
 #. TRANSLATORS: %s is the Device ID
 #: src/chatty-fp-row.c:131
 #, c-format
 msgid "Device ID %s fingerprint:"
 msgstr "Відбиток ідентифікатора пристрою %s:"
 
+#: src/chatty-invite-view.c:72
+#, c-format
+msgid "Do you want to join “%s”"
+msgstr "Хочете долучитися до «%s»"
+
 #: src/chatty-list-row.c:118
 msgid "Owner"
 msgstr "Власник"
@@ -360,7 +373,7 @@ msgstr "Модератор"
 msgid "Member"
 msgstr "Член"
 
-#: src/chatty-manager.c:727
+#: src/chatty-manager.c:733
 #, c-format
 msgid "“%s” is not a valid URI"
 msgstr "«%s» не є коректною адресою"
@@ -369,6 +382,12 @@ msgstr "«%s» не є коректною адресою"
 msgid "Copy"
 msgstr "Копіювати"
 
+#: src/chatty-message.c:233
+msgid "Got an encrypted message, but couldn't decrypt due to missing keys"
+msgstr ""
+"Отримано зашифроване повідомлення, але його не вдалося розшифрувати через "
+"брак ключів"
+
 #: src/chatty-notification.c:181
 msgid "Open Message"
 msgstr "Відкрити повідомлення"
@@ -382,7 +401,7 @@ msgstr "Нове повідомлення від %s"
 msgid "Message Received"
 msgstr "Надійшло повідомлення"
 
-#: src/purple/chatty-purple-notify.c:44 src/matrix/matrix-utils.c:505
+#: src/purple/chatty-purple-notify.c:44
 msgid "Close"
 msgstr "Закрити"
 
@@ -412,11 +431,11 @@ msgstr "Користувач %s додав %s до контактів"
 msgid "Authorize %s?"
 msgstr "Уповноважити %s?"
 
-#: src/purple/chatty-purple.c:264 src/matrix/matrix-utils.c:509
+#: src/purple/chatty-purple.c:264
 msgid "Reject"
 msgstr "Відмовити"
 
-#: src/purple/chatty-purple.c:265 src/matrix/matrix-utils.c:510
+#: src/purple/chatty-purple.c:265
 msgid "Accept"
 msgstr "Прийняти"
 
@@ -425,74 +444,57 @@ msgstr "Прийняти"
 msgid "Add %s to contact list"
 msgstr "Додати %s до списку контактів"
 
-#: src/purple/chatty-purple.c:587
+#: src/purple/chatty-purple.c:586
 msgid "Login failed"
 msgstr "Невдала спроба увійти"
 
-#: src/purple/chatty-purple.c:592
+#: src/purple/chatty-purple.c:591
 msgid "Please check ID and password"
 msgstr "Будь ласка, перевірте ідентифікатор і пароль"
 
-#: src/chatty-secret-store.c:98
-#, c-format
-msgid "Chatty password for \"%s\""
-msgstr "Пароль Chatty до «%s»"
+#: src/chatty-window.c:465
+msgid "You shall no longer be notified for new messages, continue?"
+msgstr "Програма більше не сповіщатиме вас про нові повідомлення. Продовжити?"
 
-#: src/chatty-window.c:399
+#: src/chatty-window.c:515
 #, c-format
 msgid "Delete chat with “%s”"
 msgstr "Вилучити спілкування з «%s»"
 
-#: src/chatty-window.c:400
+#: src/chatty-window.c:516
 msgid "This deletes the conversation history"
 msgstr "У результаті буде вилучено журнал спілкування"
 
-#: src/chatty-window.c:402
+#: src/chatty-window.c:518
 #, c-format
 msgid "Disconnect group chat “%s”"
 msgstr "Від'єднатися від групового спілкування «%s»"
 
-#: src/chatty-window.c:403
+#: src/chatty-window.c:519
 msgid "This removes chat from chats list"
 msgstr "У результаті спілкування буде вилучено зі списку спілкувань"
 
-#: src/chatty-window.c:415
+#: src/chatty-window.c:530
 msgid "Delete"
 msgstr "Вилучити"
 
-#: src/chatty-window.c:493
-msgid "You shall no longer be notified for new messages, continue?"
-msgstr "Програма більше не сповіщатиме вас про нові повідомлення. Продовжити?"
-
-#: src/chatty-window.c:568
-msgid "Archived"
-msgstr "Архівовано"
-
-#: src/chatty-window.c:621
-msgid "An SMS and XMPP messaging client"
-msgstr "Клієнт обміну повідомленнями SMS і XMPP"
-
-#: src/chatty-window.c:628
-msgid "translator-credits"
-msgstr "Юрій Чорноіван <yurchor@ukr.net>, 2020"
-
-#: src/chatty-window.c:978
+#: src/chatty-window.c:886
 msgid "Any Protocol"
 msgstr "Будь-який протокол"
 
-#: src/chatty-window.c:979 src/ui/chatty-settings-dialog.ui:627
+#: src/chatty-window.c:887 src/ui/chatty-settings-dialog.ui:627
 msgid "Matrix"
 msgstr "Matrix"
 
-#: src/chatty-window.c:980
+#: src/chatty-window.c:888
 msgid "SMS/MMS"
 msgstr "SMS/MMS"
 
-#: src/chatty-window.c:983 src/ui/chatty-settings-dialog.ui:615
+#: src/chatty-window.c:891 src/ui/chatty-settings-dialog.ui:615
 msgid "XMPP"
 msgstr "XMPP"
 
-#: src/chatty-window.c:986 src/ui/chatty-settings-dialog.ui:641
+#: src/chatty-window.c:894 src/ui/chatty-settings-dialog.ui:641
 msgid "Telegram"
 msgstr "Telegram"
 
@@ -593,115 +595,93 @@ msgstr "З'єднання…"
 msgid "disconnected"
 msgstr "від'єднано"
 
-#: src/dialogs/chatty-settings-dialog.c:255
-#: src/dialogs/chatty-settings-dialog.c:405
+#: src/dialogs/chatty-settings-dialog.c:251
+#, c-format
+msgid "Failed to verify server: %s"
+msgstr "Не вдалося перевірити сервер: %s"
+
+#: src/dialogs/chatty-settings-dialog.c:253
 msgid "Failed to verify server"
 msgstr "Не вдалося перевірити сервер"
 
-#: src/dialogs/chatty-settings-dialog.c:306
-#: src/dialogs/chatty-settings-dialog.c:385
+#: src/dialogs/chatty-settings-dialog.c:259
 msgid "Couldn't get Home server address"
 msgstr "Не вдалося отримати адресу домашнього сервера."
 
-#: src/dialogs/chatty-settings-dialog.c:510
+#: src/dialogs/chatty-settings-dialog.c:438
 #: src/ui/chatty-ma-account-details.ui:182
 #: src/ui/chatty-pp-account-details.ui:199
 msgid "Delete Account"
 msgstr "Вилучити запис"
 
-#: src/dialogs/chatty-settings-dialog.c:513
+#: src/dialogs/chatty-settings-dialog.c:441
 #, c-format
 msgid "Delete account %s?"
 msgstr "Вилучити запис %s?"
 
-#: src/dialogs/chatty-settings-dialog.c:654
+#: src/dialogs/chatty-settings-dialog.c:598
 msgid "Restart chatty to disable purple"
 msgstr "Перезапустіть chatty, щоб вимкнути purple"
 
-#: src/dialogs/chatty-settings-dialog.c:656
+#: src/dialogs/chatty-settings-dialog.c:600
 #: src/ui/chatty-settings-dialog.ui:512
 msgid "Enable purple plugin"
 msgstr "Увімкнути додаток purple"
 
-#: src/dialogs/chatty-settings-dialog.c:670
+#: src/dialogs/chatty-settings-dialog.c:614
 #: src/ui/chatty-settings-dialog.ui:279
 msgid "SMS and MMS Settings"
 msgstr "Параметри SMS і MMS"
 
-#: src/dialogs/chatty-settings-dialog.c:672
+#: src/dialogs/chatty-settings-dialog.c:616
 #: src/ui/chatty-settings-dialog.ui:298
 msgid "Purple Settings"
 msgstr "Параметри Purple"
 
-#: src/dialogs/chatty-settings-dialog.c:674
+#: src/dialogs/chatty-settings-dialog.c:618
 msgid "New Account"
 msgstr "Новий обліковий запис"
 
-#: src/dialogs/chatty-settings-dialog.c:676
+#: src/dialogs/chatty-settings-dialog.c:620
 #: src/ui/chatty-settings-dialog.ui:364
 msgid "Blocked Contacts"
 msgstr "Заблоковані контакти"
 
-#: src/dialogs/chatty-settings-dialog.c:678 src/ui/chatty-settings-dialog.ui:7
-#: src/ui/chatty-window.ui:30
+#: src/dialogs/chatty-settings-dialog.c:622 src/ui/chatty-header-bar.ui:244
+#: src/ui/chatty-settings-dialog.ui:7
 msgid "Preferences"
 msgstr "Параметри"
 
-#: src/dialogs/chatty-settings-dialog.c:707
+#: src/dialogs/chatty-settings-dialog.c:650
 msgid "Select Protocol"
 msgstr "Виберіть протокол"
 
 #. TRANSLATORS: Only translate 'or'
-#: src/dialogs/chatty-settings-dialog.c:948
+#: src/dialogs/chatty-settings-dialog.c:894
 msgid "@user:matrix.org or user@example.com"
 msgstr "@user:matrix.org або user@example.com"
 
-#: src/dialogs/chatty-settings-dialog.c:1057
+#: src/dialogs/chatty-settings-dialog.c:1003
 msgid "Unblock contact"
 msgstr "Розблокувати контакт"
 
-#: src/matrix/chatty-ma-account.c:296
+#: src/matrix/chatty-ma-account.c:106
 msgid "Incorrect password"
 msgstr "Помилковий пароль"
 
-#: src/matrix/chatty-ma-account.c:299
+#: src/matrix/chatty-ma-account.c:109
 msgid "_OK"
 msgstr "_Гаразд"
 
-#: src/matrix/chatty-ma-account.c:300 src/ui/chatty-dialog-new-chat.ui:74
+#: src/matrix/chatty-ma-account.c:110 src/ui/chatty-dialog-new-chat.ui:74
 #: src/ui/chatty-settings-dialog.ui:47 src/ui/chatty-settings-dialog.ui:108
 msgid "_Cancel"
 msgstr "_Скасувати"
 
-#: src/matrix/chatty-ma-account.c:306
+#: src/matrix/chatty-ma-account.c:117
 #, c-format
-msgid "Please enter password for “%s”"
-msgstr "Будь ласка, введіть пароль до «%s»"
-
-#: src/matrix/matrix-utils.c:474
-#, c-format
-msgid "The certificate for ‘%s’ has unknown CA"
-msgstr "Сертифікат «%s» містить невідомий запис служби сертифікації"
-
-#: src/matrix/matrix-utils.c:476
-#, c-format
-msgid "The certificate for ‘%s’ is self-signed"
-msgstr "Сертифікат «%s» є самопідписаним"
-
-#: src/matrix/matrix-utils.c:480
-#, c-format
-msgid "The certificate for ‘%s’ has expired"
-msgstr "Строк дії сертифіката «%s» вичерпано"
-
-#: src/matrix/matrix-utils.c:484
-#, c-format
-msgid "The certificate for ‘%s’ has been revoked"
-msgstr "Сертифікат «%s» було відкликано"
-
-#: src/matrix/matrix-utils.c:493
-#, c-format
-msgid "Error validating certificate for ‘%s’"
-msgstr "Помилка під час спроби перевірити чинність сертифіката «%s»"
+msgid "Please enter password for “%s”, homeserver: %s"
+msgstr "Будь ласка, введіть пароль до «%s», домашній сервер: %s"
 
 #. TRANSLATORS: Timestamp for minute accuracy, e.g. “2020-08-11 15:27”.
 #. See https://developer.gnome.org/glib/stable/glib-GDateTime.html#g-date-time-format
@@ -759,10 +739,84 @@ msgstr "Додати до контактів"
 msgid "Remove Attachment"
 msgstr "Вилучити долучення"
 
-#: src/ui/chatty-info-dialog.ui:17 src/ui/chatty-window.ui:139
+#: src/ui/chatty-header-bar.ui:24 src/ui/chatty-header-bar.ui:133
+#: src/ui/chatty-settings-dialog.ui:27
+msgid "Back"
+msgstr "Назад"
+
+#: src/ui/chatty-header-bar.ui:45
+msgid "Add Chat"
+msgstr "Додати спілкування"
+
+#: src/ui/chatty-header-bar.ui:65
+msgid "Menu"
+msgstr "Меню"
+
+#: src/ui/chatty-header-bar.ui:87
+msgid "Search"
+msgstr "Пошук"
+
+#: src/ui/chatty-header-bar.ui:198 src/ui/chatty-list-row.ui:151
+msgid "Call"
+msgstr "Виклик"
+
+#: src/ui/chatty-header-bar.ui:236
+msgctxt "show archived chat list when clicked"
+msgid "Archived"
+msgstr "Архівовано"
+
+#: src/ui/chatty-header-bar.ui:251
+msgid "Keyboard _Shortcuts"
+msgstr "_Клавіатурні скорочення"
+
+#: src/ui/chatty-header-bar.ui:258
+msgid "Help"
+msgstr "Довідка"
+
+#: src/ui/chatty-header-bar.ui:266
+msgid "About Chats"
+msgstr "Про «Спілкування»"
+
+#: src/ui/chatty-header-bar.ui:286
+msgid "New Message…"
+msgstr "Нове повідомлення…"
+
+#: src/ui/chatty-header-bar.ui:294
+msgid "New SMS/MMS Message…"
+msgstr "Нове повідомлення SMS/MMS…"
+
+#: src/ui/chatty-header-bar.ui:302
+msgid "New Group Message…"
+msgstr "Нове групове повідомлення…"
+
+#: src/ui/chatty-header-bar.ui:322 src/ui/chatty-info-dialog.ui:17
 msgid "Chat Details"
 msgstr "Подробиці спілкування"
 
+#: src/ui/chatty-header-bar.ui:338
+msgid "Leave Chat"
+msgstr "Полишити спілкування"
+
+#: src/ui/chatty-header-bar.ui:346
+msgid "Block Contact"
+msgstr "Заблокувати контакт"
+
+#: src/ui/chatty-header-bar.ui:354
+msgid "Unblock Contact"
+msgstr "Розблокувати контакт"
+
+#: src/ui/chatty-header-bar.ui:363
+msgid "Archive chat"
+msgstr "Архівувати спілкування"
+
+#: src/ui/chatty-header-bar.ui:371
+msgid "Unarchive chat"
+msgstr "Скасувати архівування"
+
+#: src/ui/chatty-header-bar.ui:380
+msgid "Delete Chat"
+msgstr "Вилучити спілкування"
+
 #: src/ui/chatty-info-dialog.ui:64
 msgid "Apply"
 msgstr "Застосувати"
@@ -771,6 +825,16 @@ msgstr "Застосувати"
 msgid "Invite"
 msgstr "Запросити"
 
+#: src/ui/chatty-invite-view.ui:71
+#| msgid "Accept"
+msgid "_Accept"
+msgstr "При_йняти"
+
+#: src/ui/chatty-invite-view.ui:100
+#| msgid "Reject"
+msgid "_Reject"
+msgstr "Від_!мовити"
+
 #: src/ui/chatty-ma-chat-info.ui:67 src/ui/chatty-ma-account-details.ui:233
 msgid "Matrix ID"
 msgstr "Ід. Matrix"
@@ -837,10 +901,6 @@ msgstr "Типово порожній"
 msgid "Group Members"
 msgstr "Учасники групи"
 
-#: src/ui/chatty-list-row.ui:151 src/ui/chatty-window.ui:433
-msgid "Call"
-msgstr "Виклик"
-
 #: src/ui/chatty-ma-account-details.ui:93
 msgid "Name"
 msgstr "Ім'я"
@@ -881,10 +941,6 @@ msgstr "Пароль"
 msgid "Own Fingerprint"
 msgstr "Власний відбиток"
 
-#: src/ui/chatty-settings-dialog.ui:27
-msgid "Back"
-msgstr "Назад"
-
 #: src/ui/chatty-settings-dialog.ui:60
 msgid "_Add"
 msgstr "_Додати"
@@ -995,59 +1051,6 @@ msgstr "Домашній сервер"
 msgid "Add _new account…"
 msgstr "Додати _новий обліковий запис…"
 
-#: src/ui/chatty-window.ui:17
-msgctxt "show archived chat list when clicked"
-msgid "Archived"
-msgstr "Архівовано"
-
-#: src/ui/chatty-window.ui:41
-msgid "Keyboard _Shortcuts"
-msgstr "_Клавіатурні скорочення"
-
-#: src/ui/chatty-window.ui:48
-msgid "Help"
-msgstr "Довідка"
-
-#: src/ui/chatty-window.ui:57
-msgid "About Chats"
-msgstr "Про «Спілкування»"
-
-#: src/ui/chatty-window.ui:85
-msgid "New Message…"
-msgstr "Нове повідомлення…"
-
-#: src/ui/chatty-window.ui:98
-msgid "New SMS/MMS Message…"
-msgstr "Нове повідомлення SMS/MMS…"
-
-#: src/ui/chatty-window.ui:111
-msgid "New Group Message…"
-msgstr "Нове групове повідомлення…"
-
-#: src/ui/chatty-window.ui:164
-msgid "Leave Chat"
-msgstr "Полишити спілкування"
-
-#: src/ui/chatty-window.ui:177
-msgid "Block Contact"
-msgstr "Заблокувати контакт"
-
-#: src/ui/chatty-window.ui:190
-msgid "Unblock Contact"
-msgstr "Розблокувати контакт"
-
-#: src/ui/chatty-window.ui:203
-msgid "Archive chat"
-msgstr "Архівувати спілкування"
-
-#: src/ui/chatty-window.ui:216
-msgid "Unarchive chat"
-msgstr "Скасувати архівування"
-
-#: src/ui/chatty-window.ui:229
-msgid "Delete Chat"
-msgstr "Вилучити спілкування"
-
 #: src/ui/help-overlay.ui:14
 msgctxt "shortcut window"
 msgid "General"
diff --git a/src/chatty-application.c b/src/chatty-application.c
index b8273ddd3026ad1671dd9af634fe96d7600d6c48..ac47aa1f2090f62ecab82985bc1cadcda681ff15 100644
--- a/src/chatty-application.c
+++ b/src/chatty-application.c
@@ -28,6 +28,8 @@
 # include "version.h"
 #endif
 
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include <cmatrix.h>
 #include <glib/gi18n.h>
 #include <handy.h>
 
@@ -119,6 +121,60 @@ application_open_uri (ChattyApplication *self)
   return G_SOURCE_REMOVE;
 }
 
+static void
+chatty_application_show_about (GSimpleAction *action,
+                               GVariant      *parameter,
+                               gpointer       user_data)
+{
+  ChattyApplication *self = user_data;
+
+  static const gchar *authors[] = {
+    "Adrien Plazas <kekun.plazas@laposte.net>",
+    "Andrea Schäfer <mosibasu@me.com>",
+    "Benedikt Wildenhain <benedikt.wildenhain@hs-bochum.de>",
+    "Chris Talbot (kop316) <chris@talbothome.com>",
+    "Guido Günther <agx@sigxcpu.org>",
+    "Julian Sparber <jsparber@gnome.org>",
+    "Leland Carlye <leland.carlye@protonmail.com>",
+    "Mohammed Sadiq https://www.sadiqpk.org/",
+    "Richard Bayerle (OMEMO Plugin) https://github.com/gkdr/lurch",
+    "Ruslan Marchenko <me@ruff.mobi>",
+    "and more...",
+    NULL
+  };
+
+  static const gchar *artists[] = {
+    "Tobias Bernard <tbernard@gnome.org>",
+    NULL
+  };
+
+  static const gchar *documenters[] = {
+    "Heather Ellsworth <heather.ellsworth@puri.sm>",
+    NULL
+  };
+
+  if (!self->main_window)
+    return;
+
+  /*
+   * “program-name” defaults to g_get_application_name().
+   * Don’t set it explicitly so that there is one less
+   * string to translate.
+   */
+  gtk_show_about_dialog (gtk_application_get_active_window (user_data),
+                         "logo-icon-name", CHATTY_APP_ID,
+                         "version", GIT_VERSION,
+                         "comments", _("An SMS and XMPP messaging client"),
+                         "website", "https://source.puri.sm/Librem5/chatty",
+                         "copyright", "© 2018–2022 Purism SPC",
+                         "license-type", GTK_LICENSE_GPL_3_0,
+                         "authors", authors,
+                         "artists", artists,
+                         "documenters", documenters,
+                         "translator-credits", _("translator-credits"),
+                         NULL);
+}
+
 static void
 chatty_application_show_help (GSimpleAction *action,
 			      GVariant      *parameter,
@@ -273,26 +329,28 @@ chatty_application_command_line (GApplication            *application,
 
   options = g_application_command_line_get_options_dict (command_line);
 
+  if (g_variant_dict_contains (options, "nologin"))
+    chatty_manager_disable_auto_login (chatty_manager_get_default (), TRUE);
+
+#ifdef PURPLE_ENABLED
+  if (g_variant_dict_contains (options, "debug"))
+    chatty_purple_enable_debug ();
+#endif
+
   self->show_window = TRUE;
   if (g_variant_dict_contains (options, "daemon")) {
     /* Hold application only the first time daemon mode is set */
     if (!self->daemon)
       g_application_hold (application);
 
+    chatty_manager_load (self->manager);
+
     self->show_window = FALSE;
     self->daemon = TRUE;
 
     g_debug ("Enable daemon mode");
   }
 
-  if (g_variant_dict_contains (options, "nologin"))
-    chatty_manager_disable_auto_login (chatty_manager_get_default (), TRUE);
-
-#ifdef PURPLE_ENABLED
-  if (g_variant_dict_contains (options, "debug"))
-    chatty_purple_enable_debug ();
-#endif
-
   arguments = g_application_command_line_get_arguments (command_line, &argc);
 
   /* Keep only the last URI, if there are many */
@@ -308,9 +366,11 @@ chatty_application_command_line (GApplication            *application,
 }
 
 static const GActionEntry app_entries[] = {
+  { "about", chatty_application_show_about },
   { "help", chatty_application_show_help, },
   { "open-chat", chatty_application_open_chat, "(ssi)" },
-  { "show-window", chatty_application_show_window } };
+  { "show-window", chatty_application_show_window }
+};
 
 static void
 chatty_application_startup (GApplication *application)
@@ -329,6 +389,7 @@ chatty_application_startup (GApplication *application)
   g_info ("%s %s, git version: %s", PACKAGE_NAME, PACKAGE_VERSION, GIT_VERSION);
 
   hdy_init ();
+  cm_init (TRUE);
 
   g_set_application_name (_("Chats"));
 
@@ -347,7 +408,6 @@ chatty_application_startup (GApplication *application)
   g_signal_connect_object (self->manager, "open-chat",
                            G_CALLBACK (application_open_chat),
                            self, G_CONNECT_SWAPPED);
-  chatty_manager_load (self->manager);
 
   g_signal_connect_object (self, "window-removed",
                            G_CALLBACK (app_window_removed_cb),
@@ -374,6 +434,8 @@ chatty_application_activate (GApplication *application)
 
   g_assert (GTK_IS_APPLICATION (app));
 
+  chatty_manager_load (self->manager);
+
   if (!self->main_window && self->show_window) {
     g_set_weak_pointer (&self->main_window, chatty_window_new (app));
     g_info ("New main window created");
diff --git a/src/chatty-avatar.c b/src/chatty-avatar.c
index 3f21ca165eb54f9f3026a42e84afe7cab302b5d3..f7c54a219fd82ee78dab1a58053e18b89e4af582 100644
--- a/src/chatty-avatar.c
+++ b/src/chatty-avatar.c
@@ -17,6 +17,7 @@
 #include <handy.h>
 
 #include "chatty-chat.h"
+#include "chatty-ma-key-chat.h"
 #include "chatty-mm-chat.h"
 #include "chatty-avatar.h"
 
@@ -56,7 +57,10 @@ avatar_changed_cb (ChattyAvatar *self)
   if (self->item)
     avatar = (GLoadableIcon *)chatty_item_get_avatar (self->item);
 
-  hdy_avatar_set_loadable_icon (HDY_AVATAR (self->avatar), avatar);
+  if (CHATTY_IS_MA_KEY_CHAT (self->item))
+    hdy_avatar_set_icon_name (HDY_AVATAR (self->avatar), "system-lock-screen-symbolic");
+  else
+    hdy_avatar_set_loadable_icon (HDY_AVATAR (self->avatar), avatar);
 }
 
 static void
@@ -66,6 +70,8 @@ item_name_changed_cb (ChattyAvatar *self)
       !chatty_contact_is_dummy (CHATTY_CONTACT (self->item)))
     chatty_avatar_set_title (self, chatty_item_get_name (self->item));
 
+  hdy_avatar_set_show_initials (HDY_AVATAR (self->avatar), !CHATTY_IS_MA_KEY_CHAT (self->item));
+
   if (CHATTY_IS_MM_CHAT (self->item)) {
     gboolean has_name;
 
diff --git a/src/chatty-chat-view.c b/src/chatty-chat-view.c
index ccb69c77765adb793f47196acc5e0a22748bf961..c94c826bbae626fb5ad5f8bfddf5a54ff1e41899 100644
--- a/src/chatty-chat-view.c
+++ b/src/chatty-chat-view.c
@@ -30,7 +30,6 @@ struct _ChattyChatView
   GtkStack    parent_instance;
 
   GtkWidget  *message_view;
-  GtkWidget  *empty_view;
 
   GtkWidget  *message_list;
   GtkWidget  *loading_spinner;
@@ -285,6 +284,12 @@ chat_view_attachment_revealer_notify_cb (ChattyChatView *self)
   has_text = gtk_text_buffer_get_char_count (self->message_input_buffer) > 0;
 
   gtk_widget_set_visible (self->send_message_button, has_files || has_text);
+
+  if (!has_files)
+    {
+      gtk_widget_set_sensitive (self->send_file_button, TRUE);
+      gtk_widget_set_sensitive (self->message_input, TRUE);
+    }
 }
 
 static void
@@ -429,6 +434,8 @@ chat_view_show_file_chooser (ChattyChatView *self)
 
     /* Currently multiple files are allowed only for MMS chats */
     gtk_widget_set_sensitive (self->send_file_button, CHATTY_IS_MM_CHAT (self->chat));
+    /* Files with message content is supported only by MMS chats */
+    gtk_widget_set_sensitive (self->message_input, CHATTY_IS_MM_CHAT (self->chat));
   }
 
   gtk_widget_destroy (dialog);
@@ -445,8 +452,7 @@ chat_view_send_file_button_clicked_cb (ChattyChatView *self,
   if (CHATTY_IS_MM_CHAT (self->chat)) {
     chat_view_show_file_chooser (self);
   } else if (CHATTY_IS_MA_CHAT (self->chat)) {
-    /* TODO */
-
+    chat_view_show_file_chooser (self);
   } else {
 #ifdef PURPLE_ENABLED
     chatty_pp_chat_show_file_upload (CHATTY_PP_CHAT (self->chat));
@@ -525,7 +531,19 @@ chat_view_send_message_button_clicked_cb (ChattyChatView *self)
   gtk_text_buffer_delete (self->message_input_buffer, &start, &end);
 
   if (self->draft_message) {
-    chatty_message_set_text (self->draft_message, "");
+    g_autofree char *uid = NULL;
+
+    g_clear_object (&self->draft_message);
+
+    uid = g_uuid_string_random ();
+    /* chatty-history expects the content of the message to not change.
+     * So instead of changing the content and resaving, create a new one
+     * with empty content to avoid a possible crash as the history thread
+     * may read the freed memory content otherwise
+     */
+    self->draft_message = chatty_message_new (NULL, "", uid, time (NULL),
+                                              CHATTY_MESSAGE_TEXT,
+                                              CHATTY_DIRECTION_OUT, CHATTY_STATUS_DRAFT);
     chatty_history_add_message (self->history, self->chat, self->draft_message);
   }
 
@@ -881,7 +899,6 @@ chatty_chat_view_class_init (ChattyChatViewClass *klass)
                                                "ui/chatty-chat-view.ui");
 
   gtk_widget_class_bind_template_child (widget_class, ChattyChatView, message_view);
-  gtk_widget_class_bind_template_child (widget_class, ChattyChatView, empty_view);
 
   gtk_widget_class_bind_template_child (widget_class, ChattyChatView, scroll_down_button);
   gtk_widget_class_bind_template_child (widget_class, ChattyChatView, message_list);
@@ -921,7 +938,6 @@ chatty_chat_view_init (ChattyChatView *self)
 
   gtk_widget_init_template (GTK_WIDGET (self));
   gtk_list_box_set_placeholder (GTK_LIST_BOX (self->message_list), self->no_message_status);
-  gtk_stack_set_visible_child (GTK_STACK (self), self->empty_view);
 
   g_signal_connect_after (G_OBJECT (self), "file-requested",
                           G_CALLBACK (chat_view_file_requested_cb), self);
@@ -1007,11 +1023,6 @@ chatty_chat_view_set_chat (ChattyChatView *self,
                                               CHATTY_DIRECTION_OUT, CHATTY_STATUS_DRAFT);
   }
 
-  if (chat)
-    gtk_stack_set_visible_child (GTK_STACK (self), self->message_view);
-  else
-    gtk_stack_set_visible_child (GTK_STACK (self), self->empty_view);
-
   if (!chat) {
     gtk_list_box_bind_model (GTK_LIST_BOX (self->message_list),
                              NULL, NULL, NULL, NULL);
diff --git a/src/chatty-chat.c b/src/chatty-chat.c
index 2365677c938dc30efec142054fc95235fa9da1ba..e21d7dc03fbbf9d4b2db0deb8772512801482867 100644
--- a/src/chatty-chat.c
+++ b/src/chatty-chat.c
@@ -52,6 +52,7 @@ enum {
   PROP_0,
   PROP_ENCRYPT,
   PROP_BUDDY_TYPING,
+  PROP_CHAT_STATE,
   PROP_LOADING_HISTORY,
   N_PROPS
 };
@@ -89,6 +90,14 @@ chatty_chat_real_is_im (ChattyChat *self)
   return priv->is_im;
 }
 
+static ChattyChatState
+chatty_chat_real_get_chat_state (ChattyChat *self)
+{
+  g_assert (CHATTY_IS_CHAT (self));
+
+  return CHATTY_CHAT_JOINED;
+}
+
 static gboolean
 chatty_chat_real_has_file_upload (ChattyChat *self)
 {
@@ -257,6 +266,55 @@ chatty_chat_real_set_encryption_async (ChattyChat          *self,
                            "Setting encryption not supported");
 }
 
+static void
+chatty_chat_real_accept_invite_async (ChattyChat          *self,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_assert (CHATTY_IS_CHAT (self));
+
+  g_task_report_new_error (self, callback, user_data,
+                           chatty_chat_real_accept_invite_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Accept invite not supported");
+}
+
+static gboolean
+chatty_chat_real_accept_invite_finish (ChattyChat   *self,
+                                        GAsyncResult *result,
+                                        GError       **error)
+{
+  g_assert (CHATTY_IS_CHAT (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+chatty_chat_real_reject_invite_async (ChattyChat          *self,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_assert (CHATTY_IS_CHAT (self));
+
+  g_task_report_new_error (self, callback, user_data,
+                           chatty_chat_real_reject_invite_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Reject invite not supported");
+}
+
+static gboolean
+chatty_chat_real_reject_invite_finish (ChattyChat   *self,
+                                       GAsyncResult *result,
+                                       GError       **error)
+{
+  g_assert (CHATTY_IS_CHAT (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
 
 static gboolean
 chatty_chat_real_set_encryption_finish (ChattyChat   *self,
@@ -391,6 +449,10 @@ chatty_chat_get_property (GObject    *object,
       g_value_set_boolean (value, chatty_chat_get_buddy_typing (self));
       break;
 
+    case PROP_CHAT_STATE:
+      g_value_set_boolean (value, chatty_chat_get_chat_state (self));
+      break;
+
     case PROP_LOADING_HISTORY:
       g_value_set_boolean (value, chatty_chat_is_loading_history (self));
       break;
@@ -453,6 +515,7 @@ chatty_chat_class_init (ChattyChatClass *klass)
 
   klass->set_data = chatty_chat_real_set_data;
   klass->is_im = chatty_chat_real_is_im;
+  klass->get_chat_state = chatty_chat_real_get_chat_state;
   klass->has_file_upload = chatty_chat_real_has_file_upload;
   klass->get_chat_name = chatty_chat_real_get_chat_name;
   klass->get_account = chatty_chat_real_get_account;
@@ -472,6 +535,10 @@ chatty_chat_class_init (ChattyChatClass *klass)
   klass->get_encryption = chatty_chat_real_get_encryption;
   klass->set_encryption = chatty_chat_real_set_encryption;
   klass->set_encryption_async = chatty_chat_real_set_encryption_async;
+  klass->accept_invite_async = chatty_chat_real_accept_invite_async;
+  klass->accept_invite_finish = chatty_chat_real_accept_invite_finish;
+  klass->reject_invite_async = chatty_chat_real_reject_invite_async;
+  klass->reject_invite_finish = chatty_chat_real_reject_invite_finish;
   klass->set_encryption_finish = chatty_chat_real_set_encryption_finish;
   klass->get_buddy_typing = chatty_chat_real_get_buddy_typing;
   klass->set_typing = chatty_chat_real_set_typing;
@@ -486,6 +553,13 @@ chatty_chat_class_init (ChattyChatClass *klass)
                           FALSE,
                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
 
+  properties[PROP_CHAT_STATE] =
+    g_param_spec_boolean ("chat-state",
+                          "Chat state",
+                          "Whether the chat is having invite state or not",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
   properties[PROP_BUDDY_TYPING] =
     g_param_spec_boolean ("buddy-typing",
                           "Buddy typing",
@@ -646,6 +720,14 @@ chatty_chat_generate_name (ChattyChat *self,
                           name_a ?: "", count - 1);
 }
 
+ChattyChatState
+chatty_chat_get_chat_state (ChattyChat *self)
+{
+  g_return_val_if_fail (CHATTY_IS_CHAT (self), CHATTY_CHAT_UNKNOWN);
+
+  return CHATTY_CHAT_GET_CLASS (self)->get_chat_state (self);
+}
+
 gboolean
 chatty_chat_has_file_upload (ChattyChat *self)
 {
@@ -920,6 +1002,48 @@ chatty_chat_set_typing (ChattyChat *self,
   CHATTY_CHAT_GET_CLASS (self)->set_typing (self, !!is_typing);
 }
 
+void
+chatty_chat_accept_invite_async (ChattyChat          *self,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_return_if_fail (CHATTY_IS_CHAT (self));
+
+  CHATTY_CHAT_GET_CLASS (self)->accept_invite_async (self, callback, user_data);
+}
+
+gboolean
+chatty_chat_accept_invite_finish (ChattyChat    *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_return_val_if_fail (CHATTY_IS_CHAT (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return CHATTY_CHAT_GET_CLASS (self)->accept_invite_finish (self, result, error);
+}
+
+void
+chatty_chat_reject_invite_async (ChattyChat          *self,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_return_if_fail (CHATTY_IS_CHAT (self));
+
+  CHATTY_CHAT_GET_CLASS (self)->reject_invite_async (self, callback, user_data);
+}
+
+gboolean
+chatty_chat_reject_invite_finish (ChattyChat    *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_return_val_if_fail (CHATTY_IS_CHAT (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return CHATTY_CHAT_GET_CLASS (self)->reject_invite_finish (self, result, error);
+}
+
 gboolean
 chatty_chat_get_buddy_typing (ChattyChat *self)
 {
diff --git a/src/chatty-chat.h b/src/chatty-chat.h
index 2ac607e5d8b906805eb07e45dad9cc0a0be4ae19..7787ba5346bda68232cb3364431a2fe7a573a578 100644
--- a/src/chatty-chat.h
+++ b/src/chatty-chat.h
@@ -33,6 +33,7 @@ struct _ChattyChatClass
                                            gpointer    history_db);
   gboolean          (*is_im)              (ChattyChat *self);
   gboolean          (*has_file_upload)    (ChattyChat *self);
+  ChattyChatState   (*get_chat_state)     (ChattyChat *self);
   const char       *(*get_chat_name)      (ChattyChat *self);
   ChattyAccount    *(*get_account)        (ChattyChat *self);
   GListModel       *(*get_messages)       (ChattyChat *self);
@@ -71,6 +72,18 @@ struct _ChattyChatClass
   gboolean          (*set_encryption_finish) (ChattyChat    *self,
                                               GAsyncResult  *result,
                                               GError       **error);
+  void              (*accept_invite_async)   (ChattyChat           *self,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+  gboolean          (*accept_invite_finish)  (ChattyChat           *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+  void              (*reject_invite_async)   (ChattyChat           *self,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+  gboolean          (*reject_invite_finish)  (ChattyChat           *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
   gboolean          (*get_buddy_typing)   (ChattyChat *self);
   void              (*set_typing)         (ChattyChat *self,
                                            gboolean    is_typing);
@@ -96,6 +109,7 @@ void                chatty_chat_set_data           (ChattyChat *self,
 gboolean            chatty_chat_is_im              (ChattyChat *self);
 char               *chatty_chat_generate_name      (ChattyChat *self,
                                                     GListModel *members);
+ChattyChatState     chatty_chat_get_chat_state     (ChattyChat *self);
 gboolean            chatty_chat_has_file_upload    (ChattyChat *self);
 const char         *chatty_chat_get_chat_name      (ChattyChat *self);
 ChattyAccount      *chatty_chat_get_account        (ChattyChat *self);
@@ -136,6 +150,19 @@ void                chatty_chat_set_encryption_async (ChattyChat     *self,
 gboolean            chatty_chat_set_encryption_finish (ChattyChat    *self,
                                                        GAsyncResult  *result,
                                                        GError       **error);
+
+void                chatty_chat_accept_invite_async   (ChattyChat          *self,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+gboolean            chatty_chat_accept_invite_finish  (ChattyChat           *self,
+                                                       GAsyncResult         *result,
+                                                       GError              **error);
+void                chatty_chat_reject_invite_async   (ChattyChat          *self,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+gboolean            chatty_chat_reject_invite_finish  (ChattyChat           *self,
+                                                       GAsyncResult         *result,
+                                                       GError              **error);
 gboolean            chatty_chat_get_buddy_typing   (ChattyChat *self);
 void                chatty_chat_set_typing         (ChattyChat *self,
                                                     gboolean    is_typing);
diff --git a/src/chatty-enums.h b/src/chatty-enums.h
index ea029fbc0b905bf244e1c95a0bff4b202a31ce76..e51853ea18b6c195b9f520c2535563d07f00bcb4 100644
--- a/src/chatty-enums.h
+++ b/src/chatty-enums.h
@@ -153,3 +153,12 @@ typedef enum
   CHATTY_ITEM_ARCHIVED,
   CHATTY_ITEM_BLOCKED,
 } ChattyItemState;
+
+typedef enum
+{
+  CHATTY_CHAT_UNKNOWN,
+  CHATTY_CHAT_VERIFICATION,
+  CHATTY_CHAT_INVITED,
+  CHATTY_CHAT_JOINED,
+  CHATTY_CHAT_LEFT
+} ChattyChatState;
diff --git a/src/chatty-header-bar.c b/src/chatty-header-bar.c
new file mode 100644
index 0000000000000000000000000000000000000000..31b380494c30eb69a00e82e950715e06c548ec95
--- /dev/null
+++ b/src/chatty-header-bar.c
@@ -0,0 +1,353 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-header-bar.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "chatty-header-bar"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib/gi18n.h>
+
+#include "chatty-avatar.h"
+#include "chatty-manager.h"
+#include "chatty-mm-account.h"
+#include "matrix/chatty-ma-chat.h"
+#include "mm/chatty-mm-chat.h"
+#include "chatty-header-bar.h"
+
+struct _ChattyHeaderBar
+{
+  GtkBox      parent_instance;
+
+  GtkWidget  *leaflet;
+
+  GtkWidget  *main_header_bar;
+  GtkWidget  *back_button;
+  GtkWidget  *add_chat_button;
+  GtkWidget  *search_button;
+
+  GtkWidget  *content_header_bar;
+  GtkWidget  *content_avatar;
+  GtkWidget  *content_title;
+  GtkWidget  *content_menu_button;
+  GtkWidget  *call_button;
+
+  GtkWidget  *sidebar_group;
+  GtkWidget  *content_group;
+  GtkWidget  *header_group;
+
+  GtkWidget  *new_chat_button;
+  GtkWidget  *new_sms_mms_button;
+  GtkWidget  *new_group_chat_button;
+
+  GtkWidget  *leave_button;
+  GtkWidget  *block_button;
+  GtkWidget  *unblock_button;
+  GtkWidget  *archive_button;
+  GtkWidget  *unarchive_button;
+  GtkWidget  *delete_button;
+
+  GBinding   *title_binding;
+
+  ChattyItem *item;
+
+  gulong      content_handler;
+};
+
+G_DEFINE_TYPE (ChattyHeaderBar, chatty_header_bar, GTK_TYPE_BOX)
+
+enum {
+  BACK_CLICKED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static void
+header_bar_update_item_state_button (ChattyHeaderBar *self,
+                                     ChattyItem      *item)
+{
+  ChattyItemState state;
+
+  gtk_widget_hide (self->block_button);
+  gtk_widget_hide (self->unblock_button);
+  gtk_widget_hide (self->archive_button);
+  gtk_widget_hide (self->unarchive_button);
+
+  if (!item || !CHATTY_IS_MM_CHAT (item))
+    return;
+
+  state = chatty_item_get_state (item);
+
+  if (state == CHATTY_ITEM_VISIBLE) {
+    gtk_widget_show (self->block_button);
+    gtk_widget_show (self->archive_button);
+  } else if (state == CHATTY_ITEM_ARCHIVED) {
+    gtk_widget_show (self->unarchive_button);
+  } else if (state == CHATTY_ITEM_BLOCKED) {
+    gtk_widget_show (self->unblock_button);
+  }
+}
+
+static void
+header_bar_chat_changed_cb (ChattyHeaderBar *self,
+                            ChattyItem      *item)
+{
+  GListModel *users;
+
+  users = chatty_chat_get_users (CHATTY_CHAT (item));
+
+  /* allow changing state only for 1:1 SMS/MMS chats  */
+  if (item == self->item &&
+      CHATTY_IS_MM_CHAT (item) &&
+      g_list_model_get_n_items (users) == 1)
+    header_bar_update_item_state_button (self, item);
+}
+
+static void
+header_bar_active_protocols_changed_cb (ChattyHeaderBar *self)
+{
+  ChattyManager *manager;
+  ChattyAccount *mm_account;
+  ChattyProtocol protocols;
+  gboolean has_mms, has_sms, has_im;
+
+  g_assert (CHATTY_IS_HEADER_BAR (self));
+
+  manager = chatty_manager_get_default ();
+  mm_account = chatty_manager_get_mm_account (manager);
+  protocols = chatty_manager_get_active_protocols (manager);
+  has_mms = chatty_mm_account_has_mms_feature (CHATTY_MM_ACCOUNT (mm_account));
+  has_sms = !!(protocols & CHATTY_PROTOCOL_MMS_SMS);
+  has_im  = !!(protocols & ~CHATTY_PROTOCOL_MMS_SMS);
+
+  gtk_widget_set_sensitive (self->add_chat_button, has_sms || has_im);
+  gtk_widget_set_sensitive (self->new_group_chat_button, has_im);
+  gtk_widget_set_visible (self->new_sms_mms_button, has_mms && has_sms);
+}
+
+static void
+header_back_clicked_cb (ChattyHeaderBar *self)
+{
+  g_assert (CHATTY_IS_HEADER_BAR (self));
+
+  g_signal_emit (self, signals[BACK_CLICKED], 0);
+}
+
+static void
+chatty_header_bar_map (GtkWidget *widget)
+{
+  ChattyHeaderBar *self = (ChattyHeaderBar *)widget;
+  ChattyManager *manager;
+
+  manager = chatty_manager_get_default ();
+  g_signal_connect_object (manager, "notify::active-protocols",
+                           G_CALLBACK (header_bar_active_protocols_changed_cb), self,
+                           G_CONNECT_SWAPPED);
+
+  header_bar_active_protocols_changed_cb (self);
+
+  GTK_WIDGET_CLASS (chatty_header_bar_parent_class)->map (widget);
+}
+
+static void
+chatty_header_bar_dispose (GObject *object)
+{
+  ChattyHeaderBar *self = (ChattyHeaderBar *)object;
+
+  g_clear_object (&self->item);
+
+  G_OBJECT_CLASS (chatty_header_bar_parent_class)->dispose (object);
+}
+
+static void
+chatty_header_bar_class_init (ChattyHeaderBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = chatty_header_bar_dispose;
+
+  widget_class->map = chatty_header_bar_map;
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/Chatty/"
+                                               "ui/chatty-header-bar.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, leaflet);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, main_header_bar);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, back_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, add_chat_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, search_button);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_header_bar);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_avatar);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_menu_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, call_button);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, sidebar_group);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, content_group);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, header_group);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, new_chat_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, new_sms_mms_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, new_group_chat_button);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, leave_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, block_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, unblock_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, archive_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, unarchive_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyHeaderBar, delete_button);
+
+  gtk_widget_class_bind_template_callback (widget_class, header_back_clicked_cb);
+
+  signals [BACK_CLICKED] =
+    g_signal_new ("back-clicked",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+chatty_header_bar_init (ChattyHeaderBar *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+chatty_header_bar_set_can_search (ChattyHeaderBar *self,
+                                  gboolean         can_search)
+{
+  g_return_if_fail (CHATTY_IS_HEADER_BAR (self));
+
+  gtk_widget_set_visible (self->search_button, can_search);
+}
+
+void
+chatty_header_bar_show_archived (ChattyHeaderBar *self,
+                                 gboolean         show_archived)
+{
+  g_return_if_fail (CHATTY_IS_HEADER_BAR (self));
+
+  if (show_archived) {
+    hdy_header_bar_set_title (HDY_HEADER_BAR (self->main_header_bar), _("Archived"));
+    gtk_widget_show (self->back_button);
+    gtk_widget_hide (self->add_chat_button);
+  } else {
+    hdy_header_bar_set_title (HDY_HEADER_BAR (self->main_header_bar), _("Chats"));
+    gtk_widget_hide (self->back_button);
+    gtk_widget_show (self->add_chat_button);
+  }
+}
+
+void
+chatty_header_bar_set_item (ChattyHeaderBar *self,
+                            ChattyItem      *item)
+{
+  g_return_if_fail (CHATTY_IS_HEADER_BAR (self));
+  g_return_if_fail (!item || CHATTY_IS_ITEM (item));
+
+  if (self->item == item)
+    return;
+
+  if (self->item)
+    g_clear_signal_handler (&self->content_handler, self->item);
+
+  g_set_object (&self->item, item);
+
+  g_clear_object (&self->title_binding);
+  gtk_label_set_label (GTK_LABEL (self->content_title), "");
+  gtk_widget_set_visible (self->content_menu_button, !!item);
+
+  header_bar_update_item_state_button (self, item);
+  gtk_widget_set_visible (self->content_avatar, !!item);
+  gtk_widget_hide (self->call_button);
+
+  if (item) {
+    gtk_widget_set_visible (self->leave_button, !CHATTY_IS_MM_CHAT (item));
+    /* We can't delete MaChat */
+    gtk_widget_set_visible (self->delete_button, !CHATTY_IS_MA_CHAT (item));
+
+    if (CHATTY_IS_MM_CHAT (item)) {
+      GListModel *users;
+      const char *name;
+
+      users = chatty_chat_get_users (CHATTY_CHAT (item));
+      name = chatty_chat_get_chat_name (CHATTY_CHAT (item));
+
+      /* allow changing state only for 1:1 SMS/MMS chats  */
+      if (g_list_model_get_n_items (users) == 1)
+        header_bar_update_item_state_button (self, item);
+
+      if (g_list_model_get_n_items (users) == 1 &&
+          chatty_utils_username_is_valid (name, CHATTY_PROTOCOL_MMS_SMS)) {
+        g_autoptr(ChattyMmBuddy) buddy = NULL;
+        g_autoptr(GAppInfo) app_info = NULL;
+
+        app_info = g_app_info_get_default_for_uri_scheme ("tel");
+        buddy = g_list_model_get_item (users, 0);
+
+        if (app_info)
+          gtk_widget_show (self->call_button);
+      }
+    }
+
+    chatty_avatar_set_item (CHATTY_AVATAR (self->content_avatar), item);
+    self->title_binding = g_object_bind_property (item, "name",
+                                                  self->content_title, "label",
+                                                  G_BINDING_SYNC_CREATE);
+
+    if (CHATTY_IS_CHAT (item))
+      self->content_handler = g_signal_connect_object (item, "changed",
+                                                       G_CALLBACK (header_bar_chat_changed_cb),
+                                                       self, G_CONNECT_SWAPPED);
+  }
+}
+
+void
+chatty_header_bar_set_content_box (ChattyHeaderBar *self,
+                                   GtkWidget       *widget)
+{
+  GtkWidget *child;
+
+  g_return_if_fail (CHATTY_IS_HEADER_BAR (self));
+  g_return_if_fail (HDY_IS_LEAFLET (widget));
+
+  g_object_bind_property (widget, "visible-child-name",
+                          self->leaflet, "visible-child-name",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (widget, "folded",
+                          self->header_group, "decorate-all",
+                          G_BINDING_SYNC_CREATE);
+
+  child = hdy_leaflet_get_child_by_name (HDY_LEAFLET (widget), "sidebar");
+  gtk_size_group_add_widget (GTK_SIZE_GROUP (self->sidebar_group), child);
+
+  child = hdy_leaflet_get_child_by_name (HDY_LEAFLET (widget), "content");
+  gtk_size_group_add_widget (GTK_SIZE_GROUP (self->content_group), child);
+}
+
+void
+chatty_header_bar_set_search_bar (ChattyHeaderBar *self,
+                                  GtkWidget       *widget)
+{
+  g_return_if_fail (CHATTY_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_WIDGET (widget));
+
+  g_object_bind_property (widget, "search-mode-enabled",
+                          self->search_button, "active",
+                          G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL);
+}
diff --git a/src/chatty-header-bar.h b/src/chatty-header-bar.h
new file mode 100644
index 0000000000000000000000000000000000000000..93cf6c703a0e05a38fa2426f41e3fdb8f82d9250
--- /dev/null
+++ b/src/chatty-header-bar.h
@@ -0,0 +1,35 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-header-bar.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "chatty-chat.h"
+
+G_BEGIN_DECLS
+
+#define CHATTY_TYPE_HEADER_BAR (chatty_header_bar_get_type ())
+
+G_DECLARE_FINAL_TYPE (ChattyHeaderBar, chatty_header_bar, CHATTY, HEADER_BAR, GtkBox)
+
+void        chatty_header_bar_set_can_search   (ChattyHeaderBar *self,
+                                                gboolean         can_search);
+void        chatty_header_bar_show_archived    (ChattyHeaderBar *self,
+                                                gboolean         show_archived);
+void        chatty_header_bar_set_item         (ChattyHeaderBar *self,
+                                                ChattyItem      *item);
+void        chatty_header_bar_set_content_box  (ChattyHeaderBar *self,
+                                                GtkWidget       *widget);
+void        chatty_header_bar_set_search_bar   (ChattyHeaderBar *self,
+                                                GtkWidget       *widget);
+
+G_END_DECLS
diff --git a/src/chatty-history.c b/src/chatty-history.c
index 9a75ae9c465c6d0d91fe7fbd0eb6bf8ba636a191..278eecfac87e6221fa4ee936cb01ed7234fc0359 100644
--- a/src/chatty-history.c
+++ b/src/chatty-history.c
@@ -494,8 +494,7 @@ chatty_history_create_schema (ChattyHistory *self,
   /* XXX: SELECT * FROM files sounds better, WHERE file.id != x feels better too.
    * So what to name? file or files?
    */
-  sql = "BEGIN TRANSACTION;"
-
+  sql =
     "CREATE TABLE IF NOT EXISTS mime_type ("
     "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
     "name TEXT NOT NULL UNIQUE);"
@@ -620,7 +619,7 @@ chatty_history_create_schema (ChattyHistory *self,
     "FROM users "
     "WHERE users.username='"MM_NUMBER"';"
 
-    "COMMIT;";
+    "PRAGMA user_version = " STRING (HISTORY_VERSION) ";";
 
   status = sqlite3_exec (self->db, sql, NULL, NULL, &error);
 
@@ -640,36 +639,6 @@ chatty_history_create_schema (ChattyHistory *self,
   return FALSE;
 }
 
-static gboolean
-chatty_history_update_version (ChattyHistory *self,
-                               GTask         *task)
-{
-  char *error = NULL;
-  const char *sql;
-  int status;
-
-  g_assert (CHATTY_IS_HISTORY (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  sql = "PRAGMA user_version = " STRING (HISTORY_VERSION) ";";
-
-  status = sqlite3_exec (self->db, sql, NULL, NULL, &error);
-
-  if (status == SQLITE_OK)
-    return TRUE;
-
-  g_task_return_new_error (task,
-                           G_IO_ERROR,
-                           G_IO_ERROR_FAILED,
-                           "Couldn't set db version. errno: %d, desc: %s. %s",
-                           status, sqlite3_errmsg (self->db), error);
-  sqlite3_free (error);
-
-  return FALSE;
-}
-
 static int
 insert_or_ignore_user (ChattyHistory  *self,
                        ChattyProtocol  protocol,
@@ -1076,13 +1045,8 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
     return FALSE;
 
   status = sqlite3_exec (self->db,
-
-                         "BEGIN TRANSACTION;"
                          "UPDATE chatty_im SET account='"MM_NUMBER"' "
                          "WHERE chatty_im.account='SMS';"
-                         "COMMIT;"
-
-                         "BEGIN TRANSACTION;"
 
                          /*** Users ***/
                          /* XMPP IM accounts */
@@ -1343,13 +1307,13 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                          "AND chatty_chat.who IS NULL or chatty_chat.who GLOB '@?*:?*' "
                          "ORDER BY timestamp ASC, chatty_chat.id ASC;"
 
-                         "COMMIT;",
+                         "PRAGMA user_version = 4;",
                          NULL, NULL, &error);
 
   if (!e_phone_number_is_supported ())
     g_debug ("Not compiled with libphonenumber");
 
-  if (status == SQLITE_OK) {
+  {
     sqlite3_stmt *stmt;
 
     /* Get all numbers in international format */
@@ -1358,8 +1322,6 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                                  "SELECT DISTINCT account,who FROM chatty_im "
                                  "WHERE account GLOB '+[0-9]*[^@]*[0-9]';",
                                  -1, &stmt, NULL);
-    if (status == SQLITE_OK)
-      status = sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, &error);
 
     while (sqlite3_step (stmt) == SQLITE_ROW) {
       g_autofree char *account_number = NULL;
@@ -1429,13 +1391,10 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                   "AND u.type="STRING(CHATTY_ID_PHONE_VALUE) ";",
                   NULL, NULL, &error);
 
-    if (status == SQLITE_DONE || status == SQLITE_OK)
-      status = sqlite3_exec (self->db, "COMMIT;", NULL, NULL, &error);
-
     sqlite3_finalize (stmt);
   }
 
-  if (status == SQLITE_OK) {
+  {
     sqlite3_stmt *stmt;
     status = sqlite3_prepare_v2 (self->db,
                                  /* Telegram Chats */
@@ -1443,9 +1402,6 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                                  "WHERE account GLOB '+[0-9]*[^@:.]*[0-9]';",
                                  -1, &stmt, NULL);
 
-    if (status == SQLITE_OK)
-      status = sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, &error);
-
     while (sqlite3_step (stmt) == SQLITE_ROW) {
       g_autofree char *account_number = NULL;
       g_autofree char *sender_number = NULL;
@@ -1566,13 +1522,10 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                   "AND u.type="STRING(CHATTY_ID_PHONE_VALUE) ";",
                   NULL, NULL, &error);
 
-    if (status == SQLITE_DONE || status == SQLITE_OK)
-      status = sqlite3_exec (self->db, "COMMIT;", NULL, NULL, &error);
-
     sqlite3_finalize (stmt);
   }
 
-  if (status == SQLITE_OK) {
+  {
     sqlite3_stmt *stmt;
     status = sqlite3_prepare_v2 (self->db,
                                  /* SMS users with phone numbers sorted */
@@ -1586,9 +1539,6 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                                  "WHERE chatty_im.account='"MM_NUMBER"' ORDER BY chatty_im.id ASC;",
                                  -1, &stmt, NULL);
 
-    if (status == SQLITE_OK)
-      status = sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, &error);
-
     while (sqlite3_step (stmt) == SQLITE_ROW) {
       g_autofree char *sender_number = NULL;
       sqlite3_stmt *insert_stmt = NULL;
@@ -1638,26 +1588,17 @@ chatty_history_migrate_db_to_v1_to_v3 (ChattyHistory *self,
                   "AND u.type="STRING(CHATTY_ID_PHONE_VALUE) ";",
                   NULL, NULL, &error);
 
-    if (status == SQLITE_DONE || status == SQLITE_OK)
-      status = sqlite3_exec (self->db, "COMMIT;", NULL, NULL, &error);
-
     sqlite3_finalize (stmt);
   }
 
   /* Drop old tables */
-  if (status == SQLITE_OK)
-    status = sqlite3_exec (self->db,
-                           "BEGIN TRANSACTION;"
-                           "DROP TABLE chatty_chat;"
-                           "DROP TABLE chatty_im;"
-                           "COMMIT;", NULL, NULL, &error);
-
-  if (status == SQLITE_OK || status == SQLITE_DONE) {
-    /* Update user_version pragma */
-    if (!chatty_history_update_version (self, task))
-      return FALSE;
+  status = sqlite3_exec (self->db,
+                         "DROP TABLE chatty_chat;"
+                         "DROP TABLE chatty_im;",
+                         NULL, NULL, &error);
+
+  if (status == SQLITE_OK || status == SQLITE_DONE)
     return TRUE;
-  }
 
   g_task_return_new_error (task,
                            G_IO_ERROR,
@@ -1684,8 +1625,8 @@ chatty_history_migrate_db_to_v2 (ChattyHistory *self,
   chatty_history_backup (self);
 
   status = sqlite3_exec (self->db,
-                         "BEGIN TRANSACTION;"
-                         "PRAGMA foreign_keys=OFF;"
+                         /* "BEGIN TRANSACTION;" */
+                         /* "PRAGMA foreign_keys=OFF;" */
 
                          "DROP TABLE IF EXISTS media;"
                          "DROP TABLE IF EXISTS files;"
@@ -1755,15 +1696,12 @@ chatty_history_migrate_db_to_v2 (ChattyHistory *self,
                          "DROP TABLE IF EXISTS threads;"
                          "ALTER TABLE temp_threads RENAME TO threads;"
 
-                         "COMMIT;",
+                         "PRAGMA user_version = 2;",
+                         /* "COMMIT;", */
                          NULL, NULL, &error);
 
-  if (status == SQLITE_OK || status == SQLITE_DONE) {
-    /* Update user_version pragma */
-    if (!chatty_history_update_version (self, task))
-      return FALSE;
+  if (status == SQLITE_OK || status == SQLITE_DONE)
     return TRUE;
-  }
 
   g_task_return_new_error (task,
                            G_IO_ERROR,
@@ -1790,21 +1728,14 @@ chatty_history_migrate_db_to_v3 (ChattyHistory *self,
   chatty_history_backup (self);
 
   status = sqlite3_exec (self->db,
-                         "BEGIN TRANSACTION;"
-                         "PRAGMA foreign_keys=OFF;"
-
                          "ALTER TABLE threads ADD COLUMN visibility INT NOT NULL DEFAULT "
                          STRING(THREAD_VISIBILITY_VISIBLE) ";"
 
-                         "COMMIT;",
+                         "PRAGMA user_version = 3;",
                          NULL, NULL, &error);
 
-  if (status == SQLITE_OK || status == SQLITE_DONE) {
-    /* Update user_version pragma */
-    if (!chatty_history_update_version (self, task))
-      return FALSE;
+  if (status == SQLITE_OK || status == SQLITE_DONE)
     return TRUE;
-  }
 
   g_task_return_new_error (task,
                            G_IO_ERROR,
@@ -1831,9 +1762,6 @@ chatty_history_migrate_db_to_v4 (ChattyHistory *self,
   chatty_history_backup (self);
 
   status = sqlite3_exec (self->db,
-                         "BEGIN TRANSACTION;"
-                         "PRAGMA foreign_keys=OFF;"
-
                          "ALTER TABLE threads ADD COLUMN notification INTEGER NOT NULL DEFAULT 1;"
                          "ALTER TABLE messages ADD COLUMN subject TEXT;"
 
@@ -1928,15 +1856,11 @@ chatty_history_migrate_db_to_v4 (ChattyHistory *self,
                          "DROP TABLE IF EXISTS image;"
                          "DROP TABLE IF EXISTS video;"
 
-                         "COMMIT;",
+                         "PRAGMA user_version = 4;",
                          NULL, NULL, &error);
 
-  if (status == SQLITE_OK || status == SQLITE_DONE) {
-    /* Update user_version pragma */
-    if (!chatty_history_update_version (self, task))
-      return FALSE;
+  if (status == SQLITE_OK || status == SQLITE_DONE)
     return TRUE;
-  }
 
   g_task_return_new_error (task,
                            G_IO_ERROR,
@@ -2028,18 +1952,22 @@ history_open_db (ChattyHistory *self,
   if (status == SQLITE_OK) {
     self->db = db;
 
+    sqlite3_exec (self->db, "PRAGMA foreign_keys = OFF;", NULL, NULL, NULL);
+    sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
     if (db_exists) {
-      if (!chatty_history_migrate (self, task))
+      if (!chatty_history_migrate (self, task)) {
+        sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
         return;
+      }
     } else {
-      if (!chatty_history_create_schema (self, task))
-        return;
-
-      if (!chatty_history_update_version (self, task))
+      if (!chatty_history_create_schema (self, task)) {
+        sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
         return;
+      }
     }
 
     sqlite3_exec (self->db, "PRAGMA foreign_keys = ON;", NULL, NULL, NULL);
+    sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
     g_task_return_boolean (task, TRUE);
   } else {
     g_task_return_boolean (task, FALSE);
@@ -2565,9 +2493,12 @@ history_add_message (ChattyHistory *self,
   if ((!who || !*who) && direction == CHATTY_DIRECTION_IN && chatty_chat_is_im (chat))
     who = chatty_chat_get_chat_name (chat);
 
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
   thread_id = insert_or_ignore_thread (self, chat, task);
-  if (!thread_id)
+  if (!thread_id) {
+    sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
     return;
+  }
 
   sender_id = insert_or_ignore_user (self, chatty_item_get_protocols (CHATTY_ITEM (chat)), who, alias, task);
 
@@ -2576,6 +2507,7 @@ history_add_message (ChattyHistory *self,
     int message_id = 0;
 
     if (!msg) {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
       g_task_return_boolean (task, TRUE);
       return;
     }
@@ -2605,6 +2537,7 @@ history_add_message (ChattyHistory *self,
 
     status = sqlite3_step (stmt);
     sqlite3_finalize (stmt);
+    sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
 
     if (status == SQLITE_DONE)
       g_task_return_boolean (task, TRUE);
@@ -2667,6 +2600,7 @@ history_add_message (ChattyHistory *self,
   if (status == SQLITE_ROW)
     history_add_files (self, message, sqlite3_column_int (stmt, 0));
   sqlite3_finalize (stmt);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
 
   if (status == SQLITE_DONE || status == SQLITE_ROW)
     g_task_return_boolean (task, TRUE);
@@ -2750,7 +2684,6 @@ history_get_chats (ChattyHistory *self,
   ChattyAccount *account;
   sqlite3_stmt *stmt;
   const char *user_id;
-  int protocol, encrypted;
 
   g_assert (CHATTY_IS_HISTORY (self));
   g_assert (G_IS_TASK (task));
@@ -2764,16 +2697,11 @@ history_get_chats (ChattyHistory *self,
   }
 
   account = g_object_get_data (G_OBJECT (task), "account");
-  /* We currently handle only matrix accounts */
-  g_assert (CHATTY_IS_MA_ACCOUNT (account) || CHATTY_IS_MM_ACCOUNT (account));
+  /* We currently handle only MM accounts */
+  g_assert (CHATTY_IS_MM_ACCOUNT (account));
 
   user_id = chatty_item_get_username (CHATTY_ITEM (account));
 
-  if (CHATTY_IS_MA_ACCOUNT (account))
-    protocol = PROTOCOL_MATRIX;
-  else
-    protocol = PROTOCOL_MMS_SMS;
-
   sqlite3_prepare_v2 (self->db,
                       /*           0           1             2              3                4  */
                       "SELECT threads.id,threads.name,threads.alias,threads.encrypted,threads.type,"
@@ -2786,7 +2714,7 @@ history_get_chats (ChattyHistory *self,
                       "WHERE visibility!=" STRING(THREAD_VISIBILITY_HIDDEN),
                       -1, &stmt, NULL);
   history_bind_text (stmt, 1, user_id, "binding when getting threads");
-  history_bind_int (stmt, 2, protocol, "binding when getting threads");
+  history_bind_int (stmt, 2, PROTOCOL_MMS_SMS, "binding when getting threads");
 
   while (sqlite3_step (stmt) == SQLITE_ROW) {
     g_autoptr(GPtrArray) messages = NULL;
@@ -2794,6 +2722,7 @@ history_get_chats (ChattyHistory *self,
     const char *name, *alias;
     ChattyChat *chat;
     int thread_id, visibility;
+    int unread_count;
 
     if (!threads)
       threads = g_ptr_array_new_full (30, g_object_unref);
@@ -2801,7 +2730,6 @@ history_get_chats (ChattyHistory *self,
     thread_id = sqlite3_column_int (stmt, 0);
     name = (const char *)sqlite3_column_text (stmt, 1);
     alias = (const char *)sqlite3_column_text (stmt, 2);
-    encrypted = sqlite3_column_int (stmt, 3);
     visibility = sqlite3_column_int (stmt, 7);
 
     if (sqlite3_column_text (stmt, 5)) {
@@ -2810,9 +2738,7 @@ history_get_chats (ChattyHistory *self,
       file->path = g_strdup ((const char *)sqlite3_column_text (stmt, 6));
     }
 
-    if (CHATTY_IS_MA_ACCOUNT (account)) {
-      chat = (gpointer)chatty_ma_chat_new (name, alias, file, encrypted);
-    } else if (sqlite3_column_int (stmt, 4) == THREAD_GROUP_CHAT) {
+    if (sqlite3_column_int (stmt, 4) == THREAD_GROUP_CHAT) {
       chat = (gpointer)chatty_mm_chat_new (name, alias, CHATTY_PROTOCOL_MMS, FALSE,
                                            history_value_to_visibility (visibility));
     } else {
@@ -2822,22 +2748,14 @@ history_get_chats (ChattyHistory *self,
 
     messages = get_messages_before_time (self, chat, NULL, thread_id, INT_MAX, 1);
 
-    /* For now, we handle unread count only for MM accounts */
-    if (CHATTY_IS_MM_ACCOUNT (account)) {
-      int unread_count;
+    unread_count = get_unread_message_count (self, thread_id);
+    chatty_chat_set_unread_count (chat, unread_count);
 
-      unread_count = get_unread_message_count (self, thread_id);
-      chatty_chat_set_unread_count (chat, unread_count);
-    }
-
-    if (CHATTY_IS_MA_ACCOUNT (account))
-      chatty_ma_chat_add_messages (CHATTY_MA_CHAT (chat), messages);
-    else
-      chatty_mm_chat_prepend_messages (CHATTY_MM_CHAT (chat), messages);
+    chatty_mm_chat_prepend_messages (CHATTY_MM_CHAT (chat), messages);
 
     g_ptr_array_insert (threads, -1, chat);
 
-    if (protocol == PROTOCOL_MMS_SMS) {
+    {
       g_autoptr(GPtrArray) members = NULL;
 
       members = get_sms_thread_members (self, thread_id);
@@ -2909,10 +2827,13 @@ history_update_user (ChattyHistory *self,
     return;
   }
 
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
   id = insert_or_ignore_user (self, protocol, user_name, name, task);
 
-  if (!id)
+  if (!id) {
+    sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
     return;
+  }
 
   file_info = chatty_item_get_avatar_file (CHATTY_ITEM (account));
   file_id = add_file_info (self, file_info);
@@ -2926,6 +2847,8 @@ history_update_user (ChattyHistory *self,
 
   sqlite3_step (stmt);
   sqlite3_finalize (stmt);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
   g_task_return_boolean (task, TRUE);
 }
 
@@ -3017,8 +2940,8 @@ history_load_account (ChattyHistory *self,
 
   if (!user_name || !*user_name) {
     g_task_return_new_error (task,
-                             G_IO_ERROR, G_IO_ERROR_FAILED,
-                             "Error: username name is empty");
+                             G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                             "Username name is empty");
     return;
   }
 
@@ -3075,10 +2998,14 @@ history_set_last_read_msg (ChattyHistory *self,
 
   if (message)
     uid = chatty_message_get_uid (message);
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
   thread_id = insert_or_ignore_thread (self, chat, task);
 
-  if (!thread_id)
+  if (!thread_id) {
+    sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
     return;
+  }
 
   sqlite3_prepare_v2 (self->db,
                       "SELECT messages.id FROM messages "
@@ -3098,6 +3025,7 @@ history_set_last_read_msg (ChattyHistory *self,
   history_bind_int (stmt, 2, thread_id, "binding when setting last read message");
   sqlite3_step (stmt);
   sqlite3_finalize (stmt);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
 
   g_task_return_boolean (task, TRUE);
 }
@@ -3635,10 +3563,9 @@ chatty_history_get_chats_async (ChattyHistory       *self,
   g_return_if_fail (CHATTY_IS_HISTORY (self));
   g_return_if_fail (CHATTY_IS_ACCOUNT (account));
 
-  /* Currently we handle only matrix and SMS accounts */
+  /* Currently we handle only MM accounts */
   protocol = chatty_account_get_protocol_name (account);
-  if (!g_str_equal (protocol, "Matrix") &&
-      !g_str_equal (protocol, "SMS"))
+  if (!g_str_equal (protocol, "SMS"))
     g_return_if_reached ();
 
   task = g_task_new (self, NULL, callback, user_data);
diff --git a/src/chatty-image-item.c b/src/chatty-image-item.c
index be9ac4b1e0d9b2614521661c5547ae8cec57d8a7..d8bfe4f64b005fb81b1b3d84702c2366c390a0fc 100644
--- a/src/chatty-image-item.c
+++ b/src/chatty-image-item.c
@@ -36,50 +36,69 @@ struct _ChattyImageItem
 G_DEFINE_TYPE (ChattyImageItem, chatty_image_item, GTK_TYPE_BIN)
 
 
-static gboolean
-item_set_image (gpointer user_data)
+static void
+image_item_paint (ChattyImageItem *self,
+                  GdkPixbuf       *pixbuf)
 {
-  ChattyImageItem *self = user_data;
-  g_autoptr(GdkPixbuf) pixbuf = NULL;
-  cairo_surface_t *surface = NULL;
-  g_autofree char *path = NULL;
-  ChattyFileInfo *file;
   GtkStyleContext *sc;
-  GList *files;
-  int scale_factor;
 
-  files = chatty_message_get_files (self->message);
-  g_return_val_if_fail (files && files->data, G_SOURCE_REMOVE);
-  file = files->data;
-  scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
   sc = gtk_widget_get_style_context (self->image);
 
-  if (file->path) {
-    if (self->protocol == CHATTY_PROTOCOL_MMS_SMS || self->protocol == CHATTY_PROTOCOL_MMS)
-      path = g_build_filename (g_get_user_data_dir (), "chatty", file->path, NULL);
-    else
-      path = g_build_filename (g_get_user_cache_dir (), "chatty", file->path, NULL);
-  }
+  if (pixbuf) {
+    cairo_surface_t *surface = NULL;
 
-  if (path)
-    pixbuf = gdk_pixbuf_new_from_file_at_scale (path, 240 * scale_factor,
-                                                -1, TRUE, NULL);
-  if (pixbuf)
     surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, 0,
                                                     gtk_widget_get_window (GTK_WIDGET (self)));
-
-  if (file->status == CHATTY_FILE_DOWNLOADED && pixbuf) {
     gtk_style_context_remove_class (sc, "dim-label");
     gtk_image_set_from_surface (GTK_IMAGE (self->image), surface);
+    g_clear_pointer (&surface, cairo_surface_destroy);
   } else {
     gtk_style_context_add_class (sc, "dim-label");
     gtk_image_set_from_icon_name (GTK_IMAGE (self->image),
                                   "image-x-generic-symbolic",
                                   GTK_ICON_SIZE_BUTTON);
+    return;
   }
+}
+
+static void
+image_item_get_stream_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  g_autoptr(ChattyImageItem) self = user_data;
+  g_autoptr(GInputStream) stream = NULL;
+  g_autoptr(GdkPixbuf) pixbuf = NULL;
+  int scale_factor;
+
+  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
+    return;
+
+  stream = chatty_message_get_file_stream_finish (self->message, result, NULL);
+
+  if (!stream)
+    return;
+
+  scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
+  pixbuf = gdk_pixbuf_new_from_stream_at_scale (stream, 240 * scale_factor,
+                                                -1, TRUE, NULL, NULL);
+  image_item_paint (self, pixbuf);
+}
+
+static gboolean
+item_set_image (gpointer user_data)
+{
+  ChattyImageItem *self = user_data;
 
-  g_clear_pointer (&surface, cairo_surface_destroy);
+  /* It's possible that we get signals after dispose().
+   * Fix warning in those cases
+   */
+  if (!self->message)
+    return G_SOURCE_REMOVE;
 
+  chatty_message_get_file_stream_async (self->message, NULL, self->protocol, NULL,
+                                        image_item_get_stream_cb,
+                                        g_object_ref (self));
   return G_SOURCE_REMOVE;
 }
 
diff --git a/src/chatty-invite-view.c b/src/chatty-invite-view.c
new file mode 100644
index 0000000000000000000000000000000000000000..0ea01944a36c32762718b457736542f1815170e0
--- /dev/null
+++ b/src/chatty-invite-view.c
@@ -0,0 +1,224 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-invite-view.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "chatty-invite-view"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib/gi18n.h>
+
+#include "chatty-avatar.h"
+#include "chatty-manager.h"
+#include "chatty-invite-view.h"
+
+struct _ChattyInviteView
+{
+  GtkBox         parent_instance;
+
+  GtkWidget     *invite_title;
+  GtkWidget     *chat_avatar;
+  GtkWidget     *invite_subtitle;
+
+  GtkWidget     *accept_button;
+  GtkWidget     *accept_spinner;
+  GtkWidget     *reject_button;
+  GtkWidget     *reject_spinner;
+
+  ChattyAccount *account;
+  ChattyChat    *chat;
+
+  gulong         name_handler;
+  gulong         account_handler;
+};
+
+G_DEFINE_TYPE (ChattyInviteView, chatty_invite_view, GTK_TYPE_BOX)
+
+static void
+invite_account_status_changed_cb (ChattyInviteView *self)
+{
+  gboolean can_connect;
+
+  g_assert (CHATTY_IS_INVITE_VIEW (self));
+
+  if (!self->chat || chatty_chat_get_chat_state (self->chat) != CHATTY_CHAT_INVITED)
+    return;
+
+  can_connect = chatty_account_get_status (self->account) == CHATTY_CONNECTED;
+  gtk_widget_set_sensitive (self->accept_button, can_connect);
+}
+
+static void
+chat_invite_name_changed_cb (ChattyInviteView *self)
+{
+  g_autofree char *title = NULL;
+  const char *name;
+
+  g_assert (CHATTY_IS_INVITE_VIEW (self));
+
+  if (!self->chat || chatty_chat_get_chat_state (self->chat) != CHATTY_CHAT_INVITED)
+    return;
+
+  name = chatty_item_get_name (CHATTY_ITEM (self->chat));
+  /* TRANSLATORS: %s is the chat room name */
+  title = g_strdup_printf (_("Do you want to join “%s”"), name);
+  gtk_label_set_label (GTK_LABEL (self->invite_title), title);
+}
+
+static void
+chat_accept_invite_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  g_autoptr(ChattyInviteView) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  if (gtk_widget_in_destruction (user_data))
+    return;
+
+  gtk_spinner_stop (GTK_SPINNER (self->accept_spinner));
+  gtk_widget_set_sensitive (self->accept_button, TRUE);
+
+  if (chatty_chat_accept_invite_finish (self->chat, result, &error)) {
+    ChattyManager *manager;
+
+    manager = chatty_manager_get_default ();
+    /* The item will be deleted from invite list and added to joined list */
+    g_signal_emit_by_name (manager, "chat-deleted", self->chat);
+  }
+
+  if (error)
+    g_warning ("Error: %s", error->message);
+}
+
+static void
+invite_accept_clicked_cb (ChattyInviteView *self)
+{
+  g_assert (CHATTY_IS_INVITE_VIEW (self));
+
+  gtk_spinner_start (GTK_SPINNER (self->accept_spinner));
+  gtk_widget_set_sensitive (self->accept_button, FALSE);
+  chatty_chat_accept_invite_async (self->chat,
+                                   chat_accept_invite_cb,
+                                   g_object_ref (self));
+}
+
+static void
+chat_reject_invite_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  g_autoptr(ChattyInviteView) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  if (gtk_widget_in_destruction (user_data))
+    return;
+
+  gtk_spinner_stop (GTK_SPINNER (self->reject_spinner));
+  gtk_widget_set_sensitive (self->accept_button, TRUE);
+
+  if (chatty_chat_reject_invite_finish (self->chat, result, &error)) {
+    ChattyManager *manager;
+
+    manager = chatty_manager_get_default ();
+    g_signal_emit_by_name (manager, "chat-deleted", self->chat);
+  }
+
+  if (error)
+    g_warning ("Error: %s", error->message);
+}
+
+static void
+invite_reject_clicked_cb (ChattyInviteView *self)
+{
+  g_assert (CHATTY_IS_INVITE_VIEW (self));
+
+  gtk_spinner_start (GTK_SPINNER (self->reject_spinner));
+  gtk_widget_set_sensitive (self->accept_button, FALSE);
+  chatty_chat_reject_invite_async (self->chat,
+                                   chat_reject_invite_cb,
+                                   g_object_ref (self));
+}
+
+static void
+chatty_invite_view_dispose (GObject *object)
+{
+  ChattyInviteView *self = (ChattyInviteView *)object;
+
+  g_clear_object (&self->chat);
+
+  G_OBJECT_CLASS (chatty_invite_view_parent_class)->dispose (object);
+}
+
+static void
+chatty_invite_view_class_init (ChattyInviteViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = chatty_invite_view_dispose;
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/Chatty/"
+                                               "ui/chatty-invite-view.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, invite_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, chat_avatar);
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, invite_subtitle);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, accept_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, accept_spinner);
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, reject_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyInviteView, reject_spinner);
+
+  gtk_widget_class_bind_template_callback (widget_class, invite_accept_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, invite_reject_clicked_cb);
+}
+
+static void
+chatty_invite_view_init (ChattyInviteView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+chatty_invite_view_set_chat (ChattyInviteView *self,
+                             ChattyChat       *chat)
+{
+  g_return_if_fail (CHATTY_IS_INVITE_VIEW (self));
+  g_return_if_fail (!chat || CHATTY_IS_CHAT (chat));
+
+  if (self->chat) {
+    g_clear_signal_handler (&self->account_handler, self->account);
+    g_clear_signal_handler (&self->name_handler, self->chat);
+    chatty_avatar_set_item (CHATTY_AVATAR (self->chat_avatar), NULL);
+  }
+
+  g_set_object (&self->chat, chat);
+
+  if (chat) {
+    if (chatty_chat_get_chat_state (chat) != CHATTY_CHAT_INVITED)
+      return;
+
+    self->account = chatty_chat_get_account (chat);
+    chatty_avatar_set_item (CHATTY_AVATAR (self->chat_avatar), CHATTY_ITEM (chat));
+
+    self->account_handler = g_signal_connect_object (self->account, "notify::status",
+                                                     G_CALLBACK (invite_account_status_changed_cb),
+                                                     self, G_CONNECT_SWAPPED);
+    self->name_handler = g_signal_connect_object (chat, "notify::name",
+                                                  G_CALLBACK (chat_invite_name_changed_cb),
+                                                  self, G_CONNECT_SWAPPED);
+    invite_account_status_changed_cb (self);
+    chat_invite_name_changed_cb (self);
+  }
+}
diff --git a/src/chatty-invite-view.h b/src/chatty-invite-view.h
new file mode 100644
index 0000000000000000000000000000000000000000..890c4304a9b417d3d831ef2346efe1fcbd8de3c9
--- /dev/null
+++ b/src/chatty-invite-view.h
@@ -0,0 +1,28 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-invite-view.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "chatty-chat.h"
+
+G_BEGIN_DECLS
+
+#define CHATTY_TYPE_INVITE_VIEW (chatty_invite_view_get_type ())
+
+G_DECLARE_FINAL_TYPE (ChattyInviteView, chatty_invite_view, CHATTY, INVITE_VIEW, GtkBox)
+
+void          chatty_invite_view_set_chat       (ChattyInviteView *self,
+                                                 ChattyChat       *chat);
+
+G_END_DECLS
diff --git a/src/chatty-list-row.c b/src/chatty-list-row.c
index 9b8bd64df6f1c9dd645cc04580d241c92f992ed0..555aec02fa3a58fb4ca4bf8f5bfe2de13fcfb598 100644
--- a/src/chatty-list-row.c
+++ b/src/chatty-list-row.c
@@ -20,6 +20,7 @@
 #include "chatty-contact.h"
 #include "chatty-contact-list.h"
 #include "chatty-chat.h"
+#include "chatty-ma-key-chat.h"
 #include "chatty-avatar.h"
 #include "chatty-clock.h"
 #include "chatty-list-row.h"
@@ -206,6 +207,11 @@ chatty_list_row_update (ChattyListRow *self)
     last_message_time = chatty_chat_get_last_msg_time (item);
     gtk_widget_set_visible (self->last_modified, last_message_time > 0);
 
+    if (chatty_chat_get_chat_state (CHATTY_CHAT (self->item)) == CHATTY_CHAT_INVITED) {
+      gtk_label_set_text (GTK_LABEL (self->unread_message_count), "!");
+      gtk_widget_show (self->unread_message_count);
+    }
+
     chatty_list_row_update_last_modified (self);
   }
 
@@ -373,6 +379,9 @@ chatty_list_row_set_item (ChattyListRow *self,
                           self->title, "label",
                           G_BINDING_SYNC_CREATE);
 
+  if (CHATTY_IS_MA_KEY_CHAT (item))
+    self->hide_chat_details = TRUE;
+
   if (CHATTY_IS_CHAT (item))
     g_signal_connect_object (item, "changed",
                              G_CALLBACK (chatty_list_row_update),
diff --git a/src/chatty-log.c b/src/chatty-log.c
index b3a2347b8b9fb389be3d46298f2b434837cc9ad8..3725907be6dcc782432f1c297dbd2fb8302f9598 100644
--- a/src/chatty-log.c
+++ b/src/chatty-log.c
@@ -23,16 +23,91 @@ static int verbosity;
 gboolean any_domain;
 gboolean no_anonymize;
 gboolean stderr_is_journal;
+gboolean fatal_criticals, fatal_warnings;
+
+/* Copied from GLib, LGPLv2.1+ */
+static void
+_g_log_abort (gboolean breakpoint)
+{
+  gboolean debugger_present;
+
+  if (g_test_subprocess ())
+    {
+      /* If this is a test case subprocess then it probably caused
+       * this error message on purpose, so just exit() rather than
+       * abort()ing, to avoid triggering any system crash-reporting
+       * daemon.
+       */
+      _exit (1);
+    }
+
+#ifdef G_OS_WIN32
+  debugger_present = IsDebuggerPresent ();
+#else
+  /* Assume GDB is attached. */
+  debugger_present = TRUE;
+#endif /* !G_OS_WIN32 */
+
+  if (debugger_present && breakpoint)
+    G_BREAKPOINT ();
+  else
+    g_abort ();
+}
+
+static gboolean
+should_show_log_for_level (GLogLevelFlags log_level,
+                           int            verbosity_level)
+{
+  if (verbosity_level >= 5)
+    return TRUE;
+
+  if (log_level & CHATTY_LOG_LEVEL_TRACE)
+    return verbosity_level >= 4;
+
+  if (log_level & G_LOG_LEVEL_DEBUG)
+    return verbosity_level >= 3;
+
+  if (log_level & G_LOG_LEVEL_INFO)
+    return verbosity_level >= 2;
+
+  if (log_level & G_LOG_LEVEL_MESSAGE)
+    return verbosity_level >= 1;
+
+  return FALSE;
+}
+
+static gboolean
+matches_domain (const char *log_domains,
+                const char *domain)
+{
+  g_auto(GStrv) domain_list = NULL;
+
+  if (!log_domains || !*log_domains ||
+      !domain || !*domain)
+    return FALSE;
+
+  domain_list = g_strsplit (log_domains, ",", -1);
+
+  for (guint i = 0; domain_list[i]; i++)
+    {
+      if (g_str_has_prefix (domain, domain_list[i]))
+        return TRUE;
+    }
+
+  return FALSE;
+}
 
 static gboolean
 should_log (const char     *log_domain,
             GLogLevelFlags  log_level)
 {
+  g_assert (log_domain);
+
   /* Ignore custom flags set */
   log_level = log_level & ~CHATTY_LOG_DETAILED;
 
   /* Don't skip serious logs */
-  if (log_level & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_WARNING))
+  if (log_level & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING))
     return TRUE;
 
   if (any_domain && domains) {
@@ -44,53 +119,27 @@ should_log (const char     *log_domain,
     return verbosity >= 4;
   }
 
-  if (!any_domain && domains && log_domain && strstr (domains, log_domain))
-    return TRUE;
-
-  switch ((int)log_level)
-    {
-    case G_LOG_LEVEL_MESSAGE:
-      if (verbosity < 1)
-        return FALSE;
-      break;
-
-    case G_LOG_LEVEL_INFO:
-      if (verbosity < 2)
-        return FALSE;
-      break;
-
-    case G_LOG_LEVEL_DEBUG:
-      if (verbosity < 3)
-        return FALSE;
-      break;
-
-    case CHATTY_LOG_LEVEL_TRACE:
-      if (verbosity < 4)
-        return FALSE;
-      break;
-
-    default:
-      break;
-    }
+  if (!domains && g_str_has_prefix (log_domain, DEFAULT_DOMAIN_PREFIX))
+    return should_show_log_for_level (log_level, verbosity);
 
-  if (!log_domain)
-    log_domain = "**";
+  if (domains && matches_domain (domains, log_domain))
+    return should_show_log_for_level (log_level, verbosity);
 
-  /* Skip logs from other domains if verbosity level is low */
-  if (any_domain && !domains &&
-      verbosity < 5 &&
-      log_level > G_LOG_LEVEL_MESSAGE &&
-      !strstr (log_domain, DEFAULT_DOMAIN_PREFIX))
+  /* If we didn't handle domains in the preceding statement,
+   * we should no longer log them */
+  if (domains)
     return FALSE;
 
   /* GdkPixbuf logs are too much verbose, skip unless asked not to. */
-  if (log_level >= G_LOG_LEVEL_MESSAGE &&
-      verbosity < 7 &&
+  if (verbosity < 8 &&
       g_strcmp0 (log_domain, "GdkPixbuf") == 0 &&
       (!domains || !strstr (domains, log_domain)))
     return FALSE;
 
-  return TRUE;
+  if (verbosity >= 6)
+    return TRUE;
+
+  return FALSE;
 }
 
 static void
@@ -170,18 +219,15 @@ chatty_log_write (GLogLevelFlags   log_level,
                   gpointer         user_data)
 {
   g_autoptr(GString) log_str = NULL;
-  FILE *stream;
+  FILE *stream = stdout;
   gboolean can_color;
 
-  if (stderr_is_journal)
-    if (g_log_writer_journald (log_level, fields, n_fields, user_data) == G_LOG_WRITER_HANDLED)
-      return G_LOG_WRITER_HANDLED;
+  if (stderr_is_journal &&
+      g_log_writer_journald (log_level, fields, n_fields, user_data) == G_LOG_WRITER_HANDLED)
+    return G_LOG_WRITER_HANDLED;
 
-  if (log_level & (G_LOG_LEVEL_ERROR |
-                   G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING))
+  if (log_level & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING))
     stream = stderr;
-  else
-    stream = stdout;
 
   log_str = g_string_new (NULL);
 
@@ -202,7 +248,6 @@ chatty_log_write (GLogLevelFlags   log_level,
   }
 
   can_color = g_log_writer_supports_color (fileno (stream));
-  
   log_str_append_log_domain (log_str, log_domain, can_color);
   g_string_append_printf (log_str, "[%5d]:", getpid ());
 
@@ -239,6 +284,16 @@ chatty_log_write (GLogLevelFlags   log_level,
   fprintf (stream, "%s\n", log_str->str);
   fflush (stream);
 
+  if (fatal_criticals &&
+      (log_level & G_LOG_LEVEL_CRITICAL))
+    log_level |= G_LOG_FLAG_FATAL;
+  else if (fatal_warnings &&
+           (log_level & (G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING)))
+    log_level |= G_LOG_FLAG_FATAL;
+
+  if (log_level & (G_LOG_FLAG_FATAL | G_LOG_LEVEL_ERROR))
+    _g_log_abort (!(log_level & G_LOG_FLAG_RECURSION));
+
   return G_LOG_WRITER_HANDLED;
 }
 
@@ -261,24 +316,17 @@ chatty_log_handler (GLogLevelFlags   log_level,
         log_message = field->value;
     }
 
-  if (!should_log (log_domain, log_level))
-    return G_LOG_WRITER_HANDLED;
-
   if (!log_domain)
     log_domain = "**";
 
   if (!log_message)
     log_message = "(NULL) message";
 
-  if (any_domain)
-    return chatty_log_write (log_level, log_domain, log_message,
-                             fields, n_fields, user_data);
-
-  if (!log_domain || strstr (domains, log_domain))
-    return chatty_log_write (log_level, log_domain, log_message,
-                             fields, n_fields, user_data);
+  if (!should_log (log_domain, log_level))
+    return G_LOG_WRITER_HANDLED;
 
-  return G_LOG_WRITER_HANDLED;
+  return chatty_log_write (log_level, log_domain, log_message,
+                           fields, n_fields, user_data);
 }
 
 static void
@@ -302,13 +350,18 @@ chatty_log_init (void)
       if (!domains || g_str_equal (domains, "all"))
         any_domain = TRUE;
 
-      if (domains && g_str_equal (domains, "no-anonymize"))
+      if (domains && strstr (domains, "no-anonymize"))
         {
           any_domain = TRUE;
           no_anonymize = TRUE;
           g_clear_pointer (&domains, g_free);
         }
 
+      if (g_strcmp0 (g_getenv ("G_DEBUG"), "fatal-criticals") == 0)
+        fatal_criticals = TRUE;
+      else if (g_strcmp0 (g_getenv ("G_DEBUG"), "fatal-warnings") == 0)
+        fatal_warnings = TRUE;
+
       stderr_is_journal = g_log_writer_is_journald (fileno (stderr));
       g_log_set_writer_func (chatty_log_handler, NULL, NULL);
       g_once_init_leave (&initialized, 1);
@@ -393,6 +446,8 @@ void
 chatty_log_anonymize_value (GString    *str,
                             const char *value)
 {
+  gunichar c, next_c, prev_c;
+
   if (!value || !*value)
     return;
 
@@ -407,30 +462,30 @@ chatty_log_anonymize_value (GString    *str,
       return;
     }
 
-  if (!isalnum (*value))
-    g_string_append_c (str, *value++);
-
-  if (*value)
-    g_string_append_c (str, *value++);
+  if (!g_utf8_validate (value, -1, NULL))
+    {
+      g_string_append (str, "******");
+      return;
+    }
 
-  if (*value)
-    g_string_append_c (str, *value++);
+  c = g_utf8_get_char (value);
+  g_string_append_unichar (str, c);
 
-  if (!*value)
-    return;
+  value = g_utf8_next_char (value);
 
-  /* Replace all but the last two alnum chars with 'x' */
   while (*value)
     {
-      while (isalnum (*value) && value[1] && value[2])
-        {
-          if (value[1] == ':' || value[1] == '@' || value[1] == ' ' ||
-              value[-1] == ':' || value[-1] == '@' || value[-1] == ' ')
-            g_string_append_c (str, *value);
-          else
-            g_string_append_c (str, '#');
-          value++;
-        }
-      g_string_append_c (str, *value++);
+      prev_c = c;
+      c = g_utf8_get_char (value);
+
+      value = g_utf8_next_char (value);
+      next_c = g_utf8_get_char (value);
+
+      if (!g_unichar_isalnum (c))
+        g_string_append_unichar (str, c);
+      else if (!g_unichar_isalnum (prev_c) || !g_unichar_isalnum (next_c))
+        g_string_append_unichar (str, c);
+      else
+        g_string_append_c (str, '#');
     }
 }
diff --git a/src/chatty-main-view.c b/src/chatty-main-view.c
new file mode 100644
index 0000000000000000000000000000000000000000..bed839e176ccb6f78676ebfb068ba235fb41723b
--- /dev/null
+++ b/src/chatty-main-view.c
@@ -0,0 +1,127 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-main-view.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "chatty-main-view"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "chatty-chat.h"
+#include "chatty-ma-key-chat.h"
+#include "chatty-history.h"
+#include "chatty-invite-view.h"
+#include "chatty-verification-view.h"
+#include "chatty-chat-view.h"
+#include "chatty-main-view.h"
+
+struct _ChattyMainView
+{
+  GtkBox        parent_instance;
+
+  GtkWidget    *main_stack;
+  GtkWidget    *content_view;
+  GtkWidget    *invite_view;
+  GtkWidget    *verification_view;
+  GtkWidget    *empty_view;
+
+  ChattyItem   *item;
+};
+
+G_DEFINE_TYPE (ChattyMainView, chatty_main_view, GTK_TYPE_BOX)
+
+static void
+chatty_main_view_dispose (GObject *object)
+{
+  ChattyMainView *self = (ChattyMainView *)object;
+
+  g_clear_object (&self->item);
+
+  G_OBJECT_CLASS (chatty_main_view_parent_class)->dispose (object);
+}
+
+static void
+chatty_main_view_class_init (ChattyMainViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = chatty_main_view_dispose;
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/Chatty/"
+                                               "ui/chatty-main-view.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyMainView, main_stack);
+  gtk_widget_class_bind_template_child (widget_class, ChattyMainView, content_view);
+  gtk_widget_class_bind_template_child (widget_class, ChattyMainView, invite_view);
+  gtk_widget_class_bind_template_child (widget_class, ChattyMainView, verification_view);
+  gtk_widget_class_bind_template_child (widget_class, ChattyMainView, empty_view);
+
+  g_type_ensure (CHATTY_TYPE_CHAT_VIEW);
+  g_type_ensure (CHATTY_TYPE_VERIFICATION_VIEW);
+}
+
+static void
+chatty_main_view_init (ChattyMainView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+chatty_main_view_set_db (ChattyMainView *self,
+                         gpointer        db)
+{
+  g_return_if_fail (CHATTY_IS_MAIN_VIEW (self));
+  g_return_if_fail (CHATTY_IS_HISTORY (db));
+
+  chatty_chat_view_set_db (CHATTY_CHAT_VIEW (self->content_view), db);
+}
+
+void
+chatty_main_view_set_item (ChattyMainView *self,
+                           ChattyItem     *item)
+{
+  g_return_if_fail (CHATTY_IS_MAIN_VIEW (self));
+  g_return_if_fail (!item || CHATTY_IS_ITEM (item));
+
+  g_set_object (&self->item, item);
+  chatty_verification_view_set_item (CHATTY_VERIFICATION_VIEW (self->verification_view), item);
+
+  if (!item || (CHATTY_IS_CHAT (item) && !CHATTY_IS_MA_KEY_CHAT (item)))
+    chatty_chat_view_set_chat (CHATTY_CHAT_VIEW (self->content_view), (ChattyChat *)item);
+
+  if (CHATTY_IS_CHAT (item)) {
+    ChattyChatState state;
+
+    state = chatty_chat_get_chat_state (CHATTY_CHAT (item));
+
+    if (state == CHATTY_CHAT_INVITED)
+      chatty_invite_view_set_chat (CHATTY_INVITE_VIEW (self->invite_view), (ChattyChat *)item);
+
+    if (state == CHATTY_CHAT_INVITED)
+      gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->invite_view);
+    else if (state == CHATTY_CHAT_VERIFICATION)
+      gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->verification_view);
+    else
+      gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->content_view);
+  } else {
+    gtk_stack_set_visible_child (GTK_STACK (self->main_stack), self->empty_view);
+  }
+}
+
+ChattyItem *
+chatty_main_view_get_item (ChattyMainView *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MAIN_VIEW (self), NULL);
+
+  return self->item;
+}
diff --git a/src/chatty-main-view.h b/src/chatty-main-view.h
new file mode 100644
index 0000000000000000000000000000000000000000..0b453d3f9d584c5a855de8d318c384728f804b03
--- /dev/null
+++ b/src/chatty-main-view.h
@@ -0,0 +1,30 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-main-view.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "chatty-item.h"
+
+G_BEGIN_DECLS
+
+#define CHATTY_TYPE_MAIN_VIEW (chatty_main_view_get_type ())
+
+G_DECLARE_FINAL_TYPE (ChattyMainView, chatty_main_view, CHATTY, MAIN_VIEW, GtkBox)
+
+void           chatty_main_view_set_db            (ChattyMainView *self,
+                                                   gpointer        db);
+void           chatty_main_view_set_item          (ChattyMainView *self,
+                                                   ChattyItem     *item);
+ChattyItem    *chatty_main_view_get_item          (ChattyMainView *self);
+
+G_END_DECLS
diff --git a/src/chatty-manager.c b/src/chatty-manager.c
index 3868ecb064df89cea54be15d00af314e95618ebc..4713447b8632f9472de2464081803691632e020b 100644
--- a/src/chatty-manager.c
+++ b/src/chatty-manager.c
@@ -18,6 +18,8 @@
 #define LIBFEEDBACK_USE_UNSTABLE_API
 #include <libfeedback.h>
 #include <glib/gi18n.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 #include "contrib/gtk.h"
 
 #include "chatty-settings.h"
@@ -107,11 +109,24 @@ manager_sort_chat_item (ChattyChat *a,
                         ChattyChat *b,
                         gpointer    user_data)
 {
+  ChattyChatState a_state, b_state;
   time_t a_time, b_time;
 
   g_assert (CHATTY_IS_CHAT (a));
   g_assert (CHATTY_IS_CHAT (b));
 
+  a_state = chatty_chat_get_chat_state (a);
+  b_state = chatty_chat_get_chat_state (b);
+
+  if (a_state == CHATTY_CHAT_VERIFICATION ||
+      b_state == CHATTY_CHAT_VERIFICATION)
+    return a_state == CHATTY_CHAT_VERIFICATION ? -1 : 1;
+
+  if (a_state != b_state &&
+      (a_state == CHATTY_CHAT_INVITED ||
+       b_state == CHATTY_CHAT_INVITED))
+    return a_state == CHATTY_CHAT_INVITED ? -1 : 1;
+
   a_time = chatty_chat_get_last_msg_time (a);
   b_time = chatty_chat_get_last_msg_time (b);
 
@@ -418,8 +433,7 @@ chatty_manager_load (ChattyManager *self)
   chatty_mm_account_load_async (self->mm_account, NULL, NULL);
 
   /* Matrix Setup */
-  self->matrix = chatty_matrix_new (chatty_manager_get_history (self),
-                                    self->disable_auto_login);
+  self->matrix = chatty_matrix_new (self->disable_auto_login);
   manager_add_to_flat_model (self->accounts,
                              chatty_matrix_get_account_list (self->matrix));
   manager_add_to_flat_model (self->chat_list,
@@ -661,8 +675,7 @@ chatty_manager_find_account_with_name (ChattyManager  *self,
   g_return_val_if_fail (CHATTY_IS_MANAGER (self), NULL);
   g_return_val_if_fail (account_id && *account_id, NULL);
 
-  if (protocol & CHATTY_PROTOCOL_MATRIX &&
-      chatty_settings_get_experimental_features (chatty_settings_get_default ()))
+  if (protocol & CHATTY_PROTOCOL_MATRIX)
     return chatty_matrix_find_account_with_name (self->matrix, account_id);
 
 #ifdef PURPLE_ENABLED
@@ -685,15 +698,12 @@ chatty_manager_find_chat_with_name (ChattyManager  *self,
     return chatty_mm_account_find_chat (self->mm_account, chat_id);
 
 #ifdef PURPLE_ENABLED
-  if (protocol & (CHATTY_PROTOCOL_XMPP | CHATTY_PROTOCOL_TELEGRAM) ||
-      (!chatty_settings_get_experimental_features (chatty_settings_get_default ()) &&
-       protocol & CHATTY_PROTOCOL_MATRIX))
+  if (protocol & (CHATTY_PROTOCOL_XMPP | CHATTY_PROTOCOL_TELEGRAM))
     return chatty_purple_find_chat_with_name (chatty_purple_get_default (),
                                               protocol, account_id, chat_id);
 #endif
 
-  if (chatty_settings_get_experimental_features (chatty_settings_get_default ())
-      && protocol == CHATTY_PROTOCOL_MATRIX)
+  if (protocol == CHATTY_PROTOCOL_MATRIX)
     return chatty_matrix_find_chat_with_name (self->matrix, protocol, account_id, chat_id);
 
   return NULL;
@@ -749,3 +759,26 @@ chatty_manager_get_history (ChattyManager *self)
 
   return self->history;
 }
+
+gpointer
+chatty_manager_matrix_client_new (ChattyManager *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MANAGER (self), NULL);
+
+  if (!self->matrix)
+    return NULL;
+
+  return chatty_matrix_client_new (self->matrix);
+}
+
+gboolean
+chatty_manager_has_matrix_with_id (ChattyManager *self,
+                                   const char    *user_id)
+{
+  g_return_val_if_fail (CHATTY_IS_MANAGER (self), FALSE);
+
+  if (!self->matrix)
+    return FALSE;
+
+  return chatty_matrix_has_user_id (self->matrix, user_id);
+}
diff --git a/src/chatty-manager.h b/src/chatty-manager.h
index 47e690ae941df948209a30705b25c18e04b7e8fd..b1d5c61389fed165ef2001b0789741b30d618be5 100644
--- a/src/chatty-manager.h
+++ b/src/chatty-manager.h
@@ -64,5 +64,8 @@ gboolean        chatty_manager_set_uri                (ChattyManager      *self,
                                                        const char         *uri,
                                                        const char         *name);
 ChattyHistory  *chatty_manager_get_history            (ChattyManager      *self);
+gpointer        chatty_manager_matrix_client_new      (ChattyManager      *self);
+gboolean        chatty_manager_has_matrix_with_id     (ChattyManager *self,
+                                                         const char    *user_id);
 
 G_END_DECLS
diff --git a/src/chatty-message.c b/src/chatty-message.c
index 91a24c91e9e56f4c49acc8397fbca0de15f5884a..f6f66889be2d894cbafd1f21593b7a9485f9cac5 100644
--- a/src/chatty-message.c
+++ b/src/chatty-message.c
@@ -15,6 +15,8 @@
 # include "config.h"
 #endif
 
+#include <glib/gi18n.h>
+
 #include "chatty-mm-buddy.h"
 #include "chatty-message.h"
 #include "chatty-utils.h"
@@ -36,6 +38,7 @@ struct _ChattyMessage
   char            *message;
   char            *uid;
   char            *id;
+  CmEvent         *cm_event;
 
   ChattyFileInfo  *preview;
   GList           *files;
@@ -66,6 +69,7 @@ chatty_message_finalize (GObject *object)
   ChattyMessage *self = (ChattyMessage *)object;
 
   g_clear_object (&self->user);
+  g_clear_object (&self->cm_event);
   g_free (self->message);
   g_free (self->subject);
   g_free (self->uid);
@@ -133,6 +137,132 @@ chatty_message_new (ChattyItem         *user,
   return self;
 }
 
+ChattyMessage *
+chatty_message_new_from_event (ChattyItem *user,
+                               CmEvent    *event)
+{
+  ChattyMessage *self;
+  const char *body = NULL;
+  ChattyMsgType type = CHATTY_MESSAGE_UNKNOWN;
+  ChattyMsgDirection direction;
+  ChattyMsgStatus status;
+
+  g_return_val_if_fail (CM_IS_EVENT (event), NULL);
+  g_return_val_if_fail (CHATTY_IS_ITEM (user), NULL);
+
+  if (CM_IS_ROOM_MESSAGE_EVENT (event)) {
+    switch (cm_room_message_event_get_msg_type ((gpointer)event))
+      {
+      case CM_CONTENT_TYPE_TEXT:
+      case CM_CONTENT_TYPE_EMOTE:
+      case CM_CONTENT_TYPE_NOTICE:
+      case CM_CONTENT_TYPE_SERVER_NOTICE:
+        type = CHATTY_MESSAGE_TEXT;
+        break;
+
+      case CM_CONTENT_TYPE_IMAGE:
+        type = CHATTY_MESSAGE_IMAGE;
+        break;
+
+      case CM_CONTENT_TYPE_FILE:
+        type = CHATTY_MESSAGE_FILE;
+        break;
+
+      case CM_CONTENT_TYPE_AUDIO:
+        type = CHATTY_MESSAGE_AUDIO;
+        break;
+
+      case CM_CONTENT_TYPE_LOCATION:
+        type = CHATTY_MESSAGE_LOCATION;
+        break;
+
+      case CM_CONTENT_TYPE_VIDEO:
+        type = CHATTY_MESSAGE_VIDEO;
+        break;
+
+      case CM_CONTENT_TYPE_UNKNOWN:
+      default:
+        break;
+      }
+  }
+
+  switch (cm_event_get_state (event))
+    {
+    case CM_EVENT_STATE_DRAFT:
+      status = CHATTY_STATUS_DRAFT;
+      break;
+
+    case CM_EVENT_STATE_RECEIVED:
+      status = CHATTY_STATUS_RECEIVED;
+      break;
+
+    case CM_EVENT_STATE_WAITING:
+    case CM_EVENT_STATE_SENDING:
+      status = CHATTY_STATUS_SENDING;
+      break;
+
+    case CM_EVENT_STATE_SENDING_FAILED:
+      status = CHATTY_STATUS_SENDING_FAILED;
+      break;
+
+    case CM_EVENT_STATE_SENT:
+      status = CHATTY_STATUS_SENT;
+      break;
+
+    case CM_EVENT_STATE_UNKNOWN:
+    default:
+      status = CHATTY_STATUS_UNKNOWN;
+      break;
+    }
+
+  if (CM_IS_ROOM_MESSAGE_EVENT (event))
+    body = cm_room_message_event_get_body ((gpointer)event);
+
+  if ((!body || !*body) &&
+      cm_event_get_m_type (event) == CM_M_ROOM_ENCRYPTED)
+    direction = CHATTY_DIRECTION_SYSTEM;
+  else if (cm_event_get_state (event) == CM_EVENT_STATE_WAITING ||
+           cm_event_get_state (event) == CM_EVENT_STATE_SENT ||
+           cm_event_get_state (event) == CM_EVENT_STATE_SENDING ||
+           cm_event_get_state (event) == CM_EVENT_STATE_SENDING_FAILED)
+    direction = CHATTY_DIRECTION_OUT;
+  else
+    direction = CHATTY_DIRECTION_IN;
+
+  if (direction == CHATTY_DIRECTION_SYSTEM)
+    body = _("Got an encrypted message, but couldn't decrypt due to missing keys");
+  else if (CM_IS_ROOM_MESSAGE_EVENT (event))
+    body = cm_room_message_event_get_body ((gpointer)event);
+  else
+    body = "";
+
+  self = chatty_message_new (user, body,
+                             cm_event_get_id (event),
+                             cm_event_get_time_stamp (event) / 1000,
+                             type, direction, status);
+
+  self->cm_event = g_object_ref (event);
+  self->user = g_object_ref (user);
+
+  if (type == CHATTY_MESSAGE_IMAGE ||
+      type == CHATTY_MESSAGE_FILE ||
+      type == CHATTY_MESSAGE_AUDIO ||
+      type == CHATTY_MESSAGE_VIDEO) {
+    /* Add some dummy data, we handle files appropriately elsewhere */
+    chatty_message_add_file_from_path (self, "/");
+  }
+
+  return self;
+}
+
+CmEvent *
+chatty_message_get_cm_event (ChattyMessage *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MESSAGE (self), NULL);
+
+  return self->cm_event;
+}
+
 const char *
 chatty_message_get_subject (ChattyMessage *self)
 {
@@ -185,6 +315,115 @@ chatty_message_get_files (ChattyMessage *self)
   return self->files;
 }
 
+static void
+message_event_get_file_stream_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  ChattyMessage *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  ChattyFileInfo *file;
+  ChattyFileStatus old_status;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CHATTY_IS_MESSAGE (self));
+
+  file = self->files->data;
+  file->file_stream = cm_room_message_event_get_file_finish (CM_ROOM_MESSAGE_EVENT (object), result, &error);
+
+  old_status = file->status;
+
+  if (file->file_stream)
+    g_atomic_int_set (&file->status, CHATTY_FILE_DOWNLOADED);
+  else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+           g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) ||
+           g_error_matches (error, G_IO_ERROR, G_IO_ERROR_HOST_UNREACHABLE))
+    g_atomic_int_set (&file->status, CHATTY_FILE_UNKNOWN);
+  else
+    g_atomic_int_set (&file->status, CHATTY_FILE_ERROR);
+
+  if (old_status != file->status)
+    chatty_message_emit_updated (self);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_pointer (task, g_object_ref (file->file_stream), g_object_unref);
+}
+
+void
+chatty_message_get_file_stream_async (ChattyMessage       *self,
+                                      ChattyFileInfo      *file,
+                                      ChattyProtocol       protocol,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CHATTY_IS_MESSAGE (self));
+  g_return_if_fail (self->files);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  if (!file)
+    file = self->files->data;
+
+  if (!file || !file->path) {
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                             "Message has no file");
+    return;
+  }
+
+  if (file->file_stream) {
+    g_seekable_seek (G_SEEKABLE (file->file_stream), 0, G_SEEK_SET, NULL, NULL);
+    g_task_return_pointer (task, g_object_ref (file->file_stream), g_object_unref);
+    return;
+  }
+
+  g_atomic_int_set (&file->status, CHATTY_FILE_DOWNLOADING);
+  chatty_message_emit_updated (self);
+
+  if (self->cm_event) {
+    cm_room_message_event_get_file_async (CM_ROOM_MESSAGE_EVENT (self->cm_event),
+                                          cancellable,
+                                          NULL, NULL,
+                                          message_event_get_file_stream_cb, task);
+  } else {
+    g_autofree char *path = NULL;
+
+    if (protocol == CHATTY_PROTOCOL_MMS_SMS || protocol == CHATTY_PROTOCOL_MMS)
+      path = g_build_filename (g_get_user_data_dir (), "chatty", file->path, NULL);
+    else
+      path = g_build_filename (g_get_user_cache_dir (), "chatty", file->path, NULL);
+
+    if (!file->file)
+      file->file = g_file_new_for_path (path);
+
+    file->file_stream = (gpointer)g_file_read (file->file, NULL, NULL);
+    g_task_return_pointer (task, g_object_ref (file->file_stream), g_object_unref);
+
+    file->status = CHATTY_FILE_DOWNLOADED;
+    chatty_message_emit_updated (self);
+  }
+}
+
+GInputStream *
+chatty_message_get_file_stream_finish (ChattyMessage  *self,
+                                       GAsyncResult   *result,
+                                       GError        **error)
+{
+  g_return_val_if_fail (CHATTY_IS_MESSAGE (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+
 /**
  * chatty_message_set_files:
  * @self: A #ChattyMessage
@@ -444,7 +683,8 @@ chatty_message_set_status (ChattyMessage   *self,
 {
   g_return_if_fail (CHATTY_IS_MESSAGE (self));
 
-  self->status = status;
+  g_atomic_int_set (&self->status, status);
+
   if (mtime)
     self->time = mtime;
 
diff --git a/src/chatty-message.h b/src/chatty-message.h
index c924d427238140c0a004a328ab0fa9a170e42f05..8041e8142bc4fe723ff2605feacbda9494c50078 100644
--- a/src/chatty-message.h
+++ b/src/chatty-message.h
@@ -11,6 +11,8 @@
 
 #pragma once
 
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include <cmatrix.h>
 #include <glib-object.h>
 
 #include "chatty-item.h"
@@ -29,6 +31,9 @@ ChattyMessage      *chatty_message_new             (ChattyItem         *user,
                                                     ChattyMsgType       type,
                                                     ChattyMsgDirection  direction,
                                                     ChattyMsgStatus     status);
+ChattyMessage      *chatty_message_new_from_event  (ChattyItem         *user,
+                                                    CmEvent            *event);
+CmEvent            *chatty_message_get_cm_event    (ChattyMessage      *self);
 
 const char         *chatty_message_get_subject     (ChattyMessage      *self);
 void                chatty_message_set_subject     (ChattyMessage      *self,
@@ -39,6 +44,15 @@ void                chatty_message_set_encrypted   (ChattyMessage      *self,
                                                     gboolean            is_encrypted);
 
 GList              *chatty_message_get_files       (ChattyMessage      *self);
+void                chatty_message_get_file_stream_async   (ChattyMessage       *self,
+                                                            ChattyFileInfo      *file,
+                                                            ChattyProtocol       protocol,
+                                                            GCancellable        *cancellable,
+                                                            GAsyncReadyCallback  callback,
+                                                            gpointer             user_data);
+GInputStream       *chatty_message_get_file_stream_finish  (ChattyMessage       *self,
+                                                            GAsyncResult        *result,
+                                                            GError             **error);
 void                chatty_message_set_files       (ChattyMessage      *self,
                                                     GList              *files);
 void                chatty_message_add_file_from_path (ChattyMessage   *self,
diff --git a/src/chatty-secret-store.c b/src/chatty-secret-store.c
index 4e9c8d9b66064366c6901b80e69c1e7c69665c05..2e03bdd6a3ed5ed84eb7652553dba74c338b04b4 100644
--- a/src/chatty-secret-store.c
+++ b/src/chatty-secret-store.c
@@ -43,84 +43,6 @@ secret_store_get_schema (void)
   return &password_schema;
 }
 
-void
-chatty_secret_store_save_async (ChattyAccount       *account,
-                                char                *access_token,
-                                const char          *device_id,
-                                char                *pickle_key,
-                                GCancellable        *cancellable,
-                                GAsyncReadyCallback  callback,
-                                gpointer             user_data)
-{
-  const SecretSchema *schema;
-  g_autofree char *label = NULL;
-  const char *server, *old_pass, *username;
-  char *password = NULL, *token = NULL, *key = NULL;
-  char *credentials;
-
-  /* Currently we support matrix accounts only  */
-  g_return_if_fail (CHATTY_IS_MA_ACCOUNT (account));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  old_pass = chatty_account_get_password (account);
-
-  if (old_pass && *old_pass)
-    password = g_strescape (old_pass, NULL);
-  if (access_token && *access_token)
-    token = g_strescape (access_token, NULL);
-  if (pickle_key && *pickle_key)
-    key = g_strescape (pickle_key, NULL);
-
-  if (!device_id)
-    device_id = "";
-
-  if (CHATTY_IS_MA_ACCOUNT (account))
-    username = chatty_ma_account_get_login_username (CHATTY_MA_ACCOUNT (account));
-  else
-    username = chatty_item_get_username (CHATTY_ITEM (account));
-
-  CHATTY_TRACE (username, "Saving account, has password: %d, has access token: %d"
-                "has device-id: %d",
-                password && *password, token && *token,
-                device_id && *device_id);
-
-  /* We don't use json APIs here so that we can manage memory better (and securely free them)  */
-  /* TODO: Use a non-pageable memory */
-  /* XXX: We use a dumb string search, so don't change the order or spacing of the format string */
-  credentials = g_strdup_printf ("{\"username\": \"%s\",  \"password\": \"%s\","
-                                 "\"access-token\": \"%s\", "
-                                 "\"pickle-key\": \"%s\", \"device-id\": \"%s\"}",
-                                 chatty_item_get_username (CHATTY_ITEM (account)),
-                                 password ? password : "", token ? token : "",
-                                 key ? key : "", device_id);
-  schema = secret_store_get_schema ();
-  server = chatty_ma_account_get_homeserver (CHATTY_MA_ACCOUNT (account));
-  label = g_strdup_printf (_("Chatty password for \"%s\""), username);
-
-  secret_password_store (schema, NULL, label, credentials,
-                         cancellable, callback, user_data,
-                         CHATTY_USERNAME_ATTRIBUTE, username,
-                         CHATTY_SERVER_ATTRIBUTE, server,
-                         CHATTY_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR,
-                         NULL);
-
-  matrix_utils_free_buffer (access_token);
-  matrix_utils_free_buffer (credentials);
-  matrix_utils_free_buffer (pickle_key);
-  matrix_utils_free_buffer (password);
-  matrix_utils_free_buffer (token);
-  matrix_utils_free_buffer (key);
-}
-
-gboolean
-chatty_secret_store_save_finish (GAsyncResult  *result,
-                                 GError       **error)
-{
-  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
-
-  return secret_password_store_finish (result, error);
-}
-
 static void
 secret_load_cb (GObject      *object,
                 GAsyncResult *result,
@@ -142,19 +64,15 @@ secret_load_cb (GObject      *object,
   }
 
   for (GList *item = secrets; item; item = item->next) {
-    ChattyMaAccount *account;
-
     if (!accounts)
       accounts = g_ptr_array_new_full (5, g_object_unref);
 
-    account = chatty_ma_account_new_secret (item->data);
-
-    if (account)
-      g_ptr_array_insert (accounts, -1, account);
+    if (item->data)
+      g_ptr_array_insert (accounts, -1, item->data);
   }
 
   if (secrets)
-    g_list_free_full (secrets, g_object_unref);
+    g_list_free (secrets);
 
   g_task_return_pointer (task, accounts, (GDestroyNotify)g_ptr_array_unref);
 }
diff --git a/src/chatty-secret-store.h b/src/chatty-secret-store.h
index 7e54b5704fef8848f5ae486a8f5fc796420dca68..d5da5f8052366aba0f1bd269f02648a8208b588c 100644
--- a/src/chatty-secret-store.h
+++ b/src/chatty-secret-store.h
@@ -21,16 +21,6 @@
 
 G_BEGIN_DECLS
 
-void       chatty_secret_store_save_async  (ChattyAccount       *account,
-                                            char                *access_token,
-                                            const char          *device_id,
-                                            char                *pickle_key,
-                                            GCancellable        *cancellable,
-                                            GAsyncReadyCallback  callback,
-                                            gpointer             user_data);
-gboolean   chatty_secret_store_save_finish (GAsyncResult        *result,
-                                            GError             **error);
-
 void       chatty_secret_load_async        (GCancellable        *cancellable,
                                             GAsyncReadyCallback  callback,
                                             gpointer             user_data);
diff --git a/src/chatty-text-item.c b/src/chatty-text-item.c
index 4e74aaf923ebc3bef02c9a939280b3b60f5f0d2b..58f99b682ed5718817eff8eb4b3cb821d83a8ce7 100644
--- a/src/chatty-text-item.c
+++ b/src/chatty-text-item.c
@@ -221,13 +221,11 @@ static void
 text_item_update_message (ChattyTextItem *self)
 {
   g_autoptr(GString) str = NULL;
-  ChattySettings *settings;
   const char *text;
 
   g_assert (CHATTY_IS_TEXT_ITEM (self));
   g_assert (self->message);
 
-  settings = chatty_settings_get_default ();
   text = chatty_message_get_text (self->message);
   str = g_string_sized_new (256);
 
@@ -259,8 +257,7 @@ text_item_update_message (ChattyTextItem *self)
     }
   }
 
-  if ((self->protocol == CHATTY_PROTOCOL_MATRIX &&
-       chatty_settings_get_experimental_features (settings)) ||
+  if (self->protocol == CHATTY_PROTOCOL_MATRIX ||
       self->protocol & (CHATTY_PROTOCOL_MMS_SMS | CHATTY_PROTOCOL_MMS)) {
     g_autofree char *content = NULL;
     const char *subject;
diff --git a/src/chatty-utils.c b/src/chatty-utils.c
index c95d938c24bdd7286096778f0bfe062260a59c44..ce83df4d9833ada024475f279c222963a25306be 100644
--- a/src/chatty-utils.c
+++ b/src/chatty-utils.c
@@ -422,6 +422,8 @@ chatty_file_info_free (ChattyFileInfo *file_info)
   if (!file_info)
     return;
 
+  g_clear_object (&file_info->file);
+  g_clear_object (&file_info->file_stream);
   g_free (file_info->file_name);
   g_free (file_info->url);
   g_free (file_info->path);
@@ -550,6 +552,26 @@ utils_create_thumbnail (GTask        *task,
     return;
   }
 
+#if defined(GNOME_DESKTOP_PLATFORM_VERSION) && GNOME_DESKTOP_PLATFORM_VERSION >= 43
+  thumbnail = gnome_desktop_thumbnail_factory_generate_thumbnail (factory, uri, content_type, NULL, &error);
+  if (!thumbnail) {
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to create thumbnail for file: %s (%s)", uri, error->message);
+
+    g_warning ("Failed to create thumbnail for file: %s", uri);
+
+    g_error_free (error);
+    return;
+  }
+
+  gnome_desktop_thumbnail_factory_save_thumbnail (factory, thumbnail, uri, mtime, NULL, &error);
+  if (error) {
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to create thumbnail for file: %s (%s)", uri, error->message);
+    g_error_free (error);
+    return;
+  }
+
+  g_task_return_boolean (task, TRUE);
+#else
   thumbnail = gnome_desktop_thumbnail_factory_generate_thumbnail (factory, uri, content_type);
 
   if (thumbnail) {
@@ -562,6 +584,7 @@ utils_create_thumbnail (GTask        *task,
   }
 
   g_task_return_boolean (task, TRUE);
+#endif
 }
 
 void
diff --git a/src/chatty-utils.h b/src/chatty-utils.h
index 9b7e95f7f881b8dfafe0e9c0e746a1e10beb5dfc..4527251749c3b8d92541a28643fbc58ed2ecba04 100644
--- a/src/chatty-utils.h
+++ b/src/chatty-utils.h
@@ -21,7 +21,8 @@ struct _ChattyFileInfo {
   char *url;
   char *path;
   char *mime_type;
-  gpointer user_data;
+  GFile        *file;
+  GInputStream *file_stream;
   gsize width;
   gsize height;
   gsize size;
diff --git a/src/chatty-verification-view.c b/src/chatty-verification-view.c
new file mode 100644
index 0000000000000000000000000000000000000000..282ebdefb887242e5fc1c50f588f738d25acc9ff
--- /dev/null
+++ b/src/chatty-verification-view.c
@@ -0,0 +1,508 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-verification-view.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "chatty-verification-view"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib/gi18n.h>
+
+#include "chatty-manager.h"
+#include "chatty-avatar.h"
+#include "chatty-ma-key-chat.h"
+#include "chatty-verification-view.h"
+
+static const char *emojis[][2] = {
+  /* TRANSLATORS: You may copy translations from https://github.com/matrix-org/matrix-spec-proposals/blob/old_master/data-definitions/sas-emoji.json
+   * if available */
+  {"🐶", N_("Dog")}, /* "U+1F436" */
+  {"🐱", N_("Cat")}, /* "U+1F431" */
+  {"🦁", N_("Lion")}, /* "U+1F981" */
+  {"🐎", N_("Horse")}, /* "U+1F40E" */
+  {"🦄", N_("Unicorn")}, /* "U+1F984" */
+  {"🐷", N_("Pig")}, /* "U+1F437" */
+  {"🐘", N_("Elephant")}, /* "U+1F418" */
+  {"🐰", N_("Rabbit")}, /* "U+1F430" */
+  {"🐼", N_("Panda")}, /* "U+1F43C" */
+  {"🐓", N_("Rooster")}, /* "U+1F413" */
+  {"🐧", N_("Penguin")}, /* "U+1F427" */
+  {"🐢", N_("Turtle")}, /* "U+1F422" */
+  {"🐟", N_("Fish")}, /* "U+1F41F" */
+  {"🐙", N_("Octopus")}, /* "U+1F419" */
+  {"🦋", N_("Butterfly")}, /* "U+1F98B" */
+  {"🌷", N_("Flower")}, /* "U+1F337" */
+  {"🌳", N_("Tree")}, /* "U+1F333" */
+  {"🌵", N_("Cactus")}, /* "U+1F335" */
+  {"🍄", N_("Mushroom")}, /* "U+1F344" */
+  {"🌏", N_("Globe")}, /* "U+1F30F" */
+  {"🌙", N_("Moon")}, /* "U+1F319" */
+  {"☁️", N_("Cloud")}, /* "U+2601U+FE0F" */
+  {"🔥", N_("Fire")}, /* "U+1F525" */
+  {"🍌", N_("Banana")}, /* "U+1F34C" */
+  {"🍎", N_("Apple")}, /* "U+1F34E" */
+  {"🍓", N_("Strawberry")}, /* "U+1F353" */
+  {"🌽", N_("Corn")}, /* "U+1F33D" */
+  {"🍕", N_("Pizza")}, /* "U+1F355" */
+  {"🎂", N_("Cake")}, /* "U+1F382" */
+  {"❤️", N_("Heart")}, /* "U+2764U+FE0F" */
+  {"😀", N_("Smiley")}, /* "U+1F600" */
+  {"🤖", N_("Robot")}, /* "U+1F916" */
+  {"🎩", N_("Hat")}, /* "U+1F3A9" */
+  {"👓", N_("Glasses")}, /* "U+1F453" */
+  {"🔧", N_("Spanner")}, /* "U+1F527" */
+  {"🎅", N_("Santa")}, /* "U+1F385" */
+  {"👍", N_("Thumbs Up")}, /* "U+1F44D" */
+  {"☂️", N_("Umbrella")}, /* "U+2602U+FE0F" */
+  {"⌛", N_("Hourglass")}, /* "U+231B" */
+  {"⏰", N_("Clock")}, /* "U+23F0" */
+  {"🎁", N_("Gift")}, /* "U+1F381" */
+  {"💡", N_("Light Bulb")}, /* "U+1F4A1" */
+  {"📕", N_("Book")}, /* "U+1F4D5" */
+  {"✏️", N_("Pencil")}, /* "U+270FU+FE0F" */
+  {"📎", N_("Paperclip")}, /* "U+1F4CE" */
+  {"✂️", N_("Scissors")}, /* "U+2702U+FE0F" */
+  {"🔒", N_("Lock")}, /* "U+1F512" */
+  {"🔑", N_("Key")}, /* "U+1F511" */
+  {"🔨", N_("Hammer")}, /* "U+1F528" */
+  {"☎️", N_("Telephone")}, /* "U+260EU+FE0F" */
+  {"🏁", N_("Flag")}, /* "U+1F3C1" */
+  {"🚂", N_("Train")}, /* "U+1F682" */
+  {"🚲", N_("Bicycle")}, /* "U+1F6B2" */
+  {"✈️", N_("Aeroplane")}, /* "U+2708U+FE0F" */
+  {"🚀", N_("Rocket")}, /* "U+1F680" */
+  {"🏆", N_("Trophy")}, /* "U+1F3C6" */
+  {"âš½", N_("Ball")}, /* "U+26BD" */
+  {"🎸", N_("Guitar")}, /* "U+1F3B8" */
+  {"🎺", N_("Trumpet")}, /* "U+1F3BA" */
+  {"🔔", N_("Bell")}, /* "U+1F514" */
+  {"âš“", N_("Anchor")}, /* "U+2693" */
+  {"🎧", N_("Headphones")}, /* "U+1F3A7" */
+  {"📁", N_("Folder")}, /* "U+1F4C1" */
+  {"📌", N_("Pin")}, /* "U+1F4CC" */
+};
+
+struct _ChattyVerificationView
+{
+  GtkBox            parent_instance;
+
+  GtkWidget        *user_avatar;
+  GtkWidget        *name_label;
+  GtkWidget        *username_label;
+
+  GtkWidget        *continue_button;
+  GtkWidget        *continue_spinner;
+  GtkWidget        *cancel_button;
+  GtkWidget        *cancel_spinner;
+
+  GtkWidget        *verification_dialog;
+  GtkWidget        *verification_type_button;
+  GtkWidget        *content_stack;
+
+  GtkWidget        *decimal_content;
+  GtkWidget        *decimal1_label;
+  GtkWidget        *decimal2_label;
+  GtkWidget        *decimal3_label;
+
+  GtkWidget        *emoji_content;
+  GtkWidget        *emoji1_label;
+  GtkWidget        *emoji1_title;
+  GtkWidget        *emoji2_label;
+  GtkWidget        *emoji2_title;
+  GtkWidget        *emoji3_label;
+  GtkWidget        *emoji3_title;
+  GtkWidget        *emoji4_label;
+  GtkWidget        *emoji4_title;
+  GtkWidget        *emoji5_label;
+  GtkWidget        *emoji5_title;
+  GtkWidget        *emoji6_label;
+  GtkWidget        *emoji6_title;
+  GtkWidget        *emoji7_label;
+  GtkWidget        *emoji7_title;
+
+  ChattyMaKeyChat *item;
+
+  GBinding        *name_binding;
+
+  gulong           update_handler;
+  gulong           delete_handler;
+
+  gboolean         emoji_set;
+  gboolean         emoji_shown;
+};
+
+G_DEFINE_TYPE (ChattyVerificationView, chatty_verification_view, GTK_TYPE_BOX)
+
+static void
+show_verification_dailog (ChattyVerificationView *self)
+{
+  GtkWidget *window;
+
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+  gtk_window_set_transient_for (GTK_WINDOW (self->verification_dialog), GTK_WINDOW (window));
+  gtk_window_present (GTK_WINDOW (self->verification_dialog));
+  gtk_spinner_stop (GTK_SPINNER (self->continue_spinner));
+
+  self->emoji_shown = TRUE;
+}
+
+static void
+verification_update_emoji (ChattyVerificationView *self)
+{
+  g_autoptr(GPtrArray) labels = NULL;
+  g_autoptr(GPtrArray) titles = NULL;
+  char *decimal_str;
+  GPtrArray *emoji;
+  guint16 *decimal;
+
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  if (self->emoji_set)
+    return;
+
+  emoji = chatty_ma_key_get_emoji (self->item);
+  decimal = chatty_ma_key_get_decimal (self->item);
+
+  if (!emoji || !decimal)
+    return;
+
+  self->emoji_set = TRUE;
+  labels = g_ptr_array_new ();
+  g_ptr_array_add (labels, self->emoji1_label);
+  g_ptr_array_add (labels, self->emoji2_label);
+  g_ptr_array_add (labels, self->emoji3_label);
+  g_ptr_array_add (labels, self->emoji4_label);
+  g_ptr_array_add (labels, self->emoji5_label);
+  g_ptr_array_add (labels, self->emoji6_label);
+  g_ptr_array_add (labels, self->emoji7_label);
+
+  titles = g_ptr_array_new ();
+  g_ptr_array_add (titles, self->emoji1_title);
+  g_ptr_array_add (titles, self->emoji2_title);
+  g_ptr_array_add (titles, self->emoji3_title);
+  g_ptr_array_add (titles, self->emoji4_title);
+  g_ptr_array_add (titles, self->emoji5_title);
+  g_ptr_array_add (titles, self->emoji6_title);
+  g_ptr_array_add (titles, self->emoji7_title);
+
+  for (guint i = 0; i < emoji->len; i++) {
+    GtkLabel *label_widget, *title_widget;
+    const char *title = "";
+    char *label;
+
+    label = emoji->pdata[i];
+
+    for (guint j = 0; j < G_N_ELEMENTS (emojis); j++) {
+      if (g_strcmp0 (label, emojis[j][0]) != 0)
+        continue;
+
+      title = emojis[j][1];
+      break;
+    }
+
+    label_widget = labels->pdata[i];
+    title_widget = titles->pdata[i];
+
+    gtk_label_set_label (label_widget, label);
+    gtk_label_set_label (title_widget, gettext (title));
+  }
+
+  decimal_str = g_strdup_printf ("%4u", (int)decimal[0]);
+  gtk_label_set_label (GTK_LABEL (self->decimal1_label), decimal_str);
+  g_free (decimal_str);
+
+  decimal_str = g_strdup_printf ("%4u", (int)decimal[1]);
+  gtk_label_set_label (GTK_LABEL (self->decimal2_label), decimal_str);
+  g_free (decimal_str);
+
+  decimal_str = g_strdup_printf ("%4u", (int)decimal[2]);
+  gtk_label_set_label (GTK_LABEL (self->decimal3_label), decimal_str);
+  g_free (decimal_str);
+}
+
+static void
+verification_item_updated_cb (ChattyVerificationView *self)
+{
+  CmEvent *event;
+
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  event = chatty_ma_key_chat_get_event (self->item);
+
+  if (g_object_get_data (G_OBJECT (event), "cancel") ||
+      g_object_get_data (G_OBJECT (event), "completed")) {
+    ChattyManager *manager;
+
+    manager = chatty_manager_get_default ();
+    g_signal_emit_by_name (manager, "chat-deleted", self->item);
+    return;
+  }
+
+  if (!self->emoji_shown &&
+      g_object_get_data (G_OBJECT (event), "key")) {
+    verification_update_emoji (self);
+    show_verification_dailog (self);
+  }
+}
+
+static void
+verification_item_deleted_cb (ChattyVerificationView *self)
+{
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+}
+
+static void
+verification_key_continue_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  g_autoptr(ChattyVerificationView) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (CHATTY_IS_MA_KEY_CHAT (object));
+
+  chatty_ma_key_cancel_finish (CHATTY_MA_KEY_CHAT (object), result, &error);
+
+  if (error) {
+    gtk_spinner_stop (GTK_SPINNER (self->continue_spinner));
+    gtk_widget_set_sensitive (self->continue_button, TRUE);
+    g_warning ("Error: %s", error->message);
+  }
+}
+
+static void
+verification_continue_clicked_cb (ChattyVerificationView *self)
+{
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  gtk_spinner_start (GTK_SPINNER (self->continue_spinner));
+  gtk_widget_set_sensitive (self->continue_button, FALSE);
+
+  chatty_ma_key_accept_async (CHATTY_MA_KEY_CHAT (self->item),
+                              verification_key_continue_cb,
+                              g_object_ref (self));
+}
+
+static void
+verification_key_cancel_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  g_autoptr(ChattyVerificationView) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (CHATTY_IS_MA_KEY_CHAT (object));
+
+  gtk_spinner_stop (GTK_SPINNER (self->cancel_spinner));
+  if (chatty_ma_key_cancel_finish (CHATTY_MA_KEY_CHAT (object), result, &error)) {
+    ChattyManager *manager;
+
+    manager = chatty_manager_get_default ();
+    g_signal_emit_by_name (manager, "chat-deleted", object);
+  }
+
+  if (error)
+    g_warning ("Error: %s", error->message);
+}
+
+static void
+verification_cancel_clicked_cb (ChattyVerificationView *self,
+                                GtkWidget              *widget)
+{
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  gtk_spinner_start (GTK_SPINNER (self->cancel_spinner));
+  gtk_widget_set_sensitive (self->continue_button, FALSE);
+  gtk_widget_hide (self->verification_dialog);
+
+  chatty_ma_key_cancel_async (CHATTY_MA_KEY_CHAT (self->item),
+                              verification_key_cancel_cb,
+                              g_object_ref (self));
+}
+
+static void
+verification_key_match_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  g_autoptr(ChattyVerificationView) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (CHATTY_IS_MA_KEY_CHAT (object));
+
+  gtk_widget_hide (self->verification_dialog);
+  chatty_ma_key_match_finish (CHATTY_MA_KEY_CHAT (object), result, &error);
+
+  if (error)
+    g_warning ("Error: %s", error->message);
+}
+
+static void
+verification_type_clicked_cb (ChattyVerificationView *self)
+{
+  GtkWidget *visible_child;
+
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  visible_child = gtk_stack_get_visible_child (GTK_STACK (self->content_stack));
+
+  if (visible_child == self->decimal_content)
+    visible_child = self->emoji_content;
+  else
+    visible_child = self->decimal_content;
+
+  gtk_stack_set_visible_child (GTK_STACK (self->content_stack), visible_child);
+}
+
+static void
+verification_match_clicked_cb (ChattyVerificationView *self)
+{
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  gtk_widget_set_sensitive (self->continue_button, FALSE);
+  gtk_spinner_start (GTK_SPINNER (self->continue_spinner));
+  chatty_ma_key_match_async (CHATTY_MA_KEY_CHAT (self->item),
+                             verification_key_match_cb,
+                             g_object_ref (self));
+}
+
+static void
+verification_content_child_changed_cb (ChattyVerificationView *self)
+{
+  const char *button_label;
+
+  g_assert (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  if (gtk_stack_get_visible_child (GTK_STACK (self->content_stack)) == self->decimal_content)
+    button_label = _("Show Emojis");
+  else
+    button_label = _("Show Numbers");
+
+  gtk_button_set_label (GTK_BUTTON (self->verification_type_button), button_label);
+}
+
+static void
+chatty_verification_view_dispose (GObject *object)
+{
+  ChattyVerificationView *self = (ChattyVerificationView *)object;
+
+  g_clear_object (&self->item);
+  g_clear_object (&self->name_binding);
+
+  G_OBJECT_CLASS (chatty_verification_view_parent_class)->dispose (object);
+}
+
+static void
+chatty_verification_view_class_init (ChattyVerificationViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = chatty_verification_view_dispose;
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/Chatty/"
+                                               "ui/chatty-verification-view.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, user_avatar);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, name_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, username_label);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, continue_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, continue_spinner);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, cancel_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, cancel_spinner);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, verification_dialog);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, verification_type_button);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, content_stack);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal_content);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal1_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal2_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, decimal3_label);
+
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji_content);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji1_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji1_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji2_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji2_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji3_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji3_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji4_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji4_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji5_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji5_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji6_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji6_title);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji7_label);
+  gtk_widget_class_bind_template_child (widget_class, ChattyVerificationView, emoji7_title);
+
+  gtk_widget_class_bind_template_callback (widget_class, verification_continue_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, verification_cancel_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, verification_type_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, verification_match_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, verification_content_child_changed_cb);
+}
+
+static void
+chatty_verification_view_init (ChattyVerificationView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+  verification_content_child_changed_cb (self);
+}
+
+void
+chatty_verification_view_set_item (ChattyVerificationView *self,
+                                   ChattyItem             *item)
+{
+  ChattyItem *sender;
+
+  g_return_if_fail (CHATTY_IS_VERIFICATION_VIEW (self));
+
+  if (self->item) {
+    g_clear_object (&self->name_binding);
+    chatty_avatar_set_item (CHATTY_AVATAR (self->user_avatar), NULL);
+    g_clear_signal_handler (&self->update_handler, self->item);
+    g_clear_signal_handler (&self->delete_handler, self->item);
+    gtk_spinner_stop (GTK_SPINNER (self->continue_spinner));
+    gtk_spinner_stop (GTK_SPINNER (self->cancel_spinner));
+    gtk_widget_hide (self->verification_dialog);
+    g_clear_object (&self->item);
+  }
+
+  if (!CHATTY_IS_MA_KEY_CHAT (item))
+    return;
+
+  g_set_object (&self->item, (ChattyMaKeyChat *)item);
+
+  if (!item)
+    return;
+
+  gtk_widget_set_sensitive (self->continue_button, TRUE);
+  self->update_handler = g_signal_connect_object (item, "changed",
+                                                  G_CALLBACK (verification_item_updated_cb),
+                                                  self, G_CONNECT_SWAPPED);
+  self->delete_handler = g_signal_connect_object (item, "deleted",
+                                                  G_CALLBACK (verification_item_deleted_cb),
+                                                  self, G_CONNECT_SWAPPED);
+  sender = chatty_ma_key_chat_get_sender (CHATTY_MA_KEY_CHAT (item));
+  chatty_avatar_set_item (CHATTY_AVATAR (self->user_avatar), sender);
+
+  self->name_binding = g_object_bind_property (sender, "name",
+                                               self->name_label, "label",
+                                               G_BINDING_SYNC_CREATE);
+  gtk_label_set_label (GTK_LABEL (self->username_label),
+                       chatty_item_get_username (CHATTY_ITEM (sender)));
+}
diff --git a/src/chatty-verification-view.h b/src/chatty-verification-view.h
new file mode 100644
index 0000000000000000000000000000000000000000..b6f8ab664ff4f7bc232ef28b842f22839f8ebfc6
--- /dev/null
+++ b/src/chatty-verification-view.h
@@ -0,0 +1,28 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-verification-view.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "chatty-item.h"
+
+G_BEGIN_DECLS
+
+#define CHATTY_TYPE_VERIFICATION_VIEW (chatty_verification_view_get_type ())
+
+G_DECLARE_FINAL_TYPE (ChattyVerificationView, chatty_verification_view, CHATTY, VERIFICATION_VIEW, GtkBox)
+
+void        chatty_verification_view_set_item     (ChattyVerificationView *self,
+                                                   ChattyItem             *item);
+
+G_END_DECLS
+
diff --git a/src/chatty-window.c b/src/chatty-window.c
index 9e1439bb6cb2033c96024ffb2f99efa26b0742fb..be4328a0e60d37a34dd531d38c4e2acad904f8ae 100644
--- a/src/chatty-window.c
+++ b/src/chatty-window.c
@@ -19,6 +19,7 @@
 #include <libgd/gd.h>
 #include "contrib/gtk.h"
 
+#include "chatty-header-bar.h"
 #include "chatty-window.h"
 #include "chatty-history.h"
 #include "chatty-avatar.h"
@@ -28,7 +29,7 @@
 #include "chatty-settings.h"
 #include "chatty-mm-chat.h"
 #include "chatty-chat-list.h"
-#include "chatty-chat-view.h"
+#include "chatty-main-view.h"
 #include "chatty-manager.h"
 #include "chatty-utils.h"
 #include "chatty-selectable-row.h"
@@ -48,46 +49,23 @@ struct _ChattyWindow
 
   GtkWidget *chat_list;
 
+  GtkWidget *header_bar;
   GtkWidget *content_box;
+  GtkWidget *content_view;
   GtkWidget *sidebar;
-  GtkWidget *header_box;
-  GtkWidget *header_group;
-
-  GtkWidget *sub_header_icon;
-  GtkWidget *sub_header_label;
 
   GtkWidget *new_chat_dialog;
   GtkWidget *chat_info_dialog;
 
-  GtkWidget *search_button;
   GtkWidget *chats_search_bar;
   GtkWidget *chats_search_entry;
 
-  GtkWidget *header_chat_list_new_msg_popover;
-
-  GtkWidget *menu_new_message_button;
-  GtkWidget *menu_new_sms_mms_message_button;
-  GtkWidget *menu_new_group_message_button;
-  GtkWidget *header_bar;
-  GtkWidget *header_back_button;
-  GtkWidget *header_add_chat_button;
-  GtkWidget *call_button;
-  GtkWidget *header_sub_menu_button;
-  GtkWidget *leave_button;
-  GtkWidget *delete_button;
-  GtkWidget *block_button;
-  GtkWidget *unblock_button;
-  GtkWidget *archive_button;
-  GtkWidget *unarchive_button;
-
-  GtkWidget *chat_view;
   GtkWidget *settings_dialog;
 
   GtkWidget *protocol_list;
   GtkWidget *protocol_any_row;
   gulong     chat_changed_handler;
 
-  GBinding  *header_label_binding;
   GdTaggedEntryTag *protocol_tag;
 
   ChattyManager *manager;
@@ -99,29 +77,6 @@ struct _ChattyWindow
 
 G_DEFINE_TYPE (ChattyWindow, chatty_window, HDY_TYPE_APPLICATION_WINDOW)
 
-static void
-window_update_item_state_button (ChattyWindow *self,
-                                 ChattyItem   *item)
-{
-  ChattyItemState state;
-
-  state = chatty_item_get_state (item);
-
-  gtk_widget_hide (self->block_button);
-  gtk_widget_hide (self->unblock_button);
-  gtk_widget_hide (self->archive_button);
-  gtk_widget_hide (self->unarchive_button);
-
-  if (state == CHATTY_ITEM_VISIBLE) {
-    gtk_widget_show (self->block_button);
-    gtk_widget_show (self->archive_button);
-  } else if (state == CHATTY_ITEM_ARCHIVED) {
-    gtk_widget_show (self->unarchive_button);
-  } else if (state == CHATTY_ITEM_BLOCKED) {
-    gtk_widget_show (self->unblock_button);
-  }
-}
-
 static void
 window_chat_changed_cb (ChattyWindow *self,
                         ChattyChat   *chat)
@@ -132,8 +87,6 @@ window_chat_changed_cb (ChattyWindow *self,
 
   /* allow changing state only for 1:1 SMS/MMS chats  */
   if (CHATTY_IS_MM_CHAT (chat) && g_list_model_get_n_items (users) == 1) {
-    if (chat == chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)))
-      window_update_item_state_button (self, CHATTY_ITEM (chat));
     chatty_chat_list_refilter (CHATTY_CHAT_LIST (self->chat_list));
   }
 }
@@ -144,16 +97,10 @@ window_set_item (ChattyWindow *self,
 {
   g_assert (CHATTY_IS_WINDOW (self));
 
-  chatty_avatar_set_item (CHATTY_AVATAR (self->sub_header_icon), CHATTY_ITEM (chat));
-  g_clear_object (&self->header_label_binding);
-  gtk_label_set_label (GTK_LABEL (self->sub_header_label), "");
   g_clear_signal_handler (&self->chat_changed_handler,
-                          chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)));
+                          chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)));
 
   if (CHATTY_IS_CHAT (chat)) {
-    self->header_label_binding = g_object_bind_property (chat, "name",
-                                                         self->sub_header_label, "label",
-                                                         G_BINDING_SYNC_CREATE);
     self->chat_changed_handler = g_signal_connect_object (chat, "changed",
                                                           G_CALLBACK (window_chat_changed_cb),
                                                           self, G_CONNECT_SWAPPED);
@@ -162,8 +109,8 @@ window_set_item (ChattyWindow *self,
   if (!chat)
     hdy_leaflet_set_visible_child_name (HDY_LEAFLET (self->content_box), "sidebar");
 
-  chatty_chat_view_set_chat (CHATTY_CHAT_VIEW (self->chat_view), chat);
-  gtk_widget_set_visible (self->header_sub_menu_button, !!chat);
+  chatty_header_bar_set_item (CHATTY_HEADER_BAR (self->header_bar), CHATTY_ITEM (chat));
+  chatty_main_view_set_item (CHATTY_MAIN_VIEW (self->content_view), CHATTY_ITEM (chat));
 }
 
 static void
@@ -177,7 +124,7 @@ chatty_window_update_search_mode (ChattyWindow *self)
   model = chatty_chat_list_get_filter_model (CHATTY_CHAT_LIST (self->chat_list));
   has_child = g_list_model_get_n_items (model) > 0;
 
-  gtk_widget_set_visible (self->search_button, has_child);
+  chatty_header_bar_set_can_search (CHATTY_HEADER_BAR (self->header_bar), has_child);
 
   if (!has_child)
     hdy_search_bar_set_search_mode (HDY_SEARCH_BAR (self->chats_search_bar), FALSE);
@@ -213,22 +160,18 @@ chatty_window_open_item (ChattyWindow *self,
 }
 
 static void
-window_call_button_clicked_cb (ChattyWindow *self)
+window_search_enable_changed_cb (ChattyWindow *self)
 {
-  g_autoptr(GError) error = NULL;
-  g_autofree char *uri = NULL;
-  ChattyChat *chat;
+  gboolean enabled;
 
   g_assert (CHATTY_IS_WINDOW (self));
 
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
-  g_return_if_fail (CHATTY_IS_MM_CHAT (chat));
+  enabled = hdy_search_bar_get_search_mode (HDY_SEARCH_BAR (self->chats_search_bar));
 
-  uri = g_strconcat ("tel://", chatty_chat_get_chat_name (chat), NULL);
-
-  CHATTY_INFO (uri, "Calling uri:");
-  if (!gtk_show_uri_on_window (NULL, uri, GDK_CURRENT_TIME, &error))
-    g_warning ("Failed to launch call: %s", error->message);
+  /* Reset protocol filter */
+  if (!enabled &&
+      self->protocol_any_row != self->selected_protocol_row)
+    gtk_widget_activate (self->protocol_any_row);
 }
 
 static void
@@ -253,17 +196,6 @@ search_tag_button_clicked_cb (ChattyWindow     *self,
   gtk_widget_activate (self->protocol_any_row);
 }
 
-static void
-window_search_toggled_cb (ChattyWindow    *self,
-                          GtkToggleButton *button)
-{
-  g_assert (CHATTY_IS_WINDOW (self));
-
-  if (!gtk_toggle_button_get_active (button) &&
-      self->protocol_any_row != self->selected_protocol_row)
-    gtk_widget_activate (self->protocol_any_row);
-}
-
 static void
 window_chat_list_selection_changed (ChattyWindow   *self,
                                     ChattyChatList *list)
@@ -281,8 +213,8 @@ window_chat_list_selection_changed (ChattyWindow   *self,
 
     model = chatty_chat_list_get_filter_model (CHATTY_CHAT_LIST (self->chat_list));
     if (g_list_model_get_n_items (model) == 0) {
-      chatty_chat_view_set_chat (CHATTY_CHAT_VIEW (self->chat_view), NULL);
-      gtk_widget_hide (self->header_sub_menu_button);
+      chatty_header_bar_set_item (CHATTY_HEADER_BAR (self->header_bar), NULL);
+      chatty_main_view_set_item (CHATTY_MAIN_VIEW (self->content_view), NULL);
     }
 
     return;
@@ -291,7 +223,7 @@ window_chat_list_selection_changed (ChattyWindow   *self,
   chat = chat_list->pdata[0];
   g_return_if_fail (CHATTY_IS_CHAT (chat));
 
-  if (chat == chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)))
+  if (chat == (gpointer)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)))
     return;
 
 #ifdef PURPLE_ENABLED
@@ -315,7 +247,7 @@ notify_fold_cb (ChattyWindow *self)
 {
   gboolean folded;
 
-  folded = hdy_leaflet_get_folded (HDY_LEAFLET (self->header_box));
+  folded = hdy_leaflet_get_folded (HDY_LEAFLET (self->content_box));
   chatty_chat_list_set_selection_mode (CHATTY_CHAT_LIST (self->chat_list), !folded);
 
   if (folded) {
@@ -331,17 +263,6 @@ window_content_box_changed (ChattyWindow *self)
     window_set_item (self, NULL);
   }
 }
-static void
-window_show_unarchived_clicked_cb (ChattyWindow *self)
-{
-  g_assert (CHATTY_IS_WINDOW (self));
-
-  hdy_header_bar_set_title (HDY_HEADER_BAR (self->header_bar), _("Chats"));
-  chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), FALSE);
-  gtk_widget_hide (self->header_back_button);
-  gtk_widget_show (self->header_add_chat_button);
-  chatty_window_update_search_mode (self);
-}
 
 static void
 window_show_new_chat_dialog (ChattyWindow *self,
@@ -357,39 +278,225 @@ window_show_new_chat_dialog (ChattyWindow *self,
 }
 
 static void
-window_new_message_clicked_cb (ChattyWindow *self)
+window_back_clicked_cb (ChattyWindow *self)
 {
-  window_show_new_chat_dialog (self, FALSE);
+  g_assert (CHATTY_IS_WINDOW (self));
+
+  if (chatty_chat_list_is_archived (CHATTY_CHAT_LIST (self->chat_list))) {
+    chatty_header_bar_show_archived (CHATTY_HEADER_BAR (self->header_bar), FALSE);
+    chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), FALSE);
+    chatty_window_update_search_mode (self);
+  } else {
+    window_set_item (self, NULL);
+  }
 }
 
 static void
-window_new_sms_mms_message_clicked_cb (ChattyWindow *self)
+window_search_protocol_changed_cb (ChattyWindow *self,
+                                   GtkWidget    *selected_row,
+                                   GtkListBox   *box)
 {
-  window_show_new_chat_dialog (self, TRUE);
+  GdTaggedEntry *entry;
+  GtkWidget *old_row;
+
+  g_assert (CHATTY_IS_WINDOW (self));
+  g_assert (GTK_IS_LIST_BOX (box));
+
+  entry = GD_TAGGED_ENTRY (self->chats_search_entry);
+  old_row = self->selected_protocol_row;
+
+  if (old_row == selected_row)
+    return;
+
+  self->protocol_filter = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (selected_row), "protocol"));
+  chatty_chat_list_filter_protocol (CHATTY_CHAT_LIST (self->chat_list), self->protocol_filter);
+  chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (old_row), FALSE);
+  chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (selected_row), TRUE);
+  self->selected_protocol_row = selected_row;
+
+  if (selected_row == self->protocol_any_row) {
+    gd_tagged_entry_remove_tag (entry, self->protocol_tag);
+  } else {
+    const char *title;
+
+    gd_tagged_entry_add_tag (entry, self->protocol_tag);
+    title = chatty_selectable_row_get_title (CHATTY_SELECTABLE_ROW (selected_row));
+    gd_tagged_entry_tag_set_label (self->protocol_tag, title);
+  }
+}
+
+static void
+window_chat_deleted_cb (ChattyWindow *self,
+                        ChattyChat   *chat)
+{
+  g_assert (CHATTY_IS_WINDOW (self));
+  g_assert (CHATTY_IS_CHAT (chat));
+
+  if (chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)) != (gpointer)chat)
+    return;
+
+  window_set_item (self, NULL);
 }
 
 static void
-window_new_muc_clicked_cb (ChattyWindow *self)
+protocol_list_header_func (GtkListBoxRow *row,
+                           GtkListBoxRow *before,
+                           gpointer       user_data)
 {
-  GtkWidget *dialog;
+  if (!before) {
+    gtk_list_box_row_set_header (row, NULL);
+  } else if (!gtk_list_box_row_get_header (row)) {
+    GtkWidget *separator;
+
+    separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
+    gtk_widget_show (separator);
+    gtk_list_box_row_set_header (row, separator);
+  }
+}
+
+static void
+new_chat_selection_changed_cb (ChattyWindow        *self,
+                               ChattyNewChatDialog *dialog)
+{
+  g_autoptr(GString) users = g_string_new (NULL);
+  GListModel *model;
+  guint n_items;
+  const char *name;
 
   g_assert (CHATTY_IS_WINDOW (self));
+  g_assert (CHATTY_IS_NEW_CHAT_DIALOG (dialog));
 
-  dialog = chatty_new_muc_dialog_new (GTK_WINDOW (self));
-  gtk_window_present (GTK_WINDOW (dialog));
+  model = chatty_new_chat_dialog_get_selected_items (dialog);
+  n_items = g_list_model_get_n_items (model);
+
+  if (n_items == 0)
+    goto end;
+
+  for (guint i = 0; i < n_items; i++) {
+    g_autoptr(ChattyItem) item = NULL;
+    const char *phone_number;
+
+    item = g_list_model_get_item (model, i);
+
+    if (CHATTY_IS_CONTACT (item)) {
+      phone_number = chatty_item_get_username (item);
+      g_string_append (users, phone_number);
+      g_string_append (users, ",");
+    }
+  }
+
+  /* Remove the trailing "," */
+  if (users->len >= 1)
+    g_string_truncate (users, users->len - 1);
+
+  if (n_items == 1) {
+    g_autoptr(ChattyItem) item = NULL;
+
+    item = g_list_model_get_item (model, 0);
+
+    if (!CHATTY_IS_CONTACT (item) ||
+        !chatty_contact_is_dummy (CHATTY_CONTACT (item))) {
+      chatty_window_open_item (self, item);
+      goto end;
+    }
+  }
+
+  name = chatty_new_chat_dialog_get_chat_title (dialog);
+  chatty_window_set_uri (self, users->str, name);
+
+ end:
+  gtk_widget_hide (GTK_WIDGET (dialog));
 }
 
 static void
-window_back_clicked_cb (ChattyWindow *self)
+chatty_window_archive_chat (GSimpleAction *action,
+                            GVariant      *parameter,
+                            gpointer       user_data)
 {
+  ChattyWindow *self = user_data;
+  ChattyItem *item;
+
   g_assert (CHATTY_IS_WINDOW (self));
 
-  window_set_item (self, NULL);
+  item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
+  g_return_if_fail (CHATTY_IS_MM_CHAT (item));
+  g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item)));
+
+  chatty_item_set_state (item, CHATTY_ITEM_ARCHIVED);
+}
+
+static void
+chatty_window_unarchive_chat (GSimpleAction *action,
+                              GVariant      *parameter,
+                              gpointer       user_data)
+{
+  ChattyWindow *self = user_data;
+  ChattyItem *item;
+
+  g_assert (CHATTY_IS_WINDOW (self));
+
+  item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
+  g_return_if_fail (CHATTY_IS_MM_CHAT (item));
+  g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item)));
+
+  chatty_item_set_state (item, CHATTY_ITEM_VISIBLE);
+}
+
+static void
+chatty_window_block_chat (GSimpleAction *action,
+                          GVariant      *parameter,
+                          gpointer       user_data)
+{
+  ChattyWindow *self = user_data;
+  GtkWidget *message;
+  ChattyItem *item;
+  int result;
+
+  g_assert (CHATTY_IS_WINDOW (self));
+
+  item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
+  g_return_if_fail (CHATTY_IS_MM_CHAT (item));
+  g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item)));
+
+  message = gtk_message_dialog_new (GTK_WINDOW (self),
+                                    GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
+                                    GTK_MESSAGE_INFO,
+                                    GTK_BUTTONS_OK_CANCEL,
+                                    _("You shall no longer be notified for new messages, continue?"));
+
+  result = gtk_dialog_run (GTK_DIALOG (message));
+  gtk_widget_destroy (message);
+
+  if (result == GTK_RESPONSE_CANCEL)
+    return;
+
+  chatty_item_set_state (item, CHATTY_ITEM_BLOCKED);
+}
+
+static void
+chatty_window_unblock_chat (GSimpleAction *action,
+                            GVariant      *parameter,
+                            gpointer       user_data)
+{
+  ChattyWindow *self = user_data;
+  ChattyItem *item;
+
+  g_assert (CHATTY_IS_WINDOW (self));
+
+  item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
+  g_return_if_fail (CHATTY_IS_MM_CHAT (item));
+  g_return_if_fail (chatty_chat_is_im (CHATTY_CHAT (item)));
+
+  chatty_item_set_state (item, CHATTY_ITEM_VISIBLE);
+
 }
 
 static void
-window_delete_buddy_clicked_cb (ChattyWindow *self)
+chatty_window_delete_chat (GSimpleAction *action,
+                           GVariant      *parameter,
+                           gpointer       user_data)
 {
+  ChattyWindow *self = user_data;
   g_autofree char *text = NULL;
   GtkWidget *dialog;
   ChattyChat *chat;
@@ -399,7 +506,7 @@ window_delete_buddy_clicked_cb (ChattyWindow *self)
 
   g_assert (CHATTY_IS_WINDOW (self));
 
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
+  chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
   g_return_if_fail (chat);
 
   name = chatty_item_get_name (CHATTY_ITEM (chat));
@@ -419,16 +526,12 @@ window_delete_buddy_clicked_cb (ChattyWindow *self)
                                    "%s", text);
 
   gtk_dialog_add_buttons (GTK_DIALOG (dialog),
-                          _("Cancel"),
-                          GTK_RESPONSE_CANCEL,
-                          _("Delete"),
-                          GTK_RESPONSE_OK,
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Delete"), GTK_RESPONSE_OK,
                           NULL);
 
   gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
-                                            "%s",
-                                            sub_text);
-
+                                            "%s", sub_text);
   gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_CANCEL);
   gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_CENTER_ON_PARENT);
 
@@ -442,31 +545,32 @@ window_delete_buddy_clicked_cb (ChattyWindow *self)
       chatty_pp_chat_delete (CHATTY_PP_CHAT (chat));
     } else
 #endif
-    if (CHATTY_IS_MM_CHAT (chat)) {
-      chatty_mm_chat_delete (CHATTY_MM_CHAT (chat));
-    } else {
-      g_return_if_reached ();
-    }
+      if (CHATTY_IS_MM_CHAT (chat)) {
+        chatty_mm_chat_delete (CHATTY_MM_CHAT (chat));
+      } else {
+        g_return_if_reached ();
+      }
 
     window_set_item (self, NULL);
-    gtk_widget_hide (self->call_button);
 
-    if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->header_box)))
+    if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->content_box)))
       chatty_chat_list_select_first (CHATTY_CHAT_LIST (self->chat_list));
   }
 
   gtk_widget_destroy (dialog);
 }
 
-
 static void
-window_leave_chat_clicked_cb (ChattyWindow *self)
+chatty_window_leave_chat (GSimpleAction *action,
+                          GVariant      *parameter,
+                          gpointer       user_data)
 {
+  ChattyWindow *self = user_data;
   ChattyChat *chat;
 
   g_assert (CHATTY_IS_WINDOW (self));
 
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
+  chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
   g_warn_if_fail (chat);
 
   if (chat) {
@@ -478,89 +582,35 @@ window_leave_chat_clicked_cb (ChattyWindow *self)
 
   window_set_item (self, NULL);
 
-  if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->header_box)))
+  if (!hdy_leaflet_get_folded (HDY_LEAFLET (self->content_box)))
     chatty_chat_list_select_first (CHATTY_CHAT_LIST (self->chat_list));
 }
 
 static void
-window_block_contact_clicked_cb (ChattyWindow *self)
+chatty_window_show_archived (GSimpleAction *action,
+                             GVariant      *parameter,
+                             gpointer       user_data)
 {
-  GtkWidget *message;
-  ChattyChat *chat;
-  int result;
-
-  g_assert (CHATTY_IS_WINDOW (self));
-
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
-  g_return_if_fail (CHATTY_IS_MM_CHAT (chat));
-  g_return_if_fail (chatty_chat_is_im (chat));
-
-  message = gtk_message_dialog_new (GTK_WINDOW (self),
-                                    GTK_DIALOG_MODAL | GTK_DIALOG_USE_HEADER_BAR,
-                                    GTK_MESSAGE_INFO,
-                                    GTK_BUTTONS_OK_CANCEL,
-                                    _("You shall no longer be notified for new messages, continue?"));
-
-  result = gtk_dialog_run (GTK_DIALOG (message));
-  gtk_widget_destroy (message);
-
-  if (result == GTK_RESPONSE_CANCEL)
-    return;
-
-  chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_BLOCKED);
-}
-
-static void
-window_unblock_contact_clicked_cb (ChattyWindow *self)
-{
-  ChattyChat *chat;
-
-  g_assert (CHATTY_IS_WINDOW (self));
-
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
-  g_return_if_fail (CHATTY_IS_MM_CHAT (chat));
-  g_return_if_fail (chatty_chat_is_im (chat));
-
-  chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_VISIBLE);
-}
-
-static void
-window_archive_chat_clicked_cb (ChattyWindow *self)
-{
-  ChattyChat *chat;
-
-  g_assert (CHATTY_IS_WINDOW (self));
-
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
-  g_return_if_fail (CHATTY_IS_MM_CHAT (chat));
-  g_return_if_fail (chatty_chat_is_im (chat));
-
-  chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_ARCHIVED);
-}
-
-static void
-window_unarchive_chat_clicked_cb (ChattyWindow *self)
-{
-  ChattyChat *chat;
+  ChattyWindow *self = user_data;
 
   g_assert (CHATTY_IS_WINDOW (self));
 
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
-  g_return_if_fail (CHATTY_IS_MM_CHAT (chat));
-  g_return_if_fail (chatty_chat_is_im (chat));
-
-  chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_VISIBLE);
+  chatty_header_bar_show_archived (CHATTY_HEADER_BAR (self->header_bar), TRUE);
+  chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), TRUE);
 }
 
 static void
-window_show_chat_info_clicked_cb (ChattyWindow *self)
+chatty_window_show_chat_details (GSimpleAction *action,
+                                 GVariant      *parameter,
+                                 gpointer       user_data)
 {
+  ChattyWindow *self = user_data;
   ChattyInfoDialog *dialog;
   ChattyChat *chat;
 
   g_assert (CHATTY_IS_WINDOW (self));
 
-  chat = chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
+  chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
   g_return_if_fail (CHATTY_IS_CHAT (chat));
 
   dialog = CHATTY_INFO_DIALOG (self->chat_info_dialog);
@@ -570,19 +620,12 @@ window_show_chat_info_clicked_cb (ChattyWindow *self)
 }
 
 static void
-chatty_window_show_archived (ChattyWindow *self)
+chatty_window_show_settings (GSimpleAction *action,
+                             GVariant      *parameter,
+                             gpointer       user_data)
 {
-  g_assert (CHATTY_IS_WINDOW (self));
-
-  hdy_header_bar_set_title (HDY_HEADER_BAR (self->header_bar), _("Archived"));
-  chatty_chat_list_show_archived (CHATTY_CHAT_LIST (self->chat_list), TRUE);
-  gtk_widget_show (self->header_back_button);
-  gtk_widget_hide (self->header_add_chat_button);
-}
+  ChattyWindow *self = user_data;
 
-static void
-chatty_window_show_settings_dialog (ChattyWindow *self)
-{
   g_assert (CHATTY_IS_WINDOW (self));
 
   if (!self->settings_dialog)
@@ -590,191 +633,64 @@ chatty_window_show_settings_dialog (ChattyWindow *self)
   gtk_window_present (GTK_WINDOW (self->settings_dialog));
 }
 
-/* Copied from chatty-dialogs.c written by Andrea Schäfer <mosibasu@me.com> */
-static void
-chatty_window_show_about_dialog (ChattyWindow *self)
-{
-  static const gchar *authors[] = {
-    "Adrien Plazas <kekun.plazas@laposte.net>",
-    "Andrea Schäfer <mosibasu@me.com>",
-    "Benedikt Wildenhain <benedikt.wildenhain@hs-bochum.de>",
-    "Chris Talbot (kop316) <chris@talbothome.com>",
-    "Guido Günther <agx@sigxcpu.org>",
-    "Julian Sparber <jsparber@gnome.org>",
-    "Leland Carlye <leland.carlye@protonmail.com>",
-    "Mohammed Sadiq https://www.sadiqpk.org/",
-    "Richard Bayerle (OMEMO Plugin) https://github.com/gkdr/lurch",
-    "Ruslan Marchenko <me@ruff.mobi>",
-    "and more...",
-    NULL
-  };
-
-  static const gchar *artists[] = {
-    "Tobias Bernard <tbernard@gnome.org>",
-    NULL
-  };
-
-  static const gchar *documenters[] = {
-    "Heather Ellsworth <heather.ellsworth@puri.sm>",
-    NULL
-  };
-
-  /*
-   * “program-name” defaults to g_get_application_name().
-   * Don’t set it explicitly so that there is one less
-   * string to translate.
-   */
-  gtk_show_about_dialog (GTK_WINDOW (self),
-                         "logo-icon-name", CHATTY_APP_ID,
-                         "version", GIT_VERSION,
-                         "comments", _("An SMS and XMPP messaging client"),
-                         "website", "https://source.puri.sm/Librem5/chatty",
-                         "copyright", "© 2018–2022 Purism SPC",
-                         "license-type", GTK_LICENSE_GPL_3_0,
-                         "authors", authors,
-                         "artists", artists,
-                         "documenters", documenters,
-                         "translator-credits", _("translator-credits"),
-                         NULL);
-}
-
 static void
-window_search_protocol_changed_cb (ChattyWindow *self,
-                                   GtkWidget    *selected_row,
-                                   GtkListBox   *box)
+chatty_window_start_new_chat (GSimpleAction *action,
+                              GVariant      *parameter,
+                              gpointer       user_data)
 {
-  GdTaggedEntry *entry;
-  GtkWidget *old_row;
+  ChattyWindow *self = user_data;
 
   g_assert (CHATTY_IS_WINDOW (self));
-  g_assert (GTK_IS_LIST_BOX (box));
-
-  entry = GD_TAGGED_ENTRY (self->chats_search_entry);
-  old_row = self->selected_protocol_row;
-
-  if (old_row == selected_row)
-    return;
-
-  self->protocol_filter = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (selected_row), "protocol"));
-  chatty_chat_list_filter_protocol (CHATTY_CHAT_LIST (self->chat_list), self->protocol_filter);
-  chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (old_row), FALSE);
-  chatty_selectable_row_set_selected (CHATTY_SELECTABLE_ROW (selected_row), TRUE);
-  self->selected_protocol_row = selected_row;
-
-  if (selected_row == self->protocol_any_row) {
-    gd_tagged_entry_remove_tag (entry, self->protocol_tag);
-  } else {
-    const char *title;
 
-    gd_tagged_entry_add_tag (entry, self->protocol_tag);
-    title = chatty_selectable_row_get_title (CHATTY_SELECTABLE_ROW (selected_row));
-    gd_tagged_entry_tag_set_label (self->protocol_tag, title);
-  }
+  window_show_new_chat_dialog (self, FALSE);
 }
 
 static void
-window_active_protocols_changed_cb (ChattyWindow *self)
+chatty_window_start_sms_mms_chat (GSimpleAction *action,
+                                  GVariant      *parameter,
+                                  gpointer       user_data)
 {
-  ChattyAccount *mm_account;
-  ChattyProtocol protocols;
-  gboolean has_mms, has_sms, has_im;
+  ChattyWindow *self = user_data;
 
   g_assert (CHATTY_IS_WINDOW (self));
 
-  mm_account = chatty_manager_get_mm_account (self->manager);
-  protocols = chatty_manager_get_active_protocols (self->manager);
-  has_mms = chatty_mm_account_has_mms_feature (CHATTY_MM_ACCOUNT (mm_account));
-  has_sms = !!(protocols & CHATTY_PROTOCOL_MMS_SMS);
-  has_im  = !!(protocols & ~CHATTY_PROTOCOL_MMS_SMS);
-
-  gtk_widget_set_sensitive (self->header_add_chat_button, has_sms || has_im);
-  gtk_widget_set_sensitive (self->menu_new_group_message_button, has_im);
-
-  gtk_widget_set_visible (self->menu_new_sms_mms_message_button,
-                          has_mms && has_sms);
+  window_show_new_chat_dialog (self, TRUE);
 }
 
 static void
-window_chat_deleted_cb (ChattyWindow *self,
-                        ChattyChat   *chat)
+chatty_window_start_group_chat (GSimpleAction *action,
+                                GVariant      *parameter,
+                                gpointer       user_data)
 {
-  g_assert (CHATTY_IS_WINDOW (self));
-  g_assert (CHATTY_IS_CHAT (chat));
-
-  if (chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)) != chat)
-    return;
-
-  window_set_item (self, NULL);
-}
+  ChattyWindow *self = user_data;
+  GtkWidget *dialog;
 
-static void
-protocol_list_header_func (GtkListBoxRow *row,
-                           GtkListBoxRow *before,
-                           gpointer       user_data)
-{
-  if (!before) {
-    gtk_list_box_row_set_header (row, NULL);
-  } else if (!gtk_list_box_row_get_header (row)) {
-    GtkWidget *separator;
+  g_assert (CHATTY_IS_WINDOW (self));
 
-    separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
-    gtk_widget_show (separator);
-    gtk_list_box_row_set_header (row, separator);
-  }
+  dialog = chatty_new_muc_dialog_new (GTK_WINDOW (self));
+  gtk_window_present (GTK_WINDOW (dialog));
 }
 
 static void
-new_chat_selection_changed_cb (ChattyWindow        *self,
-                               ChattyNewChatDialog *dialog)
+chatty_window_call_user (GSimpleAction *action,
+                         GVariant      *parameter,
+                         gpointer       user_data)
 {
-  g_autoptr(GString) users = g_string_new (NULL);
-  GListModel *model;
-  guint n_items;
-  const char *name;
+  ChattyWindow *self = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *uri = NULL;
+  ChattyChat *chat;
 
   g_assert (CHATTY_IS_WINDOW (self));
-  g_assert (CHATTY_IS_NEW_CHAT_DIALOG (dialog));
-
-  model = chatty_new_chat_dialog_get_selected_items (dialog);
-  n_items = g_list_model_get_n_items (model);
-
-  if (n_items == 0)
-    goto end;
-
-  for (guint i = 0; i < n_items; i++) {
-    g_autoptr(ChattyItem) item = NULL;
-    const char *phone_number;
-
-    item = g_list_model_get_item (model, i);
-
-    if (CHATTY_IS_CONTACT (item)) {
-      phone_number = chatty_item_get_username (item);
-      g_string_append (users, phone_number);
-      g_string_append (users, ",");
-    }
-  }
-
-  /* Remove the trailing "," */
-  if (users->len >= 1)
-    g_string_truncate (users, users->len - 1);
-
-  if (n_items == 1) {
-    g_autoptr(ChattyItem) item = NULL;
-
-    item = g_list_model_get_item (model, 0);
 
-    if (!CHATTY_IS_CONTACT (item) ||
-        !chatty_contact_is_dummy (CHATTY_CONTACT (item))) {
-      chatty_window_open_item (self, item);
-      goto end;
-    }
-  }
+  chat = (ChattyChat *)chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
+  g_return_if_fail (CHATTY_IS_MM_CHAT (chat));
 
-  name = chatty_new_chat_dialog_get_chat_title (dialog);
-  chatty_window_set_uri (self, users->str, name);
+  uri = g_strconcat ("tel://", chatty_chat_get_chat_name (chat), NULL);
 
- end:
-  gtk_widget_hide (GTK_WIDGET (dialog));
+  CHATTY_INFO (uri, "Calling uri:");
+  if (!gtk_show_uri_on_window (NULL, uri, GDK_CURRENT_TIME, &error))
+    g_warning ("Failed to launch call: %s", error->message);
 }
 
 static void
@@ -806,17 +722,29 @@ chatty_window_map (GtkWidget *widget)
                            "items-changed",
                            G_CALLBACK (chatty_window_update_search_mode), self,
                            G_CONNECT_SWAPPED);
-  g_signal_connect_object (self->manager, "notify::active-protocols",
-                           G_CALLBACK (window_active_protocols_changed_cb), self,
-                           G_CONNECT_SWAPPED);
 
   notify_fold_cb (self);
-  window_active_protocols_changed_cb (self);
   chatty_window_update_search_mode (self);
 
   GTK_WIDGET_CLASS (chatty_window_parent_class)->map (widget);
 }
 
+static const GActionEntry win_entries[] = {
+  { "archive-chat", chatty_window_archive_chat },
+  { "unarchive-chat", chatty_window_unarchive_chat },
+  { "block-chat", chatty_window_block_chat },
+  { "unblock-chat", chatty_window_unblock_chat },
+  { "delete-chat", chatty_window_delete_chat },
+  { "leave-chat", chatty_window_leave_chat },
+  { "show-archived", chatty_window_show_archived },
+  { "show-chat-details", chatty_window_show_chat_details },
+  { "show-settings", chatty_window_show_settings },
+  { "new-chat", chatty_window_start_new_chat },
+  { "new-sms-mms", chatty_window_start_sms_mms_chat },
+  { "new-group-chat", chatty_window_start_group_chat },
+  { "call-user", chatty_window_call_user },
+};
+
 static void
 chatty_window_constructed (GObject *object)
 {
@@ -824,6 +752,9 @@ chatty_window_constructed (GObject *object)
   GtkWindow    *window = (GtkWindow *)object;
   GdkRectangle  geometry;
 
+  g_action_map_add_action_entries (G_ACTION_MAP (self), win_entries,
+                                   G_N_ELEMENTS (win_entries), self);
+
   self->settings = g_object_ref (chatty_settings_get_default ());
   chatty_settings_get_window_geometry (self->settings, &geometry);
   gtk_window_set_default_size (window, geometry.width, geometry.height);
@@ -860,9 +791,9 @@ chatty_window_dispose (GObject *object)
   ChattyWindow *self = (ChattyWindow *)object;
 
   /* XXX: Why it fails without the check? */
-  if (CHATTY_IS_CHAT_VIEW (self->chat_view))
+  if (CHATTY_IS_MAIN_VIEW (self->content_view))
     g_clear_signal_handler (&self->chat_changed_handler,
-                            chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view)));
+                            chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view)));
   g_clear_object (&self->manager);
 
   G_OBJECT_CLASS (chatty_window_parent_class)->dispose (object);
@@ -886,62 +817,25 @@ chatty_window_class_init (ChattyWindowClass *klass)
                                                "/sm/puri/Chatty/"
                                                "ui/chatty-window.ui");
 
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, sub_header_label);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, sub_header_icon);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, menu_new_message_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, menu_new_sms_mms_message_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, menu_new_group_message_button);
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_bar);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_back_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_add_chat_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, call_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_sub_menu_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, leave_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, delete_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, block_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, unblock_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, archive_button);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, unarchive_button);
-
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, search_button);
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chats_search_bar);
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chats_search_entry);
 
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, content_box);
+  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, content_view);
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, sidebar);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_box);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_group);
 
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chat_list);
-
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, chat_view);
-  gtk_widget_class_bind_template_child (widget_class, ChattyWindow, header_chat_list_new_msg_popover);
-
   gtk_widget_class_bind_template_child (widget_class, ChattyWindow, protocol_list);
 
   gtk_widget_class_bind_template_callback (widget_class, notify_fold_cb);
   gtk_widget_class_bind_template_callback (widget_class, window_content_box_changed);
-  gtk_widget_class_bind_template_callback (widget_class, window_show_unarchived_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_new_message_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_new_sms_mms_message_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_new_muc_clicked_cb);
   gtk_widget_class_bind_template_callback (widget_class, window_back_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_show_chat_info_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_leave_chat_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_block_contact_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_unblock_contact_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_archive_chat_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_unarchive_chat_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_delete_buddy_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_call_button_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, window_search_enable_changed_cb);
   gtk_widget_class_bind_template_callback (widget_class, window_search_changed_cb);
   gtk_widget_class_bind_template_callback (widget_class, search_tag_button_clicked_cb);
-  gtk_widget_class_bind_template_callback (widget_class, window_search_toggled_cb);
   gtk_widget_class_bind_template_callback (widget_class, window_search_entry_activated_cb);
   gtk_widget_class_bind_template_callback (widget_class, window_chat_list_selection_changed);
-  gtk_widget_class_bind_template_callback (widget_class, chatty_window_show_archived);
-  gtk_widget_class_bind_template_callback (widget_class, chatty_window_show_settings_dialog);
-  gtk_widget_class_bind_template_callback (widget_class, chatty_window_show_about_dialog);
   gtk_widget_class_bind_template_callback (widget_class, window_search_protocol_changed_cb);
 
   g_type_ensure (CHATTY_TYPE_SELECTABLE_ROW);
@@ -969,11 +863,14 @@ chatty_window_init (ChattyWindow *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
 
+  chatty_header_bar_set_search_bar (CHATTY_HEADER_BAR (self->header_bar), self->chats_search_bar);
+  chatty_header_bar_set_content_box (CHATTY_HEADER_BAR (self->header_bar), self->content_box);
+
   self->protocol_filter = CHATTY_PROTOCOL_ANY;
   hdy_search_bar_connect_entry (HDY_SEARCH_BAR (self->chats_search_bar),
                                 GTK_ENTRY (self->chats_search_entry));
   self->manager = g_object_ref (chatty_manager_get_default ());
-  chatty_chat_view_set_db (CHATTY_CHAT_VIEW (self->chat_view),
+  chatty_main_view_set_db (CHATTY_MAIN_VIEW (self->content_view),
                            chatty_manager_get_history (self->manager));
   g_signal_connect_object (self->manager, "chat-deleted",
                            G_CALLBACK (window_chat_deleted_cb), self,
@@ -1026,10 +923,15 @@ chatty_window_set_uri (ChattyWindow *self,
 ChattyChat *
 chatty_window_get_active_chat (ChattyWindow *self)
 {
+  ChattyItem *item = NULL;
+
   g_return_val_if_fail (CHATTY_IS_WINDOW (self), NULL);
 
   if (gtk_window_has_toplevel_focus (GTK_WINDOW (self)))
-    return chatty_chat_view_get_chat (CHATTY_CHAT_VIEW (self->chat_view));
+    item = chatty_main_view_get_item (CHATTY_MAIN_VIEW (self->content_view));
+
+  if (CHATTY_IS_CHAT (item))
+    return CHATTY_CHAT (item);
 
   return NULL;
 }
@@ -1038,8 +940,6 @@ void
 chatty_window_open_chat (ChattyWindow *self,
                          ChattyChat   *chat)
 {
-  gboolean can_delete;
-
   g_return_if_fail (CHATTY_IS_WINDOW (self));
   g_return_if_fail (CHATTY_IS_CHAT (chat));
 
@@ -1048,45 +948,8 @@ chatty_window_open_chat (ChattyWindow *self,
 
   window_set_item (self, chat);
 
-  gtk_widget_set_visible (self->leave_button, !CHATTY_IS_MM_CHAT (chat));
-  /* We can't delete MaChat */
-  can_delete = !CHATTY_IS_MA_CHAT (chat);
-  gtk_widget_set_visible (self->delete_button, can_delete);
-
-  /* We shall update the ability to change state below */
-  gtk_widget_hide (self->block_button);
-  gtk_widget_hide (self->unblock_button);
-  gtk_widget_hide (self->archive_button);
-  gtk_widget_hide (self->unarchive_button);
-
-  hdy_leaflet_set_visible_child (HDY_LEAFLET (self->content_box), self->chat_view);
-  gtk_widget_hide (self->call_button);
+  hdy_leaflet_set_visible_child (HDY_LEAFLET (self->content_box), self->content_view);
 
   if (chatty_window_get_active_chat (self))
     chatty_chat_set_unread_count (chat, 0);
-
-  if (CHATTY_IS_MM_CHAT (chat)) {
-    GListModel *users;
-    const char *name;
-
-    users = chatty_chat_get_users (chat);
-    name = chatty_chat_get_chat_name (chat);
-
-    /* allow changing state only for 1:1 SMS/MMS chats  */
-    if (g_list_model_get_n_items (users) == 1) {
-      window_update_item_state_button (self, CHATTY_ITEM (chat));
-    }
-
-    if (g_list_model_get_n_items (users) == 1 &&
-        chatty_utils_username_is_valid (name, CHATTY_PROTOCOL_MMS_SMS)) {
-      g_autoptr(ChattyMmBuddy) buddy = NULL;
-      g_autoptr(GAppInfo) app_info = NULL;
-
-      app_info = g_app_info_get_default_for_uri_scheme ("tel");
-      buddy = g_list_model_get_item (users, 0);
-
-      if (app_info)
-        gtk_widget_show (self->call_button);
-    }
-  }
 }
diff --git a/src/chatty.gresource.xml b/src/chatty.gresource.xml
index b9f8a8db2cb9a35ecac1de54bc269ef83b652c21..b50cadea958d6740822c9efa31c6bee29dbe1b2b 100644
--- a/src/chatty.gresource.xml
+++ b/src/chatty.gresource.xml
@@ -1,13 +1,15 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
   <gresource prefix="/sm/puri/Chatty">
-    <file alias="matrix-filter.json">matrix/matrix-filter.json</file>
     <file preprocess="xml-stripblanks" alias="gtk/help-overlay.ui">ui/help-overlay.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-selectable-row.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-fp-row.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-list-row.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-chat-list.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-chat-view.ui</file>
+    <file preprocess="xml-stripblanks">ui/chatty-main-view.ui</file>
+    <file preprocess="xml-stripblanks">ui/chatty-verification-view.ui</file>
+    <file preprocess="xml-stripblanks">ui/chatty-invite-view.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-contact-list.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-contact-row.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-file-item.ui</file>
@@ -22,6 +24,7 @@
     <file preprocess="xml-stripblanks">ui/chatty-settings-dialog.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-dialog-new-chat.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-dialog-join-muc.ui</file>
+    <file preprocess="xml-stripblanks">ui/chatty-header-bar.ui</file>
     <file preprocess="xml-stripblanks">ui/chatty-window.ui</file>
     <file preprocess="xml-stripblanks">icons/eye-not-looking-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/eye-open-negative-filled-symbolic.svg</file>
diff --git a/src/dialogs/chatty-ma-chat-info.c b/src/dialogs/chatty-ma-chat-info.c
index 7a690296f13ad800271af05114bb6c6766040461..5ab2f5c5abdf135b3013fc7a1de36e54368cc2e3 100644
--- a/src/dialogs/chatty-ma-chat-info.c
+++ b/src/dialogs/chatty-ma-chat-info.c
@@ -127,6 +127,8 @@ chatty_ma_chat_info_set_item (ChattyChatInfo *info,
   g_signal_connect_swapped (self->chat, "notify::encrypt",
                             G_CALLBACK (ma_chat_encrypt_changed_cb),
                             self);
+  gtk_widget_set_sensitive (self->encryption_switch,
+                            chatty_ma_chat_can_set_encryption (CHATTY_MA_CHAT (self->chat)));
   ma_chat_encrypt_changed_cb (self);
 }
 
diff --git a/src/dialogs/chatty-mm-chat-info.c b/src/dialogs/chatty-mm-chat-info.c
index 806ae61ab79bef59dd175d0348ae440f07d93a59..870dcb42d9e73758e83775142c6003f8e8ee6eae 100644
--- a/src/dialogs/chatty-mm-chat-info.c
+++ b/src/dialogs/chatty-mm-chat-info.c
@@ -71,7 +71,7 @@ chatty_mm_chat_info_set_item (ChattyChatInfo *info,
                               ChattyChat     *chat)
 {
   ChattyMmChatInfo *self = (ChattyMmChatInfo *)info;
-  g_autoptr (ChattyContact) self_contact;
+  g_autoptr(ChattyContact) self_contact = NULL;
   GListModel *users;
   GtkWidget *contact_row;
   guint n_items = 0;
@@ -112,7 +112,7 @@ chatty_mm_chat_info_set_item (ChattyChatInfo *info,
      gtk_list_box_prepend (GTK_LIST_BOX (self->contacts_list_box),
                            GTK_WIDGET (contact_row));
     } else {
-      g_autoptr (ChattyContact) new_contact;
+      g_autoptr(ChattyContact) new_contact = NULL;
       const char *phone;
 
       phone = chatty_mm_buddy_get_number (buddy);
diff --git a/src/dialogs/chatty-settings-dialog.c b/src/dialogs/chatty-settings-dialog.c
index bcd505e6a1adcf463e5d8ceae99d7afbc27d3edb..00986b72a2460f06ccce6f46aa97ff33ecfcda4b 100644
--- a/src/dialogs/chatty-settings-dialog.c
+++ b/src/dialogs/chatty-settings-dialog.c
@@ -30,6 +30,8 @@
 #endif
 
 #include <glib/gi18n.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
 #include "chatty-utils.h"
 #include "matrix-utils.h"
@@ -121,22 +123,6 @@ struct _ChattySettingsDialog
 
 G_DEFINE_TYPE (ChattySettingsDialog, chatty_settings_dialog, HDY_TYPE_WINDOW)
 
-static void
-finish_cb (GObject      *object,
-           GAsyncResult *result,
-           gpointer      user_data)
-{
-  GError *error = NULL;
-  gboolean status;
-
-  status = g_task_propagate_boolean (G_TASK (result), &error);
-
-  if (error)
-    g_task_return_error (user_data, error);
-  else
-    g_task_return_boolean (user_data, status);
-}
-
 static void
 settings_apply_style (GtkWidget  *widget,
                       const char *style)
@@ -241,76 +227,49 @@ matrix_home_server_verify_cb (GObject      *object,
   ChattySettingsDialog *self = user_data;
   g_autoptr(ChattyMaAccount) account = NULL;
   g_autoptr(GError) error = NULL;
-  const char *username, *password, *server;
+  const char *homeserver, *server;
 
   g_assert (CHATTY_IS_SETTINGS_DIALOG (self));
 
   settings_dialog_set_save_state (self, FALSE);
+  /* verified homeserver URL */
+  homeserver = cm_client_get_homeserver_finish (CM_CLIENT (object), result, &error);
+  /* homeserver URL entered by the user */
   server = gtk_entry_get_text (GTK_ENTRY (self->matrix_homeserver_entry));
 
-  if (!matrix_utils_verify_homeserver_finish (result, &error)) {
+  if (!homeserver) {
     gtk_widget_set_sensitive (self->add_button, FALSE);
     g_clear_handle_id (&self->revealer_timeout_id, g_source_remove);
-    gtk_label_set_text (GTK_LABEL (self->notification_label),
-                        _("Failed to verify server"));
-    gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE);
-    self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self);
-    return;
-  }
 
-  username = gtk_entry_get_text (GTK_ENTRY (self->new_account_id_entry));
-  password = gtk_entry_get_text (GTK_ENTRY (self->new_password_entry));
+    if (error)
+      g_dbus_error_strip_remote_error (error);
 
-  account = chatty_ma_account_new (username, password);
-  chatty_ma_account_set_homeserver (account, server);
-  chatty_manager_save_account_async (chatty_manager_get_default (), CHATTY_ACCOUNT (account),
-                                     NULL, settings_save_account_cb, self);
-}
+    if (server && *server) {
+      g_autofree char *label = NULL;
 
-static void
-chatty_settings_save_matrix (ChattySettingsDialog *self,
-                             const char           *user_id,
-                             const char           *password);
-static void
-matrix_home_server_got_cb (GObject      *object,
-                           GAsyncResult *result,
-                           gpointer      user_data)
-{
-  ChattySettingsDialog *self = user_data;
-  g_autoptr(ChattyMaAccount) account = NULL;
-  g_autofree char *home_server = NULL;
-
-  g_assert (CHATTY_IS_SETTINGS_DIALOG (self));
-
-  home_server = matrix_utils_get_homeserver_finish (result, NULL);
+      if (error)
+        label = g_strdup_printf (_("Failed to verify server: %s"), error->message);
+      else
+        label = g_strdup (_("Failed to verify server"));
 
-  if (g_cancellable_is_cancelled (self->cancellable)) {
-    settings_dialog_set_save_state (self, FALSE);
-    return;
-  }
-
-  if (home_server && *home_server) {
-    const char *username, *password;
-
-    username = gtk_entry_get_text (GTK_ENTRY (self->new_account_id_entry));
-    password = gtk_entry_get_text (GTK_ENTRY (self->new_password_entry));
-
-    gtk_entry_set_text (GTK_ENTRY (self->matrix_homeserver_entry), home_server);
-    chatty_settings_save_matrix (self, username, password);
-  } else {
-    settings_dialog_set_save_state (self, FALSE);
-    gtk_widget_set_sensitive (self->add_button, FALSE);
+      gtk_label_set_text (GTK_LABEL (self->notification_label), label);
+    } else {
+      settings_dialog_set_save_state (self, FALSE);
+      gtk_label_set_text (GTK_LABEL (self->notification_label),
+                          _("Couldn't get Home server address"));
+      gtk_widget_show (self->matrix_homeserver_entry);
+      gtk_entry_grab_focus_without_selecting (GTK_ENTRY (self->matrix_homeserver_entry));
+      gtk_editable_set_position (GTK_EDITABLE (self->matrix_homeserver_entry), -1);
+    }
 
-    g_clear_handle_id (&self->revealer_timeout_id, g_source_remove);
-    gtk_label_set_text (GTK_LABEL (self->notification_label),
-                        _("Couldn't get Home server address"));
     gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE);
     self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self);
-
-    gtk_widget_show (self->matrix_homeserver_entry);
-    gtk_entry_grab_focus_without_selecting (GTK_ENTRY (self->matrix_homeserver_entry));
-    gtk_editable_set_position (GTK_EDITABLE (self->matrix_homeserver_entry), -1);
+    return;
   }
+
+  account = chatty_ma_account_new_from_client (CM_CLIENT (object));
+  chatty_manager_save_account_async (chatty_manager_get_default (), CHATTY_ACCOUNT (account),
+                                     NULL, settings_save_account_cb, self);
 }
 
 static void
@@ -318,8 +277,12 @@ chatty_settings_save_matrix (ChattySettingsDialog *self,
                              const char           *user_id,
                              const char           *password)
 {
+  g_autoptr(CmClient) cm_client = NULL;
+  g_autofree char *uri_prefixed = NULL;
+  CmAccount *cm_account;
   GtkEntry *entry;
   const char *uri;
+  gboolean uri_has_prefix;
 
   g_assert (CHATTY_IS_SETTINGS_DIALOG (self));
   g_return_if_fail (user_id && *user_id);
@@ -332,12 +295,20 @@ chatty_settings_save_matrix (ChattySettingsDialog *self,
   entry = GTK_ENTRY (self->matrix_homeserver_entry);
   uri = gtk_entry_get_text (entry);
 
-  if (uri && *uri)
-    matrix_utils_verify_homeserver_async (uri, 30, self->cancellable,
-                                          matrix_home_server_verify_cb, self);
-  else
-    matrix_utils_get_homeserver_async (user_id, 10, self->cancellable,
-                                       matrix_home_server_got_cb, self);
+  uri_has_prefix = g_str_has_prefix (uri, "http");
+
+  /* Assume https by default */
+  if (!uri_has_prefix)
+    uri_prefixed = g_strdup_printf ("https://%s", uri);
+
+  cm_client = chatty_manager_matrix_client_new (chatty_manager_get_default ());
+  cm_account = cm_client_get_account (cm_client);
+  cm_account_set_login_id (cm_account, user_id);
+  cm_client_set_homeserver (cm_client, uri_has_prefix ? uri : uri_prefixed);
+  cm_client_set_password (cm_client, password);
+  cm_client_get_homeserver_async (cm_client, self->cancellable,
+                                  matrix_home_server_verify_cb,
+                                  self);
 }
 
 static void
@@ -360,56 +331,13 @@ chatty_settings_add_clicked_cb (ChattySettingsDialog *self)
   if (is_matrix)
     settings_check_librem_one (self);
 
-  if (is_matrix && chatty_settings_get_experimental_features (chatty_settings_get_default ())) {
+  if (is_matrix) {
     chatty_settings_save_matrix (self, user_id, password);
     return;
   }
 
 #ifdef PURPLE_ENABLED
-  if (is_matrix) {
-    g_autoptr(GTask) task = NULL;
-    GtkEntry *entry;
-    const char *server_url;
-
-    entry = GTK_ENTRY (self->matrix_homeserver_entry);
-    server_url = gtk_entry_get_text (entry);
-
-    if (!server_url || !*server_url) {
-      gtk_widget_show (GTK_WIDGET (entry));
-      gtk_entry_grab_focus_without_selecting (entry);
-      gtk_editable_set_position (GTK_EDITABLE (entry), -1);
-
-      gtk_widget_set_sensitive (self->add_button, FALSE);
-      g_clear_handle_id (&self->revealer_timeout_id, g_source_remove);
-      gtk_label_set_text (GTK_LABEL (self->notification_label),
-                          _("Couldn't get Home server address"));
-      gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE);
-      self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self);
-      return;
-    }
-
-    task = g_task_new (self, NULL, NULL, NULL);
-    settings_dialog_set_save_state (self, TRUE);
-    matrix_utils_verify_homeserver_async (server_url, 10, self->cancellable,
-                                          finish_cb, task);
-
-    while (!g_task_get_completed (task))
-      g_main_context_iteration (NULL, TRUE);
-
-    settings_dialog_set_save_state (self, FALSE);
-
-    if (!g_task_propagate_boolean (task, NULL)) {
-      gtk_widget_set_sensitive (self->add_button, FALSE);
-      g_clear_handle_id (&self->revealer_timeout_id, g_source_remove);
-      gtk_label_set_text (GTK_LABEL (self->notification_label),
-                          _("Failed to verify server"));
-      gtk_revealer_set_reveal_child (GTK_REVEALER (self->notification_revealer), TRUE);
-      self->revealer_timeout_id = g_timeout_add_seconds (5, dialog_notification_timeout_cb, self);
-      return;
-    }
-
-    account = (ChattyAccount *)chatty_pp_account_new (CHATTY_PROTOCOL_MATRIX, user_id, server_url, FALSE);
-  } else if (is_telegram) {
+  if (is_telegram) {
     account = (ChattyAccount *)chatty_pp_account_new (CHATTY_PROTOCOL_TELEGRAM, user_id, NULL, FALSE);
   } else {/* XMPP */
     gboolean has_encryption;
@@ -619,13 +547,29 @@ settings_homeserver_entry_changed (ChattySettingsDialog *self,
   server = gtk_entry_get_text (entry);
 
   if (server && *server) {
-    g_autoptr(SoupURI) uri = NULL;
-
-    uri = soup_uri_new (gtk_entry_get_text (entry));
+    g_autoptr(GUri) uri = NULL;
+    g_autofree char *server_prefixed = NULL;
+    const char *scheme = NULL;
+    const char *path = NULL;
+    const char *host = NULL;
+    gboolean server_has_prefix = g_str_has_prefix (server, "http");
+
+    /* Assume https by default */
+    if (!server_has_prefix)
+      server_prefixed = g_strdup_printf ("https://%s", server);
+
+    uri = g_uri_parse (server_has_prefix ? server : server_prefixed, G_URI_FLAGS_NONE, NULL);
+    if (uri) {
+      scheme = g_uri_get_scheme (uri);
+      path = g_uri_get_path (uri);
+      host = g_uri_get_host (uri);
+    }
 
-    valid = SOUP_URI_VALID_FOR_HTTP (uri);
-    /* We need an absolute path URI */
-    valid = valid && *uri->host && g_str_equal (soup_uri_get_path (uri), "/");
+    valid = scheme && *scheme;
+    valid = valid && (g_str_equal (scheme, "http") || g_str_equal (scheme, "https"));
+    valid = valid && host && *host;
+    valid = valid && !g_str_has_suffix (host, ".");
+    valid = valid && (!path || !*path);
   }
 
   if (valid)
@@ -693,12 +637,11 @@ settings_update_new_account_view (ChattySettingsDialog *self)
   gtk_widget_grab_focus (self->new_account_id_entry);
   gtk_widget_show (self->add_button);
 
-  if (chatty_settings_get_experimental_features (chatty_settings_get_default ()))
-    gtk_widget_set_visible (self->matrix_row, TRUE);
+  gtk_widget_set_visible (self->matrix_row, TRUE);
 
 #ifdef PURPLE_ENABLED
-  if (purple_find_prpl ("prpl-matrix"))
-    gtk_widget_set_visible (self->matrix_row, TRUE);
+  /* if (purple_find_prpl ("prpl-matrix")) */
+  /*   gtk_widget_set_visible (self->matrix_row, TRUE); */
 
   gtk_widget_set_visible (self->telegram_row, purple_find_prpl ("prpl-telegram") != NULL);
 #endif
@@ -898,13 +841,16 @@ settings_new_detail_changed_cb (ChattySettingsDialog *self)
   else
     protocol = CHATTY_PROTOCOL_XMPP;
 
-  if (chatty_settings_get_experimental_features (chatty_settings_get_default ()) &&
-      gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->matrix_radio_button)))
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->matrix_radio_button)))
     protocol = CHATTY_PROTOCOL_MATRIX | CHATTY_PROTOCOL_EMAIL;
 
   valid_protocol = chatty_utils_username_is_valid (id, protocol);
   valid = valid && valid_protocol;
 
+  if (valid_protocol &&
+      gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->matrix_radio_button)))
+    valid_protocol = !chatty_manager_has_matrix_with_id (chatty_manager_get_default (), id);
+
   if (valid_protocol)
     settings_remove_style (GTK_WIDGET (self->new_account_id_entry), "error");
   else
@@ -1269,8 +1215,7 @@ chatty_settings_dialog_init (ChattySettingsDialog *self)
   gtk_widget_hide (self->xmpp_radio_button);
 #endif
 
-  if (chatty_settings_get_experimental_features (chatty_settings_get_default ()))
-    show_account_box = TRUE;
+  show_account_box = TRUE;
 
   gtk_widget_set_visible (self->accounts_list_box, show_account_box);
 
diff --git a/src/library.c b/src/library.c
new file mode 100644
index 0000000000000000000000000000000000000000..c6cd526c2b46935a3ea8d9e38e25e4dde25a5f76
--- /dev/null
+++ b/src/library.c
@@ -0,0 +1,27 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* library.h
+ *
+ * Copyright 2022 Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-2-or-later OR CC0-1.0
+ */
+
+#include <handy.h>
+#include <libgd/gd.h>
+
+#include "dialogs/chatty-chat-info.h"
+
+void __attribute__((constructor)) initialize_libraries (void);
+
+void
+__attribute__((constructor)) initialize_libraries (void)
+{
+  gtk_init (NULL, NULL);
+  hdy_init ();
+  gd_ensure_types ();
+
+  g_type_ensure (CHATTY_TYPE_CHAT_INFO);
+}
diff --git a/src/main.c b/src/main.c
index cf341f92135bdf5614d573f2544b40a0ce5e5e85..438dc376c099709913ab5124b3d2a53b219a2077 100644
--- a/src/main.c
+++ b/src/main.c
@@ -17,6 +17,45 @@
 #include "chatty-manager.h"
 #include "chatty-log.h"
 
+static void
+show_backtrace (int signum)
+{
+  g_on_error_stack_trace (g_get_prgname ());
+  g_print ("signum %d: %s\n", signum, g_strsignal (signum));
+
+  exit (128 + signum);
+}
+
+static void
+enable_backtrace (void)
+{
+  const char *env;
+
+  env = g_getenv ("LD_PRELOAD");
+
+  /* Don't log backtrace if run inside valgrind */
+  if (env && (strstr (env, "/valgrind/") || strstr (env, "/vgpreload")))
+    return;
+
+  signal (SIGABRT, show_backtrace);
+  signal (SIGTRAP, show_backtrace);
+
+#ifndef __has_feature
+#  define __has_feature(x) (0)
+#endif
+
+#if __has_feature (address_sanitizer) ||        \
+  defined(__SANITIZE_ADDRESS__) ||              \
+  defined(__SANITIZE_THREAD__)
+  return;
+#endif
+
+  /* Trap SIGSEGV only if not compiled with sanitizers */
+  /* as sanitizers shall handle this better. */
+  /* fixme: How to check if leak sanitizer is enabled? */
+  signal (SIGSEGV, show_backtrace);
+}
+
 
 int
 main (int   argc,
@@ -24,6 +63,10 @@ main (int   argc,
 {
   g_autoptr(ChattyApplication) application = NULL;
 
+  g_setenv ("SOUP_FORCE_HTTP1", "1", FALSE);
+
+  g_set_prgname (CHATTY_APP_ID);
+  enable_backtrace ();
   chatty_log_init ();
   gd_ensure_types ();
 
@@ -31,7 +74,6 @@ main (int   argc,
   bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
   bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
 
-  g_set_prgname (CHATTY_APP_ID);
   application = chatty_application_new ();
 
   return g_application_run (G_APPLICATION (application), argc, argv);
diff --git a/src/matrix/chatty-ma-account.c b/src/matrix/chatty-ma-account.c
index 54cc9a611bc05901f22e1bebf88bbe282404ec7b..2cda52f41fe5f1b66c151b8fbd480a77821ef3b0 100644
--- a/src/matrix/chatty-ma-account.c
+++ b/src/matrix/chatty-ma-account.c
@@ -1,7 +1,7 @@
 /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* chatty-ma-account.c
  *
- * Copyright 2020 Purism SPC
+ * Copyright 2020, 2022 Purism SPC
  *
  * Author(s):
  *   Mohammed Sadiq <sadiq@sadiqpk.org>
@@ -11,19 +11,15 @@
 
 #define G_LOG_DOMAIN "chatty-ma-account"
 
-#include <json-glib/json-glib.h>
 #include <libsecret/secret.h>
-#include <libsoup/soup.h>
 #include <glib/gi18n.h>
 
 #include "chatty-secret-store.h"
 #include "chatty-history.h"
-#include "matrix-api.h"
-#include "matrix-enc.h"
-#include "matrix-db.h"
 #include "matrix-utils.h"
 #include "chatty-utils.h"
 #include "chatty-ma-chat.h"
+#include "chatty-ma-key-chat.h"
 #include "chatty-ma-account.h"
 #include "chatty-log.h"
 
@@ -34,44 +30,23 @@
  * @include: "chatty-mat-account.h"
  */
 
-#define SYNC_TIMEOUT 30000 /* milliseconds */
-
 struct _ChattyMaAccount
 {
   ChattyAccount   parent_instance;
 
   char           *name;
 
-  MatrixApi      *matrix_api;
-  MatrixEnc      *matrix_enc;
-  MatrixDb       *matrix_db;
-  HdyValueObject *device_fp;
-
-  ChattyHistory  *history_db;
+  CmMatrix       *cm_matrix;
+  CmClient       *cm_client;
 
-  char           *pickle_key;
-  char           *next_batch;
+  HdyValueObject *device_fp;
 
   GListStore     *chat_list;
-  /* this will be moved to chat_list after login succeeds */
-  GPtrArray      *db_chat_list;
   GdkPixbuf      *avatar;
   ChattyFileInfo *avatar_file;
 
   ChattyStatus   status;
-  gboolean       homeserver_valid;
-  gboolean       account_enabled;
-
   gboolean       avatar_is_loading;
-  /* @is_loading is set when the account is loading
-   * from db and set to not save the change to db.
-   */
-  gboolean       is_loading;
-  gboolean       save_account_pending;
-  gboolean       save_password_pending;
-
-  /* for sending events, incremented for each event */
-  int            event_id;
   guint          connect_id;
 };
 
@@ -83,7 +58,7 @@ G_DEFINE_TYPE (ChattyMaAccount, chatty_ma_account, CHATTY_TYPE_ACCOUNT)
     if (self->status != _status) {                                      \
       self->status = _status;                                           \
       g_object_notify (G_OBJECT (self), "status");                      \
-      CHATTY_TRACE (matrix_api_get_username (self->matrix_api),         \
+      CHATTY_TRACE (cm_client_get_user_id (self->cm_client),            \
                     "status changed, connected: %s, user:",             \
                     _status == CHATTY_CONNECTING ? "connecting" :       \
                     CHATTY_LOG_BOOL (_status == CHATTY_CONNECTED));     \
@@ -113,184 +88,20 @@ ma_account_get_avatar_pixbuf_cb (GObject      *object,
   }
 }
 
-static void
-ma_account_get_avatar_cb (GObject      *object,
-                          GAsyncResult *result,
-                          gpointer      user_data)
-{
-  g_autoptr(ChattyMaAccount) self = user_data;
-
-  self->avatar_is_loading = FALSE;
-
-  if (matrix_api_get_file_finish (self->matrix_api, result, NULL)) {
-    g_clear_object (&self->avatar);
-    g_signal_emit_by_name (self, "avatar-changed");
-    chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self));
-  }
-}
-
-static ChattyMaChat *
-matrix_find_chat_with_id (ChattyMaAccount *self,
-                          const char       *room_id,
-                          guint            *index)
-{
-  guint n_items;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  if (!room_id || !*room_id)
-    return NULL;
-
-  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->chat_list));
-  for (guint i = 0; i < n_items; i++) {
-    g_autoptr(ChattyMaChat) chat = NULL;
-
-    chat = g_list_model_get_item (G_LIST_MODEL (self->chat_list), i);
-    if (chatty_ma_chat_matches_id (chat, room_id)) {
-      if (index)
-        *index = i;
-
-      return chat;
-    }
-  }
-
-  return NULL;
-}
-
-static void
-matrix_parse_device_data (ChattyMaAccount *self,
-                          JsonObject      *to_device)
-{
-  JsonObject *object;
-  JsonArray *array;
-  guint length = 0;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-  g_assert (to_device);
-
-  array = matrix_utils_json_object_get_array (to_device, "events");
-  if (array)
-    length = json_array_get_length (array);
-
-  if (length)
-    CHATTY_TRACE_MSG ("Got %d to-device events", length);
-
-  for (guint i = 0; i < length; i++) {
-    const char *type;
-
-    object = json_array_get_object_element (array, i);
-    type = matrix_utils_json_object_get_string (object, "type");
-
-    CHATTY_TRACE_MSG ("parsing to-device event, type: %s", type);
-
-    if (g_strcmp0 (type, "m.room.encrypted") == 0)
-      matrix_enc_handle_room_encrypted (self->matrix_enc, object);
-  }
-}
-
-static void
-matrix_parse_room_data (ChattyMaAccount *self,
-                        JsonObject       *rooms)
-{
-  JsonObject *joined_rooms, *left_rooms;
-  ChattyMaChat *chat;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-  g_assert (rooms);
-
-  joined_rooms = matrix_utils_json_object_get_object (rooms, "join");
-
-  if (joined_rooms) {
-    g_autoptr(GList) joined_room_ids = NULL;
-    JsonObject *room_data;
-
-    joined_room_ids = json_object_get_members (joined_rooms);
-
-    for (GList *room_id = joined_room_ids; room_id; room_id = room_id->next) {
-      guint index = 0;
-
-      chat = matrix_find_chat_with_id (self, room_id->data, &index);
-      room_data = matrix_utils_json_object_get_object (joined_rooms, room_id->data);
-
-      CHATTY_TRACE (room_id->data, "joined room, new: %d, room:", !!chat);
-
-      if (!chat) {
-        chat = g_object_new (CHATTY_TYPE_MA_CHAT, "room-id", room_id->data, NULL);
-        chatty_ma_chat_set_matrix_db (chat, self->matrix_db);
-        chatty_ma_chat_set_history_db (chat, self->history_db);
-        /* TODO */
-        /* chatty_ma_chat_set_last_batch (chat, self->next_batch); */
-        chatty_ma_chat_set_data (chat, CHATTY_ACCOUNT (self), self->matrix_api, self->matrix_enc);
-        g_object_set (chat, "json-data", room_data, NULL);
-        g_list_store_append (self->chat_list, chat);
-        g_object_unref (chat);
-      } else if (room_data) {
-        g_object_set (chat, "json-data", room_data, NULL);
-        g_list_model_items_changed (G_LIST_MODEL (self->chat_list), index, 1, 1);
-      }
-    }
-  }
-
-  left_rooms = matrix_utils_json_object_get_object (rooms, "leave");
-
-  if (left_rooms) {
-    g_autoptr(GList) left_room_ids = NULL;
-
-    left_room_ids = json_object_get_members (left_rooms);
-
-    for (GList *room_id = left_room_ids; room_id; room_id = room_id->next) {
-      chat = matrix_find_chat_with_id (self, room_id->data, NULL);
-
-      if (chat) {
-        chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_HIDDEN);
-        chatty_history_update_chat (self->history_db, CHATTY_CHAT (chat));
-        chatty_utils_remove_list_item (self->chat_list, chat);
-      }
-    }
-  }
-}
-
-static void
-handle_get_homeserver (ChattyMaAccount *self,
-                       JsonObject      *object,
-                       GError          *error)
-{
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  if (error)
-    ma_account_update_status (self, CHATTY_DISCONNECTED);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) {
-    g_warning ("Couldn't connect to ‘/.well-known/matrix/client’ ");
-    matrix_api_set_homeserver (self->matrix_api, "https://chat.librem.one");
-  }
-}
-
-static void
-handle_verify_homeserver (ChattyMaAccount *self,
-                          JsonObject      *object,
-                          GError          *error)
-{
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  if (error)
-    ma_account_update_status (self, CHATTY_DISCONNECTED);
-}
-
 static void
 handle_password_login (ChattyMaAccount *self,
-                       JsonObject      *object,
                        GError          *error)
 {
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
   /* If no error, Api is informing us that logging in succeeded.
    * Let’s update matrix_enc & set device keys to upload */
-  if (g_error_matches (error, MATRIX_ERROR, M_BAD_PASSWORD)) {
-    GtkWidget *dialog, *content, *header_bar;
+  if (g_error_matches (error, CM_ERROR, CM_ERROR_BAD_PASSWORD)) {
+    GtkWidget *dialog, *content, *header_bar, *label;
     GtkWidget *cancel_btn, *ok_btn, *entry;
-    g_autofree char *label = NULL;
+    g_autofree char *message = NULL;
     const char *password;
+    CmAccount *cm_account;
     int response;
 
     dialog = gtk_dialog_new_with_buttons (_("Incorrect password"),
@@ -303,9 +114,13 @@ handle_password_login (ChattyMaAccount *self,
     content = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
     gtk_container_set_border_width (GTK_CONTAINER (content), 18);
     gtk_box_set_spacing (GTK_BOX (content), 12);
-    label = g_strdup_printf (_("Please enter password for “%s”"),
-                             matrix_api_get_login_username (self->matrix_api));
-    gtk_container_add (GTK_CONTAINER (content), gtk_label_new (label));
+    cm_account = cm_client_get_account (self->cm_client);
+    message = g_strdup_printf (_("Please enter password for “%s”, homeserver: %s"),
+                               cm_account_get_login_id (cm_account),
+                               cm_client_get_homeserver (self->cm_client));
+    label = gtk_label_new (message);
+    gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
+    gtk_container_add (GTK_CONTAINER (content), label);
     entry = gtk_entry_new ();
     gtk_entry_set_activates_default (GTK_ENTRY (entry), TRUE);
     gtk_entry_set_visibility (GTK_ENTRY (entry), FALSE);
@@ -334,130 +149,30 @@ handle_password_login (ChattyMaAccount *self,
     if (response != GTK_RESPONSE_ACCEPT || !password || !*password) {
       chatty_account_set_enabled (CHATTY_ACCOUNT (self), FALSE);
     } else {
-      matrix_api_set_password (self->matrix_api, password);
-      self->is_loading = TRUE;
+      cm_client_set_password (self->cm_client, password);
       chatty_account_set_enabled (CHATTY_ACCOUNT (self), FALSE);
-      self->is_loading = FALSE;
       chatty_account_set_enabled (CHATTY_ACCOUNT (self), TRUE);
     }
 
     gtk_widget_destroy (dialog);
   }
 
-  if (!error) {
-    self->save_password_pending = TRUE;
-    chatty_account_save (CHATTY_ACCOUNT (self));
-
+  if (!error)
     ma_account_update_status (self, CHATTY_CONNECTED);
-  }
 }
 
 static void
-handle_upload_key (ChattyMaAccount *self,
-                   JsonObject      *object,
-                   GError          *error)
-{
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  if (object) {
-    /* XXX: check later */
-    matrix_enc_publish_one_time_keys (self->matrix_enc);
-
-    self->save_account_pending = TRUE;
-    chatty_account_save (CHATTY_ACCOUNT (self));
-  }
-}
-
-static ChattyMaChat *
-ma_account_find_chat (ChattyMaAccount *self,
-                      const char      *room_id)
+cm_account_sync_cb (gpointer   user_data,
+                    CmClient  *cm_client,
+                    CmRoom    *cm_room,
+                    GPtrArray *events,
+                    GError    *error)
 {
-  GPtrArray *chats = self->db_chat_list;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  if (!room_id || !*room_id || !chats)
-    return NULL;
-
-  for (guint i = 0; i < chats->len; i++) {
-    const char *chat_name;
-
-    chat_name = chatty_chat_get_chat_name (chats->pdata[i]);
-    if (g_strcmp0 (chat_name, room_id) == 0)
-      return g_object_ref (chats->pdata[i]);
-  }
-
-  return NULL;
-}
-
-static void
-handle_get_joined_rooms (ChattyMaAccount *self,
-                         JsonObject      *object,
-                         GError          *error)
-{
-  JsonArray *array;
-  guint length = 0;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  array = matrix_utils_json_object_get_array (object, "joined_rooms");
-
-  if (array)
-    length = json_array_get_length (array);
-
-  for (guint i = 0; i < length; i++) {
-    g_autoptr(ChattyMaChat) chat = NULL;
-    const char *room_id;
-
-    room_id = json_array_get_string_element (array, i);
-    chat = ma_account_find_chat (self, room_id);
-    if (!chat)
-      chat = g_object_new (CHATTY_TYPE_MA_CHAT, "room-id", room_id, NULL);
-    chatty_ma_chat_set_matrix_db (chat, self->matrix_db);
-    chatty_ma_chat_set_history_db (chat, self->history_db);
-    chatty_ma_chat_set_data (chat, CHATTY_ACCOUNT (self), self->matrix_api, self->matrix_enc);
-    g_list_store_append (self->chat_list, chat);
-  }
-
-  g_clear_pointer (&self->db_chat_list, g_ptr_array_unref);
-}
-
-static void
-handle_red_pill (ChattyMaAccount *self,
-                 JsonObject      *root,
-                 GError          *error)
-{
-  JsonObject *object;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  if (error)
-    return;
-
-  ma_account_update_status (self, CHATTY_CONNECTED);
-
-  object = matrix_utils_json_object_get_object (root, "to_device");
-  if (object)
-    matrix_parse_device_data (self, object);
-
-  object = matrix_utils_json_object_get_object (root, "rooms");
-  if (object)
-    matrix_parse_room_data (self, object);
-
-  self->save_account_pending = TRUE;
-  chatty_account_save (CHATTY_ACCOUNT (self));
-}
+  ChattyMaAccount *self = user_data;
 
-static void
-matrix_account_sync_cb (ChattyMaAccount *self,
-                        MatrixApi       *api,
-                        MatrixAction     action,
-                        JsonObject      *object,
-                        GError          *error)
-{
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
-  g_assert (MATRIX_IS_API (api));
-  g_assert (self->matrix_api == api);
+  g_assert (CM_IS_CLIENT (self->cm_client));
+  g_assert (self->cm_client == cm_client);
 
   if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
     return;
@@ -466,61 +181,11 @@ matrix_account_sync_cb (ChattyMaAccount *self,
     g_debug ("%s Error %d: %s", g_quark_to_string (error->domain),
              error->code, error->message);
 
-  if (error &&
-      ((error->domain == SOUP_HTTP_ERROR &&
-        error->code <= SOUP_STATUS_TLS_FAILED &&
-        error->code > SOUP_STATUS_CANCELLED) ||
-       g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) ||
-       g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) ||
-       error->domain == G_RESOLVER_ERROR ||
-       error->domain == JSON_PARSER_ERROR)) {
-    ma_account_update_status (self, CHATTY_DISCONNECTED);
-    return;
-  }
-
-  if (!error && !matrix_api_is_sync (self->matrix_api) &&
-      action != MATRIX_GET_JOINED_ROOMS) {
-    ma_account_update_status (self, CHATTY_DISCONNECTED);
-    return;
-  }
-
-  switch (action) {
-  case MATRIX_BLUE_PILL:
-    return;
-
-  case MATRIX_GET_HOMESERVER:
-    handle_get_homeserver (self, object, error);
-    return;
-
-  case MATRIX_VERIFY_HOMESERVER:
-    handle_verify_homeserver (self, object, error);
-    return;
-
-  case MATRIX_PASSWORD_LOGIN:
-    handle_password_login (self, object, error);
-    return;
-
-  case MATRIX_UPLOAD_KEY:
-    handle_upload_key (self, object, error);
-    return;
-
-  case MATRIX_GET_JOINED_ROOMS:
-    handle_get_joined_rooms (self, object, error);
-    return;
-
-  case MATRIX_RED_PILL:
-    handle_red_pill (self, object, error);
-    return;
-
-  case MATRIX_ACCESS_TOKEN_LOGIN:
-  case MATRIX_SET_TYPING:
-  case MATRIX_SEND_MESSAGE:
-  case MATRIX_SEND_IMAGE:
-  case MATRIX_SEND_VIDEO:
-  case MATRIX_SEND_FILE:
-  default:
-    break;
-  }
+  if (g_error_matches (error, CM_ERROR, CM_ERROR_BAD_PASSWORD))
+    {
+      handle_password_login (self, error);
+      return;
+    }
 }
 
 static const char *
@@ -546,7 +211,7 @@ chatty_ma_account_get_enabled (ChattyAccount *account)
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  return self->account_enabled;
+  return cm_client_get_enabled (self->cm_client);
 }
 
 static void
@@ -556,40 +221,12 @@ chatty_ma_account_set_enabled (ChattyAccount *account,
   ChattyMaAccount *self = (ChattyMaAccount *)account;
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
+  g_assert (self->cm_client);
 
-  if (self->account_enabled == enable)
-    return;
-
-  g_clear_handle_id (&self->connect_id, g_source_remove);
-
-  if (!self->matrix_enc && enable) {
-    CHATTY_TRACE_MSG ("Create new enc. user: %s has pickle: %d, has key: %d",
-                      chatty_item_get_username (CHATTY_ITEM (account)), FALSE, FALSE);
-    self->matrix_enc = matrix_enc_new (self->matrix_db, NULL, NULL);
-    matrix_api_set_enc (self->matrix_api, self->matrix_enc);
-  }
-
-  self->account_enabled = enable;
-  CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (account)),
-                "Enable account: %d, is loading: %d, user:",
-                enable, self->is_loading);
-
-  if (self->account_enabled &&
-      chatty_ma_account_can_connect (self)) {
-    ma_account_update_status (self, CHATTY_CONNECTING);
-    matrix_api_start_sync (self->matrix_api);
-  } else if (!self->account_enabled){
-    ma_account_update_status (self, CHATTY_DISCONNECTED);
-    matrix_api_stop_sync (self->matrix_api);
-  }
+  cm_client_set_enabled (self->cm_client, enable);
 
   g_object_notify (G_OBJECT (self), "enabled");
   g_object_notify (G_OBJECT (self), "status");
-
-  if (!self->is_loading) {
-    self->save_account_pending = TRUE;
-    chatty_account_save (account);
-  }
 }
 
 static const char *
@@ -600,7 +237,7 @@ chatty_ma_account_get_password (ChattyAccount *account)
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  password = matrix_api_get_password (self->matrix_api);
+  password = cm_client_get_password (self->cm_client);
 
   if (password)
     return password;
@@ -616,15 +253,18 @@ chatty_ma_account_set_password (ChattyAccount *account,
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  if (g_strcmp0 (password, matrix_api_get_password (self->matrix_api)) == 0)
-    return;
 
-  matrix_api_set_password (self->matrix_api, password);
+  if (self->cm_client)
+    {
+      if (cm_client_get_logging_in (self->cm_client) ||
+          cm_client_get_logged_in (self->cm_client))
+        return;
+    }
 
-  if (matrix_api_get_homeserver (self->matrix_api)) {
-    self->save_password_pending = TRUE;
-    chatty_account_save (account);
-  }
+  if (g_strcmp0 (password, cm_client_get_password (self->cm_client)) == 0)
+    return;
+
+  cm_client_set_password (self->cm_client, password);
 }
 
 static gboolean
@@ -635,8 +275,7 @@ account_connect (gpointer user_data)
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
   self->connect_id = 0;
-  matrix_api_start_sync (self->matrix_api);
-  ma_account_update_status (self, CHATTY_CONNECTING);
+  cm_client_start_sync (self->cm_client);
 
   return G_SOURCE_REMOVE;
 }
@@ -652,7 +291,10 @@ chatty_ma_account_connect (ChattyAccount *account,
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
   if (!chatty_account_get_enabled (account)) {
-    CHATTY_TRACE (matrix_api_get_login_username (self->matrix_api),
+    CmAccount *cm_account;
+
+    cm_account = cm_client_get_account (self->cm_client);
+    CHATTY_TRACE (cm_account_get_login_id (cm_account),
                   "Trying to connect disabled account, username:");
     return;
   }
@@ -675,7 +317,7 @@ chatty_ma_account_disconnect (ChattyAccount *account)
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  matrix_api_stop_sync (self->matrix_api);
+  cm_client_stop_sync (self->cm_client);
   ma_account_update_status (self, CHATTY_DISCONNECTED);
 }
 
@@ -686,17 +328,6 @@ chatty_ma_account_get_remember_password (ChattyAccount *self)
   return TRUE;
 }
 
-static void
-chatty_ma_account_save (ChattyAccount *account)
-{
-  ChattyMaAccount *self = (ChattyMaAccount *)account;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-  g_return_if_fail (matrix_api_get_login_username (self->matrix_api));
-
-  chatty_ma_account_save_async (self, FALSE, NULL, NULL, NULL);
-}
-
 static void
 chatty_ma_account_delete (ChattyAccount *account)
 {
@@ -713,15 +344,15 @@ chatty_ma_account_get_device_fp (ChattyAccount *account)
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  device_id = matrix_api_get_device_id (self->matrix_api);
+  device_id = cm_client_get_device_id (self->cm_client);
   g_clear_object (&self->device_fp);
 
-  if (!self->device_fp && device_id) {
+  if (device_id) {
     g_autoptr(GString) fp = NULL;
     const char *str;
 
     fp = g_string_new (NULL);
-    str = matrix_enc_get_ed25519_key (self->matrix_enc);
+    str = cm_client_get_ed25519_key (self->cm_client);
 
     while (str && *str) {
       g_autofree char *chunk = g_strndup (str, 4);
@@ -755,7 +386,7 @@ ma_account_leave_chat_cb (GObject      *object,
   chat = g_task_get_task_data (task);
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  success = matrix_api_leave_chat_finish (self->matrix_api, result, &error);
+  success = cm_room_leave_finish (CM_ROOM (object), result, &error);
   CHATTY_TRACE_MSG ("Leaving chat: %s(%s), success: %d",
                     chatty_item_get_name (CHATTY_ITEM (chat)),
                     chatty_chat_get_chat_name (chat),
@@ -773,7 +404,6 @@ ma_account_leave_chat_cb (GObject      *object,
 
     old_state = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "state"));
     chatty_item_set_state (CHATTY_ITEM (chat), old_state);
-    chatty_history_update_chat (self->history_db, chat);
   }
 
   if (error)
@@ -809,11 +439,10 @@ chatty_ma_account_leave_chat_async (ChattyAccount       *account,
   g_object_set_data (G_OBJECT (task), "state",
                      GINT_TO_POINTER (chatty_item_get_state (CHATTY_ITEM (chat))));
   chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_HIDDEN);
-  chatty_history_update_chat (self->history_db, chat);
-  matrix_api_leave_chat_async (self->matrix_api,
-                               chatty_chat_get_chat_name (chat),
-                               ma_account_leave_chat_cb,
-                               g_steal_pointer (&task));
+  cm_room_leave_async (chatty_ma_chat_get_cm_room (CHATTY_MA_CHAT (chat)),
+                       NULL,
+                       ma_account_leave_chat_cb,
+                       g_steal_pointer (&task));
 }
 
 static ChattyProtocol
@@ -854,27 +483,12 @@ chatty_ma_account_get_username (ChattyItem *item)
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  if (matrix_api_get_username (self->matrix_api))
-    return matrix_api_get_username (self->matrix_api);
+  if (self->cm_client && cm_client_get_user_id (self->cm_client))
+    return cm_client_get_user_id (self->cm_client);
 
   return "";
 }
 
-static void
-chatty_ma_account_set_username (ChattyItem *item,
-                                const char *username)
-{
-  ChattyMaAccount *self = (ChattyMaAccount *)item;
-
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  matrix_api_set_login_username (self->matrix_api, username);
-
-  /* If in test, also set username */
-  if (g_test_initialized ())
-    matrix_api_set_username (self->matrix_api, username);
-}
-
 static ChattyFileInfo *
 chatty_ma_account_get_avatar_file (ChattyItem *item)
 {
@@ -913,11 +527,6 @@ chatty_ma_account_get_avatar (ChattyItem *item)
                                    ma_account_get_avatar_pixbuf_cb,
                                    g_object_ref (self));
 
-  } else {
-    matrix_api_get_file_async (self->matrix_api, NULL, self->avatar_file,
-                               NULL, NULL,
-                               ma_account_get_avatar_cb,
-                               g_object_ref (self));
   }
 
   return NULL;
@@ -937,7 +546,7 @@ ma_account_set_user_avatar_cb (GObject      *object,
   self = g_task_get_source_object (task);
   g_assert (CHATTY_MA_ACCOUNT (self));
 
-  matrix_api_set_user_avatar_finish (self->matrix_api, result, &error);
+  cm_account_set_user_avatar_finish (CM_ACCOUNT (object), result, &error);
 
   if (error)
     g_task_return_error (task, error);
@@ -957,7 +566,6 @@ ma_account_set_user_avatar_cb (GObject      *object,
 
     g_clear_pointer (&self->avatar_file, chatty_file_info_free);
     g_clear_object (&self->avatar);
-    chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self));
     g_signal_emit_by_name (self, "avatar-changed");
   }
 }
@@ -971,6 +579,7 @@ chatty_ma_account_set_avatar_async (ChattyItem          *item,
 {
   ChattyMaAccount *self = (ChattyMaAccount *)item;
   g_autoptr(GTask) task = NULL;
+  GFile *file;
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
@@ -982,7 +591,9 @@ chatty_ma_account_set_avatar_async (ChattyItem          *item,
       return;
     }
 
-  matrix_api_set_user_avatar_async (self->matrix_api, file_name, cancellable,
+  file = g_file_new_for_path (file_name);
+  cm_account_set_user_avatar_async (cm_client_get_account (self->cm_client),
+                                    file, cancellable,
                                     ma_account_set_user_avatar_cb,
                                     g_steal_pointer (&task));
 }
@@ -995,19 +606,12 @@ chatty_ma_account_finalize (GObject *object)
   g_clear_handle_id (&self->connect_id, g_source_remove);
   g_list_store_remove_all (self->chat_list);
 
-  g_clear_object (&self->matrix_api);
-  g_clear_object (&self->matrix_enc);
   g_clear_object (&self->device_fp);
   g_clear_object (&self->chat_list);
   g_clear_object (&self->avatar);
-  g_clear_object (&self->matrix_db);
-  g_clear_object (&self->history_db);
-  g_clear_pointer (&self->db_chat_list, g_ptr_array_unref);
   g_clear_pointer (&self->avatar_file, chatty_file_info_free);
 
   g_free (self->name);
-  g_free (self->pickle_key);
-  g_free (self->next_batch);
 
   G_OBJECT_CLASS (chatty_ma_account_parent_class)->finalize (object);
 }
@@ -1025,7 +629,6 @@ chatty_ma_account_class_init (ChattyMaAccountClass *klass)
   item_class->get_name = chatty_ma_account_get_name;
   item_class->set_name = chatty_ma_account_set_name;
   item_class->get_username = chatty_ma_account_get_username;
-  item_class->set_username = chatty_ma_account_set_username;
   item_class->get_avatar_file = chatty_ma_account_get_avatar_file;
   item_class->get_avatar = chatty_ma_account_get_avatar;
   item_class->set_avatar_async = chatty_ma_account_set_avatar_async;
@@ -1039,7 +642,6 @@ chatty_ma_account_class_init (ChattyMaAccountClass *klass)
   account_class->connect      = chatty_ma_account_connect;
   account_class->disconnect   = chatty_ma_account_disconnect;
   account_class->get_remember_password = chatty_ma_account_get_remember_password;
-  account_class->save = chatty_ma_account_save;
   account_class->delete = chatty_ma_account_delete;
   account_class->get_device_fp = chatty_ma_account_get_device_fp;
   account_class->leave_chat_async = chatty_ma_account_leave_chat_async;
@@ -1048,399 +650,176 @@ chatty_ma_account_class_init (ChattyMaAccountClass *klass)
 static void
 chatty_ma_account_init (ChattyMaAccount *self)
 {
-  self->chat_list = g_list_store_new (CHATTY_TYPE_MA_CHAT);
-
-  self->matrix_api = matrix_api_new (NULL);
-  matrix_api_set_sync_callback (self->matrix_api,
-                                (MatrixCallback)matrix_account_sync_cb, self);
-}
-
-ChattyMaAccount *
-chatty_ma_account_new (const char *username,
-                       const char *password)
-{
-  ChattyMaAccount *self;
-
-  g_return_val_if_fail (username, NULL);
-
-  self = g_object_new (CHATTY_TYPE_MA_ACCOUNT, NULL);
-
-  chatty_item_set_username (CHATTY_ITEM (self), username);
-  chatty_account_set_password (CHATTY_ACCOUNT (self), password);
-  CHATTY_DEBUG_DETAILED (username, "New Matrix account");
-
-  return self;
-}
-
-gboolean
-chatty_ma_account_can_connect (ChattyMaAccount *self)
-{
-  g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), FALSE);
-
-  return matrix_api_can_connect (self->matrix_api);
-}
-
-/**
- * chatty_ma_account_get_login_username:
- * @self: A #ChattyMaAccount
- *
- * Get the username set when @self was created.  This
- * can be different from chatty_item_get_username().
- *
- * Say for example the user may have logged in using
- * an email address.  So If you want to get the original
- * username (which is the mail) which was used for login,
- * use this method.
- */
-
-const char *
-chatty_ma_account_get_login_username (ChattyMaAccount *self)
-{
-  g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), "");
-
-  return matrix_api_get_login_username (self->matrix_api);
-}
-
-static char *
-ma_account_get_value (const char *str,
-                      const char *key)
-{
-  const char *start, *end;
-
-  if (!str || !*str)
-    return NULL;
-
-  g_assert (key && *key);
-
-  start = strstr (str, key);
-  if (start) {
-    start = start + strlen (key);
-    while (*start && *start++ != '"')
-      ;
-
-    end = start - 1;
-    do {
-      end++;
-      end = strchr (end, '"');
-    } while (end && *(end - 1) == '\\' && *(end - 2) != '\\');
-
-    if (end && end > start)
-      return g_strndup (start, end - start);
-  }
-
-  return NULL;
-}
-
-ChattyMaAccount *
-chatty_ma_account_new_secret (gpointer secret_retrievable)
-{
-  ChattyMaAccount *self = NULL;
-  g_autoptr(GHashTable) attributes = NULL;
-  SecretRetrievable *item = secret_retrievable;
-  g_autoptr(SecretValue) value = NULL;
-  const char *homeserver, *credentials = NULL;
-  const char *username, *login_username;
-  char *password, *token, *device_id;
-  char *password_str, *token_str = NULL;
-
-  g_return_val_if_fail (SECRET_IS_RETRIEVABLE (item), NULL);
-
-  value = secret_retrievable_retrieve_secret_sync (item, NULL, NULL);
-
-  if (value)
-    credentials = secret_value_get_text (value);
-
-  if (!credentials)
-    return NULL;
-
-  attributes = secret_retrievable_get_attributes (item);
-  login_username = g_hash_table_lookup (attributes, CHATTY_USERNAME_ATTRIBUTE);
-  homeserver = g_hash_table_lookup (attributes, CHATTY_SERVER_ATTRIBUTE);
-
-  password = ma_account_get_value (credentials, "\"password\"");
-  g_return_val_if_fail (password, NULL);
-  password_str = g_strcompress (password);
-
-  self = chatty_ma_account_new (login_username, password_str);
-  token = ma_account_get_value (credentials, "\"access-token\"");
-  device_id = ma_account_get_value (credentials, "\"device-id\"");
-  username = ma_account_get_value (credentials, "\"username\"");
-
-  if (username && *username)
-    matrix_api_set_username (self->matrix_api, username);
-  chatty_ma_account_set_homeserver (self, homeserver);
-
-  if (token)
-    token_str = g_strcompress (token);
-
-  if (token && device_id) {
-    self->pickle_key = ma_account_get_value (credentials, "\"pickle-key\"");
-    matrix_api_set_access_token (self->matrix_api, token_str, device_id);
-  }
-
-  matrix_utils_free_buffer (device_id);
-  matrix_utils_free_buffer (password);
-  matrix_utils_free_buffer (password_str);
-  matrix_utils_free_buffer (token);
-  matrix_utils_free_buffer (token_str);
-
-  return self;
+  self->chat_list = g_list_store_new (CHATTY_TYPE_CHAT);
 }
 
 static void
-db_load_account_cb (GObject      *object,
-                    GAsyncResult *result,
-                    gpointer      user_data)
+joined_rooms_changed (ChattyMaAccount *self,
+                      int              position,
+                      int              removed,
+                      int              added,
+                      GListModel      *model)
 {
-  ChattyMaAccount *self = user_data;
-  GTask *task = (GTask *)result;
-  g_autoptr(GError) error = NULL;
-  gboolean enabled;
+  g_autoptr(GPtrArray) items = NULL;
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
-  g_assert (G_IS_TASK (task));
+  g_assert (G_IS_LIST_MODEL (model));
 
-  if (!self->matrix_enc) {
-    const char *pickle;
-
-    pickle = g_object_get_data (G_OBJECT (task), "pickle");
-    CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)),
-                  "Create new enc. has pickle: %d, has key: %d, user:",
-                  !!pickle, !!self->pickle_key);
-    self->matrix_enc = matrix_enc_new (self->matrix_db, pickle, self->pickle_key);
-    matrix_api_set_enc (self->matrix_api, self->matrix_enc);
-    if (!pickle)
-      matrix_api_set_access_token (self->matrix_api, NULL, NULL);
-    g_clear_pointer (&self->pickle_key, matrix_utils_free_buffer);
-  }
-
-  if (!matrix_db_load_account_finish (self->matrix_db, result, &error)) {
-    if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("Error loading account %s: %s",
-                 chatty_item_get_username (CHATTY_ITEM (self)),
-                 error->message);
-    return;
-  }
+  for (guint i = position; i < position + added; i++)
+    {
+      g_autoptr(CmRoom) room = NULL;
+      ChattyMaChat *chat;
 
-  enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled"));
-  self->next_batch = g_strdup (g_object_get_data (G_OBJECT (task), "batch"));
-  CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)),
-                "Loaded from db. enabled: %d, has next-batch: %d, user:",
-                !!enabled, !!self->next_batch);
+      if (!items)
+          items = g_ptr_array_new_with_free_func (g_object_unref);
 
-  self->is_loading = TRUE;
+      room = g_list_model_get_item (model, i);
+      chat = chatty_ma_chat_new_with_room (room);
+      chatty_ma_chat_set_data (chat, CHATTY_ACCOUNT (self), self->cm_client);
+      g_ptr_array_add (items, chat);
+    }
 
-  matrix_api_set_next_batch (self->matrix_api, self->next_batch);
-  chatty_account_set_enabled (CHATTY_ACCOUNT (self), enabled);
-  self->is_loading = FALSE;
+  g_list_store_splice (self->chat_list, position, removed,
+                       items ? items->pdata : NULL, added);
 }
 
 static void
-db_load_chats_cb (GObject      *object,
-                  GAsyncResult *result,
-                  gpointer      user_data)
+key_verifications_changed (ChattyMaAccount *self,
+                           int              position,
+                           int              removed,
+                           int              added,
+                           GListModel      *model)
 {
-  ChattyMaAccount *self = user_data;
-  GTask *task = (GTask *)result;
-  GPtrArray *chats = NULL;
-  g_autoptr(GError) error = NULL;
+  g_autoptr(GPtrArray) items = NULL;
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
-  g_assert (G_IS_TASK (task));
+  g_assert (G_IS_LIST_MODEL (model));
 
-  chats = chatty_history_get_chats_finish (self->history_db, result, &error);
-  self->db_chat_list = chats;
-  CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)),
-                "Loaded %u chats from db, user:",
-                !chats ? 0 : chats->len);
+  for (guint i = position; i < position + added; i++) {
+    g_autoptr(CmEvent) event = NULL;
+    ChattyMaKeyChat *chat;
 
-  if (error)
-    g_warning ("Error getting chats: %s", error->message);
+    if (!items)
+      items = g_ptr_array_new_with_free_func (g_object_unref);
 
+    event = g_list_model_get_item (model, i);
+    chat = chatty_ma_key_chat_new (self, event);
+    g_ptr_array_add (items, chat);
+  }
 
-  matrix_db_load_account_async (self->matrix_db, CHATTY_ACCOUNT (self),
-                                matrix_api_get_device_id (self->matrix_api),
-                                db_load_account_cb, self);
+  g_list_store_splice (self->chat_list, position, removed,
+                       items ? items->pdata : NULL, added);
 }
 
 static void
-history_db_load_account_cb (GObject      *object,
-                            GAsyncResult *result,
-                            gpointer      user_data)
+client_status_changed_cb (ChattyMaAccount *self)
 {
-  g_autoptr(ChattyMaAccount) self = user_data;
-  const char *name, *avatar_url, *avatar_path;
-  g_autoptr(GError) error = NULL;
-  ChattyFileInfo *file;
+  ChattyStatus status = CHATTY_DISCONNECTED;
 
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  chatty_history_load_account_finish (self->history_db, result, &error);
-
-  if (error)
-    g_warning ("error loading account: %s", error->message);
-
-  name = g_object_get_data (G_OBJECT (result), "name");
-  avatar_url = g_object_get_data (G_OBJECT (result), "avatar-url");
-  avatar_path = g_object_get_data (G_OBJECT (result), "avatar-path");
-
-  self->name = g_strdup (name);
-  g_object_notify (G_OBJECT (self), "name");
+  if (!cm_client_get_enabled (self->cm_client))
+    status = CHATTY_DISCONNECTED;
+  else if (cm_client_is_sync (self->cm_client))
+    status = CHATTY_CONNECTED;
+  else if (cm_client_get_logging_in (self->cm_client) ||
+           cm_client_get_logged_in (self->cm_client))
+    status = CHATTY_CONNECTING;
 
-  file = g_new0 (ChattyFileInfo, 1);
-  file->url = g_strdup (avatar_url);
-  file->path = g_strdup (avatar_path);
-  self->avatar_file = file;
-
-  chatty_history_get_chats_async (self->history_db, CHATTY_ACCOUNT (self),
-                                  db_load_chats_cb, self);
-}
-
-void
-chatty_ma_account_set_db (ChattyMaAccount *self,
-                          gpointer         matrix_db,
-                          gpointer         history_db)
-{
-  g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self));
-  g_return_if_fail (MATRIX_IS_DB (matrix_db));
-  g_return_if_fail (CHATTY_IS_HISTORY (history_db));
-  g_return_if_fail (!self->matrix_db);
-  g_return_if_fail (!self->history_db);
-
-  self->matrix_db = g_object_ref (matrix_db);
-  self->history_db = g_object_ref (history_db);
-  chatty_history_load_account_async (self->history_db, CHATTY_ACCOUNT (self),
-                                     history_db_load_account_cb,
-                                     g_object_ref (self));
+  ma_account_update_status (self, status);
 }
 
 static void
-ma_account_db_save_cb (GObject      *object,
-                       GAsyncResult *result,
-                       gpointer      user_data)
+ma_account_set_client (ChattyMaAccount *self,
+                       CmClient        *client)
 {
-  ChattyMaAccount *self;
-  g_autoptr(GTask) task = user_data;
-  GError *error = NULL;
-  gboolean status;
-
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (G_IS_TASK (task));
+  GListModel *joined_rooms, *invited_rooms, *key_verifications;
 
-  self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
-
-  status = matrix_db_save_account_finish (self->matrix_db, result, &error);
-  if (error || !status)
-    CHATTY_TRACE_MSG ("Saving %s failed",
-                      chatty_item_get_username (CHATTY_ITEM (self)));
-
-  if (error || !status)
-    self->save_account_pending = TRUE;
-
-  if (error)
-    g_task_return_error (task, error);
-  else
-    g_task_return_boolean (task, status);
+  g_assert (CM_IS_CLIENT (client));
+  g_assert (!self->cm_client);
+
+  self->cm_client = client;
+  cm_client_set_device_name (client, "Chatty");
+  cm_client_set_sync_callback (client,
+                               cm_account_sync_cb,
+                               self, NULL);
+
+  g_signal_connect_object (self->cm_client, "status-changed",
+                           G_CALLBACK (client_status_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+
+  joined_rooms = cm_client_get_joined_rooms (client);
+  g_signal_connect_object (joined_rooms, "items-changed",
+                           G_CALLBACK (joined_rooms_changed), self,
+                           G_CONNECT_SWAPPED);
+  joined_rooms_changed (self, 0, 0, g_list_model_get_n_items (joined_rooms), joined_rooms);
+
+  invited_rooms = cm_client_get_invited_rooms (client);
+  g_signal_connect_object (invited_rooms, "items-changed",
+                           G_CALLBACK (joined_rooms_changed), self,
+                           G_CONNECT_SWAPPED);
+  joined_rooms_changed (self, 0, 0, g_list_model_get_n_items (invited_rooms), invited_rooms);
+
+  key_verifications = cm_client_get_key_verifications (client);
+  g_signal_connect_object (key_verifications, "items-changed",
+                           G_CALLBACK (key_verifications_changed), self,
+                           G_CONNECT_SWAPPED);
+  key_verifications_changed (self, 0, 0, g_list_model_get_n_items (key_verifications), key_verifications);
 }
 
-static void
-ma_account_save_cb (GObject      *object,
-                    GAsyncResult *result,
-                    gpointer      user_data)
+ChattyMaAccount *
+chatty_ma_account_new_from_client (CmClient *cm_client)
 {
   ChattyMaAccount *self;
-  g_autoptr(GTask) task = user_data;
-  GError *error = NULL;
-  gboolean status;
 
-  g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (G_IS_TASK (task));
+  g_return_val_if_fail (CM_IS_CLIENT (cm_client), NULL);
 
-  self = g_task_get_source_object (task);
-  g_assert (CHATTY_IS_MA_ACCOUNT (self));
+  self = g_object_new (CHATTY_TYPE_MA_ACCOUNT, NULL);
+  ma_account_set_client (self, g_object_ref (cm_client));
 
-  status = chatty_secret_store_save_finish (result, &error);
+  return self;
+}
 
-  if (error || !status)
-    self->save_password_pending = TRUE;
+CmClient *
+chatty_ma_account_get_cm_client (ChattyMaAccount *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), NULL);
 
-  if (error) {
-    g_task_return_error (task, error);
-  } else if (self->save_account_pending) {
-    char *pickle = NULL;
-
-    if (matrix_api_get_access_token (self->matrix_api))
-      pickle = matrix_enc_get_account_pickle (self->matrix_enc);
-
-    self->save_account_pending = FALSE;
-    matrix_db_save_account_async (self->matrix_db, CHATTY_ACCOUNT (self),
-                                  chatty_account_get_enabled (CHATTY_ACCOUNT (self)),
-                                  pickle,
-                                  matrix_api_get_device_id (self->matrix_api),
-                                  matrix_api_get_next_batch (self->matrix_api),
-                                  ma_account_db_save_cb, g_steal_pointer (&task));
-  } else {
-    g_task_return_boolean (task, status);
-  }
+  return self->cm_client;
 }
 
-void
-chatty_ma_account_save_async (ChattyMaAccount     *self,
-                              gboolean             force,
-                              GCancellable        *cancellable,
-                              GAsyncReadyCallback  callback,
-                              gpointer             user_data)
+gboolean
+chatty_ma_account_can_connect (ChattyMaAccount *self)
 {
-  GTask *task;
+  g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), FALSE);
 
-  g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-  g_return_if_fail (*chatty_ma_account_get_login_username (self));
+  return cm_client_can_connect (self->cm_client);
+}
 
-  if (!*chatty_account_get_password (CHATTY_ACCOUNT (self)))
-    return;
+/**
+ * chatty_ma_account_get_login_username:
+ * @self: A #ChattyMaAccount
+ *
+ * Get the username set when @self was created.  This
+ * can be different from chatty_item_get_username().
+ *
+ * Say for example the user may have logged in using
+ * an email address.  So If you want to get the original
+ * username (which is the mail) which was used for login,
+ * use this method.
+ */
 
-  g_return_if_fail (*chatty_ma_account_get_homeserver (self));
+const char *
+chatty_ma_account_get_login_username (ChattyMaAccount *self)
+{
+  CmAccount *cm_account;
 
-  task = g_task_new (self, cancellable, callback, user_data);
-  if (self->save_password_pending || force) {
-    char *key = NULL;
-
-    if (self->matrix_enc && matrix_api_get_access_token (self->matrix_api))
-      key = matrix_enc_get_pickle_key (self->matrix_enc);
-
-    self->save_password_pending = FALSE;
-    chatty_secret_store_save_async (CHATTY_ACCOUNT (self),
-                                    g_strdup (matrix_api_get_access_token (self->matrix_api)),
-                                    matrix_api_get_device_id (self->matrix_api),
-                                    key, cancellable,
-                                    ma_account_save_cb, task);
-  } else if (self->save_account_pending) {
-    char *pickle = NULL;
-
-    if (matrix_api_get_access_token (self->matrix_api))
-      pickle = matrix_enc_get_account_pickle (self->matrix_enc);
-
-    self->save_account_pending = FALSE;
-    matrix_db_save_account_async (self->matrix_db, CHATTY_ACCOUNT (self),
-                                  chatty_account_get_enabled (CHATTY_ACCOUNT (self)),
-                                  pickle,
-                                  matrix_api_get_device_id (self->matrix_api),
-                                  matrix_api_get_next_batch (self->matrix_api),
-                                  ma_account_db_save_cb, task);
-  }
-}
+  g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), "");
 
-gboolean
-chatty_ma_account_save_finish (ChattyMaAccount  *self,
-                               GAsyncResult     *result,
-                               GError          **error)
-{
-  g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  cm_account = cm_client_get_account (self->cm_client);
 
-  return g_task_propagate_boolean (G_TASK (result), error);
+  return cm_account_get_login_id (cm_account);
 }
 
 const char *
@@ -1450,7 +829,7 @@ chatty_ma_account_get_homeserver (ChattyMaAccount *self)
 
   g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), "");
 
-  homeserver = matrix_api_get_homeserver (self->matrix_api);
+  homeserver = cm_client_get_homeserver (self->cm_client);
 
   if (homeserver)
     return homeserver;
@@ -1464,7 +843,7 @@ chatty_ma_account_set_homeserver (ChattyMaAccount *self,
 {
   g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self));
 
-  matrix_api_set_homeserver (self->matrix_api, server_url);
+  cm_client_set_homeserver (self->cm_client, server_url);
 }
 
 const char *
@@ -1473,7 +852,7 @@ chatty_ma_account_get_device_id (ChattyMaAccount *self)
   const char *device_id;
   g_return_val_if_fail (CHATTY_IS_MA_ACCOUNT (self), "");
 
-  device_id = matrix_api_get_device_id (self->matrix_api);
+  device_id = cm_client_get_device_id (self->cm_client);
 
   if (device_id)
     return device_id;
@@ -1504,7 +883,6 @@ ma_get_details_cb (GObject      *object,
 {
   ChattyMaAccount *self;
   g_autoptr(GTask) task = user_data;
-  char *name, *avatar_url;
   GError *error = NULL;
 
   g_assert (G_IS_TASK (task));
@@ -1512,9 +890,8 @@ ma_get_details_cb (GObject      *object,
   self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  matrix_api_get_user_info_finish (self->matrix_api,
-                                   &name, &avatar_url,
-                                   result, &error);
+  cm_user_load_info_finish (CM_USER (object),
+                            result, &error);
 
   if (error)
     g_task_return_error (task, error);
@@ -1522,19 +899,18 @@ ma_get_details_cb (GObject      *object,
     ChattyFileInfo *file;
 
     CHATTY_TRACE_MSG ("Got user info for %s",
-                      matrix_api_get_username (self->matrix_api));
+                      cm_client_get_user_id (self->cm_client));
 
     g_free (self->name);
-    self->name = name;
+    self->name = g_strdup (cm_user_get_display_name (CM_USER (object)));
     file = self->avatar_file;
 
-    if (g_strcmp0 (file->url, avatar_url) != 0) {
+    if (file && g_strcmp0 (file->url, cm_user_get_avatar_url (CM_USER (object))) != 0) {
       g_clear_pointer (&file->path, g_free);
       g_free (file->url);
-      file->url = avatar_url;
+      file->url = g_strdup (cm_user_get_avatar_url (CM_USER (object)));
     }
 
-    chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self));
     g_object_notify (G_OBJECT (self), "name");
     g_task_return_boolean (task, TRUE);
   }
@@ -1547,18 +923,20 @@ chatty_ma_account_get_details_async (ChattyMaAccount     *self,
                                      gpointer             user_data)
 {
   g_autoptr(GTask) task = NULL;
+  CmAccount *account;
 
   g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
+  account = cm_client_get_account (self->cm_client);
 
   if (self->name)
     g_task_return_boolean (task, TRUE);
   else
-    matrix_api_get_user_info_async (self->matrix_api, NULL, cancellable,
-                                    ma_get_details_cb,
-                                    g_steal_pointer (&task));
+    cm_user_load_info_async (CM_USER (account), cancellable,
+                             ma_get_details_cb,
+                             g_steal_pointer (&task));
 }
 
 gboolean
@@ -1586,7 +964,9 @@ ma_set_name_cb (GObject      *object,
   self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  matrix_api_set_name_finish (self->matrix_api, result, &error);
+  cm_account_set_display_name_finish (CM_ACCOUNT (object), result, &error);
+  CHATTY_TRACE (chatty_item_get_username (CHATTY_ITEM (self)),
+                "Setting name %s user:", CHATTY_LOG_SUCESS (!error));
 
   if (error)
     g_task_return_error (task, error);
@@ -1597,7 +977,6 @@ ma_set_name_cb (GObject      *object,
     g_free (self->name);
     self->name = g_strdup (name);
 
-    chatty_history_update_user (self->history_db, CHATTY_ACCOUNT (self));
     g_object_notify (G_OBJECT (self), "name");
     g_task_return_boolean (task, TRUE);
   }
@@ -1618,8 +997,9 @@ chatty_ma_account_set_name_async (ChattyMaAccount     *self,
   task = g_task_new (self, cancellable, callback, user_data);
   g_task_set_task_data (task, g_strdup (name), g_free);
 
-  matrix_api_set_name_async (self->matrix_api, name, cancellable,
-                             ma_set_name_cb, task);
+  cm_account_set_display_name_async (cm_client_get_account (self->cm_client),
+                                     name, cancellable,
+                                     ma_set_name_cb, task);
 }
 
 gboolean
@@ -1648,9 +1028,9 @@ ma_get_3pid_cb (GObject      *object,
   self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  matrix_api_get_3pid_finish (self->matrix_api,
-                              &emails, &phones,
-                              result, &error);
+  cm_account_get_3pids_finish (CM_ACCOUNT (object),
+                               &emails, &phones,
+                               result, &error);
 
   if (error)
     g_task_return_error (task, error);
@@ -1677,9 +1057,10 @@ chatty_ma_account_get_3pid_async (ChattyMaAccount     *self,
 
   task = g_task_new (self, cancellable, callback, user_data);
 
-  matrix_api_get_3pid_async (self->matrix_api, cancellable,
-                             ma_get_3pid_cb,
-                             g_steal_pointer (&task));
+  cm_account_get_3pids_async (cm_client_get_account (self->cm_client),
+                              cancellable,
+                              ma_get_3pid_cb,
+                              g_steal_pointer (&task));
 }
 
 gboolean
@@ -1714,7 +1095,7 @@ ma_delete_3pid_cb (GObject      *object,
   self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_ACCOUNT (self));
 
-  matrix_api_delete_3pid_finish (self->matrix_api, result, &error);
+  cm_account_delete_3pid_finish (CM_ACCOUNT (object), result, &error);
 
   if (error)
     g_task_return_error (task, error);
@@ -1731,14 +1112,19 @@ chatty_ma_account_delete_3pid_async (ChattyMaAccount     *self,
                                      gpointer             user_data)
 {
   GTask *task = NULL;
+  const char *type_str = NULL;
 
   g_return_if_fail (CHATTY_IS_MA_ACCOUNT (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
 
-  matrix_api_delete_3pid_async (self->matrix_api,
-                                value, type, cancellable,
+  if (type == CHATTY_ID_PHONE)
+    type_str = "msisdn";
+  else
+    type_str = "email";
+  cm_account_delete_3pid_async (cm_client_get_account (self->cm_client),
+                                value, type_str, cancellable,
                                 ma_delete_3pid_cb, task);
 }
 
@@ -1761,6 +1147,6 @@ chatty_ma_account_add_chat (ChattyMaAccount *self,
   g_return_if_fail (CHATTY_IS_MA_CHAT (chat));
 
   chatty_ma_chat_set_data (CHATTY_MA_CHAT (chat), CHATTY_ACCOUNT (self),
-                           self->matrix_api, self->matrix_enc);
+                           self->cm_client);
   g_list_store_append (self->chat_list, chat);
 }
diff --git a/src/matrix/chatty-ma-account.h b/src/matrix/chatty-ma-account.h
index 310bb61e90fc0c1ca5a019dfb7a647b7c051c046..73fba4bc29b2cb3b894e7594c7411bdac922add7 100644
--- a/src/matrix/chatty-ma-account.h
+++ b/src/matrix/chatty-ma-account.h
@@ -12,6 +12,8 @@
 #pragma once
 
 #include <glib-object.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
 #include "chatty-chat.h"
 #include "chatty-enums.h"
@@ -23,22 +25,10 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (ChattyMaAccount, chatty_ma_account, CHATTY, MA_ACCOUNT, ChattyAccount)
 
-ChattyMaAccount  *chatty_ma_account_new                (const char      *username,
-                                                        const char      *password);
-gboolean         chatty_ma_account_can_connect         (ChattyMaAccount *self);
+ChattyMaAccount  *chatty_ma_account_new_from_client    (CmClient        *cm_client);
+CmClient         *chatty_ma_account_get_cm_client      (ChattyMaAccount *self);
+gboolean          chatty_ma_account_can_connect        (ChattyMaAccount *self);
 const char       *chatty_ma_account_get_login_username (ChattyMaAccount *self);
-ChattyMaAccount  *chatty_ma_account_new_secret         (gpointer         secret_retrievable);
-void              chatty_ma_account_set_db             (ChattyMaAccount *self,
-                                                        gpointer         matrix_db,
-                                                        gpointer         history_db);
-void              chatty_ma_account_save_async         (ChattyMaAccount *self,
-                                                        gboolean         force,
-                                                        GCancellable    *cancellable,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean          chatty_ma_account_save_finish        (ChattyMaAccount *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
 const char       *chatty_ma_account_get_homeserver     (ChattyMaAccount *self);
 void              chatty_ma_account_set_homeserver     (ChattyMaAccount *self,
                                                         const char      *server_url);
diff --git a/src/matrix/chatty-ma-buddy.c b/src/matrix/chatty-ma-buddy.c
index 81b5567416c4dff198e058ac795386c0b064e1fe..cb8aa3709e40f31e960e5c503f17bad9e783c538 100644
--- a/src/matrix/chatty-ma-buddy.c
+++ b/src/matrix/chatty-ma-buddy.c
@@ -1,7 +1,7 @@
 /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* chatty-ma-buddy.c
  *
- * Copyright 2020 Purism SPC
+ * Copyright 2020, 2022 Purism SPC
  *
  * Author(s):
  *   Mohammed Sadiq <sadiq@sadiqpk.org>
@@ -22,32 +22,13 @@ struct _ChattyMaBuddy
 {
   ChattyItem      parent_instance;
 
-  char           *matrix_id;
-  char           *name;
-  GList          *devices;
-
-  MatrixApi      *matrix_api;
-  MatrixEnc      *matrix_enc;
+  CmUser         *cm_user;
 
   /* generated using g_str_hash for faster comparison */
   guint           id_hash;
   gboolean        is_self;
 };
 
-struct _BuddyDevice
-{
-
-  char *device_id;
-  char *device_name;
-  char *curve_key; /* Public part Curve25519 identity key pair */
-  char *ed_key;    /* Public part of Ed25519 fingerprint key pair */
-  char *one_time_key;
-
-  gboolean meagolm_v1;
-  gboolean olm_v1;
-};
-
-
 G_DEFINE_TYPE (ChattyMaBuddy, chatty_ma_buddy, CHATTY_TYPE_ITEM)
 
 enum {
@@ -57,20 +38,6 @@ enum {
 
 static guint signals[N_SIGNALS];
 
-static void
-chatty_ma_device_free (BuddyDevice *device)
-{
-  if (!device)
-    return;
-
-  g_free (device->device_id);
-  g_free (device->device_name);
-  g_free (device->curve_key);
-  g_free (device->ed_key);
-  g_free (device->one_time_key);
-  g_free (device);
-}
-
 static ChattyProtocol
 chatty_ma_buddy_get_protocols (ChattyItem *item)
 {
@@ -85,45 +52,32 @@ chatty_ma_buddy_matches (ChattyItem     *item,
 {
   ChattyMaBuddy *self = (ChattyMaBuddy *)item;
 
-  if (needle == self->matrix_id)
-    return TRUE;
-
-  if (!needle || !self->matrix_id)
+  if (!self->cm_user || !cm_user_get_id (self->cm_user))
     return FALSE;
 
-  return strcasestr (needle, self->matrix_id) != NULL;
+  if (needle == cm_user_get_id (self->cm_user))
+    return TRUE;
+
+  return strcasestr (needle, cm_user_get_id (self->cm_user)) != NULL;
 }
 
 static const char *
 chatty_ma_buddy_get_name (ChattyItem *item)
 {
   ChattyMaBuddy *self = (ChattyMaBuddy *)item;
+  const char *name = NULL;
 
   g_assert (CHATTY_IS_MA_BUDDY (self));
 
-  if (self->name)
-    return self->name;
-
-  if (self->matrix_id)
-    return self->matrix_id;
-
-  return "";
-}
-
-static void
-chatty_ma_buddy_set_name (ChattyItem *item,
-                          const char *name)
-{
-  ChattyMaBuddy *self = (ChattyMaBuddy *)item;
+  if (!self->cm_user)
+    return "";
 
-  g_assert (CHATTY_IS_MA_BUDDY (self));
+  name = cm_user_get_display_name (self->cm_user);
 
-  g_free (self->name);
+  if (name)
+    return name;
 
-  if (!name || !*name)
-    self->name = NULL;
-  else
-    self->name = g_strdup (name);
+  return cm_user_get_id (self->cm_user);
 }
 
 /*
@@ -143,8 +97,8 @@ chatty_ma_buddy_get_username (ChattyItem *item)
 
   g_assert (CHATTY_IS_MA_BUDDY (self));
 
-  if (self->matrix_id)
-    return self->matrix_id;
+  if (self->cm_user)
+    return cm_user_get_id (self->cm_user);
 
   return "";
 }
@@ -164,14 +118,7 @@ chatty_ma_buddy_dispose (GObject *object)
 {
   ChattyMaBuddy *self = (ChattyMaBuddy *)object;
 
-  g_clear_pointer (&self->matrix_id, g_free);
-  g_clear_pointer (&self->name, g_free);
-
-  g_clear_object (&self->matrix_api);
-  g_clear_object (&self->matrix_enc);
-
-  g_list_free_full (self->devices, (GDestroyNotify)chatty_ma_device_free);
-  self->devices = NULL;
+  g_clear_object (&self->cm_user);
 
   G_OBJECT_CLASS (chatty_ma_buddy_parent_class)->dispose (object);
 }
@@ -187,7 +134,6 @@ chatty_ma_buddy_class_init (ChattyMaBuddyClass *klass)
   item_class->get_protocols = chatty_ma_buddy_get_protocols;
   item_class->matches  = chatty_ma_buddy_matches;
   item_class->get_name = chatty_ma_buddy_get_name;
-  item_class->set_name = chatty_ma_buddy_set_name;
   item_class->get_username = chatty_ma_buddy_get_username;
   item_class->get_avatar = chatty_ma_buddy_get_avatar;
 
@@ -212,228 +158,35 @@ chatty_ma_buddy_init (ChattyMaBuddy *self)
 }
 
 ChattyMaBuddy *
-chatty_ma_buddy_new (const char *matrix_id,
-                     MatrixApi  *api,
-                     MatrixEnc  *enc)
+chatty_ma_buddy_new_with_user (CmUser *user)
 {
   ChattyMaBuddy *self;
 
-  g_return_val_if_fail (matrix_id && *matrix_id == '@', NULL);
-  g_return_val_if_fail (MATRIX_IS_API (api), NULL);
-  g_return_val_if_fail (MATRIX_IS_ENC (enc), NULL);
+  g_return_val_if_fail (CM_IS_USER (user), NULL);
 
   self = g_object_new (CHATTY_TYPE_MA_BUDDY, NULL);
-  self->matrix_id = g_strdup (matrix_id);
-  self->matrix_api = g_object_ref (api);
-  self->matrix_enc = g_object_ref (enc);
-
-  if (g_str_equal (matrix_id, matrix_api_get_username (api)))
-    self->is_self = TRUE;
+  self->cm_user = g_object_ref (user);
 
   return self;
 }
 
-guint
-chatty_ma_buddy_get_id_hash (ChattyMaBuddy *self)
-{
-  g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), 0);
-
-  if (!self->id_hash && self->matrix_id)
-    self->id_hash = g_str_hash (self->matrix_id);
-
-  return self->id_hash;
-}
-
-void
-chatty_ma_buddy_add_devices (ChattyMaBuddy *self,
-                             JsonObject    *root)
+gboolean
+chatty_ma_buddy_matches_cm_user (ChattyMaBuddy *self,
+                                 CmUser        *user)
 {
-  g_autoptr(GList) members = NULL;
-  JsonObject *object, *child;
-  BuddyDevice *device;
-
-  g_return_if_fail (CHATTY_IS_MA_BUDDY (self));
-  g_return_if_fail (root);
-
-  members = json_object_get_members (root);
-
-  for (GList *member = members; member; member = member->next) {
-    g_autofree char *device_name = NULL;
-    const char *device_id, *user, *key;
-    JsonArray *array;
-    char *key_name;
-
-    child = matrix_utils_json_object_get_object (root, member->data);
-    device_id = matrix_utils_json_object_get_string (child, "device_id");
-    user = matrix_utils_json_object_get_string (child, "user_id");
-
-    if (g_strcmp0 (user, self->matrix_id) != 0) {
-      g_warning ("‘%s’ and ‘%s’ are not the same users", user, self->matrix_id);
-      continue;
-    }
-
-    if (self->is_self &&
-        g_strcmp0 (device_id, matrix_api_get_device_id (self->matrix_api)) == 0)
-      continue;
-
-    if (g_strcmp0 (member->data, device_id) != 0) {
-      g_warning ("‘%s’ and ‘%s’ are not the same device", (char *)member->data, device_id);
-      continue;
-    }
-
-    object = matrix_utils_json_object_get_object (child, "unsigned");
-    device_name = g_strdup (matrix_utils_json_object_get_string (object, "device_display_name"));
-
-    key_name = g_strconcat ("ed25519:", device_id, NULL);
-    object = matrix_utils_json_object_get_object (child, "keys");
-    key = matrix_utils_json_object_get_string (object, key_name);
-    g_free (key_name);
-
-    if (!matrix_enc_verify (self->matrix_enc, child, self->matrix_id, device_id, key)) {
-      g_warning ("failed to verify signature for %s with device %s", self->matrix_id, device_id);
-      continue;
-    }
-
-    device = g_new0 (BuddyDevice, 1);
-    device->device_id = g_strdup (device_id);
-    device->device_name = g_steal_pointer (&device_name);
-    device->ed_key = g_strdup (key);
-
-    key_name = g_strconcat ("curve25519:", device_id, NULL);
-    object = matrix_utils_json_object_get_object (child, "keys");
-    key = matrix_utils_json_object_get_string (object, key_name);
-    device->curve_key = g_strdup (key);
-    g_free (key_name);
-
-    array = matrix_utils_json_object_get_array (child, "algorithms");
-    for (guint i = 0; array && i < json_array_get_length (array); i++) {
-      const char *algorithm;
-
-      algorithm = json_array_get_string_element (array, i);
-      if (g_strcmp0 (algorithm, ALGORITHM_MEGOLM) == 0)
-        device->meagolm_v1 = TRUE;
-      else if (g_strcmp0 (algorithm, ALGORITHM_OLM) == 0)
-        device->olm_v1 = TRUE;
-    }
-
-    self->devices = g_list_prepend (self->devices, device);
-  }
-}
+  g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), FALSE);
+  g_return_val_if_fail (CM_IS_USER (user), FALSE);
 
-GList *
-chatty_ma_buddy_get_devices (ChattyMaBuddy *self)
-{
-  g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), NULL);
-
-  return g_list_copy (self->devices);
+  return user == self->cm_user;
 }
 
-/**
- * chatty_ma_buddy_device_key_json:
- * @self: A #ChattyMaBuddy
- *
- * Get A JSON object with all the devices
- * that we don't have an one time key for.
- *
- * The JSON created will have the following format:
- *
- *  {
- *    "@alice:example.com": {
- *      "JLAFKJWSCS": "signed_curve25519"
- *     },
- *    "@bob:example.com": {
- *      "JOJOAEWBZY": "signed_curve25519"
- *  }
- *
- * Returns: (transfer full): A #JsonObject
- */
-JsonObject *
-chatty_ma_buddy_device_key_json (ChattyMaBuddy *self)
-{
-  JsonObject *object;
-
-  g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), NULL);
-
-  if (!self->devices)
-    return NULL;
-
-  object = json_object_new ();
-
-  for (GList *node = self->devices; node; node = node->next) {
-    BuddyDevice *device = node->data;
-
-    if (!device->one_time_key)
-      json_object_set_string_member (object, device->device_id, "signed_curve25519");
-  }
-
-  return object;
-}
-
-void
-chatty_ma_buddy_add_one_time_keys (ChattyMaBuddy *self,
-                                   JsonObject    *root)
-{
-  JsonObject *object, *child;
-
-  g_return_if_fail (CHATTY_IS_MA_BUDDY (self));
-  g_return_if_fail (root);
-
-  for (GList *item = self->devices; item; item = item->next) {
-    g_autoptr(GList) members = NULL;
-    BuddyDevice *device = item->data;
-
-    child = matrix_utils_json_object_get_object (root, device->device_id);
-
-    if (!child) {
-      g_warning ("device '%s' not found", device->device_id);
-      continue;
-    }
-
-    members = json_object_get_members (child);
-
-    for (GList *node = members; node; node = node->next) {
-      object = matrix_utils_json_object_get_object (child, node->data);
-
-      if (matrix_enc_verify (self->matrix_enc, object, self->matrix_id,
-                             device->device_id, device->ed_key)) {
-        const char *key;
-
-        key = matrix_utils_json_object_get_string (object, "key");
-        g_free (device->one_time_key);
-        device->one_time_key = g_strdup (key);
-      }
-    }
-  }
-}
-
-const char *
-chatty_ma_device_get_id (BuddyDevice *device)
-{
-  g_return_val_if_fail (device, "");
-
-  return device->device_id;
-}
-
-const char *
-chatty_ma_device_get_ed_key (BuddyDevice *device)
-{
-  g_return_val_if_fail (device, "");
-
-  return device->ed_key;
-}
-
-const char *
-chatty_ma_device_get_curve_key (BuddyDevice *device)
+guint
+chatty_ma_buddy_get_id_hash (ChattyMaBuddy *self)
 {
-  g_return_val_if_fail (device, "");
-
-  return device->curve_key;
-}
+  g_return_val_if_fail (CHATTY_IS_MA_BUDDY (self), 0);
 
-char *
-chatty_ma_device_get_one_time_key (BuddyDevice *device)
-{
-  g_return_val_if_fail (device, g_strdup (""));
+  if (!self->id_hash && self->cm_user)
+    self->id_hash = g_str_hash (cm_user_get_id (self->cm_user));
 
-  return g_steal_pointer (&device->one_time_key);
+  return self->id_hash;
 }
diff --git a/src/matrix/chatty-ma-buddy.h b/src/matrix/chatty-ma-buddy.h
index fc0e65f2026ef5d1f5d212067217d09186fb210a..3a5dfa086053120a47f2fe87145c116ee4522130 100644
--- a/src/matrix/chatty-ma-buddy.h
+++ b/src/matrix/chatty-ma-buddy.h
@@ -12,35 +12,20 @@
 #pragma once
 
 #include <glib-object.h>
-#include <json-glib/json-glib.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
 #include "chatty-item.h"
-#include "matrix-api.h"
-#include "matrix-enc.h"
 
 G_BEGIN_DECLS
 
-typedef struct _BuddyDevice BuddyDevice;
-
 #define CHATTY_TYPE_MA_BUDDY (chatty_ma_buddy_get_type ())
 
 G_DECLARE_FINAL_TYPE (ChattyMaBuddy, chatty_ma_buddy, CHATTY, MA_BUDDY, ChattyItem)
 
-ChattyMaBuddy   *chatty_ma_buddy_new               (const char    *matrix_id,
-                                                    MatrixApi     *api,
-                                                    MatrixEnc     *enc);
-const char      *chatty_ma_buddy_get_id            (ChattyMaBuddy *self);
+ChattyMaBuddy   *chatty_ma_buddy_new_with_user     (CmUser        *user);
+gboolean         chatty_ma_buddy_matches_cm_user   (ChattyMaBuddy *self,
+                                                    CmUser        *user);
 guint            chatty_ma_buddy_get_id_hash       (ChattyMaBuddy *self);
-void             chatty_ma_buddy_add_devices       (ChattyMaBuddy *self,
-                                                    JsonObject    *root);
-GList           *chatty_ma_buddy_get_devices       (ChattyMaBuddy *self);
-JsonObject      *chatty_ma_buddy_device_key_json   (ChattyMaBuddy *self);
-void             chatty_ma_buddy_add_one_time_keys (ChattyMaBuddy *self,
-                                                    JsonObject    *root);
-
-const char      *chatty_ma_device_get_id           (BuddyDevice   *device);
-const char      *chatty_ma_device_get_ed_key       (BuddyDevice   *device);
-const char      *chatty_ma_device_get_curve_key    (BuddyDevice   *device);
-char            *chatty_ma_device_get_one_time_key (BuddyDevice   *device);
 
 G_END_DECLS
diff --git a/src/matrix/chatty-ma-chat.c b/src/matrix/chatty-ma-chat.c
index e6bd91a77e8d13a43f9e3d2ee98f05db71b674f9..f7004128a209085da244491f27bd56705e794c8e 100644
--- a/src/matrix/chatty-ma-chat.c
+++ b/src/matrix/chatty-ma-chat.c
@@ -16,20 +16,16 @@
 #endif
 
 #include <glib/gi18n.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
 #include "contrib/gtk.h"
-#include "chatty-history.h"
 #include "chatty-utils.h"
-#include "matrix-api.h"
-#include "matrix-db.h"
-#include "matrix-enc.h"
 #include "matrix-utils.h"
 #include "chatty-ma-buddy.h"
 #include "chatty-ma-chat.h"
 #include "chatty-log.h"
 
-#define CHATTY_COLOR_BLUE "4A8FD9"
-
 /**
  * SECTION: chatty-chat
  * @title: ChattyChat
@@ -45,152 +41,81 @@ struct _ChattyMaChat
   ChattyChat           parent_instance;
 
   char                *room_name;
-  char                *generated_name;
   char                *room_id;
-  char                *encryption;
-  char                *prev_batch;
-  char                *last_batch;
   GdkPixbuf           *avatar;
   ChattyFileInfo      *avatar_file;
   GCancellable        *avatar_cancellable;
-  ChattyMaBuddy       *self_buddy;
   GListStore          *buddy_list;
   GListStore          *message_list;
-  GtkSortListModel    *sorted_message_list;
-
-  /* Pending messages to be sent.  Queue messages here when
-     @self is busy (eg: claiming keys for encrypted chat) */
-  GQueue              *message_queue;
+  GtkFilterListModel  *filtered_event_list;
 
-  JsonObject       *json_data;
   ChattyAccount    *account;
-  MatrixApi        *matrix_api;
-  MatrixEnc        *matrix_enc;
-  MatrixDb         *matrix_db;
-  ChattyHistory    *history_db;
+  CmClient         *cm_client;
+  CmRoom           *cm_room;
 
   ChattyItemState visibility_state;
-  gint64          highlight_count;
   int             unread_count;
-  int             room_name_update_ts;
 
-  int            message_timeout_id;
-  guint          is_sending_message : 1;
   guint          notification_shown : 1;
 
-  guint          user_list_loading : 1;
-  guint          user_list_loaded : 1;
-  guint          state_is_sync    : 1;
-  guint          state_is_syncing : 1;
-  /* Set if the complete buddy list is loaded */
-  guint          claiming_keys : 1;
-  guint          keys_claimed : 1;
-  guint          prev_batch_loading : 1;
   guint          history_is_loading : 1;
   guint          avatar_is_loading : 1;
-  guint          saving_room_to_db  : 1;
-  guint          room_db_loaded : 1;
 
-  guint          room_name_loaded : 1;
   guint          buddy_typing : 1;
-  /* Set if server says we are typing */
-  guint          self_typing : 1;
-  /* The time when self_typing was updated */
-  /* Uses g_get_monotonic_time(), we only need the interval */
-  gint64         self_typing_set_time;
 };
 
 G_DEFINE_TYPE (ChattyMaChat, chatty_ma_chat, CHATTY_TYPE_CHAT)
 
-enum {
-  PROP_0,
-  PROP_JSON_DATA,
-  PROP_ROOM_ID,
-  N_PROPS
-};
-
-static GParamSpec *properties[N_PROPS];
-
-static void matrix_send_message_from_queue (ChattyMaChat *self);
-
-static int
-sort_message (gconstpointer a,
-              gconstpointer b,
-              gpointer      user_data)
-{
-  time_t time_a, time_b;
-
-  time_a = chatty_message_get_time ((gpointer)a);
-  time_b = chatty_message_get_time ((gpointer)b);
+/* Private */
+CmStatus cm_room_get_status (CmRoom *self);
 
-  return time_a - time_b;
-}
-
-static void
-chatty_mat_chat_update_name (ChattyMaChat *self)
+static gboolean
+ma_chat_filter_event_list (ChattyMessage *message,
+                           ChattyMaChat  *self)
 {
+  g_assert (CHATTY_IS_MESSAGE (message));
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  if (self->room_name)
-    return;
-
-  g_free (self->generated_name);
-  self->generated_name = chatty_chat_generate_name (CHATTY_CHAT (self),
-                                                    G_LIST_MODEL (self->buddy_list));
-  g_signal_emit_by_name (self, "avatar-changed");
-  chatty_history_update_chat (self->history_db, CHATTY_CHAT (self));
+  return !!chatty_message_get_cm_event (message);
 }
 
 static ChattyMaBuddy *
-ma_chat_find_buddy (ChattyMaChat *self,
-                    GListModel   *model,
-                    const char   *matrix_id,
-                    guint        *index)
+ma_chat_find_cm_user (ChattyMaChat *self,
+                      GListModel   *model,
+                      CmUser       *user,
+                      guint        *index,
+                      gboolean      add_if_missing)
 {
   guint n_items;
-  guint id_hash;
 
   g_assert (CHATTY_IS_MA_CHAT (self));
   g_assert (G_IS_LIST_MODEL (model));
-  g_return_val_if_fail (matrix_id && *matrix_id, NULL);
 
   n_items = g_list_model_get_n_items (model);
-  id_hash = g_str_hash (matrix_id);
 
   for (guint i = 0; i < n_items; i++) {
     g_autoptr(ChattyMaBuddy) buddy = NULL;
 
     buddy = g_list_model_get_item (model, i);
-    if (id_hash == chatty_ma_buddy_get_id_hash (buddy) &&
-        g_str_equal (chatty_item_get_username (CHATTY_ITEM (buddy)), matrix_id)) {
+    if (chatty_ma_buddy_matches_cm_user (buddy, user)) {
       if (index)
         *index = i;
 
-      return buddy;
+      return g_steal_pointer (&buddy);
     }
   }
 
-  return NULL;
-}
-
-static ChattyMaBuddy *
-ma_chat_add_buddy (ChattyMaChat *self,
-                   GListStore   *store,
-                   const char   *matrix_id)
-{
-  g_autoptr(ChattyMaBuddy) buddy = NULL;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
+  if (add_if_missing)
+    {
+      ChattyMaBuddy *buddy;
 
-  if (!matrix_id || *matrix_id != '@')
-    g_return_val_if_reached (NULL);
+      buddy = chatty_ma_buddy_new_with_user (user);
+      g_list_store_append (self->buddy_list, buddy);
 
-  buddy = chatty_ma_buddy_new (matrix_id,
-                               self->matrix_api,
-                               self->matrix_enc);
-  g_list_store_append (store, buddy);
+      return buddy;
+    }
 
-  return buddy;
+  return NULL;
 }
 
 static void
@@ -209,1100 +134,55 @@ ma_chat_get_avatar_pixbuf_cb (GObject      *object,
   if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
     g_warning ("Error loading avatar file: %s", error->message);
 
-  if (!error) {
-    g_set_object (&self->avatar, pixbuf);
-    g_signal_emit_by_name (self, "avatar-changed");
-  }
-
-  self->avatar_is_loading = FALSE;
-}
-
-#if 0
-static void
-chat_got_room_avatar_cb (GObject      *object,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  g_autoptr(ChattyMaChat) self = user_data;
-
-  if (matrix_api_get_file_finish (self->matrix_api, result, NULL)) {
-    g_clear_object (&self->avatar);
-    g_signal_emit_by_name (self, "avatar-changed");
-    chatty_history_update_chat (self->history_db, CHATTY_CHAT (self));
-  }
-}
-#endif
-
-static ChattyFileInfo *
-ma_chat_new_file (ChattyMaChat *self,
-                  JsonObject   *object,
-                  JsonObject   *content)
-{
-  ChattyFileInfo *file = NULL;
-  const char *url;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  url = matrix_utils_json_object_get_string (object, "url");
-
-  if (url && g_str_has_prefix (url, "mxc://")) {
-    file = g_new0 (ChattyFileInfo, 1);
-
-    url = url + strlen ("mxc://");
-    file->url = g_strconcat (matrix_api_get_homeserver (self->matrix_api),
-                             "/_matrix/media/r0/download/", url, NULL);
-    file->file_name = g_strdup (matrix_utils_json_object_get_string (object, "body"));
-    object = matrix_utils_json_object_get_object (content, "info");
-    file->mime_type = g_strdup (matrix_utils_json_object_get_string (object, "mimetype"));
-    file->height = matrix_utils_json_object_get_int (object, "h");
-    file->width = matrix_utils_json_object_get_int (object, "w");
-    file->size = matrix_utils_json_object_get_int (object, "size");
-  }
-
-  return file;
-}
-
-static void
-handle_m_room_member (ChattyMaChat *self,
-                      JsonObject   *object)
-{
-  GListModel *model;
-  ChattyMaBuddy *buddy;
-  JsonObject *content;
-  const char *membership, *sender, *name;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-  g_assert (object);
-
-  sender = matrix_utils_json_object_get_string (object, "sender");
-  content = matrix_utils_json_object_get_object (object, "content");
-  membership = matrix_utils_json_object_get_string (content, "membership");
-
-  model = G_LIST_MODEL (self->buddy_list);
-  buddy = ma_chat_find_buddy (self, model, sender, NULL);
-  name = matrix_utils_json_object_get_string (content, "displayname");
-
-  if (g_strcmp0 (membership, "join") == 0) {
-    if (!buddy)
-      buddy = ma_chat_add_buddy (self, self->buddy_list, sender);
-    chatty_item_set_name (CHATTY_ITEM (buddy), name);
-    self->keys_claimed = FALSE;
-  } else if (buddy && g_strcmp0 (membership, "leave") == 0) {
-    chatty_utils_remove_list_item (self->buddy_list, buddy);
-    g_clear_pointer (&self->generated_name, g_free);
-    self->keys_claimed = FALSE;
-  }
-}
-
-static void
-handle_m_room_name (ChattyMaChat *self,
-                    JsonObject   *root)
-{
-  JsonObject *content;
-  const char *name;
-
-  g_assert (CHATTY_MA_CHAT (self));
-
-  content = matrix_utils_json_object_get_object (root, "content");
-  name = matrix_utils_json_object_get_string (content, "name");
-
-  if (name && g_strcmp0 (name, self->room_name) != 0) {
-    g_free (self->room_name);
-    self->room_name = g_strdup (name);
-    g_object_notify (G_OBJECT (self), "name");
-    g_signal_emit_by_name (self, "avatar-changed");
-  }
-}
-
-static void
-handle_m_room_encryption (ChattyMaChat *self,
-                          JsonObject   *root)
-{
-  JsonObject *content;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  if (!root || self->encryption)
-    return;
-
-  content = matrix_utils_json_object_get_object (root, "content");
-  self->encryption = g_strdup (matrix_utils_json_object_get_string (content, "algorithm"));
-
-  if (self->encryption)
-    g_object_notify (G_OBJECT (self), "encrypt");
-}
-
-#if 0
-static void
-handle_m_room_avatar (ChattyMaChat *self,
-                      JsonObject   *root)
-{
-  JsonObject *content;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  g_clear_pointer (&self->avatar_file, chatty_file_info_free);
-  content = matrix_utils_json_object_get_object (root, "content");
-  self->avatar_file = ma_chat_new_file (self, content, content);
-
-  g_cancellable_cancel (self->avatar_cancellable);
-  g_clear_object (&self->avatar_cancellable);
-  self->avatar_cancellable = g_cancellable_new ();
-
-  matrix_api_get_file_async (self->matrix_api, NULL, self->avatar_file,
-                             NULL, NULL,
-                             chat_got_room_avatar_cb,
-                             g_object_ref (self));
-}
-#endif
-
-static void
-ma_chat_download_cb (GObject      *object,
-                     GAsyncResult *result,
-                     gpointer      user_data)
-{
-  g_autoptr(GError) error = NULL;
-  ChattyMaChat *self = user_data;
-  GTask *task = G_TASK (result);
-  ChattyMessage *message;
-  ChattyFileInfo *file;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  file = g_object_get_data (G_OBJECT (task), "file");
-  message = g_object_get_data (G_OBJECT (task), "message");
-  g_return_if_fail (file);
-  g_return_if_fail (message);
-
-  if (matrix_api_get_file_finish (self->matrix_api, result, &error))
-    file->status = CHATTY_FILE_DOWNLOADED;
-  else
-    file->status = CHATTY_FILE_ERROR;
-
-  chatty_history_add_message (self->history_db, CHATTY_CHAT (self), message);
-  chatty_message_emit_updated (message);
-}
-
-static void
-ma_chat_parse_base64_value (guchar     **out,
-                            gsize       *out_len,
-                            const char  *value)
-{
-  g_autofree char *base64 = NULL;
-  gsize len, padded_len;
-
-  g_assert (out);
-  g_assert (out_len);
-
-  if (!value)
-    return;
-
-  len = strlen (value);
-  /* base64 is always multiple of 4, so add space for padding */
-  if (len % 4)
-    padded_len = len + 4 - len % 4;
-  else
-    padded_len = len;
-  base64 = malloc (padded_len + 1);
-  strcpy (base64, value);
-  memset (base64 + len, '=', padded_len - len);
-  base64[padded_len] = '\0';
-
-  *out = g_base64_decode (base64, out_len);
-}
-
-static void
-chat_handle_m_media (ChattyMaChat  *self,
-                     ChattyMessage *message,
-                     JsonObject    *content,
-                     const char    *type,
-                     gboolean       encrypted)
-{
-  ChattyFileInfo *file = NULL;
-  JsonObject *object;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-  g_assert (CHATTY_IS_MESSAGE (message));
-  g_assert (content);
-  g_assert (type);
-
-  CHATTY_TRACE_MSG ("Got media, type: %s, encrypted: %d", type, !!encrypted);
-
-  if (encrypted)
-    object = matrix_utils_json_object_get_object (content, "file");
-  else
-    object = content;
-
-  if (!matrix_utils_json_object_get_string (object, "url"))
-    return;
-
-  if (!g_str_equal (type, "m.image") &&
-      !g_str_equal (type, "m.video") &&
-      !g_str_equal (type, "m.file") &&
-      !g_str_equal (type, "m.audio"))
-    return;
-
-  file = ma_chat_new_file (self, object, content);
-
-  if (encrypted && file) {
-    g_autoptr(MatrixFileEncInfo) info = NULL;
-    JsonObject *json_key;
-
-    object = matrix_utils_json_object_get_object (content, "file");
-    json_key = matrix_utils_json_object_get_object (object, "key");
-
-    if (g_strcmp0 (matrix_utils_json_object_get_string (object, "v"), "v2") != 0 ||
-        g_strcmp0 (matrix_utils_json_object_get_string (json_key, "alg"), "A256CTR") != 0 ||
-        !matrix_utils_json_object_get_bool (json_key, "ext") ||
-        g_strcmp0 (matrix_utils_json_object_get_string (json_key, "kty"), "oct") != 0)
-      return;
-
-    info = g_new0 (MatrixFileEncInfo, 1);
-    info->aes_iv_base64 = g_strdup (matrix_utils_json_object_get_string (object, "iv"));
-    info->aes_key_base64 = g_strdup (matrix_utils_json_object_get_string (json_key, "k"));
-    /* XXX: update doc: uses basae64url */
-    g_strdelimit (info->aes_key_base64, "_", '/');
-    g_strdelimit (info->aes_key_base64, "-", '+');
-
-    object = matrix_utils_json_object_get_object (object, "hashes");
-    info->sha256_base64 = g_strdup (matrix_utils_json_object_get_string (object, "sha256"));
-
-    ma_chat_parse_base64_value (&info->aes_iv, &info->aes_iv_len, info->aes_iv_base64);
-    ma_chat_parse_base64_value (&info->aes_key, &info->aes_key_len, info->aes_key_base64);
-    ma_chat_parse_base64_value (&info->sha256, &info->sha256_len, info->sha256_base64);
-
-    if (info->aes_iv_len == 16 && info->aes_key_len == 32 && info->sha256_len == 32)
-      file->user_data = g_steal_pointer (&info);
-
-    if (file->user_data)
-      matrix_db_save_file_url_async (self->matrix_db, message, file, 2,
-                                     CHATTY_ALGORITHM_A256CTR, CHATTY_KEY_TYPE_OCT, TRUE,
-                                     NULL, NULL);
-  }
-
-  if (!file)
-    return;
-
-  g_object_set_data_full (G_OBJECT (message), "file-url", g_strdup (file->url), g_free);
-  chatty_message_set_files (message, g_list_append (NULL, file));
-  return;
-}
-
-static void
-matrix_add_message_from_data (ChattyMaChat  *self,
-                              ChattyMaBuddy *buddy,
-                              JsonObject    *root,
-                              JsonObject    *object,
-                              gboolean       encrypted)
-{
-  g_autoptr(ChattyMessage) message = NULL;
-  JsonObject *content;
-  const char *body, *type;
-  ChattyMsgDirection direction = CHATTY_DIRECTION_IN;
-  ChattyMsgType msg_type;
-  const char *uuid;
-  time_t ts;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-  g_assert (object);
-
-  content = matrix_utils_json_object_get_object (object, "content");
-  type = matrix_utils_json_object_get_string (content, "msgtype");
-
-  if (!type)
-    return;
-
-  if (g_str_equal (type, "m.image"))
-    msg_type = CHATTY_MESSAGE_IMAGE;
-  else if (g_str_equal (type, "m.video"))
-    msg_type = CHATTY_MESSAGE_VIDEO;
-  else if (g_str_equal (type, "m.file"))
-    msg_type = CHATTY_MESSAGE_FILE;
-  else if (g_str_equal (type, "m.audio"))
-    msg_type = CHATTY_MESSAGE_AUDIO;
-  else if (g_str_equal (type, "m.location"))
-    msg_type = CHATTY_MESSAGE_LOCATION;
-  else
-    msg_type = CHATTY_MESSAGE_TEXT;
-
-  body = matrix_utils_json_object_get_string (content, "body");
-  if (root)
-    uuid = matrix_utils_json_object_get_string (root, "event_id");
-  else
-    uuid = matrix_utils_json_object_get_string (object, "event_id");
-
-  /* timestamp is in milliseconds */
-  ts = matrix_utils_json_object_get_int (object, "origin_server_ts");
-  ts = ts / 1000;
-
-  if (buddy == self->self_buddy)
-    direction = CHATTY_DIRECTION_OUT;
-
-  CHATTY_TRACE_MSG ("Got message, direction: %s, type %s",
-                    direction == CHATTY_DIRECTION_OUT ? "out" : "in", type);
-
-  if (direction == CHATTY_DIRECTION_OUT && uuid) {
-    JsonObject *data_unsigned;
-    const char *transaction_id;
-    guint n_items = 0, limit;
-
-    if (root)
-      data_unsigned = matrix_utils_json_object_get_object (root, "unsigned");
-    else
-      data_unsigned = matrix_utils_json_object_get_object (object, "unsigned");
-    transaction_id = matrix_utils_json_object_get_string (data_unsigned, "transaction_id");
-
-    if (transaction_id)
-      n_items = g_list_model_get_n_items (G_LIST_MODEL (self->message_list));
-
-    if (n_items > 50)
-      limit = n_items - 50;
-    else
-      limit = 0;
-
-    /* Note: i, limit and n_items are unsigned */
-    for (guint i = n_items - 1; i + 1 > limit; i--) {
-      g_autoptr(ChattyMessage) msg = NULL;
-      const char *event_id;
-
-      msg = g_list_model_get_item (G_LIST_MODEL (self->message_list), i);
-      event_id = g_object_get_data (G_OBJECT (msg), "event-id");
-
-      if (event_id && g_str_equal (event_id, transaction_id)) {
-        chatty_message_set_uid (msg, uuid);
-        chatty_history_add_message (self->history_db, CHATTY_CHAT (self), msg);
-        return;
-      }
-    }
-  }
-
-  /* We should move to more precise time (ie, time in ms) as it is already provided */
-  message = chatty_message_new (CHATTY_ITEM (buddy), body, uuid, ts, msg_type, direction, 0);
-  chatty_message_set_encrypted (message, encrypted);
-
-  if (msg_type != CHATTY_MESSAGE_TEXT)
-    chat_handle_m_media (self, message, content, type, encrypted);
-
-  g_list_store_append (self->message_list, message);
-  chatty_history_add_message (self->history_db, CHATTY_CHAT (self), message);
-}
-
-static void
-handle_m_room_encrypted (ChattyMaChat  *self,
-                         ChattyMaBuddy *buddy,
-                         JsonObject    *root)
-{
-  g_autofree char *plaintext = NULL;
-  JsonObject *content;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  if (!root)
-    return;
-
-  content = matrix_utils_json_object_get_object (root, "content");
-  if (content)
-    plaintext = matrix_enc_handle_join_room_encrypted (self->matrix_enc,
-                                                       self->room_id,
-                                                       content);
-
-  if (plaintext) {
-    g_autoptr(JsonObject) message = NULL;
-
-    message = matrix_utils_string_to_json_object (plaintext);
-    matrix_add_message_from_data (self, buddy, root, message, TRUE);
-  }
-}
-
-static gboolean
-chat_resend_message (gpointer user_data)
-{
-  ChattyMaChat *self = user_data;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  self->message_timeout_id = 0;
-  matrix_send_message_from_queue (self);
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-ma_chat_send_message_cb (GObject      *object,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  g_autoptr(ChattyMaChat) self = user_data;
-  g_autoptr(GError) error = NULL;
-  ChattyMessage *message;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  self->is_sending_message = FALSE;
-
-  matrix_api_send_message_finish (self->matrix_api, result, &error);
-  message = g_object_get_data (G_OBJECT (result), "message");
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("Error sending message: %s", error->message);
-
-    if (g_error_matches (error, MATRIX_ERROR, M_LIMIT_EXCEEDED) &&
-        !self->message_timeout_id) {
-      int timeout;
-
-      timeout = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (result), "retry-after"));
-
-      if (!timeout)
-        timeout = 2000;
-
-      self->message_timeout_id = g_timeout_add (timeout, chat_resend_message, self);
-    }
-
-    g_queue_push_head (self->message_queue, g_object_ref (message));
-    return;
-  }
-
-  matrix_send_message_from_queue (self);
-}
-
-static void
-matrix_send_message_from_queue (ChattyMaChat *self)
-{
-  g_autoptr(ChattyMessage) message = NULL;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  if (self->is_sending_message ||
-      !self->message_queue ||
-      !self->message_queue->length ||
-      self->message_timeout_id)
-    return;
-
-  message = g_queue_pop_head (self->message_queue);
-  self->is_sending_message = TRUE;
-  matrix_api_send_message_async (self->matrix_api, CHATTY_CHAT (self),
-                                 self->room_id, message,
-                                 ma_chat_send_message_cb,
-                                 g_object_ref (self));
-}
-
-static void
-ma_chat_handle_ephemeral (ChattyMaChat *self,
-                          JsonObject   *root)
-{
-  JsonObject *object;
-  JsonArray *array;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-  g_assert (root);
-
-  array = matrix_utils_json_object_get_array (root, "events");
-
-  if (array) {
-    g_autoptr(GList) elements = NULL;
-
-    elements = json_array_get_elements (array);
-
-    for (GList *node = elements; node; node = node->next) {
-      const char *type;
-
-      object = json_node_get_object (node->data);
-      type = matrix_utils_json_object_get_string (object, "type");
-      object = matrix_utils_json_object_get_object (object, "content");
-
-      if (g_strcmp0 (type, "m.typing") == 0) {
-        array = matrix_utils_json_object_get_array (object, "user_ids");
-
-        if (array) {
-          const char *username, *name = NULL;
-          guint typing_count = 0;
-          gboolean buddy_typing = FALSE;
-          gboolean self_typing = FALSE;
-
-          typing_count = json_array_get_length (array);
-          buddy_typing = typing_count >= 2;
-
-          /* Handle the first item so that we don’t have to
-             handle buddy_typing in the loop */
-          username = matrix_api_get_username (self->matrix_api);
-          if (typing_count)
-            name = json_array_get_string_element (array, 0);
-
-          if (g_strcmp0 (name, username) == 0)
-            self_typing = TRUE;
-          else if (typing_count)
-            buddy_typing = TRUE;
-
-          /* Check if the server says we are typing too */
-          for (guint i = 0; !self_typing && i < typing_count; i++)
-            if (g_str_equal (json_array_get_string_element (array, i), username))
-              self_typing = TRUE;
-
-          if (self->self_typing != self_typing) {
-            self->self_typing = self_typing;
-            self->self_typing_set_time = g_get_monotonic_time ();
-          }
-
-          if (self->buddy_typing != buddy_typing) {
-            self->buddy_typing = buddy_typing;
-            g_object_notify (G_OBJECT (self), "buddy-typing");
-          }
-        }
-      }
-    }
-  }
-}
-
-static void
-upload_out_group_key_cb (GObject      *obj,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  ChattyMaChat *self = user_data;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  self->claiming_keys = FALSE;
-  if (matrix_api_upload_group_keys_finish (self->matrix_api, result, &error))
-    self->keys_claimed = TRUE;
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("error uploading group keys: %s", error->message);
-    return;
-  }
-
-  matrix_send_message_from_queue (self);
-}
-
-static void
-claim_key_cb (GObject      *obj,
-              GAsyncResult *result,
-              gpointer      user_data)
-{
-  ChattyMaChat *self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GList) members = NULL;
-  g_autoptr(GError) error = NULL;
-  JsonObject *object;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  root = matrix_api_claim_keys_finish (self->matrix_api, result, &error);
-
-  if (error) {
-    self->claiming_keys = FALSE;
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("error: %s", error->message);
-    return;
-  }
-
-  object = matrix_utils_json_object_get_object (root, "one_time_keys");
-  if (object)
-    members = json_object_get_members (object);
-
-  for (GList *member = members; member; member = member->next) {
-    ChattyMaBuddy *buddy;
-    JsonObject *keys;
-
-    buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list),
-                                member->data, NULL);
-
-    if (!buddy) {
-      g_warning ("‘%s’ not found in buddy list", (char *)member->data);
-      continue;
-    }
-
-    keys = matrix_utils_json_object_get_object (object, member->data);
-    chatty_ma_buddy_add_one_time_keys (buddy, keys);
-  }
-
-  matrix_api_upload_group_keys_async (self->matrix_api,
-                                      self->room_id,
-                                      G_LIST_MODEL (self->buddy_list),
-                                      upload_out_group_key_cb,
-                                      self);
-}
-
-static void
-query_key_cb (GObject      *obj,
-              GAsyncResult *result,
-              gpointer      user_data)
-{
-  ChattyMaChat *self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GList) members = NULL;
-  g_autoptr(GError) error = NULL;
-  JsonObject *object;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-  g_return_if_fail (!self->keys_claimed);
-
-  root = matrix_api_query_keys_finish (self->matrix_api, result, &error);
-
-  if (error) {
-    self->claiming_keys = FALSE;
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("error: %s", error->message);
-    return;
-  }
-
-  object = matrix_utils_json_object_get_object (root, "device_keys");
-  if (object)
-    members = json_object_get_members (object);
-
-  /* TODO: avoid blocked devices (once we implement blocking) */
-  for (GList *member = members; member; member = member->next) {
-    ChattyMaBuddy *buddy;
-    JsonObject *device;
-
-    buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list),
-                                member->data, NULL);
-
-    if (!buddy) {
-      g_warning ("‘%s’ not found in buddy list", (char *)member->data);
-      continue;
-    }
-
-    device = matrix_utils_json_object_get_object (object, member->data);
-    chatty_ma_buddy_add_devices (buddy, device);
-  }
-
-  matrix_api_claim_keys_async (self->matrix_api,
-                               G_LIST_MODEL (self->buddy_list),
-                               claim_key_cb, self);
-}
-
-static void
-get_chat_users_cb (GObject      *obj,
-                   GAsyncResult *result,
-                   gpointer      user_data)
-{
-  g_autoptr(ChattyMaChat) self = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  g_autoptr(GList) members = NULL;
-  g_autoptr(GError) error = NULL;
-  JsonObject *joined;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  object = matrix_api_get_room_users_finish (self->matrix_api, result, &error);
-
-  self->user_list_loading = FALSE;
-  self->user_list_loaded = !error;
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("Error loading chat members: %s", error->message);
-
-    return;
-  }
-
-  joined = matrix_utils_json_object_get_object (object, "joined");
-  members = json_object_get_members (joined);
-
-  for (GList *member = members; member; member = member->next) {
-    ChattyMaBuddy *buddy;
-    JsonObject *data;
-    const char *name;
-
-    buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list), member->data, NULL);
-    if (!buddy)
-      buddy = ma_chat_add_buddy (self, self->buddy_list, member->data);
-
-    data = json_object_get_object_member (joined, member->data);
-    name = matrix_utils_json_object_get_string (data, "display_name");
-    chatty_item_set_name (CHATTY_ITEM (buddy), name);
-  };
-
-  self->claiming_keys = TRUE;
-  matrix_api_query_keys_async (self->matrix_api,
-                               G_LIST_MODEL (self->buddy_list),
-                               NULL, query_key_cb, self);
-}
-
-#if 0
-static void
-get_room_state_cb (GObject      *obj,
-                   GAsyncResult *result,
-                   gpointer      user_data)
-{
-  ChattyMaChat *self = user_data;
-  g_autoptr(GError) error = NULL;
-  g_autoptr(JsonArray) array = NULL;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  array = matrix_api_get_room_state_finish (self->matrix_api, result, &error);
-  self->state_is_syncing = FALSE;
-
-  CHATTY_TRACE_MSG ("Got room state, room: %s (%s), success: %d",
-                    self->room_id, chatty_item_get_name (CHATTY_ITEM (self)), !error);
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("error: %s", error->message);
-    return;
-  }
-
-  if (json_array_get_length (array) == 0)
-    return;
-
-  self->state_is_sync = TRUE;
-  for (guint i = 0; i < json_array_get_length (array); i++) {
-    JsonObject *object;
-    const char *type;
-
-    object = json_array_get_object_element (array, i);
-    type = matrix_utils_json_object_get_string (object, "type");
-
-    if (!type || !*type)
-      continue;
-
-    if (g_str_equal (type, "m.room.member"))
-      handle_m_room_member (self, object, self->buddy_list);
-    else if (g_str_equal (type, "m.room.name"))
-      handle_m_room_name (self, object);
-    else if (g_str_equal (type, "m.room.encryption"))
-      handle_m_room_encryption (self, object);
-    else if (g_str_equal (type, "m.room.avatar"))
-      handle_m_room_avatar (self, object);
-    /* TODO */
-    /* else if (g_str_equal (type, "m.room.power_levels")) */
-    /*   handle_m_room_power_levels (self, object); */
-    /* else if (g_str_equal (type, "m.room.guest_access")) */
-    /*   handle_m_room_guest_access (self, object); */
-    /* else if (g_str_equal (type, "m.room.create")) */
-    /*   handle_m_room_create (self, object); */
-    /* else if (g_str_equal (type, "m.room.history_visibility")) */
-    /*   handle_m_room_history_visibility (self, object); */
-    /* else if (g_str_equal (type, "m.room.join_rules")) */
-    /*   handle_m_room_join_rules (self, object); */
-    /* else */
-    /*   g_warn_if_reached (); */
-  }
-
-  /* Clear pointer so that it’s generated when requested */
-  g_clear_pointer (&self->generated_name, g_free);
-  g_object_notify (G_OBJECT (self), "name");
-
-  if (self->message_queue->length > 0) {
-    if (!self->claiming_keys)
-      matrix_api_query_keys_async (self->matrix_api,
-                                   G_LIST_MODEL (self->buddy_list),
-                                   NULL, query_key_cb, self);
-    else if (self->keys_claimed)
-      matrix_send_message_from_queue (self);
-  }
-
-  g_object_notify (G_OBJECT (self), "name");
-  g_signal_emit_by_name (self, "avatar-changed");
-
-  chatty_history_update_chat (self->history_db, CHATTY_CHAT (self));
-}
-#endif
-
-static void
-get_room_name_cb (GObject      *obj,
-                  GAsyncResult *result,
-                  gpointer      user_data)
-{
-  ChattyMaChat *self = user_data;
-  g_autoptr(GError) error = NULL;
-  g_autoptr(JsonObject) object = NULL;
-  const char *name;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-  self->state_is_sync = TRUE;
-  self->state_is_syncing = FALSE;
-
-  object = matrix_api_get_room_name_finish (self->matrix_api, result, &error);
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
-        !g_error_matches (error, MATRIX_ERROR, M_NOT_FOUND))
-      g_warning ("error getting room name: %s", error->message);
-    return;
-  }
-
-  name = matrix_utils_json_object_get_string (object, "name");
-  g_free (self->room_name);
-  self->room_name = g_strdup (name);
-  chatty_history_update_chat (self->history_db, CHATTY_CHAT (self));
-
-  CHATTY_TRACE (self->room_id, "Got room name, room:");
-
-  g_object_notify (G_OBJECT (self), "name");
-}
-
-static void
-get_room_encryption_cb (GObject      *obj,
-                        GAsyncResult *result,
-                        gpointer      user_data)
-{
-  ChattyMaChat *self = user_data;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  self->encryption = matrix_api_get_room_encryption_finish (self->matrix_api, result, &error);
-
-  CHATTY_TRACE (self->room_id, "Got room encryption state: encrypted: %d, room:",
-                !!self->encryption);
-  g_object_notify (G_OBJECT (self), "encrypt");
-
-  CHATTY_TRACE (self->room_id, "Getting room name for");
-  matrix_api_get_room_name_async (self->matrix_api,
-                                  self->room_id,
-                                  get_room_name_cb,
-                                  self);
-}
-
-static void
-parse_chat_array (ChattyMaChat *self,
-                  JsonArray    *array)
-{
-  g_autoptr(GList) events = NULL;
-
-  if (!array)
-    return;
-
-  events = json_array_get_elements (array);
-  CHATTY_TRACE_MSG ("Got %u events", json_array_get_length (array));
-
-  for (GList *event = events; event; event = event->next) {
-    ChattyMaBuddy *buddy;
-    JsonObject *object;
-    const char *type, *sender;
-
-    object = json_node_get_object (event->data);
-    type = matrix_utils_json_object_get_string (object, "type");
-    sender = matrix_utils_json_object_get_string (object, "sender");
-
-    if (!type || !*type || !sender || !*sender)
-      continue;
-
-    buddy = ma_chat_find_buddy (self, G_LIST_MODEL (self->buddy_list), sender, NULL);
-
-    if (!buddy)
-      buddy = ma_chat_add_buddy (self, self->buddy_list, sender);
-
-    if (!self->self_buddy &&
-        g_strcmp0 (sender, chatty_item_get_username (CHATTY_ITEM (self))) == 0)
-      g_set_object (&self->self_buddy, buddy);
-
-    if (g_str_equal (type, "m.room.name")) {
-      handle_m_room_name (self, object);
-    } else if (g_str_equal (type, "m.room.message")) {
-      matrix_add_message_from_data (self, buddy, NULL, object, FALSE);
-    } else if (g_str_equal (type, "m.room.encryption")) {
-      handle_m_room_encryption (self, object);
-    } else if (g_str_equal (type, "m.room.encrypted")) {
-      handle_m_room_encrypted (self, buddy, object);
-    } else if (g_str_equal (type, "m.room.member")) {
-      handle_m_room_member (self, object);
-    }
-  }
-
-  if (!self->room_name_loaded) {
-    self->room_name_loaded = TRUE;
-    g_object_notify (G_OBJECT (self), "name");
-    g_signal_emit_by_name (self, "avatar-changed");
-  }
-}
-
-static void
-matrix_chat_set_json_data (ChattyMaChat *self,
-                           JsonObject   *object)
-{
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  g_clear_pointer (&self->json_data, json_object_unref);
-  self->json_data = object;
-
-  if (!object)
-    return;
-
-  object = matrix_utils_json_object_get_object (self->json_data, "ephemeral");
-  if (object)
-    ma_chat_handle_ephemeral (self, object);
-
-  object = matrix_utils_json_object_get_object (self->json_data, "unread_notifications");
-  if (object)
-    self->highlight_count = matrix_utils_json_object_get_int (object, "highlight_count");
-
-  object = matrix_utils_json_object_get_object (self->json_data, "timeline");
-  parse_chat_array (self, matrix_utils_json_object_get_array (object, "events"));
-
-  if (object && matrix_utils_json_object_get_bool (object, "limited"))
-    chatty_ma_chat_set_prev_batch (self, g_strdup (matrix_utils_json_object_get_string (object, "prev_batch")));
-
-  object = matrix_utils_json_object_get_object (self->json_data, "unread_notifications");
-  if (object) {
-    guint old_count;
-
-    old_count = self->unread_count;
-    self->unread_count = matrix_utils_json_object_get_int (object, "notification_count");
-    chatty_chat_show_notification (CHATTY_CHAT (self), NULL);
-    g_signal_emit_by_name (self, "changed", 0);
-
-    /* Reset notification state on new messages */
-    if (self->unread_count > old_count)
-      self->notification_shown = FALSE;
-  }
-}
-
-static void
-get_messages_cb (GObject      *obj,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  ChattyMaChat *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  root = matrix_api_load_prev_batch_finish (self->matrix_api, result, &error);
-  self->prev_batch_loading = FALSE;
-  self->history_is_loading = FALSE;
-  g_object_notify (G_OBJECT (self), "loading-history");
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_warning ("error: %s", error->message);
-    g_task_return_boolean (task, FALSE);
-    return;
-  }
-
-  parse_chat_array (self, matrix_utils_json_object_get_array (root, "chunk"));
-
-  /* If start and end are same, we no longer have events to load */
-  if (g_strcmp0 (matrix_utils_json_object_get_string (root, "end"),
-                 matrix_utils_json_object_get_string (root, "start")) == 0)
-    chatty_ma_chat_set_prev_batch (self, NULL);
-  else
-    chatty_ma_chat_set_prev_batch (self, g_strdup (matrix_utils_json_object_get_string (root, "end")));
-
-  if (self->prev_batch) {
-    GListModel *model;
-    guint message_count;
-
-    model = G_LIST_MODEL (chatty_chat_get_messages (CHATTY_CHAT (self)));
-    message_count = GPOINTER_TO_UINT(g_object_get_data (G_OBJECT (task), "count"));
-
-    /* Load more items if no message was loaded.  This can happen
-     * when no event in the loaded events was a room message.
-     */
-    if (chatty_chat_get_encryption (CHATTY_CHAT (self)) != CHATTY_ENCRYPTION_ENABLED &&
-        g_list_model_get_n_items (model) == message_count)
-      chatty_chat_load_past_messages (CHATTY_CHAT (self), -1);
-  }
-}
-
-static void
-db_room_room_cb (GObject      *object,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  ChattyMaChat *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  g_object_freeze_notify (G_OBJECT (self));
-
-  self->room_db_loaded = TRUE;
-  self->history_is_loading = FALSE;
-  g_object_notify (G_OBJECT (self), "loading-history");
-
-  g_free (self->prev_batch);
-  self->prev_batch = matrix_db_load_room_finish (self->matrix_db, result, &error);
-  CHATTY_TRACE (self->room_id, "Load from db, success: %d, has prev-batch: %d, chat:",
-                !error, !!self->prev_batch);
-
-  if (error)
-    g_warning ("Error loading prev batch: %s", error->message);
-
-  if (self->prev_batch) {
-    self->history_is_loading = TRUE;
-    g_object_notify (G_OBJECT (self), "loading-history");
-    matrix_api_load_prev_batch_async (self->matrix_api,
-                                      self->room_id,
-                                      self->prev_batch,
-                                      self->last_batch,
-                                      get_messages_cb,
-                                      g_steal_pointer (&task));
+  if (!error) {
+    g_set_object (&self->avatar, pixbuf);
+    g_signal_emit_by_name (self, "avatar-changed");
   }
 
-  g_object_thaw_notify (G_OBJECT (self));
+  self->avatar_is_loading = FALSE;
 }
 
 static void
-ma_chat_load_db_messages_cb (GObject      *object,
-                             GAsyncResult *result,
-                             gpointer      user_data)
+ma_chat_get_past_events_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
 {
-  ChattyMaChat *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(GPtrArray) messages = NULL;
+  g_autoptr(ChattyMaChat) self = user_data;
+  /* g_autoptr(GPtrArray) events = NULL; */
   g_autoptr(GError) error = NULL;
 
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  g_object_freeze_notify (G_OBJECT (self));
+  cm_room_load_past_events_finish (CM_ROOM (object), result, &error);
 
-  messages = chatty_history_get_messages_finish (self->history_db, result, &error);
   self->history_is_loading = FALSE;
   g_object_notify (G_OBJECT (self), "loading-history");
 
-  if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    g_warning ("Error fetching messages from db: %s,", error->message);
+  if (error)
+    g_warning ("Error: %s", error->message);
+}
+
+static gboolean
+chatty_ma_chat_is_im (ChattyChat *chat)
+{
+  return TRUE;
+}
 
-  CHATTY_TRACE_MSG ("Messages loaded from db: %u", !messages ? 0 : messages->len);
+static ChattyChatState
+chatty_ma_chat_get_chat_state (ChattyChat *chat)
+{
+  ChattyMaChat *self = (ChattyMaChat *)chat;
 
-  if (messages && messages->len) {
-    g_list_store_splice (self->message_list, 0, 0, messages->pdata, messages->len);
-    chatty_chat_show_notification (CHATTY_CHAT (self), NULL);
-    g_signal_emit_by_name (self, "changed", 0);
-    g_task_return_boolean (task, TRUE);
-  } else if (!messages && self->prev_batch) {
-    self->history_is_loading = TRUE;
-    g_object_notify (G_OBJECT (self), "loading-history");
-    matrix_api_load_prev_batch_async (self->matrix_api,
-                                      self->room_id,
-                                      self->prev_batch,
-                                      self->last_batch,
-                                      get_messages_cb,
-                                      g_steal_pointer (&task));
-  } else if (!self->room_db_loaded &&
-             matrix_api_get_device_id (self->matrix_api)) {
-    self->history_is_loading = TRUE;
-    g_object_notify (G_OBJECT (self), "loading-history");
-    matrix_db_load_room_async (self->matrix_db, self->account,
-                               matrix_api_get_device_id (self->matrix_api),
-                               self->room_id,
-                               db_room_room_cb,
-                               g_steal_pointer (&task));
-  }
+  g_assert (CHATTY_IS_MA_CHAT (self));
+
+  if (cm_room_get_status (self->cm_room) == CM_STATUS_INVITE)
+    return CHATTY_CHAT_INVITED;
 
-  g_object_thaw_notify (G_OBJECT (self));
+  return CHATTY_CHAT_JOINED;
 }
 
 static gboolean
-chatty_ma_chat_is_im (ChattyChat *chat)
+chatty_ma_chat_has_file_upload (ChattyChat *chat)
 {
   return TRUE;
 }
@@ -1322,31 +202,18 @@ chatty_ma_chat_real_past_messages (ChattyChat *chat,
                                    int         count)
 {
   ChattyMaChat *self = (ChattyMaChat *)chat;
-  GListModel *model;
-  GTask *task;
-  guint n_items;
 
   g_assert (CHATTY_IS_MA_CHAT (self));
   g_assert (count > 0);
 
-  if (self->history_is_loading)
-    return;
-
   CHATTY_TRACE (self->room_id, "Loading %d past messages from", count);
 
   self->history_is_loading = TRUE;
   g_object_notify (G_OBJECT (self), "loading-history");
 
-  model = chatty_chat_get_messages (chat);
-  n_items = g_list_model_get_n_items (model);
-
-  task = g_task_new (self, NULL, NULL, NULL);
-  g_object_set_data (G_OBJECT (task), "count", GUINT_TO_POINTER (n_items));
-
-  chatty_history_get_messages_async (self->history_db, chat,
-                                     g_list_model_get_item (model, 0),
-                                     count, ma_chat_load_db_messages_cb,
-                                     task);
+  cm_room_load_past_events_async (self->cm_room,
+                                  ma_chat_get_past_events_cb,
+                                  g_object_ref (self));
 }
 
 static gboolean
@@ -1366,7 +233,7 @@ chatty_ma_chat_get_messages (ChattyChat *chat)
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  return G_LIST_MODEL (self->sorted_message_list);
+  return G_LIST_MODEL (self->filtered_event_list);
 }
 
 static ChattyAccount *
@@ -1387,7 +254,7 @@ chatty_ma_chat_get_encryption (ChattyChat *chat)
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  if (self->encryption)
+  if (self->cm_room && cm_room_is_encrypted (self->cm_room))
     return CHATTY_ENCRYPTION_ENABLED;
 
   return CHATTY_ENCRYPTION_DISABLED;
@@ -1408,16 +275,13 @@ ma_chat_set_encryption_cb (GObject      *object,
   self = g_task_get_source_object (task);
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  ret = matrix_api_set_room_encryption_finish (self->matrix_api, result, &error);
+  ret = cm_room_enable_encryption_finish (self->cm_room, result, &error);
   CHATTY_DEBUG (self->room_id, "Setting encryption, success: %d, room:", ret);
 
   if (error) {
     g_warning ("Failed to set encryption: %s", error->message);
     g_task_return_error (task, error);
   } else {
-    if (ret)
-      self->encryption = g_strdup ("encrypted");
-
     g_object_notify (G_OBJECT (self), "encrypt");
     g_task_return_boolean (task, ret);
   }
@@ -1433,7 +297,7 @@ chatty_ma_chat_set_encryption_async (ChattyChat          *chat,
   g_autoptr(GTask) task = NULL;
 
   g_assert (CHATTY_IS_MA_CHAT (self));
-  g_assert (self->matrix_api);
+  g_assert (self->cm_client);
 
   task = g_task_new (self, NULL, callback, user_data);
 
@@ -1445,18 +309,80 @@ chatty_ma_chat_set_encryption_async (ChattyChat          *chat,
     return;
   }
 
-  if (enable &&
-      chatty_chat_get_encryption (chat) == CHATTY_ENCRYPTION_ENABLED) {
-    g_debug ("Encryption is already enabled");
-    g_task_return_boolean (task, TRUE);
+  CHATTY_DEBUG (self->room_id, "Setting encryption for room:");
+  cm_room_enable_encryption_async (self->cm_room, NULL,
+                                   ma_chat_set_encryption_cb,
+                                   g_steal_pointer (&task));
+}
 
-    return;
-  }
+static void
+ma_chat_accept_invite_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  ChattyMaChat *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean success;
 
-  CHATTY_DEBUG (self->room_id, "Setting encryption for room:");
-  matrix_api_set_room_encryption_async (self->matrix_api, self->room_id,
-                                        ma_chat_set_encryption_cb,
-                                        g_steal_pointer (&task));
+  self = g_task_get_source_object (task);
+  success = cm_room_accept_invite_finish (self->cm_room, result, &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+static void
+chatty_ma_chat_accept_invite_async (ChattyChat          *chat,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  ChattyMaChat *self = (ChattyMaChat *)chat;
+  GTask *task;
+
+  g_assert (CHATTY_IS_MA_CHAT (chat));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  cm_room_accept_invite_async (self->cm_room, NULL,
+                               ma_chat_accept_invite_cb,
+                               task);
+}
+
+static void
+ma_chat_reject_invite_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  ChattyMaChat *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  gboolean success;
+
+  self = g_task_get_source_object (task);
+  success = cm_room_reject_invite_finish (self->cm_room, result, &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+static void
+chatty_ma_chat_reject_invite_async (ChattyChat          *chat,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  ChattyMaChat *self = (ChattyMaChat *)chat;
+  GTask *task;
+
+  g_assert (CHATTY_IS_MA_CHAT (chat));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  cm_room_reject_invite_async (self->cm_room, NULL,
+                               ma_chat_reject_invite_cb,
+                               task);
 }
 
 static const char *
@@ -1469,7 +395,7 @@ chatty_ma_chat_get_last_message (ChattyChat *chat)
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  model = G_LIST_MODEL (self->message_list);
+  model = G_LIST_MODEL (self->filtered_event_list);
   n_items = g_list_model_get_n_items (model);
 
   if (n_items == 0)
@@ -1487,7 +413,7 @@ chatty_ma_chat_get_unread_count (ChattyChat *chat)
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  return self->unread_count;
+  return cm_room_get_unread_notification_counts (self->cm_room);
 }
 
 static void
@@ -1500,8 +426,7 @@ chat_set_read_marker_cb (GObject      *object,
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  if (matrix_api_set_read_marker_finish (self->matrix_api, result, &error)) {
-    self->unread_count = 0;
+  if (cm_room_set_read_marker_finish (self->cm_room, result, &error)) {
     g_signal_emit_by_name (self, "changed", 0);
   } else if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
     g_warning ("Error updating read marker: %s", error->message);
@@ -1515,27 +440,49 @@ chatty_ma_chat_set_unread_count (ChattyChat *chat,
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  if (self->unread_count == unread_count)
-    return;
-
   if (unread_count == 0) {
-    g_autoptr(ChattyMessage) message = NULL;
+    g_autoptr(CmEvent) event = NULL;
     GListModel *model;
     guint n_items;
 
-    model = G_LIST_MODEL (self->message_list);
+    model = cm_room_get_events_list (self->cm_room);
     n_items = g_list_model_get_n_items (model);
 
     if (n_items == 0)
       return;
 
-    message = g_list_model_get_item (model, n_items - 1);
-    matrix_api_set_read_marker_async (self->matrix_api, self->room_id, message,
-                                      chat_set_read_marker_cb, self);
+    event = g_list_model_get_item (model, n_items - 1);
+    cm_room_set_read_marker_async (self->cm_room,
+                                   event, event,
+                                   chat_set_read_marker_cb, self);
+  }
+}
+
+static void
+ma_chage_send_message_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  ChattyMaChat *self;
+  g_autoptr(GTask) task = user_data;
+  g_autofree char *event_id = NULL;
+  ChattyMessage *message;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  message = g_task_get_task_data (task);
+  g_assert (CHATTY_IS_MA_CHAT (self));
+  g_assert (CHATTY_IS_MESSAGE (message));
+
+  event_id = cm_room_send_text_finish (self->cm_room, result, NULL);
+
+  /* We add only failed to send messages.  If sending succeeded,
+   * we shall get the same via the /sync responses */
+  if (event_id) {
+    chatty_message_set_status (message, CHATTY_STATUS_SENT, 0);
   } else {
-    self->unread_count = unread_count;
-    chatty_chat_show_notification (CHATTY_CHAT (chat), NULL);
-    g_signal_emit_by_name (self, "changed", 0);
+    chatty_message_set_status (message, CHATTY_STATUS_SENDING_FAILED, 0);
   }
 }
 
@@ -1546,30 +493,59 @@ chatty_ma_chat_send_message_async (ChattyChat          *chat,
                                    gpointer             user_data)
 {
   ChattyMaChat *self = (ChattyMaChat *)chat;
+  const char *event_id;
+  GList *files = NULL;
+  GFile *file = NULL;
+  GTask *task;
 
   g_assert (CHATTY_IS_MA_CHAT (self));
   g_assert (CHATTY_IS_MESSAGE (message));
 
-  chatty_message_set_user (message, CHATTY_ITEM (self->self_buddy));
   chatty_message_set_status (message, CHATTY_STATUS_SENDING, 0);
 
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_task_data (task, g_object_ref (message), g_object_unref);
   g_list_store_append (self->message_list, message);
-  g_queue_push_tail (self->message_queue, g_object_ref (message));
-
-  if (chatty_chat_get_encryption (chat) != CHATTY_ENCRYPTION_ENABLED ||
-      self->keys_claimed)
-    matrix_send_message_from_queue (self);
-  else if (!self->user_list_loaded && !self->user_list_loading) {
-    self->user_list_loading = TRUE;
-    matrix_api_get_room_users_async (self->matrix_api, self->room_id,
-                                     get_chat_users_cb,
-                                     g_object_ref (self));
-  } else if (!self->state_is_syncing && !self->claiming_keys) {
-    self->claiming_keys = TRUE;
-    matrix_api_query_keys_async (self->matrix_api,
-                                 G_LIST_MODEL (self->buddy_list),
-                                 NULL, query_key_cb, self);
+
+  files = chatty_message_get_files (message);
+
+  if (files) {
+    ChattyFileInfo *file_info;
+    file_info = files->data;
+
+    if (file_info && file_info->path)
+      file = g_file_new_for_path (file_info->path);
+
   }
+
+  if (file)
+    event_id = cm_room_send_file_async (self->cm_room, file, NULL,
+                                        NULL, NULL, NULL,
+                                        ma_chage_send_message_cb, task);
+  else
+    event_id = cm_room_send_text_async (self->cm_room,
+                                        chatty_message_get_text (message),
+                                        NULL,
+                                        ma_chage_send_message_cb, task);
+  g_object_set_data_full (G_OBJECT (message), "event-id", g_strdup (event_id), g_free);
+}
+
+static void
+ma_chat_download_cb (GObject      *object,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  ChattyMaChat *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GInputStream) istream = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CHATTY_IS_MA_CHAT (self));
+
+  istream = chatty_message_get_file_stream_finish (CHATTY_MESSAGE (object), result, &error);
 }
 
 static void
@@ -1578,15 +554,38 @@ chatty_ma_chat_get_files_async (ChattyChat          *chat,
                                 GAsyncReadyCallback  callback,
                                 gpointer             user_data)
 {
+  g_autoptr(GTask) task = NULL;
   ChattyMaChat *self = CHATTY_MA_CHAT (chat);
+  ChattyFileInfo *file;
   GList *files;
 
-  g_assert (CHATTY_IS_MA_CHAT (self));
   g_assert (CHATTY_IS_MESSAGE (message));
 
+  task = g_task_new (self, NULL, callback, user_data);
+  g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), g_object_unref);
+
   files = chatty_message_get_files (message);
-  matrix_api_get_file_async (self->matrix_api, message, files->data, NULL, NULL,
-                             ma_chat_download_cb, self);
+  if (!files)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "No file found in message");
+      return;
+    }
+
+
+  file = files->data;
+  if (file->status != CHATTY_FILE_UNKNOWN)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "File URL missing or invalid file state");
+      return;
+    }
+
+  g_object_set_data (G_OBJECT (task), "file", file);
+
+  chatty_message_get_file_stream_async (message, NULL, CHATTY_PROTOCOL_MATRIX, NULL,
+                                        ma_chat_download_cb,
+                                        g_steal_pointer (&task));
 }
 
 static gboolean
@@ -1607,13 +606,7 @@ chatty_ma_chat_set_typing (ChattyChat *chat,
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  if (self->self_typing == is_typing &&
-      (g_get_monotonic_time () - self->self_typing_set_time) < G_TIME_SPAN_SECOND * 5)
-    return;
-
-  self->self_typing = is_typing;
-  self->self_typing_set_time = g_get_monotonic_time ();
-  matrix_api_set_typing (self->matrix_api, self->room_id, is_typing);
+  cm_room_set_typing_notice_async (self->cm_room, is_typing, NULL, NULL, NULL);
 }
 
 static void
@@ -1634,20 +627,23 @@ static const char *
 chatty_ma_chat_get_name (ChattyItem *item)
 {
   ChattyMaChat *self = (ChattyMaChat *)item;
+  const char *name;
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  if (self->room_name)
-    return self->room_name;
+  name = cm_room_get_name (self->cm_room);
 
-  if (!self->room_name_loaded)
-    return self->room_id;
+  if (name && cm_room_get_past_name (self->cm_room)) {
+    g_free (self->room_name);
+    self->room_name = g_strdup_printf ("%s (was %s)", name,
+                                       cm_room_get_past_name (self->cm_room));
+  }
 
-  if (!self->generated_name)
-    chatty_mat_chat_update_name (self);
+  if (self->room_name)
+    return self->room_name;
 
-  if (self->generated_name)
-    return self->generated_name;
+  if (name && *name)
+    return name;
 
   if (self->room_id)
     return self->room_id;
@@ -1662,7 +658,7 @@ chatty_ma_chat_get_username (ChattyItem *item)
 
   g_assert (CHATTY_IS_MA_CHAT (self));
 
-  return matrix_api_get_username (self->matrix_api);
+  return cm_client_get_user_id (self->cm_client);
 }
 
 static ChattyItemState
@@ -1727,36 +723,11 @@ chatty_ma_chat_get_avatar (ChattyItem *item)
   return NULL;
 }
 
-static void
-chatty_ma_chat_set_property (GObject      *object,
-                             guint         prop_id,
-                             const GValue *value,
-                             GParamSpec   *pspec)
-{
-  ChattyMaChat *self = (ChattyMaChat *)object;
-
-  switch (prop_id)
-    {
-    case PROP_JSON_DATA:
-      matrix_chat_set_json_data (self, g_value_dup_boxed (value));
-      break;
-
-    case PROP_ROOM_ID:
-      self->room_id = g_value_dup_string (value);
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
 static void
 chatty_ma_chat_finalize (GObject *object)
 {
   ChattyMaChat *self = (ChattyMaChat *)object;
 
-  g_clear_handle_id (&self->message_timeout_id, g_source_remove);
-
   if (self->avatar_cancellable)
     g_cancellable_cancel (self->avatar_cancellable);
   g_clear_object (&self->avatar_cancellable);
@@ -1764,26 +735,12 @@ chatty_ma_chat_finalize (GObject *object)
   g_list_store_remove_all (self->message_list);
   g_list_store_remove_all (self->buddy_list);
   g_clear_object (&self->message_list);
-  g_clear_object (&self->sorted_message_list);
   g_clear_object (&self->buddy_list);
-  g_clear_object (&self->matrix_api);
-  g_clear_object (&self->matrix_enc);
-  g_clear_object (&self->self_buddy);
   g_clear_pointer (&self->avatar_file, chatty_file_info_free);
   g_clear_object (&self->avatar);
 
-  g_clear_object (&self->matrix_db);
-  g_clear_object (&self->history_db);
-
-  g_clear_pointer (&self->json_data, json_object_unref);
-  g_queue_free_full (self->message_queue, g_object_unref);
-
   g_free (self->room_name);
-  g_free (self->generated_name);
   g_free (self->room_id);
-  g_free (self->encryption);
-  g_free (self->prev_batch);
-  g_free (self->last_batch);
 
   G_OBJECT_CLASS (chatty_ma_chat_parent_class)->finalize (object);
 }
@@ -1795,7 +752,6 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass)
   ChattyItemClass *item_class = CHATTY_ITEM_CLASS (klass);
   ChattyChatClass *chat_class = CHATTY_CHAT_CLASS (klass);
 
-  object_class->set_property = chatty_ma_chat_set_property;
   object_class->finalize = chatty_ma_chat_finalize;
 
   item_class->get_name = chatty_ma_chat_get_name;
@@ -1807,6 +763,8 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass)
   item_class->get_avatar = chatty_ma_chat_get_avatar;
 
   chat_class->is_im = chatty_ma_chat_is_im;
+  chat_class->get_chat_state = chatty_ma_chat_get_chat_state;
+  chat_class->has_file_upload = chatty_ma_chat_has_file_upload;
   chat_class->get_chat_name = chatty_ma_chat_get_chat_name;
   chat_class->load_past_messages = chatty_ma_chat_real_past_messages;
   chat_class->is_loading_history = chatty_ma_chat_is_loading_history;
@@ -1814,6 +772,8 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass)
   chat_class->get_account  = chatty_ma_chat_get_account;
   chat_class->get_encryption = chatty_ma_chat_get_encryption;
   chat_class->set_encryption_async = chatty_ma_chat_set_encryption_async;
+  chat_class->accept_invite_async = chatty_ma_chat_accept_invite_async;
+  chat_class->reject_invite_async = chatty_ma_chat_reject_invite_async;
   chat_class->get_last_message = chatty_ma_chat_get_last_message;
   chat_class->get_unread_count = chatty_ma_chat_get_unread_count;
   chat_class->set_unread_count = chatty_ma_chat_set_unread_count;
@@ -1822,86 +782,173 @@ chatty_ma_chat_class_init (ChattyMaChatClass *klass)
   chat_class->get_buddy_typing = chatty_ma_chat_get_buddy_typing;
   chat_class->set_typing = chatty_ma_chat_set_typing;
   chat_class->show_notification = chatty_ma_chat_show_notification;
-
-  properties[PROP_JSON_DATA] =
-    g_param_spec_boxed ("json-data",
-                        "json-data",
-                        "json-data for the room",
-                        JSON_TYPE_OBJECT,
-                        G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
-
-  properties[PROP_ROOM_ID] =
-    g_param_spec_string ("room-id",
-                         "json-data",
-                         "json-data for the room",
-                         NULL,
-                         G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY);
-
-  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
 static void
 chatty_ma_chat_init (ChattyMaChat *self)
 {
-  g_autoptr(GtkSorter) sorter = NULL;
-
-  sorter = gtk_custom_sorter_new (sort_message, NULL, NULL);
+  g_autoptr(GtkFilter) filter = NULL;
 
   self->message_list = g_list_store_new (CHATTY_TYPE_MESSAGE);
-  self->sorted_message_list = gtk_sort_list_model_new (G_LIST_MODEL (self->message_list), sorter);
+
+  filter = gtk_custom_filter_new ((GtkCustomFilterFunc)ma_chat_filter_event_list,
+                                  g_object_ref (self),
+                                  g_object_unref);
+  self->filtered_event_list = gtk_filter_list_model_new (G_LIST_MODEL (self->message_list),
+                                                         filter);
   self->buddy_list = g_list_store_new (CHATTY_TYPE_MA_BUDDY);
-  self->message_queue = g_queue_new ();
   self->avatar_cancellable = g_cancellable_new ();
 }
 
-ChattyMaChat *
-chatty_ma_chat_new (const char     *room_id,
-                    const char     *name,
-                    ChattyFileInfo *avatar,
-                    gboolean        encrypted)
+static void
+ma_chat_name_changed_cb (ChattyMaChat *self)
 {
-  ChattyMaChat *self;
+  g_assert (CHATTY_IS_MA_CHAT (self));
+
+  g_object_notify (G_OBJECT (self), "name");
+  g_signal_emit_by_name (self, "avatar-changed");
+}
+
+static void
+ma_chat_encryption_changed_cb (ChattyMaChat *self)
+{
+  g_assert (CHATTY_IS_MA_CHAT (self));
+
+  g_object_notify (G_OBJECT (self), "encrypt");
+}
+
+static void
+joined_members_changed_cb (ChattyMaChat *self,
+                           guint         position,
+                           guint         removed,
+                           guint         added,
+                           GListModel   *model)
+{
+  g_autoptr(GPtrArray) items = NULL;
+
+  g_assert (CHATTY_IS_CHAT (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  for (guint i = position; i < position + added; i++)
+    {
+      g_autoptr(CmUser) user = NULL;
+      ChattyMaBuddy *buddy;
+
+      if (!items)
+        items = g_ptr_array_new_with_free_func (g_object_unref);
 
-  g_return_val_if_fail (room_id && *room_id, NULL);
+      user = g_list_model_get_item (model, i);
+      buddy = chatty_ma_buddy_new_with_user (user);
+      g_ptr_array_add (items, buddy);
+    }
+
+  g_list_store_splice (self->buddy_list, position, removed,
+                       items ? items->pdata : NULL, added);
+}
+
+static void
+events_list_changed_cb (ChattyMaChat *self,
+                        guint         position,
+                        guint         removed,
+                        guint         added,
+                        GListModel   *model)
+{
+  g_autoptr(GPtrArray) items = NULL;
+
+  g_assert (CHATTY_IS_CHAT (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  for (guint i = position; i < position + added; i++)
+    {
+      g_autoptr(CmEvent) event = NULL;
+      ChattyMessage *message;
+
+      if (!items)
+        items = g_ptr_array_new_with_free_func (g_object_unref);
+
+      event = g_list_model_get_item (model, i);
+
+      if (CM_IS_ROOM_MESSAGE_EVENT (event) ||
+          cm_event_is_encrypted (event)) {
+        g_autoptr(ChattyMaBuddy) user = NULL;
+
+        user = ma_chat_find_cm_user (self, G_LIST_MODEL (self->buddy_list),
+                                     cm_event_get_sender (event), NULL, TRUE);
+        message = chatty_message_new_from_event (CHATTY_ITEM (user), event);
+      } else {
+        message = g_object_new (CHATTY_TYPE_MESSAGE, NULL);
+      }
+
+      g_object_set_data_full (G_OBJECT (message), "cm-event", g_object_ref (event), g_object_unref);
+      g_ptr_array_add (items, message);
+    }
+
+  g_list_store_splice (self->message_list, position, removed,
+                       items ? items->pdata : NULL, added);
+  g_signal_emit_by_name (self, "changed", 0);
+}
 
-  self = g_object_new (CHATTY_TYPE_MA_CHAT,
-                       "room-id", room_id, NULL);
-  self->room_name = g_strdup (name);
-  self->avatar_file = avatar;
-  if (encrypted)
-    self->encryption = g_strdup ("encrypted");
+ChattyMaChat *
+chatty_ma_chat_new_with_room (CmRoom *room)
+{
+  ChattyMaChat *self;
+  GListModel *members, *events;
+
+  g_return_val_if_fail (CM_IS_ROOM (room), NULL);
+
+  self = g_object_new (CHATTY_TYPE_MA_CHAT, NULL);
+  self->room_id = g_strdup (cm_room_get_id (room));
+  self->cm_room = g_object_ref (room);
+  g_signal_connect_object (room, "notify::name",
+                           G_CALLBACK (ma_chat_name_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+  g_signal_connect_object (room, "notify::encrypted",
+                           G_CALLBACK (ma_chat_encryption_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+
+  members = cm_room_get_joined_members (room);
+  g_signal_connect_object (members, "items-changed",
+                           G_CALLBACK (joined_members_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+  joined_members_changed_cb (self, 0, 0,
+                             g_list_model_get_n_items (members),
+                             members);
+
+  events = cm_room_get_events_list (room);
+  g_signal_connect_object (events, "items-changed",
+                           G_CALLBACK (events_list_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+  events_list_changed_cb (self, 0, 0,
+                          g_list_model_get_n_items (events),
+                          events);
 
   return self;
 }
 
-void
-chatty_ma_chat_set_history_db (ChattyMaChat *self,
-                               gpointer      history_db)
+CmRoom *
+chatty_ma_chat_get_cm_room (ChattyMaChat *self)
 {
-  g_return_if_fail (CHATTY_IS_MA_CHAT (self));
-  g_return_if_fail (CHATTY_IS_HISTORY (history_db));
-  g_return_if_fail (!self->history_db);
+  g_return_val_if_fail (CHATTY_IS_MA_CHAT (self), FALSE);
 
-  self->history_db = g_object_ref (history_db);
+  return self->cm_room;
 }
 
-void
-chatty_ma_chat_set_matrix_db (ChattyMaChat *self,
-                              gpointer      matrix_db)
+gboolean
+chatty_ma_chat_can_set_encryption (ChattyMaChat *self)
 {
-  g_return_if_fail (CHATTY_IS_MA_CHAT (self));
-  g_return_if_fail (MATRIX_IS_DB (matrix_db));
-  g_return_if_fail (!self->matrix_db);
+  g_return_val_if_fail (CHATTY_IS_MA_CHAT (self), FALSE);
+
+  if (!self->cm_room)
+    return FALSE;
 
-  self->matrix_db = g_object_ref (matrix_db);
+  return cm_room_self_has_power_for_event (self->cm_room, CM_M_ROOM_ENCRYPTION);
 }
 
 /**
  * chatty_ma_chat_set_data:
  * @self: a #ChattyMaChat
  * @account: A #ChattyMaAccount
- * @api: A #MatrixApi
- * @enc: A #MatrixEnc for E2E
+ * @client: A #CmClient
  *
  * Use this function to set internal data required
  * to connect to a matrix server.
@@ -1909,25 +956,13 @@ chatty_ma_chat_set_matrix_db (ChattyMaChat *self,
 void
 chatty_ma_chat_set_data (ChattyMaChat  *self,
                          ChattyAccount *account,
-                         gpointer       api,
-                         gpointer       enc)
+                         gpointer       client)
 {
   g_return_if_fail (CHATTY_IS_MA_CHAT (self));
-  g_return_if_fail (MATRIX_IS_API (api));
+  g_return_if_fail (CM_IS_CLIENT (client));
 
   g_set_weak_pointer (&self->account, account);
-  g_set_object (&self->matrix_api, api);
-  g_set_object (&self->matrix_enc, enc);
-
-  if (!self->state_is_sync && !self->state_is_syncing &&
-      self->matrix_api && self->matrix_enc) {
-    self->state_is_syncing = TRUE;
-    CHATTY_TRACE (self->room_id, "Getting encryption state for");
-    matrix_api_get_room_encryption_async (self->matrix_api,
-                                          self->room_id,
-                                          get_room_encryption_cb,
-                                          self);
-  }
+  g_set_object (&self->cm_client, client);
 }
 
 gboolean
@@ -1941,66 +976,3 @@ chatty_ma_chat_matches_id (ChattyMaChat *self,
 
   return g_strcmp0 (self->room_id, room_id) == 0;
 }
-
-static void
-db_room_saved_cb (GObject      *object,
-                  GAsyncResult *result,
-                  gpointer      user_data)
-{
-  g_autoptr(ChattyMaChat) self = user_data;
-
-  g_assert (CHATTY_IS_MA_CHAT (self));
-
-  self->saving_room_to_db = FALSE;
-}
-
-void
-chatty_ma_chat_set_prev_batch (ChattyMaChat *self,
-                               char         *prev_batch)
-{
-  g_return_if_fail (CHATTY_IS_MA_CHAT (self));
-
-  g_clear_pointer (&self->prev_batch, g_free);
-  self->prev_batch = prev_batch;
-
-  if (self->saving_room_to_db)
-    return;
-
-  self->saving_room_to_db = TRUE;
-  matrix_db_save_room_async (self->matrix_db, self->account,
-                             matrix_api_get_device_id (self->matrix_api),
-                             self->room_id, prev_batch,
-                             db_room_saved_cb, g_object_ref (self));
-}
-
-/**
- * chatty_ma_chat_set_last_batch:
- * @self: A #ChattyMaChat
- * @last_batch: A string representing time
- *
- * The batch which we have already loaded chat
- * up to. When history is loaded from server,
- * only items after @last_batch shall be loaded.
- * This limits only loading history from server
- * not from database.
- */
-void
-chatty_ma_chat_set_last_batch (ChattyMaChat *self,
-                               const char   *last_batch)
-{
-  g_return_if_fail (CHATTY_IS_MA_CHAT (self));
-
-  g_free (self->last_batch);
-  self->last_batch = g_strdup (last_batch);
-}
-
-void
-chatty_ma_chat_add_messages (ChattyMaChat *self,
-                             GPtrArray    *messages)
-{
-  g_return_if_fail (CHATTY_IS_MA_CHAT (self));
-
-  if (messages && messages->len)
-    g_list_store_splice (self->message_list, 0, 0,
-                         messages->pdata, messages->len);
-}
diff --git a/src/matrix/chatty-ma-chat.h b/src/matrix/chatty-ma-chat.h
index 1f6e147b55c88dd6c87cfcbbc484083f3aeebc1d..74f1d5c738fdf00d2b47dde4417ce768e47bbda9 100644
--- a/src/matrix/chatty-ma-chat.h
+++ b/src/matrix/chatty-ma-chat.h
@@ -12,12 +12,10 @@
 #pragma once
 
 #include <glib-object.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
-#include "chatty-item.h"
-#include "chatty-account.h"
-#include "chatty-message.h"
 #include "chatty-chat.h"
-#include "chatty-enums.h"
 
 G_BEGIN_DECLS
 
@@ -25,25 +23,13 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (ChattyMaChat, chatty_ma_chat, CHATTY, MA_CHAT, ChattyChat)
 
-ChattyMaChat *chatty_ma_chat_new                (const char     *room_id,
-                                                 const char     *name,
-                                                 ChattyFileInfo *avatar,
-                                                 gboolean        encrypted);
-void          chatty_ma_chat_set_history_db     (ChattyMaChat  *self,
-                                                 gpointer       history_db);
-void          chatty_ma_chat_set_matrix_db      (ChattyMaChat  *self,
-                                                 gpointer       matrix_db);
+ChattyMaChat *chatty_ma_chat_new_with_room      (CmRoom        *room);
+CmRoom       *chatty_ma_chat_get_cm_room        (ChattyMaChat  *self);
+gboolean      chatty_ma_chat_can_set_encryption (ChattyMaChat  *self);
 void          chatty_ma_chat_set_data           (ChattyMaChat  *self,
                                                  ChattyAccount *account,
-                                                 gpointer       api,
-                                                 gpointer       enc);
+                                                 gpointer       client);
 gboolean      chatty_ma_chat_matches_id         (ChattyMaChat  *self,
                                                  const char    *room_id);
-void          chatty_ma_chat_set_prev_batch     (ChattyMaChat  *self,
-                                                 char          *prev_batch);
-void          chatty_ma_chat_set_last_batch     (ChattyMaChat  *self,
-                                                 const char    *last_batch);
-void          chatty_ma_chat_add_messages       (ChattyMaChat  *self,
-                                                 GPtrArray     *messages);
 
 G_END_DECLS
diff --git a/src/matrix/chatty-ma-key-chat.c b/src/matrix/chatty-ma-key-chat.c
new file mode 100644
index 0000000000000000000000000000000000000000..b6e44a2c8d2c78f143d88f875037e9a8a846d028
--- /dev/null
+++ b/src/matrix/chatty-ma-key-chat.c
@@ -0,0 +1,310 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-ma-key-chat.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "chatty-ma-key-chat"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib/gi18n.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include <cmatrix.h>
+
+#include "chatty-ma-buddy.h"
+#include "chatty-ma-account.h"
+#include "chatty-ma-key-chat.h"
+
+struct _ChattyMaKeyChat
+{
+  ChattyChat       parent_instance;
+
+  ChattyMaAccount *ma_account;
+  CmClient        *cm_client;
+  ChattyMaBuddy   *buddy;
+
+  CmEvent         *key_event;
+  ChattyChatState  chat_state;
+};
+
+G_DEFINE_TYPE (ChattyMaKeyChat, chatty_ma_key_chat, CHATTY_TYPE_CHAT)
+
+static ChattyChatState
+chatty_ma_key_chat_get_chat_state (ChattyChat *chat)
+{
+  ChattyMaKeyChat *self = (ChattyMaKeyChat *)chat;
+
+  g_assert (CHATTY_IS_MA_KEY_CHAT (self));
+
+  return self->chat_state;
+}
+
+static ChattyAccount *
+chatty_ma_key_chat_get_account (ChattyChat *chat)
+{
+  ChattyMaKeyChat *self = (ChattyMaKeyChat *)chat;
+
+  g_assert (CHATTY_IS_MA_KEY_CHAT (self));
+
+  return CHATTY_ACCOUNT (self->ma_account);
+}
+
+static const char *
+chatty_ma_key_chat_get_name (ChattyItem *item)
+{
+  return _("Key Verification");
+}
+
+static ChattyProtocol
+chatty_ma_key_chat_get_protocols (ChattyItem *item)
+{
+  return CHATTY_PROTOCOL_MATRIX;
+}
+
+static void
+chatty_ma_key_chat_finalize (GObject *object)
+{
+  ChattyMaKeyChat *self = (ChattyMaKeyChat *)object;
+
+  g_clear_object (&self->key_event);
+  g_clear_object (&self->cm_client);
+  g_clear_object (&self->buddy);
+
+  G_OBJECT_CLASS (chatty_ma_key_chat_parent_class)->finalize (object);
+}
+
+static void
+chatty_ma_key_chat_class_init (ChattyMaKeyChatClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  ChattyItemClass *item_class = CHATTY_ITEM_CLASS (klass);
+  ChattyChatClass *chat_class = CHATTY_CHAT_CLASS (klass);
+
+  object_class->finalize = chatty_ma_key_chat_finalize;
+
+  item_class->get_name = chatty_ma_key_chat_get_name;
+  item_class->get_protocols = chatty_ma_key_chat_get_protocols;
+
+  chat_class->get_chat_state = chatty_ma_key_chat_get_chat_state;
+  chat_class->get_account  = chatty_ma_key_chat_get_account;
+}
+
+static void
+chatty_ma_key_chat_init (ChattyMaKeyChat *self)
+{
+  self->chat_state = CHATTY_CHAT_VERIFICATION;
+}
+
+static void
+key_event_updated_cb (ChattyMaKeyChat *self)
+{
+  g_assert (CHATTY_IS_MA_KEY_CHAT (self));
+
+  g_signal_emit_by_name (self, "changed", 0);
+}
+
+ChattyMaKeyChat *
+chatty_ma_key_chat_new (gpointer  ma_account,
+                        CmEvent  *key_event)
+{
+  ChattyMaKeyChat *self;
+  CmEventType event_type;
+
+  g_return_val_if_fail (CHATTY_MA_ACCOUNT (ma_account), NULL);
+  g_return_val_if_fail (CM_IS_EVENT (key_event), NULL);
+
+  event_type = cm_event_get_m_type (key_event);
+  g_return_val_if_fail (event_type == CM_M_KEY_VERIFICATION_START ||
+                        event_type == CM_M_KEY_VERIFICATION_REQUEST, NULL);
+
+  self = g_object_new (CHATTY_TYPE_MA_KEY_CHAT, NULL);
+  g_set_weak_pointer (&self->ma_account, ma_account);
+  self->cm_client = g_object_ref (chatty_ma_account_get_cm_client (ma_account));
+  self->key_event = g_object_ref (key_event);
+  self->buddy = chatty_ma_buddy_new_with_user (cm_event_get_sender (key_event));
+
+  g_signal_connect_object (self->key_event, "updated",
+                           G_CALLBACK (key_event_updated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  return self;
+}
+
+CmEvent *
+chatty_ma_key_chat_get_event (ChattyMaKeyChat *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL);
+
+  return self->key_event;
+}
+
+ChattyItem *
+chatty_ma_key_chat_get_sender (ChattyMaKeyChat *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL);
+
+  return CHATTY_ITEM (self->buddy);
+}
+
+GPtrArray *
+chatty_ma_key_get_emoji (ChattyMaKeyChat *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL);
+
+  return g_object_get_data (G_OBJECT (self->key_event), "emoji");
+}
+
+/* decimal is an array of 3 */
+guint16 *
+chatty_ma_key_get_decimal (ChattyMaKeyChat *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), NULL);
+
+  return g_object_get_data (G_OBJECT (self->key_event), "decimal");
+}
+
+static void
+ma_key_cancel_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  ChattyMaKeyChat *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean success;
+
+  self = g_task_get_source_object (task);
+  success = cm_client_key_verification_cancel_finish (CM_CLIENT (object), result, &error);
+
+  if (success)
+    self->chat_state = CHATTY_CHAT_LEFT;
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+void
+chatty_ma_key_cancel_async (ChattyMaKeyChat     *self,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CHATTY_IS_MA_KEY_CHAT (self));
+
+  task = g_task_new (self, NULL, callback, user_data);
+
+  cm_client_key_verification_cancel_async (self->cm_client, self->key_event, NULL,
+                                           ma_key_cancel_cb,
+                                           task);
+}
+
+gboolean
+chatty_ma_key_cancel_finish (ChattyMaKeyChat *self,
+                             GAsyncResult    *result,
+                             GError         **error)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ma_key_accept_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean success;
+
+  success = cm_client_key_verification_continue_finish (CM_CLIENT (object), result, &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+void
+chatty_ma_key_accept_async (ChattyMaKeyChat     *self,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CHATTY_IS_MA_KEY_CHAT (self));
+
+  task = g_task_new (self, NULL, callback, user_data);
+
+  cm_client_key_verification_continue_async (self->cm_client, self->key_event, NULL,
+                                             ma_key_accept_cb,
+                                             task);
+}
+
+gboolean
+chatty_ma_key_accept_finish (ChattyMaKeyChat  *self,
+                             GAsyncResult     *result,
+                             GError          **error)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ma_key_match_cb (GObject      *object,
+                 GAsyncResult *result,
+                 gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean success;
+
+  success = cm_client_key_verification_match_finish (CM_CLIENT (object), result, &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+void
+chatty_ma_key_match_async (ChattyMaKeyChat     *self,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CHATTY_IS_MA_KEY_CHAT (self));
+
+  task = g_task_new (self, NULL, callback, user_data);
+
+  cm_client_key_verification_match_async (self->cm_client, self->key_event, NULL,
+                                          ma_key_match_cb,
+                                          task);
+}
+
+gboolean
+chatty_ma_key_match_finish (ChattyMaKeyChat  *self,
+                            GAsyncResult     *result,
+                            GError          **error)
+{
+  g_return_val_if_fail (CHATTY_IS_MA_KEY_CHAT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/src/matrix/chatty-ma-key-chat.h b/src/matrix/chatty-ma-key-chat.h
new file mode 100644
index 0000000000000000000000000000000000000000..1c5d2318bde3aeefece4fcc8bf7b782927646174
--- /dev/null
+++ b/src/matrix/chatty-ma-key-chat.h
@@ -0,0 +1,51 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* chatty-ma-key-chat.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <glib-object.h>
+
+#include "chatty-chat.h"
+
+G_BEGIN_DECLS
+
+#define CHATTY_TYPE_MA_KEY_CHAT (chatty_ma_key_chat_get_type ())
+
+G_DECLARE_FINAL_TYPE (ChattyMaKeyChat, chatty_ma_key_chat, CHATTY, MA_KEY_CHAT, ChattyChat)
+
+ChattyMaKeyChat     *chatty_ma_key_chat_new          (gpointer             ma_account,
+                                                      CmEvent             *key_event);
+CmEvent             *chatty_ma_key_chat_get_event    (ChattyMaKeyChat     *self);
+ChattyItem          *chatty_ma_key_chat_get_sender   (ChattyMaKeyChat     *self);
+GPtrArray           *chatty_ma_key_get_emoji         (ChattyMaKeyChat     *self);
+guint16             *chatty_ma_key_get_decimal       (ChattyMaKeyChat     *self);
+void                 chatty_ma_key_cancel_async      (ChattyMaKeyChat     *self,
+                                                      GAsyncReadyCallback  callback,
+                                                      gpointer             user_data);
+gboolean             chatty_ma_key_cancel_finish     (ChattyMaKeyChat     *self,
+                                                      GAsyncResult        *result,
+                                                      GError             **error);
+void                 chatty_ma_key_accept_async      (ChattyMaKeyChat     *self,
+                                                      GAsyncReadyCallback  callback,
+                                                      gpointer             user_data);
+gboolean             chatty_ma_key_accept_finish     (ChattyMaKeyChat     *self,
+                                                      GAsyncResult        *result,
+                                                      GError             **error);
+void                 chatty_ma_key_match_async       (ChattyMaKeyChat     *self,
+                                                      GAsyncReadyCallback  callback,
+                                                      gpointer             user_data);
+gboolean             chatty_ma_key_match_finish      (ChattyMaKeyChat     *self,
+                                                      GAsyncResult        *result,
+                                                      GError             **error);
+
+G_END_DECLS
+
diff --git a/src/matrix/chatty-matrix.c b/src/matrix/chatty-matrix.c
index 25c2507105f979475826b95f0d1a90c9a40bb91c..6f997bd4a4495a0698601f5b732d217f24b4f537 100644
--- a/src/matrix/chatty-matrix.c
+++ b/src/matrix/chatty-matrix.c
@@ -16,13 +16,14 @@
 #endif
 
 #include "contrib/gtk.h"
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
 #include "chatty-secret-store.h"
 #include "chatty-settings.h"
 #include "chatty-ma-account.h"
 #include "chatty-ma-buddy.h"
 #include "chatty-ma-chat.h"
-#include "matrix-db.h"
 #include "chatty-matrix.h"
 #include "chatty-log.h"
 
@@ -35,65 +36,21 @@ struct _ChattyMatrix
   GListStore    *list_of_chat_list;
   GtkFlattenListModel *chat_list;
 
-  ChattyHistory *history;
-  MatrixDb      *matrix_db;
+  CmMatrix      *cm_matrix;
 
-  /* The timeout id for the callback on network changes */
-  guint          network_change_id;
-
-  gboolean       has_loaded;
   gboolean       disable_auto_login;
-  gboolean       network_available;
+  gboolean       is_ready;
 };
 
 G_DEFINE_TYPE (ChattyMatrix, chatty_matrix, G_TYPE_OBJECT)
 
-#define RECONNECT_TIMEOUT    1000 /* milliseconds */
-
-static gboolean
-matrix_reconnect (gpointer user_data)
-{
-  ChattyMatrix *self = user_data;
-  GListModel *list;
-  guint n_items;
-
-  self->network_change_id = 0;
-
-  list = G_LIST_MODEL (self->account_list);
-  n_items = g_list_model_get_n_items (list);
-
-  for (guint i = 0; i < n_items; i++) {
-    g_autoptr(ChattyAccount) account = NULL;
-
-    account = g_list_model_get_item (list, i);
-
-    if (chatty_ma_account_can_connect (CHATTY_MA_ACCOUNT (account)))
-      chatty_account_connect (account, FALSE);
-    else
-      chatty_account_disconnect (account);
-  }
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-matrix_network_changed_cb (ChattyMatrix    *self,
-                           gboolean         network_available,
-                           GNetworkMonitor *network_monitor)
-{
-  g_assert (CHATTY_IS_MATRIX (self));
-  g_assert (G_IS_NETWORK_MONITOR (network_monitor));
-
-  if (!self->has_loaded)
-    return;
-
-  g_clear_handle_id (&self->network_change_id, g_source_remove);
-  self->network_change_id = g_timeout_add (RECONNECT_TIMEOUT,
-                                           matrix_reconnect, self);
+enum {
+  PROP_0,
+  PROP_ENABLED,
+  N_PROPS
+};
 
-  g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE,
-         "Network changed, has network: %s", CHATTY_LOG_BOOL (network_available));
-}
+static GParamSpec *properties[N_PROPS];
 
 static void
 matrix_ma_account_changed_cb (ChattyMatrix    *self,
@@ -111,6 +68,20 @@ matrix_ma_account_changed_cb (ChattyMatrix    *self,
     g_list_model_items_changed (G_LIST_MODEL (self->list_of_chat_list), position, 1, 1);
 }
 
+static void
+matrix_add_clients_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  g_autoptr(ChattyMatrix) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  cm_matrix_add_clients_finish (self->cm_matrix, result, &error);
+
+  if (error)
+    g_warning ("Error saving accounts: %s", error->message);
+}
+
 static void
 matrix_secret_load_cb (GObject      *object,
                        GAsyncResult *result,
@@ -118,50 +89,44 @@ matrix_secret_load_cb (GObject      *object,
 {
   ChattyMatrix *self = user_data;
   g_autoptr(GPtrArray) accounts = NULL;
+  g_autoptr(GPtrArray) secrets = NULL;
   g_autoptr(GError) error = NULL;
 
   g_assert (CHATTY_IS_MATRIX (self));
 
-  accounts = chatty_secret_load_finish (result, &error);
+  secrets = chatty_secret_load_finish (result, &error);
   g_info ("Loading accounts from secrets %s", CHATTY_LOG_SUCESS (!error));
 
-  if (error)
-    g_warning ("Error loading secret accounts: %s", error->message);
+  if (error &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    g_warning ("Error loading secrets: %s", error->message);
 
-  if (!accounts)
+  if (!secrets || !secrets->len)
     return;
 
-  g_info ("Loaded %d matrix accounts", accounts ? accounts->len : 0);
-
-  for (guint i = 0; i < accounts->len; i++) {
-    g_signal_connect_object (accounts->pdata[i], "notify::status",
-                             G_CALLBACK (matrix_ma_account_changed_cb),
-                             self, G_CONNECT_SWAPPED);
-
-    chatty_ma_account_set_db (accounts->pdata[i], self->matrix_db, self->history);
-    g_list_store_append (self->list_of_chat_list,
-                         chatty_ma_account_get_chat_list (accounts->pdata[i]));
-  }
-
-  g_list_store_splice (self->account_list, 0, 0, accounts->pdata, accounts->len);
+  /* ... store the locally loaded accounts to cmatrix store */
+  cm_matrix_add_clients_async (self->cm_matrix, secrets,
+                               matrix_add_clients_cb,
+                               g_object_ref (self));
 }
 
 static void
-matrix_db_open_cb (GObject      *object,
-                   GAsyncResult *result,
-                   gpointer      user_data)
+chatty_matrix_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
 {
-  ChattyMatrix *self = user_data;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (CHATTY_IS_MATRIX (self));
+  ChattyMatrix *self = (ChattyMatrix *)object;
 
-  if (matrix_db_open_finish (self->matrix_db, result, &error))
-    chatty_secret_load_async (NULL, matrix_secret_load_cb, self);
-  else if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    g_warning ("Failed to open Matrix DB: %s", error->message);
+  switch (prop_id)
+    {
+    case PROP_ENABLED:
+      g_value_set_boolean (value, chatty_matrix_is_enabled (self));
+      break;
 
-  g_info ("Opening matrix db %s", CHATTY_LOG_SUCESS (!error));
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
 }
 
 static void
@@ -169,8 +134,6 @@ chatty_matrix_finalize (GObject *object)
 {
   ChattyMatrix *self = (ChattyMatrix *)object;
 
-  g_clear_handle_id (&self->network_change_id, g_source_remove);
-
   g_list_store_remove_all (self->list_of_chat_list);
   g_list_store_remove_all (self->account_list);
 
@@ -178,9 +141,6 @@ chatty_matrix_finalize (GObject *object)
   g_clear_object (&self->list_of_chat_list);
   g_clear_object (&self->chat_list);
 
-  g_clear_object (&self->history);
-  g_clear_object (&self->matrix_db);
-
   G_OBJECT_CLASS (chatty_matrix_parent_class)->finalize (object);
 }
 
@@ -190,12 +150,26 @@ chatty_matrix_class_init (ChattyMatrixClass *klass)
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->finalize = chatty_matrix_finalize;
+  object_class->get_property = chatty_matrix_get_property;
+
+  /**
+   * ChattyMatrix:enabled:
+   *
+   * Whether matrix is enabled and usable
+   */
+  properties[PROP_ENABLED] =
+    g_param_spec_boolean ("enabled",
+                          "matrix enabled",
+                          "matrix enabled",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
 static void
 chatty_matrix_init (ChattyMatrix *self)
 {
-  GNetworkMonitor *network_monitor;
   GListModel *model;
 
   self->account_list = g_list_store_new (CHATTY_TYPE_ACCOUNT);
@@ -203,49 +177,116 @@ chatty_matrix_init (ChattyMatrix *self)
 
   model = G_LIST_MODEL (self->list_of_chat_list);
   self->chat_list = gtk_flatten_list_model_new (CHATTY_TYPE_CHAT, model);
-
-  network_monitor = g_network_monitor_get_default ();
-  self->network_available = g_network_monitor_get_network_available (network_monitor);
-
-  g_signal_connect_object (network_monitor, "network-changed",
-                           G_CALLBACK (matrix_network_changed_cb), self,
-                           G_CONNECT_AFTER | G_CONNECT_SWAPPED);
-
 }
 
 ChattyMatrix *
-chatty_matrix_new (ChattyHistory *history,
-                   gboolean       disable_auto_login)
+chatty_matrix_new (gboolean disable_auto_login)
 {
   ChattyMatrix *self;
 
-  g_return_val_if_fail (CHATTY_IS_HISTORY (history), NULL);
-
   self = g_object_new (CHATTY_TYPE_MATRIX, NULL);
-  self->history = g_object_ref (history);
   self->disable_auto_login = !!disable_auto_login;
 
   return self;
 }
 
-void
-chatty_matrix_load (ChattyMatrix *self)
+gboolean
+chatty_matrix_is_enabled (ChattyMatrix *self)
 {
-  char *db_path;
+  g_return_val_if_fail (CHATTY_IS_MATRIX (self), FALSE);
 
-  if (self->has_loaded)
+  return self->is_ready;
+}
+
+static void
+matrix_open_cb (GObject      *obj,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  g_autoptr(ChattyMatrix) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GListModel *accounts;
+  gboolean success;
+
+  g_assert (CHATTY_IS_MATRIX (self));
+  g_assert (CM_IS_MATRIX (obj));
+
+  success = cm_matrix_open_finish (CM_MATRIX (obj), result, &error);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENABLED]);
+
+  if (error) {
+    g_warning ("Error plugging in to the matrix: %s", error->message);
     return;
+  }
+
+  if (success)
+    {
+      accounts = cm_matrix_get_clients_list (self->cm_matrix);
+      /* If we have no items loaded, load the items stored in chatty secret store... */
+      if (g_list_model_get_n_items (accounts) == 0)
+        chatty_secret_load_async (NULL, matrix_secret_load_cb, self);
+    }
+}
+
+static void
+matrix_client_list_changed_cb (ChattyMatrix *self,
+                               int              position,
+                               int              removed,
+                               int              added,
+                               GListModel      *model)
+{
+  g_autoptr(GPtrArray) items = NULL;
+
+  g_assert (CHATTY_IS_MATRIX (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  for (guint i = position; i < position + added; i++)
+    {
+      g_autoptr(CmClient) client = NULL;
+      ChattyMaAccount *account;
+
+      if (!items)
+        items = g_ptr_array_new_with_free_func (g_object_unref);
+
+      client = g_list_model_get_item (model, i);
+      account = chatty_ma_account_new_from_client (client);
+      g_list_store_append (self->list_of_chat_list,
+                           chatty_ma_account_get_chat_list (account));
+      g_signal_connect_object (account, "notify::status",
+                               G_CALLBACK (matrix_ma_account_changed_cb),
+                               self, G_CONNECT_SWAPPED);
+      if (g_object_get_data (G_OBJECT (client), "enable"))
+        cm_client_set_enabled (client, TRUE);
+      g_ptr_array_add (items, account);
+    }
 
-  self->has_loaded = TRUE;
+  g_list_store_splice (self->account_list, position, removed,
+                       items ? items->pdata : NULL, added);
+}
 
-  if (!chatty_settings_get_experimental_features (chatty_settings_get_default ()))
+void
+chatty_matrix_load (ChattyMatrix *self)
+{
+  g_autofree char *db_path = NULL;
+  g_autofree char *data_path = NULL;
+  g_autofree char *cache_path = NULL;
+  GListModel *client_list;
+
+  if (self->cm_matrix)
     return;
 
-  self->matrix_db = matrix_db_new ();
-  db_path =  g_build_filename (chatty_utils_get_purple_dir (), "chatty", "db", NULL);
-  matrix_db_open_async (self->matrix_db, db_path, "matrix.db",
-                        matrix_db_open_cb, self);
-  g_info ("Opening matrix db");
+  data_path = g_build_filename (g_get_user_data_dir (), "chatty", NULL);
+  cache_path = g_build_filename (g_get_user_cache_dir (), "chatty", NULL);
+  self->cm_matrix = cm_matrix_new (data_path, cache_path, "sm.puri.Chatty",
+                                   self->disable_auto_login);
+  client_list = cm_matrix_get_clients_list (self->cm_matrix);
+  g_signal_connect_object (client_list, "items-changed",
+                           G_CALLBACK (matrix_client_list_changed_cb),
+                           self, G_CONNECT_SWAPPED);
+
+  db_path = g_build_filename (chatty_utils_get_purple_dir (), "chatty", "db", NULL);
+  cm_matrix_open_async (self->cm_matrix, db_path, "matrix.db", NULL,
+                        matrix_open_cb, g_object_ref (self));
 }
 
 GListModel *
@@ -265,34 +306,6 @@ chatty_matrix_get_chat_list (ChattyMatrix *self)
 
 }
 
-static void
-matrix_db_account_delete_cb (GObject      *object,
-                             GAsyncResult *result,
-                             gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  ChattyMaAccount *account;
-  GError *error = NULL;
-  const char *username;
-  gboolean status;
-
-  g_assert (G_IS_TASK (task));
-
-  status = matrix_db_delete_account_finish (MATRIX_DB (object),
-                                            result, &error);
-
-  account = g_task_get_task_data (task);
-  username = chatty_item_get_username (CHATTY_ITEM (account));
-  if (!username || !*username)
-    username = chatty_ma_account_get_login_username (account);
-  CHATTY_DEBUG_DETAILED (username, "Deleting %s, account:", CHATTY_LOG_SUCESS (!error));
-
-  if (error)
-    g_task_return_error (task, error);
-  else
-    g_task_return_boolean (task, status);
-}
-
 static void
 matrix_account_delete_cb (GObject      *object,
                            GAsyncResult *result,
@@ -312,7 +325,7 @@ matrix_account_delete_cb (GObject      *object,
   g_assert (CHATTY_IS_MATRIX (self));
   g_assert (CHATTY_IS_MA_ACCOUNT (account));
 
-  chatty_secret_delete_finish (result, &error);
+  cm_matrix_delete_client_finish (self->cm_matrix, result, &error);
 
   username = chatty_item_get_username (CHATTY_ITEM (account));
   if (!username || !*username)
@@ -322,9 +335,7 @@ matrix_account_delete_cb (GObject      *object,
   if (error)
     g_task_return_error (task, error);
   else
-    matrix_db_delete_account_async (self->matrix_db, CHATTY_ACCOUNT (account),
-                                    matrix_db_account_delete_cb,
-                                    g_steal_pointer (&task));
+    chatty_account_delete (CHATTY_ACCOUNT (account));
 }
 
 void
@@ -334,7 +345,6 @@ chatty_matrix_delete_account_async (ChattyMatrix        *self,
                                     GAsyncReadyCallback  callback,
                                     gpointer             user_data)
 {
-  GListModel *chat_list;
   GTask *task;
 
   g_return_if_fail (CHATTY_IS_MATRIX (self));
@@ -343,14 +353,12 @@ chatty_matrix_delete_account_async (ChattyMatrix        *self,
   task = g_task_new (self, cancellable, callback, user_data);
   g_task_set_task_data (task, g_object_ref (account), g_object_unref);
 
-  chat_list = chatty_ma_account_get_chat_list (CHATTY_MA_ACCOUNT (account));
-  chatty_utils_remove_list_item (self->list_of_chat_list, chat_list);
-  chatty_utils_remove_list_item (self->account_list, account);
   chatty_account_set_enabled (account, FALSE);
 
   CHATTY_DEBUG (chatty_item_get_username (CHATTY_ITEM (account)), "Deleting account");
-
-  chatty_secret_delete_async (account, NULL, matrix_account_delete_cb, task);
+  cm_matrix_delete_client_async (self->cm_matrix,
+                                 chatty_ma_account_get_cm_client (CHATTY_MA_ACCOUNT (account)),
+                                 matrix_account_delete_cb, task);
 }
 
 gboolean
@@ -371,19 +379,20 @@ matrix_save_account_cb (GObject      *object,
                         gpointer      user_data)
 {
   ChattyMatrix *self;
-  ChattyMaAccount *account = (gpointer)object;
+  ChattyMaAccount *account;
   g_autoptr(GTask) task = user_data;
   g_autoptr(GError) error = NULL;
   const char *username;
   gboolean saved;
 
   g_assert (G_IS_TASK (task));
-  g_assert (CHATTY_IS_MA_ACCOUNT (account));
 
   self = g_task_get_source_object (task);
+  account = g_task_get_task_data (task);
   g_assert (CHATTY_IS_MATRIX (self));
+  g_assert (CHATTY_IS_MA_ACCOUNT (account));
 
-  saved = chatty_ma_account_save_finish (account, result, &error);
+  saved = cm_matrix_save_client_finish (self->cm_matrix, result, &error);
 
   username = chatty_item_get_username (CHATTY_ITEM (account));
   if (!username || !*username)
@@ -395,17 +404,6 @@ matrix_save_account_cb (GObject      *object,
     return;
   }
 
-  if (saved) {
-    g_list_store_append (self->account_list, account);
-
-    g_signal_connect_object (account, "notify::status",
-                             G_CALLBACK (matrix_ma_account_changed_cb),
-                             self, G_CONNECT_SWAPPED);
-
-    if (!self->disable_auto_login)
-      chatty_account_set_enabled (CHATTY_ACCOUNT (account), TRUE);
-  }
-
   g_task_return_boolean (task, saved);
 }
 
@@ -416,6 +414,7 @@ chatty_matrix_save_account_async (ChattyMatrix        *self,
                                   GAsyncReadyCallback  callback,
                                   gpointer             user_data)
 {
+  CmClient *cm_client;
   GTask *task;
 
   g_return_if_fail (CHATTY_IS_MATRIX (self));
@@ -423,10 +422,13 @@ chatty_matrix_save_account_async (ChattyMatrix        *self,
 
   CHATTY_DEBUG (chatty_item_get_username (CHATTY_ITEM (account)), "Saving account");
 
+  cm_client = chatty_ma_account_get_cm_client (CHATTY_MA_ACCOUNT (account));
+  g_return_if_fail (CM_IS_CLIENT (cm_client));
+
   task = g_task_new (self, cancellable, callback, user_data);
-  chatty_ma_account_set_db (CHATTY_MA_ACCOUNT (account), self->matrix_db, self->history);
-  chatty_ma_account_save_async (CHATTY_MA_ACCOUNT (account), TRUE, NULL,
-                                matrix_save_account_cb, task);
+  g_task_set_task_data (task, g_object_ref (account), g_object_unref);
+  cm_matrix_save_client_async (self->cm_matrix, cm_client,
+                               matrix_save_account_cb, task);
 }
 
 gboolean
@@ -480,8 +482,7 @@ chatty_matrix_find_chat_with_name (ChattyMatrix   *self,
   g_return_val_if_fail (account_id && *account_id, NULL);
   g_return_val_if_fail (chat_id && *chat_id, NULL);
 
-  if (!chatty_settings_get_experimental_features (chatty_settings_get_default ()) ||
-      protocol != CHATTY_PROTOCOL_MATRIX)
+  if (protocol != CHATTY_PROTOCOL_MATRIX)
     return NULL;
 
   accounts = G_LIST_MODEL (self->account_list);
@@ -510,3 +511,20 @@ chatty_matrix_find_chat_with_name (ChattyMatrix   *self,
 
   return NULL;
 }
+
+CmClient *
+chatty_matrix_client_new (ChattyMatrix *self)
+{
+  g_return_val_if_fail (CHATTY_IS_MATRIX (self), NULL);
+
+  return cm_matrix_client_new (self->cm_matrix);
+}
+
+gboolean
+chatty_matrix_has_user_id (ChattyMatrix *self,
+                           const char   *user_id)
+{
+  g_return_val_if_fail (CHATTY_IS_MATRIX (self), FALSE);
+
+  return cm_matrix_has_client_with_id (self->cm_matrix, user_id);
+}
diff --git a/src/matrix/chatty-matrix.h b/src/matrix/chatty-matrix.h
index 3cbe9acc779eadcb02a097a5cbf549792149eceb..7ec4bc529967b20d830d18ea485063b673a012cc 100644
--- a/src/matrix/chatty-matrix.h
+++ b/src/matrix/chatty-matrix.h
@@ -12,6 +12,8 @@
 #pragma once
 
 #include <glib-object.h>
+#define CMATRIX_USE_EXPERIMENTAL_API
+#include "cmatrix.h"
 
 #include "chatty-history.h"
 
@@ -21,9 +23,9 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (ChattyMatrix, chatty_matrix, CHATTY, MATRIX, GObject)
 
-ChattyMatrix  *chatty_matrix_new                     (ChattyHistory  *history,
-                                                      gboolean        disable_auto_login);
-void           chatty_matrix_load                    (ChattyMatrix   *self);
+ChattyMatrix   *chatty_matrix_new                     (gboolean        disable_auto_login);
+gboolean        chatty_matrix_is_enabled              (ChattyMatrix   *self);
+void            chatty_matrix_load                    (ChattyMatrix   *self);
 GListModel     *chatty_matrix_get_account_list       (ChattyMatrix   *self);
 GListModel     *chatty_matrix_get_chat_list          (ChattyMatrix   *self);
 void            chatty_matrix_delete_account_async   (ChattyMatrix   *self,
@@ -48,5 +50,8 @@ ChattyChat     *chatty_matrix_find_chat_with_name    (ChattyMatrix   *self,
                                                       ChattyProtocol  protocol,
                                                       const char     *account_id,
                                                       const char     *chat_id);
+CmClient       *chatty_matrix_client_new             (ChattyMatrix   *self);
+gboolean        chatty_matrix_has_user_id            (ChattyMatrix   *self,
+                                                      const char     *user_id);
 
 G_END_DECLS
diff --git a/src/matrix/matrix-api.c b/src/matrix/matrix-api.c
deleted file mode 100644
index 2a37adab8f322be95d95d05966432fddcad05325..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-api.c
+++ /dev/null
@@ -1,2561 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-api.c
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#define G_LOG_DOMAIN "chatty-matrix-api"
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
-
-#include <libsoup/soup.h>
-#include <json-glib/json-glib.h>
-#include <olm/olm.h>
-#include <sys/random.h>
-
-#include "chatty-chat.h"
-#include "chatty-ma-buddy.h"
-#include "matrix-enums.h"
-#include "matrix-utils.h"
-#include "matrix-api.h"
-#include "matrix-net.h"
-#include "chatty-log.h"
-
-/**
- * SECTION: chatty-api
- * @title: MatrixApi
- * @short_description: The Matrix HTTP API.
- * @include: "chatty-api.h"
- *
- * This class handles all communications with Matrix server
- * user REST APIs.
- */
-
-#define URI_REQUEST_TIMEOUT 60    /* seconds */
-#define SYNC_TIMEOUT        30000 /* milliseconds */
-#define TYPING_TIMEOUT      10000 /* milliseconds */
-#define KEY_TIMEOUT         10000 /* milliseconds */
-
-struct _MatrixApi
-{
-  GObject         parent_instance;
-
-   /* The username used to log in.  This can be different from
-    * the @username as this can be an email, phone number, etc. */
-  char           *login_username;
-  char           *username;
-  char           *password;
-  char           *homeserver;
-  char           *device_id;
-  char           *access_token;
-  char           *key;
-
-  MatrixEnc      *matrix_enc;
-  MatrixNet      *matrix_net;
-  GSocketAddress *gaddress;
-
-  /* Executed for every request response */
-  MatrixCallback  callback;
-  gpointer        cb_object;
-  GCancellable   *cancellable;
-  char           *next_batch;
-  char           *filter_id;
-  MatrixAction    action;
-
-  /* for sending events, incremented for each event */
-  int             event_id;
-
-  guint           full_state_loaded : 1;
-  guint           is_sync : 1;
-  /* Set when error occurs with sync enabled */
-  guint           sync_failed : 1;
-  guint           homeserver_verified : 1;
-  guint           login_success : 1;
-  guint           room_list_loaded : 1;
-  /* Set when @self has tried connecting the network atleast once */
-  guint           has_tried_connecting : 1;
-
-  guint           resync_id;
-};
-
-G_DEFINE_TYPE (MatrixApi, matrix_api, G_TYPE_OBJECT)
-
-static void matrix_verify_homeserver (MatrixApi *self);
-static void matrix_login             (MatrixApi *self);
-static void matrix_upload_key        (MatrixApi *self);
-static void matrix_start_sync        (MatrixApi *self);
-static void matrix_take_red_pill     (MatrixApi *self);
-static gboolean handle_common_errors (MatrixApi *self,
-                                      GError    *error);
-
-static void
-api_set_string_value (char       **strp,
-                      const char  *value)
-{
-  g_assert (strp);
-
-  if (value) {
-    g_free (*strp);
-    *strp = g_strdup (value);
-  }
-}
-
-static void
-api_verify_homeserver_cb (GObject      *obj,
-                          GAsyncResult *result,
-                          gpointer      user_data)
-{
-  MatrixApi *self = user_data;
-  g_autoptr(GError) error = NULL;
-  gboolean success;
-
-  g_assert (MATRIX_IS_API (self));
-
-  success = matrix_utils_verify_homeserver_finish (result, &error);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    return;
-
-  if (!self->gaddress)
-    self->gaddress = g_object_steal_data (G_OBJECT (result), "address");
-  self->has_tried_connecting = TRUE;
-
-  /* Since GTask can't have timeout, We cancel the cancellable to fake timeout */
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) {
-    g_clear_object (&self->cancellable);
-    self->cancellable = g_cancellable_new ();
-  }
-
-  if (handle_common_errors (self, error))
-    return;
-
-  if (error) {
-    CHATTY_TRACE_MSG ("Error verifying home server: %s", error->message);
-    self->callback (self->cb_object, self, MATRIX_VERIFY_HOMESERVER, NULL, error);
-    return;
-  }
-
-  if (success) {
-    self->homeserver_verified = TRUE;
-    matrix_start_sync (self);
-  } else {
-    error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, "Failed to verify homeserver");
-    self->callback (self->cb_object, self, self->action, NULL, error);
-  }
-}
-
-static void
-api_get_homeserver_cb (gpointer      object,
-                       GAsyncResult *result,
-                       gpointer      user_data)
-{
-  MatrixApi *self = user_data;
-  g_autoptr(GError) error = NULL;
-  char *homeserver;
-
-  g_assert (MATRIX_IS_API (self));
-
-  homeserver = matrix_utils_get_homeserver_finish (result, &error);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    return;
-
-  CHATTY_TRACE_MSG ("Get home server, has-error: %d, home server: %s",
-                    !error, homeserver);
-
-  if (!self->gaddress)
-    self->gaddress = g_object_steal_data (G_OBJECT (result), "address");
-  self->has_tried_connecting = TRUE;
-
-  if (!homeserver) {
-    self->sync_failed = TRUE;
-    self->callback (self->cb_object, self, self->action, NULL, error);
-
-    return;
-  }
-
-  matrix_api_set_homeserver (self, homeserver);
-  matrix_verify_homeserver (self);
-}
-
-static void
-api_send_message_cb (GObject      *obj,
-                     GAsyncResult *result,
-                     gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  ChattyMessage *message;
-  GError *error = NULL;
-  char *event_id;
-  int retry_after;
-
-  g_assert (G_IS_TASK (task));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-  retry_after = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (result), "retry-after"));
-  g_object_set_data (G_OBJECT (task), "retry-after", GINT_TO_POINTER (retry_after));
-
-  message = g_object_get_data (G_OBJECT (task), "message");
-  event_id = g_object_get_data (G_OBJECT (task), "event-id");
-
-  g_debug ("Sending message %s. event-id: %s, retry-after: %d",
-           CHATTY_LOG_SUCESS (!error), event_id, retry_after);
-
-  if (error) {
-    g_debug ("Error sending message: %s", error->message);
-    chatty_message_set_status (message, CHATTY_STATUS_SENDING_FAILED, 0);
-    g_task_return_error (task, error);
-  } else {
-    chatty_message_set_status (message, CHATTY_STATUS_SENT, 0);
-    g_task_return_boolean (task, !!object);
-  }
-}
-
-static void
-api_get_file_cb (GObject      *object,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  GError *error = NULL;
-  gboolean status;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  status = matrix_net_get_file_finish (self->matrix_net, result, &error);
-
-  if (error) {
-    g_task_return_error (task, error);
-
-    return;
-  }
-
-  g_task_return_boolean (task, status);
-}
-
-static void
-matrix_send_typing_cb (GObject      *obj,
-                       GAsyncResult *result,
-                       gpointer      user_data)
-{
-  g_autoptr(JsonObject) object = NULL;
-  g_autoptr(GError) error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error)
-    g_warning ("Error set typing: %s", error->message);
-}
-
-
-static void
-api_set_read_marker_cb (GObject      *obj,
-                        GAsyncResult *result,
-                        gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_TRACE_MSG ("Mark as read. success: %d", !error);
-
-  if (error) {
-    g_debug ("Error setting read marker: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_boolean (task, TRUE);
-  }
-}
-
-static void
-api_upload_group_keys_cb (GObject      *obj,
-                          GAsyncResult *result,
-                          gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    g_debug ("Error uploading group keys: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_boolean (task, TRUE);
-  }
-}
-
-static void
-matrix_get_room_state_cb (GObject      *obj,
-                          GAsyncResult *result,
-                          gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonArray *array;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  array = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    g_debug ("Error getting room state: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, array, (GDestroyNotify)json_array_unref);
-  }
-}
-
-static void
-matrix_get_room_name_cb (GObject      *obj,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonObject *object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      CHATTY_TRACE_MSG ("Error getting room name: %s", error->message);
-
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref);
-  }
-}
-
-static void
-matrix_get_room_encryption_cb (GObject      *obj,
-                               GAsyncResult *result,
-                               gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  const char *encryption;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error &&
-      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
-      !g_error_matches (error, MATRIX_ERROR, M_NOT_FOUND))
-    g_warning ("Error loading encryption state: %s", error->message);
-
-  encryption = matrix_utils_json_object_get_string (object, "algorithm");
-  g_task_return_pointer (task, g_strdup (encryption), g_free);
-}
-
-static void
-matrix_set_room_encryption_cb (GObject      *obj,
-                               GAsyncResult *result,
-                               gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  const char *event;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error &&
-      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
-      !g_error_matches (error, MATRIX_ERROR, M_NOT_FOUND))
-    g_warning ("Error setting encryption: %s", error->message);
-
-  event = matrix_utils_json_object_get_string (object, "event_id");
-  g_task_return_boolean (task, !!event);
-}
-
-static void
-matrix_get_members_cb (GObject      *obj,
-                       GAsyncResult *result,
-                       gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonObject *object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    g_debug ("Error getting members: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref);
-  }
-}
-
-static void
-matrix_get_messages_cb (GObject      *obj,
-                        GAsyncResult *result,
-                        gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonObject *object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    g_warning ("Error getting members: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref);
-  }
-}
-
-static void
-matrix_keys_query_cb (GObject      *obj,
-                      GAsyncResult *result,
-                      gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonObject *object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_TRACE_MSG ("Query key complete. success: %d", !error);
-
-  if (error) {
-    g_debug ("Error key query: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref);
-  }
-}
-
-static void
-matrix_keys_claim_cb (GObject      *obj,
-                      GAsyncResult *result,
-                      gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonObject *object = NULL;
-  GError *error = NULL;
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    g_debug ("Error key query: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref);
-  }
-}
-
-static gboolean
-schedule_resync (gpointer user_data)
-{
-  MatrixApi *self = user_data;
-  gboolean sync_now;
-
-  g_assert (MATRIX_IS_API (self));
-  self->resync_id = 0;
-
-  sync_now = matrix_api_can_connect (self);
-  CHATTY_TRACE (self->username, "Schedule sync. sync now: %d, user: ", sync_now);
-
-  if (sync_now)
-    matrix_start_sync (self);
-
-  return G_SOURCE_REMOVE;
-}
-
-/*
- * Handle Self fixable errors.
- *
- * Returns: %TRUE if @error was handled.
- * %FALSE otherwise
- */
-static gboolean
-handle_common_errors (MatrixApi *self,
-                      GError    *error)
-{
-  if (!error)
-    return FALSE;
-
-  CHATTY_TRACE_MSG ("Error: %s", error->message);
-
-  if (g_error_matches (error, MATRIX_ERROR, M_UNKNOWN_TOKEN)
-      && self->password) {
-    CHATTY_TRACE (self->username ? self->username : self->login_username, "Re-logging in ");
-    self->login_success = FALSE;
-    self->room_list_loaded = FALSE;
-    g_clear_pointer (&self->access_token, matrix_utils_free_buffer);
-    matrix_enc_set_details (self->matrix_enc, NULL, NULL);
-    self->callback (self->cb_object, self, MATRIX_ACCESS_TOKEN_LOGIN, NULL, NULL);
-    matrix_start_sync (self);
-
-    return TRUE;
-  }
-
-  /*
-   * The G_RESOLVER_ERROR may be suggesting that the hostname is wrong, but we don't
-   * know if it's network/DNS/Proxy error. So keep retrying.
-   */
-  if ((error->domain == SOUP_HTTP_ERROR &&
-       error->code <= SOUP_STATUS_TLS_FAILED &&
-       error->code > SOUP_STATUS_CANCELLED) ||
-      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) ||
-      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) ||
-      error->domain == G_RESOLVER_ERROR ||
-      error->domain == JSON_PARSER_ERROR) {
-
-    if (matrix_api_can_connect (self)) {
-      g_clear_handle_id (&self->resync_id, g_source_remove);
-
-      self->sync_failed = TRUE;
-      self->callback (self->cb_object, self, MATRIX_RED_PILL, NULL, NULL);
-      CHATTY_TRACE (self->username, "Schedule sync for user ");
-      self->resync_id = g_timeout_add_seconds (URI_REQUEST_TIMEOUT,
-                                               schedule_resync, self);
-      return TRUE;
-    }
-  }
-
-  return FALSE;
-}
-
-static gboolean
-handle_one_time_keys (MatrixApi  *self,
-                      JsonObject *object)
-{
-  size_t count, limit;
-
-  g_assert (MATRIX_IS_API (self));
-
-  if (!object)
-    return FALSE;
-
-  count = matrix_utils_json_object_get_int (object, "signed_curve25519");
-  limit = matrix_enc_max_one_time_keys (self->matrix_enc) / 2;
-
-  /* If we don't have enough onetime keys add some */
-  if (count < limit) {
-    CHATTY_TRACE_MSG ("generating %" G_GSIZE_FORMAT " onetime keys", limit - count);
-    matrix_enc_create_one_time_keys (self->matrix_enc, limit - count);
-
-    if (!self->key)
-      self->key = matrix_enc_get_one_time_keys_json (self->matrix_enc);
-    matrix_upload_key (self);
-
-    return TRUE;
-  }
-
-  return FALSE;
-}
-
-static void
-api_upload_filter_cb (GObject      *object,
-                      GAsyncResult *result,
-                      gpointer      user_data)
-{
-  g_autoptr(MatrixApi) self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (G_IS_TASK (result));
-
-  root = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    return;
-
-  CHATTY_DEBUG (self->username, "Uploading filter %s, user",
-                CHATTY_LOG_SUCESS (!error));
-
-  if (handle_common_errors (self, error))
-    return;
-
-  self->filter_id = g_strdup (matrix_utils_json_object_get_string (root, "filter_id"));
-
-  if (!self->filter_id)
-    self->filter_id = g_strdup ("");
-  matrix_start_sync (self);
-}
-
-static void
-matrix_upload_filter (MatrixApi *self)
-{
-  g_autoptr(GError) error = NULL;
-  g_autoptr(GBytes) data = NULL;
-  const char *data_str = NULL;
-  gsize size;
-
-  data = g_resources_lookup_data ("/sm/puri/Chatty/matrix-filter.json", 0, &error);
-
-  if (error)
-    g_warning ("Error getting filter file: %s", error->message);
-  else if (data)
-    data_str = g_bytes_get_data (data, &size);
-
-  if (!data || !data_str || !size) {
-    self->filter_id = g_strdup ("");
-    matrix_start_sync (self);
-  } else {
-    g_autofree char *uri = NULL;
-    g_autoptr(JsonParser) parser = NULL;
-    JsonObject *filter = NULL;
-    JsonNode *root = NULL;
-
-    CHATTY_DEBUG (self->username, "Uploading filter, user:");
-
-    parser = json_parser_new ();
-    json_parser_load_from_data (parser, data_str, size, &error);
-
-    if (error)
-      g_warning ("Error parsing filter file: %s", error->message);
-
-    if (!error)
-      root = json_parser_get_root (parser);
-
-    if (root)
-      filter = json_node_get_object (root);
-
-    if (error || !root || !filter) {
-      if (error)
-        g_warning ("Error getting filter file: %s", error->message);
-
-      self->filter_id = g_strdup ("");
-      matrix_start_sync (self);
-
-      return;
-    }
-
-    uri = g_strconcat ("/_matrix/client/r0/user/", self->username, "/filter", NULL);
-    matrix_net_send_json_async (self->matrix_net, 2, json_object_ref (filter),
-                                uri, SOUP_METHOD_POST,
-                                NULL, self->cancellable, api_upload_filter_cb,
-                                g_object_ref (self));
-  }
-}
-
-static void
-matrix_login_cb (GObject      *obj,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  g_autoptr(MatrixApi) self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GError) error = NULL;
-  JsonObject *object = NULL;
-  const char *value;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (G_IS_TASK (result));
-
-  root = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    return;
-
-  CHATTY_TRACE_MSG ("login %s", CHATTY_LOG_SUCESS (!error));
-
-  if (error) {
-    self->sync_failed = TRUE;
-    /* use a better code to inform invalid password */
-    if (error->code == M_FORBIDDEN)
-      error->code = M_BAD_PASSWORD;
-    self->callback (self->cb_object, self, MATRIX_PASSWORD_LOGIN, NULL, error);
-    g_debug ("Error logging in: %s", error->message);
-    return;
-  }
-
-  self->login_success = TRUE;
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */
-  value = matrix_utils_json_object_get_string (root, "user_id");
-  api_set_string_value (&self->username, value);
-
-  value = matrix_utils_json_object_get_string (root, "access_token");
-  matrix_utils_free_buffer (self->access_token);
-  self->access_token = g_strdup (value);
-  matrix_net_set_access_token (self->matrix_net, self->access_token);
-
-  value = matrix_utils_json_object_get_string (root, "device_id");
-  api_set_string_value (&self->device_id, value);
-
-  object = matrix_utils_json_object_get_object (root, "well_known");
-  object = matrix_utils_json_object_get_object (object, "m.homeserver");
-  value = matrix_utils_json_object_get_string (object, "base_url");
-  matrix_api_set_homeserver (self, value);
-
-  matrix_enc_set_details (self->matrix_enc, self->username, self->device_id);
-  g_free (self->key);
-  self->key = matrix_enc_get_device_keys_json (self->matrix_enc);
-
-  self->callback (self->cb_object, self, MATRIX_PASSWORD_LOGIN, NULL, NULL);
-  matrix_start_sync (self);
-}
-
-static void
-matrix_upload_key_cb (GObject      *obj,
-                      GAsyncResult *result,
-                      gpointer      user_data)
-{
-  g_autoptr(MatrixApi) self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GError) error = NULL;
-  JsonObject *object = NULL;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (G_IS_TASK (result));
-
-  root = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    self->sync_failed = TRUE;
-    self->callback (self->cb_object, self, MATRIX_UPLOAD_KEY, NULL, error);
-    g_debug ("Error uploading key: %s", error->message);
-    return;
-  }
-
-  self->callback (self->cb_object, self, MATRIX_UPLOAD_KEY, root, NULL);
-
-  object = matrix_utils_json_object_get_object (root, "one_time_key_counts");
-  CHATTY_TRACE_MSG ("Uploaded %ld keys",
-                    matrix_utils_json_object_get_int (object, "signed_curve25519"));
-
-  if (!handle_one_time_keys (self, object) &&
-       self->action != MATRIX_RED_PILL)
-    matrix_take_red_pill (self);
-}
-
-/* sync callback */
-static void
-matrix_take_red_pill_cb (GObject      *obj,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  g_autoptr(MatrixApi) self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GError) error = NULL;
-  JsonObject *object = NULL;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (G_IS_TASK (result));
-
-  root = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    return;
-
-  if (!self->next_batch || error || !self->full_state_loaded)
-    g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE, "sync %s, full-state: %d, next-batch: %s",
-           CHATTY_LOG_SUCESS (!error), !self->full_state_loaded, self->next_batch);
-
-  if (handle_common_errors (self, error))
-    return;
-
-  if (error) {
-    self->sync_failed = TRUE;
-    self->callback (self->cb_object, self, self->action, NULL, error);
-    g_debug ("Error syncing with time %s: %s", self->next_batch, error->message);
-    return;
-  }
-
-  self->login_success = TRUE;
-
-  object = matrix_utils_json_object_get_object (root, "device_one_time_keys_count");
-  handle_one_time_keys (self, object);
-
-  /* XXX: For some reason full state isn't loaded unless we have passed “next_batch”.
-   * So, if we haven’t, don’t mark so.
-   */
-  if (self->next_batch)
-    self->full_state_loaded = TRUE;
-
-  g_free (self->next_batch);
-  self->next_batch = g_strdup (matrix_utils_json_object_get_string (root, "next_batch"));
-
-  self->callback (self->cb_object, self, self->action, root, NULL);
-
-  /* Repeat */
-  matrix_take_red_pill (self);
-}
-
-static void
-matrix_verify_homeserver (MatrixApi *self)
-{
-  g_assert (MATRIX_IS_API (self));
-  g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE,
-         "verifying homeserver %s", self->homeserver);
-
-  self->action = MATRIX_VERIFY_HOMESERVER;
-  matrix_utils_verify_homeserver_async (self->homeserver, URI_REQUEST_TIMEOUT,
-                                        self->cancellable,
-                                        api_verify_homeserver_cb, self);
-}
-
-static void
-matrix_login (MatrixApi *self)
-{
-  JsonObject *object, *child;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (self->login_username);
-  g_assert (self->homeserver);
-  g_assert (!self->access_token);
-  g_assert (self->password && *self->password);
-
-  CHATTY_TRACE (self->login_username, "logging on server %s, account", self->homeserver);
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */
-  object = json_object_new ();
-  json_object_set_string_member (object, "type", "m.login.password");
-  json_object_set_string_member (object, "password", self->password);
-  json_object_set_string_member (object, "initial_device_display_name", "Chatty");
-
-  child = json_object_new ();
-
-  if (chatty_utils_username_is_valid (self->login_username, CHATTY_PROTOCOL_EMAIL)) {
-    json_object_set_string_member (child, "type", "m.id.thirdparty");
-    json_object_set_string_member (child, "medium", "email");
-    json_object_set_string_member (child, "address", self->login_username);
-  } else {
-    json_object_set_string_member (child, "type", "m.id.user");
-    json_object_set_string_member (child, "user", self->login_username);
-  }
-
-  json_object_set_object_member (object, "identifier", child);
-
-  matrix_net_send_json_async (self->matrix_net, 2, object,
-                              "/_matrix/client/r0/login", SOUP_METHOD_POST,
-                              NULL, self->cancellable, matrix_login_cb,
-                              g_object_ref (self));
-}
-
-static void
-matrix_upload_key (MatrixApi *self)
-{
-  char *key;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (self->key);
-
-  key = g_steal_pointer (&self->key);
-
-  matrix_net_send_data_async (self->matrix_net, 2, key, strlen (key),
-                              "/_matrix/client/r0/keys/upload", SOUP_METHOD_POST,
-                              NULL, self->cancellable, matrix_upload_key_cb,
-                              g_object_ref (self));
-}
-
-static void
-get_joined_rooms_cb (GObject      *obj,
-                     GAsyncResult *result,
-                     gpointer      user_data)
-{
-  g_autoptr(MatrixApi) self = user_data;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GError) error = NULL;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (G_IS_TASK (result));
-
-  root = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    return;
-
-  CHATTY_TRACE_MSG ("Getting joined rooms %s", CHATTY_LOG_SUCESS (!error));
-
-  if (handle_common_errors (self, error))
-    return;
-
-  self->callback (self->cb_object, self, MATRIX_GET_JOINED_ROOMS, root, error);
-
-  if (!error) {
-    self->room_list_loaded = TRUE;
-    matrix_start_sync (self);
-  }
-}
-
-static void
-matrix_get_joined_rooms (MatrixApi *self)
-{
-  g_assert (MATRIX_IS_API (self));
-  g_assert (!self->room_list_loaded);
-
-  CHATTY_TRACE_MSG ("Getting joined rooms");
-  matrix_net_send_json_async (self->matrix_net, 0, NULL,
-                              "/_matrix/client/r0/joined_rooms", SOUP_METHOD_GET,
-                              NULL, self->cancellable, get_joined_rooms_cb,
-                              g_object_ref (self));
-}
-
-static void
-matrix_start_sync (MatrixApi *self)
-{
-  g_assert (MATRIX_IS_API (self));
-
-  self->is_sync = TRUE;
-  self->sync_failed = FALSE;
-  g_clear_handle_id (&self->resync_id, g_source_remove);
-
-  if (!self->homeserver) {
-    self->action = MATRIX_GET_HOMESERVER;
-    if (!matrix_utils_username_is_complete (self->login_username)) {
-      g_autoptr(GError) error = NULL;
-
-      g_debug ("Error: No Homeserver provided");
-      self->sync_failed = TRUE;
-      error = g_error_new (MATRIX_ERROR, M_NO_HOME_SERVER, "No Homeserver provided");
-      self->callback (self->cb_object, self, self->action, NULL, error);
-    } else {
-      g_debug ("Fetching home server details from username");
-      matrix_utils_get_homeserver_async (self->login_username, URI_REQUEST_TIMEOUT, self->cancellable,
-                                         (GAsyncReadyCallback)api_get_homeserver_cb,
-                                         self);
-    }
-  } else if (!self->homeserver_verified) {
-    matrix_verify_homeserver (self);
-  } else if (!self->password){
-    g_autoptr(GError) error = NULL;
-
-    error = g_error_new (MATRIX_ERROR, M_BAD_PASSWORD, "Empty password");
-    self->callback (self->cb_object, self, MATRIX_PASSWORD_LOGIN, NULL, error);
-  } else if (!self->access_token) {
-    matrix_login (self);
-  } else if (!self->filter_id){
-    matrix_upload_filter (self);
-  } else if (!self->room_list_loaded) {
-    matrix_get_joined_rooms (self);
-  } else {
-    matrix_take_red_pill (self);
-  }
-}
-
-static void
-matrix_take_red_pill (MatrixApi *self)
-{
-  GHashTable *query;
-
-  g_assert (MATRIX_IS_API (self));
-
-  self->action = MATRIX_RED_PILL;
-  query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
-
-  if (self->login_success)
-    g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT));
-  else
-    g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT / 1000));
-
-  if (self->next_batch)
-    g_hash_table_insert (query, g_strdup ("since"), g_strdup (self->next_batch));
-  if (!self->full_state_loaded)
-    g_hash_table_insert (query, g_strdup ("full_state"), g_strdup ("true"));
-
-  matrix_net_send_json_async (self->matrix_net, 2, NULL,
-                              "/_matrix/client/r0/sync", SOUP_METHOD_GET,
-                              query, self->cancellable, matrix_take_red_pill_cb,
-                              g_object_ref (self));
-}
-
-static void
-matrix_api_finalize (GObject *object)
-{
-  MatrixApi *self = (MatrixApi *)object;
-
-  if (self->cancellable)
-    g_cancellable_cancel (self->cancellable);
-  g_clear_object (&self->cancellable);
-
-  g_clear_object (&self->matrix_enc);
-  g_clear_object (&self->matrix_net);
-  g_clear_object (&self->gaddress);
-
-  g_clear_handle_id (&self->resync_id, g_source_remove);
-
-  g_free (self->username);
-  g_free (self->login_username);
-  g_free (self->homeserver);
-  g_free (self->device_id);
-  g_free (self->filter_id);
-  matrix_utils_free_buffer (self->password);
-  matrix_utils_free_buffer (self->access_token);
-
-  g_free (self->next_batch);
-
-  G_OBJECT_CLASS (matrix_api_parent_class)->finalize (object);
-}
-
-static void
-matrix_api_class_init (MatrixApiClass *klass)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->finalize = matrix_api_finalize;
-}
-
-
-static void
-matrix_api_init (MatrixApi *self)
-{
-  self->cancellable = g_cancellable_new ();
-  self->matrix_net = matrix_net_new ();
-}
-
-/**
- * matrix_api_new:
- * @username: (nullable): A valid matrix user id
- *
- * Create a new #MatrixApi for @username.  For the
- * #MatrixApi to be usable password/access token
- * and sync_callback should be set.
- *
- * If @username is not in full form (ie,
- * @user:example.com) or an email or phone
- * number, homeserver should be set with
- * matrix_api_set_homeserver()
- *
- * Returns: (transfer full): A new #MatrixApi.
- * Free with g_object_unref().
- */
-MatrixApi *
-matrix_api_new (const char *username)
-{
-  MatrixApi *self;
-
-  self = g_object_new (MATRIX_TYPE_API, NULL);
-  self->login_username = g_strdup (username);
-
-  return self;
-}
-
-void
-matrix_api_set_enc (MatrixApi *self,
-                    MatrixEnc *enc)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (MATRIX_IS_ENC (enc));
-  g_return_if_fail (!self->matrix_enc);
-
-  g_set_object (&self->matrix_enc, enc);
-
-  if (self->username && self->device_id)
-    matrix_enc_set_details (self->matrix_enc, self->username, self->device_id);
-}
-
-/**
- * matrix_api_can_connect:
- * @self: A #MatrixApi
- *
- * Check if @self can be connected to homeserver with current
- * network state.  This function is a bit dumb: returning
- * %TRUE shall not ensure that the @self is connectable.
- * But if %FALSE is returned, @self shall not be
- * able to connect.
- */
-gboolean
-matrix_api_can_connect (MatrixApi *self)
-{
-  GNetworkMonitor *nm;
-  GInetAddress *inet;
-
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-
-  /* If never tried, assume we can connect */
-  if (!self->has_tried_connecting)
-    return TRUE;
-
-  nm = g_network_monitor_get_default ();
-
-  if (!self->gaddress || !G_IS_INET_SOCKET_ADDRESS (self->gaddress))
-    goto end;
-
-  inet = g_inet_socket_address_get_address ((GInetSocketAddress *)self->gaddress);
-
-  if (g_inet_address_get_is_loopback (inet) ||
-      g_inet_address_get_is_site_local (inet))
-    return g_network_monitor_can_reach (nm, G_SOCKET_CONNECTABLE (self->gaddress), NULL, NULL);
-
- end:
-  /* Distributions may advertise to have full network support event
-   * when connected only to local network, so this isn't always right */
-  return g_network_monitor_get_connectivity (nm) == G_NETWORK_CONNECTIVITY_FULL;
-}
-
-/**
- * matrix_api_get_username:
- * @self: A #MatrixApi
- *
- * Get the username of @self.  This will be a fully
- * qualified Matrix ID (eg: @user:example.com) if
- * @self has succeeded in synchronizing with the
- * server, otherwise NULL is returned
- *
- * Please note that this can return a different value
- * than the one set with matrix_api_set_login_username().
- *
- * Returns: (nullable): The matrix username.
- */
-const char *
-matrix_api_get_username (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-
-  return self->username;
-}
-
-void
-matrix_api_set_username (MatrixApi  *self,
-                         const char *username)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!self->username);
-
-  if (chatty_utils_username_is_valid (username, CHATTY_PROTOCOL_MATRIX))
-    self->username = g_strdup (username);
-}
-
-/**
- * matrix_api_get_login_username:
- * @self: A #MatrixApi
- *
- * Get the username as set with
- * matrix_api_set_login_username().
- *
- * Returns: The matrix username.
- */
-const char *
-matrix_api_get_login_username (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), "");
-
-  return self->login_username;
-}
-
-/**
- * matrix_api_set_login_username:
- * @self: A #MatrixApi
- * @userame: The usernamed to use for login
- *
- * Set the username of @self.  This is not required to
- * be a fully qualified Matrix ID like @user:example.com
- * and can also be an email ID or phone number.
- *
- * username can be set only once.
- */
-void
-matrix_api_set_login_username (MatrixApi  *self,
-                               const char *username)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!self->login_username);
-
-  self->login_username = g_strdup (username);
-}
-
-/**
- * matrix_api_get_password:
- * @self: A #MatrixApi
- *
- * Get the password of @self.
- *
- * Returns: (nullable): The matrix username.
- */
-const char *
-matrix_api_get_password (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-
-  return self->password;
-}
-
-/**
- * matrix_api_set_password:
- * @self: A #MatrixApi
- * @password: A valid password string
- *
- * Set the password for @self.
- */
-void
-matrix_api_set_password (MatrixApi  *self,
-                         const char *password)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  if (!password || !*password)
-    return;
-
-  matrix_utils_free_buffer (self->password);
-  self->password = g_strdup (password);
-}
-
-/**
- * matrix_api_set_sync_callback:
- * @self: A #MatrixApi
- * @callback: A #MatriCallback
- * @object: A #GObject
- *
- * Set sync callback. It’s allowed to set callback
- * only once.
- *
- * @object should be a #GObject (derived) object.
- *
- * callback shall run as `callback(@object, ...)`
- *
- * @callback shall be run for every event that’s worth
- * informing (Say, the callback won’t be run if the
- * sync response is empty).
- *
- * The @callback may run with a %NULL #GAsyncResult
- * argument.  Check the sync state before handling
- * the #GAsyncResult. See matrix_api_get_sync_state().
- */
-void
-matrix_api_set_sync_callback (MatrixApi      *self,
-                              MatrixCallback  callback,
-                              gpointer        object)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (callback);
-  g_return_if_fail (G_IS_OBJECT (object));
-  g_return_if_fail (!self->callback);
-
-  self->callback = callback;
-  g_set_weak_pointer (&self->cb_object, object);
-}
-
-const char *
-matrix_api_get_homeserver (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-
-  return self->homeserver;
-}
-
-void
-matrix_api_set_homeserver (MatrixApi  *self,
-                           const char *homeserver)
-{
-  g_autoptr(SoupURI) uri = NULL;
-  GString *host;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  uri = soup_uri_new (homeserver);
-  if (!homeserver || !uri ||
-      !SOUP_URI_VALID_FOR_HTTP (uri))
-    return;
-
-  host = g_string_new (NULL);
-  g_string_append (host, soup_uri_get_scheme (uri));
-  g_string_append (host, "://");
-  g_string_append (host, uri->host);
-  if (!soup_uri_uses_default_port (uri))
-    g_string_append_printf (host, ":%d", soup_uri_get_port (uri));
-
-  g_free (self->homeserver);
-  self->homeserver = g_string_free (host, FALSE);
-
-  matrix_net_set_homeserver (self->matrix_net, self->homeserver);
-
-  if (self->is_sync &&
-      self->sync_failed &&
-      self->action == MATRIX_GET_HOMESERVER) {
-    self->sync_failed = FALSE;
-    matrix_verify_homeserver (self);
-  }
-}
-
-/**
- * matrix_api_get_device_id:
- * @self: A #MatrixApi
- *
- * Get the device ID of @self.  If the
- * account login succeeded, the device
- * ID provided by the server is returned.
- * Otherwise, the one set with @self is
- * returned.
- *
- * Please not that the user login is done
- * only if @self has no access-token set,
- * or if the acces-token is invalid.
- *
- * Returns: (nullable): The Device ID.
- */
-const char *
-matrix_api_get_device_id (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-
-  return self->device_id;
-}
-
-const char *
-matrix_api_get_access_token (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-
-  return self->access_token;
-}
-
-void
-matrix_api_set_access_token (MatrixApi  *self,
-                             const char *access_token,
-                             const char *device_id)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  g_clear_pointer (&self->access_token, matrix_utils_free_buffer);
-  g_clear_pointer (&self->device_id, g_free);
-
-  if (!access_token || !device_id)
-    return;
-
-  self->access_token = g_strdup (access_token);
-  self->device_id = g_strdup (device_id);
-  matrix_net_set_access_token (self->matrix_net, self->access_token);
-
-  if (self->matrix_enc && self->username)
-    matrix_enc_set_details (self->matrix_enc, self->username, self->device_id);
-}
-
-const char *
-matrix_api_get_next_batch (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-
-  return self->next_batch;
-}
-
-void
-matrix_api_set_next_batch (MatrixApi  *self,
-                           const char *next_batch)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!self->next_batch);
-
-  if (next_batch)
-    self->full_state_loaded = TRUE;
-
-  self->next_batch = g_strdup (next_batch);
-}
-
-/**
- * matrix_api_start_sync:
- * @self: A #MatrixApi
- *
- * Start synchronizing with the matrix server.
- *
- * If a sync process is already in progress
- * this function simply returns.
- *
- * The process is:
- *   1. Get home server (if required)
- *   2. Verify homeserver Server-Client API
- *   3. If access token set, start sync
- *   4. Else login with password
- */
-void
-matrix_api_start_sync (MatrixApi *self)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (self->callback);
-  g_return_if_fail (self->login_username);
-
-  if (self->is_sync && !self->sync_failed)
-    return;
-
-  if (g_cancellable_is_cancelled (self->cancellable)) {
-    g_object_unref (self->cancellable);
-    self->cancellable = g_cancellable_new ();
-  }
-
-  matrix_start_sync (self);
-}
-
-gboolean
-matrix_api_is_sync (MatrixApi *self)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-
-  return self->access_token && self->login_success &&
-    self->is_sync && !self->sync_failed;
-}
-
-void
-matrix_api_stop_sync (MatrixApi *self)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  g_clear_handle_id (&self->resync_id, g_source_remove);
-  g_cancellable_cancel (self->cancellable);
-  self->is_sync = FALSE;
-  self->sync_failed = FALSE;
-}
-
-void
-matrix_api_set_upload_key (MatrixApi *self,
-                           char      *key)
-{
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (key && *key);
-
-  g_free (self->key);
-  self->key = key;
-
-  if (self->is_sync && self->action == MATRIX_RED_PILL)
-    matrix_upload_key (self);
-}
-
-void
-matrix_api_set_typing (MatrixApi  *self,
-                       const char *room_id,
-                       gboolean    is_typing)
-{
-  g_autofree char *uri = NULL;
-  JsonObject *object;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  CHATTY_TRACE_MSG ("Update typing: %d", !!is_typing);
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-typing-userid */
-  object = json_object_new ();
-  json_object_set_boolean_member (object, "typing", !!is_typing);
-  if (is_typing)
-    json_object_set_int_member (object, "timeout", TYPING_TIMEOUT);
-
-  uri = g_strconcat (self->homeserver, "/_matrix/client/r0/rooms/",
-                     room_id, "/typing/", self->username, NULL);
-
-  matrix_net_send_json_async (self->matrix_net, 0, object, uri, SOUP_METHOD_PUT,
-                              NULL, self->cancellable, matrix_send_typing_cb, NULL);
-}
-
-void
-matrix_api_get_room_state_async (MatrixApi           *self,
-                                 const char          *room_id,
-                                 GAsyncReadyCallback  callback,
-                                 gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (room_id && *room_id);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state", NULL);
-  matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET,
-                              NULL, self->cancellable, matrix_get_room_state_cb, task);
-}
-
-JsonArray *
-matrix_api_get_room_state_finish (MatrixApi     *self,
-                                  GAsyncResult  *result,
-                                  GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-static void
-matrix_get_room_users_cb (GObject      *obj,
-                          GAsyncResult *result,
-                          gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  JsonObject *object;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      g_debug ("Error getting room members: %s", error->message);
-    g_task_return_error (task, error);
-  } else {
-    g_task_return_pointer (task, object, (GDestroyNotify)json_object_unref);
-  }
-}
-
-void
-matrix_api_get_room_users_async (MatrixApi           *self,
-                                 const char          *room_id,
-                                 GAsyncReadyCallback  callback,
-                                 gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (room_id && *room_id);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/joined_members", NULL);
-  matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET,
-                              NULL, self->cancellable, matrix_get_room_users_cb, task);
-}
-
-JsonObject *
-matrix_api_get_room_users_finish (MatrixApi     *self,
-                                  GAsyncResult  *result,
-                                  GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-void
-matrix_api_get_room_name_async (MatrixApi           *self,
-                                const char          *room_id,
-                                GAsyncReadyCallback  callback,
-                                gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (room_id && *room_id);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state/m.room.name", NULL);
-  matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET,
-                              NULL, self->cancellable, matrix_get_room_name_cb, task);
-}
-
-JsonObject *
-matrix_api_get_room_name_finish (MatrixApi     *self,
-                                 GAsyncResult  *result,
-                                 GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-void
-matrix_api_get_room_encryption_async (MatrixApi           *self,
-                                      const char          *room_id,
-                                      GAsyncReadyCallback  callback,
-                                      gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (room_id && *room_id);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state/m.room.encryption", NULL);
-  matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET,
-                              NULL, self->cancellable, matrix_get_room_encryption_cb, task);
-}
-
-char *
-matrix_api_get_room_encryption_finish (MatrixApi     *self,
-                                       GAsyncResult  *result,
-                                       GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-/**
- * matrix_api_set_room_encryption_async:
- * @self: A #MatrixApi
- * @room_id: The room id to set encryption for
- * @callback: A #GAsyncReadyCallback
- * @user_data: user data passed to @callback
- *
- * Calling this method shall enable encryption.
- * There is no way to disable encryption once
- * enabled.
- *
- * To get the result, finish the call with
- * matrix_api_set_room_encryption_finish()
- */
-void
-matrix_api_set_room_encryption_async (MatrixApi           *self,
-                                      const char          *room_id,
-                                      GAsyncReadyCallback  callback,
-                                      gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  JsonObject *object;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (room_id && *room_id);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-  object = json_object_new ();
-  json_object_set_string_member (object, "algorithm", ALGORITHM_MEGOLM);
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/state/m.room.encryption", NULL);
-  matrix_net_send_json_async (self->matrix_net, 2, object, uri, SOUP_METHOD_PUT,
-                              NULL, self->cancellable, matrix_set_room_encryption_cb, task);
-}
-
-gboolean
-matrix_api_set_room_encryption_finish (MatrixApi     *self,
-                                       GAsyncResult  *result,
-                                       GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_api_get_members_async (MatrixApi           *self,
-                              const char          *room_id,
-                              GAsyncReadyCallback  callback,
-                              gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GHashTable *query;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
-                                 (GDestroyNotify)matrix_utils_free_buffer);
-  g_hash_table_insert (query, g_strdup ("membership"), g_strdup ("join"));
-
-  if (self->next_batch)
-    g_hash_table_insert (query, g_strdup ("since"), g_strdup (self->next_batch));
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-members */
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/members", NULL);
-  matrix_net_send_json_async (self->matrix_net, -1, NULL, uri, SOUP_METHOD_GET,
-                              query, self->cancellable, matrix_get_members_cb, task);
-}
-
-JsonObject *
-matrix_api_get_members_finish (MatrixApi     *self,
-                               GAsyncResult  *result,
-                               GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-void
-matrix_api_load_prev_batch_async (MatrixApi           *self,
-                                  const char          *room_id,
-                                  char                *prev_batch,
-                                  char                *last_batch,
-                                  GAsyncReadyCallback  callback,
-                                  gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GHashTable *query;
-  GTask *task;
-
-  if (!prev_batch)
-    return;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  /* Create a query to get past 30 messages */
-  query = g_hash_table_new_full (g_str_hash, g_str_equal, free,
-                                 (GDestroyNotify)matrix_utils_free_buffer);
-  g_hash_table_insert (query, g_strdup ("from"), g_strdup (prev_batch));
-  g_hash_table_insert (query, g_strdup ("dir"), g_strdup ("b"));
-  g_hash_table_insert (query, g_strdup ("limit"), g_strdup ("30"));
-  if (last_batch)
-    g_hash_table_insert (query, g_strdup ("to"), g_strdup (last_batch));
-
-  CHATTY_TRACE_MSG ("Load prev-batch");
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-messages */
-  uri = g_strconcat ("/_matrix/client/r0/rooms/", room_id, "/messages", NULL);
-  matrix_net_send_json_async (self->matrix_net, 0, NULL, uri, SOUP_METHOD_GET,
-                              query, self->cancellable, matrix_get_messages_cb, task);
-}
-
-JsonObject *
-matrix_api_load_prev_batch_finish (MatrixApi     *self,
-                                   GAsyncResult  *result,
-                                   GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-/**
- * matrix_api_query_keys_async:
- * @self: A #MatrixApi
- * @member_list: A #GListModel of #ChattyMaBuddy
- * @token: (nullable): A 'since' token string
- * @callback: A #GAsyncReadyCallback
- * @user_data: user data passed to @callback
- *
- * Get identity keys of all devices in @member_list.
- * Pass in @token (obtained via the "since" in /sync)
- * if only the device changes since the corresponding
- * /sync is needed.
- *
- * Finish the call with matrix_api_query_keys_finish()
- * to get the result.
- */
-void
-matrix_api_query_keys_async (MatrixApi           *self,
-                             GListModel          *member_list,
-                             const char          *token,
-                             GAsyncReadyCallback  callback,
-                             gpointer             user_data)
-{
-  JsonObject *object, *child;
-  GTask *task;
-  guint n_items;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (G_IS_LIST_MODEL (member_list));
-  g_return_if_fail (g_list_model_get_item_type (member_list) == CHATTY_TYPE_MA_BUDDY);
-  g_return_if_fail (g_list_model_get_n_items (member_list) > 0);
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-query */
-  object = json_object_new ();
-  json_object_set_int_member (object, "timeout", KEY_TIMEOUT);
-  if (token)
-    json_object_set_string_member (object, "token", token);
-
-  n_items = g_list_model_get_n_items (member_list);
-  child = json_object_new ();
-
-  for (guint i = 0; i < n_items; i++) {
-    g_autoptr(ChattyMaBuddy) buddy = NULL;
-
-    buddy = g_list_model_get_item (member_list, i);
-    json_object_set_array_member (child,
-                                  chatty_item_get_username (CHATTY_ITEM (buddy)),
-                                  json_array_new ());
-  }
-
-  json_object_set_object_member (object, "device_keys", child);
-  CHATTY_TRACE_MSG ("Query keys of %u members", n_items);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  matrix_net_send_json_async (self->matrix_net, 0, object,
-                              "/_matrix/client/r0/keys/query", SOUP_METHOD_POST,
-                              NULL, self->cancellable, matrix_keys_query_cb, task);
-}
-
-JsonObject *
-matrix_api_query_keys_finish (MatrixApi    *self,
-                              GAsyncResult *result,
-                              GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-/**
- * matrix_api_claim_keys_async:
- * @self: A #MatrixApi
- * @member_list: A #GListModel of #ChattyMaBuddy
- * @callback: A #GAsyncReadyCallback
- * @user_data: user data passed to @callback
- *
- * Claim a key for all devices of @members_list
- */
-void
-matrix_api_claim_keys_async (MatrixApi           *self,
-                             GListModel          *member_list,
-                             GAsyncReadyCallback  callback,
-                             gpointer             user_data)
-{
-  JsonObject *object, *child;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (G_IS_LIST_MODEL (member_list));
-  g_return_if_fail (g_list_model_get_item_type (member_list) == CHATTY_TYPE_MA_BUDDY);
-  g_return_if_fail (g_list_model_get_n_items (member_list) > 0);
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-claim */
-  object = json_object_new ();
-  json_object_set_int_member (object, "timeout", KEY_TIMEOUT);
-
-  child = json_object_new ();
-
-  for (guint i = 0; i < g_list_model_get_n_items (member_list); i++) {
-    g_autoptr(ChattyMaBuddy) buddy = NULL;
-    JsonObject *key_json;
-
-    buddy = g_list_model_get_item (member_list, i);
-    key_json = chatty_ma_buddy_device_key_json (buddy);
-
-    if (key_json)
-      json_object_set_object_member (child,
-                                     chatty_item_get_username (CHATTY_ITEM (buddy)),
-                                     key_json);
-  }
-
-  json_object_set_object_member (object, "one_time_keys", child);
-  CHATTY_TRACE_MSG ("Claiming keys");
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  matrix_net_send_json_async (self->matrix_net, 0, object,
-                              "/_matrix/client/r0/keys/claim", SOUP_METHOD_POST,
-                              NULL, self->cancellable, matrix_keys_claim_cb, task);
-}
-
-JsonObject *
-matrix_api_claim_keys_finish (MatrixApi     *self,
-                              GAsyncResult  *result,
-                              GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-void
-matrix_api_get_file_async (MatrixApi             *self,
-                           ChattyMessage         *message,
-                           ChattyFileInfo        *file,
-                           GCancellable          *cancellable,
-                           GFileProgressCallback  progress_callback,
-                           GAsyncReadyCallback    callback,
-                           gpointer               user_data)
-{
-  g_autoptr(GTask) task = NULL;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!message || CHATTY_IS_MESSAGE (message));
-
-  if (message)
-    g_object_ref (message);
-
-  task = g_task_new (self, cancellable, callback, user_data);
-
-  g_object_set_data (G_OBJECT (task), "file", file);
-  g_object_set_data_full (G_OBJECT (task), "message", message, g_object_unref);
-
-  if (file->status != CHATTY_FILE_UNKNOWN) {
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
-                             "Download not required");
-    return;
-  }
-
-  matrix_net_get_file_async (self->matrix_net, message,
-                             file, cancellable,
-                             progress_callback,
-                             api_get_file_cb,
-                             g_steal_pointer (&task));
-}
-
-gboolean
-matrix_api_get_file_finish (MatrixApi     *self,
-                            GAsyncResult  *result,
-                            GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_send_message_encrypted (MatrixApi     *self,
-                            const char    *room_id,
-                            ChattyMessage *message,
-                            JsonObject    *content,
-                            GTask         *task)
-{
-  g_autofree char *text = NULL;
-  g_autofree char *uri = NULL;
-  JsonObject *root;
-  char *id;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (content);
-
-  root = json_object_new ();
-  json_object_set_string_member (root, "type", "m.room.message");
-  json_object_set_string_member (root, "room_id", room_id);
-  json_object_set_object_member (root, "content", content);
-
-  text = matrix_utils_json_object_to_string (root, FALSE);
-  json_object_unref (root);
-  root = matrix_enc_encrypt_for_chat (self->matrix_enc, room_id, text);
-
-  self->event_id++;
-  id = g_strdup_printf ("m%"G_GINT64_FORMAT".%d",
-                        g_get_real_time () / G_TIME_SPAN_MILLISECOND,
-                        self->event_id);
-  g_object_set_data_full (G_OBJECT (message), "event-id", id, g_free);
-
-  g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message),
-                          g_object_unref);
-  CHATTY_DEBUG (room_id, "Sending encrypted message. event id: %s, room:", id);
-
-  uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.encrypted/%s", room_id, id);
-
-  matrix_net_send_json_async (self->matrix_net, 0, root, uri, SOUP_METHOD_PUT,
-                              NULL, self->cancellable, api_send_message_cb, task);
-}
-
-static void
-api_send_message (MatrixApi     *self,
-                  const char    *room_id,
-                  ChattyMessage *message,
-                  JsonObject    *content,
-                  GTask         *task)
-{
-  g_autofree char *uri = NULL;
-  char *id;
-
-  g_assert (MATRIX_IS_API (self));
-  g_assert (content);
-
-  self->event_id++;
-  id = g_strdup_printf ("m%"G_GINT64_FORMAT".%d",
-                        g_get_real_time () / G_TIME_SPAN_MILLISECOND,
-                        self->event_id);
-  g_object_set_data_full (G_OBJECT (message), "event-id", id, g_free);
-
-  g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message),
-                          g_object_unref);
-
-  CHATTY_DEBUG (room_id, "Sending message. event id: %s, room:", id);
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid */
-  uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.message/%s", room_id, id);
-  matrix_net_send_json_async (self->matrix_net, 0, content, uri, SOUP_METHOD_PUT,
-                              NULL, self->cancellable, api_send_message_cb, task);
-}
-
-void
-matrix_api_send_message_async (MatrixApi           *self,
-                               ChattyChat          *chat,
-                               const char          *room_id,
-                               ChattyMessage       *message,
-                               GAsyncReadyCallback  callback,
-                               gpointer             user_data)
-{
-  JsonObject *object;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (CHATTY_IS_MESSAGE (message));
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-  object = json_object_new ();
-  json_object_set_string_member (object, "msgtype", "m.text");
-  json_object_set_string_member (object, "body", chatty_message_get_text (message));
-
-  if (chatty_chat_get_encryption (chat) == CHATTY_ENCRYPTION_ENABLED)
-    api_send_message_encrypted (self, room_id, message, object, task);
-  else
-    api_send_message (self, room_id, message, object, task);
-}
-
-gboolean
-matrix_api_send_message_finish (MatrixApi     *self,
-                                GAsyncResult  *result,
-                                GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_api_set_read_marker_async (MatrixApi           *self,
-                                  const char          *room_id,
-                                  ChattyMessage       *message,
-                                  GAsyncReadyCallback  callback,
-                                  gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  JsonObject *root;
-  const char *id;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (CHATTY_IS_MESSAGE (message));
-
-  id = chatty_message_get_uid (message);
-  root = json_object_new ();
-  json_object_set_string_member (root, "m.fully_read", id);
-  json_object_set_string_member (root, "m.read", id);
-
-  CHATTY_TRACE_MSG ("Marking is read, message id: %s", id);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/read_markers", room_id);
-  matrix_net_send_json_async (self->matrix_net, 0, root, uri, SOUP_METHOD_POST,
-                              NULL, self->cancellable, api_set_read_marker_cb, task);
-}
-
-gboolean
-matrix_api_set_read_marker_finish (MatrixApi     *self,
-                                   GAsyncResult  *result,
-                                   GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_api_upload_group_keys_async (MatrixApi           *self,
-                                    const char          *room_id,
-                                    GListModel          *member_list,
-                                    GAsyncReadyCallback  callback,
-                                    gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  JsonObject *root, *object;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (G_IS_LIST_MODEL (member_list));
-  g_return_if_fail (g_list_model_get_item_type (member_list) == CHATTY_TYPE_MA_BUDDY);
-  g_return_if_fail (g_list_model_get_n_items (member_list) > 0);
-
-  root = json_object_new ();
-  object = matrix_enc_create_out_group_keys (self->matrix_enc, room_id, member_list);
-  json_object_set_object_member (root, "messages", object);
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-
-  self->event_id++;
-  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.room.encrypted/m%"G_GINT64_FORMAT".%d",
-                         g_get_real_time () / G_TIME_SPAN_MILLISECOND,
-                         self->event_id);
-  matrix_net_send_json_async (self->matrix_net, 0, root, uri, SOUP_METHOD_PUT,
-                              NULL, self->cancellable, api_upload_group_keys_cb, task);
-}
-
-gboolean
-matrix_api_upload_group_keys_finish (MatrixApi     *self,
-                                     GAsyncResult  *result,
-                                     GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_leave_room_cb (GObject      *obj,
-                   GAsyncResult *result,
-                   gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_DEBUG (g_task_get_task_data (G_TASK (task)),
-                "Leaving room %s", CHATTY_LOG_SUCESS (!error));
-
-  if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-    g_debug ("Error leaving room: %s", error->message);
-
-  if (error)
-    g_task_return_error (task, error);
-  else
-    g_task_return_boolean (task, TRUE);
-}
-
-void
-matrix_api_leave_chat_async (MatrixApi           *self,
-                             const char          *room_id,
-                             GAsyncReadyCallback  callback,
-                             gpointer             user_data)
-{
-  GTask *task;
-  g_autofree char *uri = NULL;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (room_id && *room_id == '!');
-
-  CHATTY_DEBUG (room_id, "Leaving room");
-
-  task = g_task_new (self, self->cancellable, callback, user_data);
-  g_task_set_task_data (task, g_strdup (room_id), g_free);
-  uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/leave", room_id);
-  matrix_net_send_json_async (self->matrix_net, 1, NULL, uri, SOUP_METHOD_POST,
-                              NULL, self->cancellable, api_leave_room_cb, task);
-}
-
-gboolean
-matrix_api_leave_chat_finish (MatrixApi     *self,
-                              GAsyncResult  *result,
-                              GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_get_user_info_cb (GObject      *obj,
-                      GAsyncResult *result,
-                      gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  const char *name, *avatar_url;
-  GError *error = NULL;
-  g_autoptr(JsonObject) object = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_TRACE_MSG ("Getting user info. success: %d", !error);
-
-  if (error) {
-    g_task_return_error (task, error);
-    return;
-  }
-
-  name = matrix_utils_json_object_get_string (object, "displayname");
-  avatar_url = matrix_utils_json_object_get_string (object, "avatar_url");
-
-  g_object_set_data_full (G_OBJECT (task), "name", g_strdup (name), g_free);
-  g_object_set_data_full (G_OBJECT (task), "avatar-url", g_strdup (avatar_url), g_free);
-
-  g_task_return_boolean (task, TRUE);
-}
-
-void
-matrix_api_get_user_info_async (MatrixApi           *self,
-                                const char          *user_id,
-                                GCancellable        *cancellable,
-                                GAsyncReadyCallback  callback,
-                                gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  if (!user_id)
-    user_id = self->username;
-
-  task = g_task_new (self, cancellable, callback, user_data);
-
-  if (!user_id) {
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
-                             "No user id provided");
-    return;
-  }
-
-  CHATTY_TRACE (user_id, "Getting user info: ");
-
-  g_task_set_task_data (task, g_strdup (user_id), g_free);
-  uri = g_strdup_printf ("/_matrix/client/r0/profile/%s", user_id);
-  matrix_net_send_json_async (self->matrix_net, 1, NULL, uri, SOUP_METHOD_GET,
-                              NULL, self->cancellable, api_get_user_info_cb, task);
-}
-
-gboolean
-matrix_api_get_user_info_finish (MatrixApi     *self,
-                                 char         **name,
-                                 char         **avatar_url,
-                                 GAsyncResult  *result,
-                                 GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  *name = g_strdup (g_object_get_data (G_OBJECT (result), "name"));
-  *avatar_url = g_strdup (g_object_get_data (G_OBJECT (result), "avatar-url"));
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_set_name_cb (GObject      *obj,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_DEBUG (self->username, "Setting name to '%s' %s for user",
-                (char *)g_task_get_task_data (task), CHATTY_LOG_SUCESS (!error));
-
-  if (error)
-    g_task_return_error (task, error);
-  else
-    g_task_return_boolean (task, TRUE);
-}
-
-void
-matrix_api_set_name_async (MatrixApi           *self,
-                           const char          *name,
-                           GCancellable        *cancellable,
-                           GAsyncReadyCallback  callback,
-                           gpointer             user_data)
-{
-  g_autofree char *uri = NULL;
-  JsonObject *root = NULL;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  CHATTY_DEBUG (self->username, "Setting name to '%s' for user", name);
-
-  if (name && *name) {
-    root = json_object_new ();
-    json_object_set_string_member (root, "displayname", name);
-  }
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_task_data (task, g_strdup (name), g_free);
-
-  uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/displayname", self->username);
-  matrix_net_send_json_async (self->matrix_net, 1, root, uri, SOUP_METHOD_PUT,
-                              NULL, self->cancellable, api_set_name_cb, task);
-}
-
-gboolean
-matrix_api_set_name_finish (MatrixApi     *self,
-                            GAsyncResult  *result,
-                            GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_set_user_avatar_cb (GObject      *obj,
-                        GAsyncResult *result,
-                        gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonObject) object = NULL;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_DEBUG (self->username, "Setting avatar %s, user:", CHATTY_LOG_SUCESS (!error));
-
-  if (error)
-    g_task_return_error (task, error);
-  else
-    g_task_return_boolean (task, TRUE);
-}
-
-void
-matrix_api_set_user_avatar_async (MatrixApi           *self,
-                                  const char          *file_name,
-                                  GCancellable        *cancellable,
-                                  GAsyncReadyCallback  callback,
-                                  gpointer             user_data)
-{
-  g_autoptr(GTask) task = NULL;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = g_task_new (self, cancellable, callback, user_data);
-
-  if (!file_name) {
-    g_autofree char *uri = NULL;
-    const char *data;
-
-    data = "{\"avatar_url\":\"\"}";
-    uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/avatar_url", self->username);
-    matrix_net_send_data_async (self->matrix_net, 2, g_strdup (data), strlen (data),
-                                uri, SOUP_METHOD_PUT, NULL, self->cancellable,
-                                api_set_user_avatar_cb, g_steal_pointer (&task));
-  } else {
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
-                             "Setting new user avatar not implemented");
-  }
-}
-
-gboolean
-matrix_api_set_user_avatar_finish (MatrixApi     *self,
-                                   GAsyncResult  *result,
-                                   GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_get_3pid_cb (GObject      *obj,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  GPtrArray *emails, *phones;
-  GError *error = NULL;
-  g_autoptr(JsonObject) object = NULL;
-  JsonArray *array;
-  guint length;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-  array = matrix_utils_json_object_get_array (object, "threepids");
-
-  CHATTY_DEBUG (self->username, "Getting 3pid %s, user:", CHATTY_LOG_SUCESS (!error));
-
-  if (!array) {
-    if (error)
-      g_task_return_error (task, error);
-    else
-      g_task_return_boolean (task, FALSE);
-
-    return;
-  }
-
-  emails = g_ptr_array_new_full (1, g_free);
-  phones = g_ptr_array_new_full (1, g_free);
-
-  length = json_array_get_length (array);
-
-  for (guint i = 0; i < length; i++) {
-    const char *type, *value;
-
-    object = json_array_get_object_element (array, i);
-    value = matrix_utils_json_object_get_string (object, "address");
-    type = matrix_utils_json_object_get_string (object, "medium");
-
-    if (g_strcmp0 (type, "email") == 0)
-      g_ptr_array_add (emails, g_strdup (value));
-    else if (g_strcmp0 (type, "msisdn") == 0)
-      g_ptr_array_add (phones, g_strdup (value));
-  }
-
-  g_object_set_data_full (G_OBJECT (task), "email", emails,
-                          (GDestroyNotify)g_ptr_array_unref);
-  g_object_set_data_full (G_OBJECT (task), "phone", phones,
-                          (GDestroyNotify)g_ptr_array_unref);
-
-  g_task_return_boolean (task, TRUE);
-}
-
-void
-matrix_api_get_3pid_async (MatrixApi           *self,
-                           GCancellable        *cancellable,
-                           GAsyncReadyCallback  callback,
-                           gpointer             user_data)
-{
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  task = g_task_new (self, cancellable, callback, user_data);
-
-  if (!self->username) {
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
-                             "user hasn't logged in yet");
-    return;
-  }
-
-  CHATTY_DEBUG (self->username, "Getting 3pid of user");
-
-  matrix_net_send_json_async (self->matrix_net, 1, NULL,
-                              "/_matrix/client/r0/account/3pid", SOUP_METHOD_GET,
-                              NULL, self->cancellable, api_get_3pid_cb, task);
-}
-
-gboolean
-matrix_api_get_3pid_finish (MatrixApi     *self,
-                            GPtrArray    **emails,
-                            GPtrArray    **phones,
-                            GAsyncResult  *result,
-                            GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  if (emails)
-    *emails = g_object_steal_data (G_OBJECT (result), "email");
-  if (phones)
-    *phones = g_object_steal_data (G_OBJECT (result), "phone");
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-api_delete_3pid_cb (GObject      *obj,
-                    GAsyncResult *result,
-                    gpointer      user_data)
-{
-  MatrixApi *self;
-  g_autoptr(GTask) task = user_data;
-  GError *error = NULL;
-  g_autoptr(JsonObject) object = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_API (self));
-
-  object = g_task_propagate_pointer (G_TASK (result), &error);
-
-  CHATTY_DEBUG (self->username, "Deleting 3pid %s", CHATTY_LOG_SUCESS (!error));
-
-  if (error) {
-    g_task_return_error (task, error);
-    return;
-  }
-
-  g_task_return_boolean (task, TRUE);
-}
-
-void
-matrix_api_delete_3pid_async (MatrixApi           *self,
-                              const char          *value,
-                              ChattyIdType         type,
-                              GCancellable        *cancellable,
-                              GAsyncReadyCallback  callback,
-                              gpointer             user_data)
-{
-  JsonObject *root;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_API (self));
-  g_return_if_fail (value && *value);
-  g_return_if_fail (type == CHATTY_ID_EMAIL || type == CHATTY_ID_PHONE);
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  if (chatty_log_get_verbosity () > 2) {
-    g_autoptr(GString) str = NULL;
-
-    str = g_string_new (NULL);
-    g_string_append (str, "user: ");
-    chatty_log_anonymize_value (str, self->username);
-    g_string_append (str, " value: ");
-    chatty_log_anonymize_value (str, value);
-
-    g_debug ("Deleting 3pid, %s", str->str);
-  }
-
-  root = json_object_new ();
-  json_object_set_string_member (root, "address", value);
-  if (type == CHATTY_ID_PHONE)
-    json_object_set_string_member (root, "medium", "msisdn");
-  else
-    json_object_set_string_member (root, "medium", "email");
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_object_set_data (G_OBJECT (task), "type", GINT_TO_POINTER (type));
-  g_object_set_data_full (G_OBJECT (task), "value",
-                          g_strdup (value), g_free);
-
-  matrix_net_send_json_async (self->matrix_net, 2, root,
-                              "/_matrix/client/r0/account/3pid/delete", SOUP_METHOD_POST,
-                              NULL, cancellable, api_delete_3pid_cb, task);
-}
-
-gboolean
-matrix_api_delete_3pid_finish (MatrixApi    *self,
-                               GAsyncResult *result,
-                               GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_API (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
diff --git a/src/matrix/matrix-api.h b/src/matrix/matrix-api.h
deleted file mode 100644
index 6d0039716d5bc5e18f3e5d7c35c1acbdbdc97eda..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-api.h
+++ /dev/null
@@ -1,235 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-api.h
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#pragma once
-
-#include <glib-object.h>
-
-#include "chatty-chat.h"
-#include "chatty-account.h"
-#include "chatty-message.h"
-#include "matrix-enc.h"
-#include "matrix-enums.h"
-
-G_BEGIN_DECLS
-
-#define MATRIX_TYPE_API (matrix_api_get_type ())
-
-G_DECLARE_FINAL_TYPE (MatrixApi, matrix_api, MATRIX, API, GObject)
-
-typedef void   (*MatrixCallback)                 (gpointer        object,
-                                                  MatrixApi      *self,
-                                                  MatrixAction    action,
-                                                  JsonObject     *jobject,
-                                                  GError         *err);
-
-MatrixApi     *matrix_api_new                    (const char     *username);
-void           matrix_api_set_enc                (MatrixApi      *self,
-                                                  MatrixEnc      *enc);
-gboolean       matrix_api_can_connect            (MatrixApi      *self);
-
-const char    *matrix_api_get_username           (MatrixApi      *self);
-void           matrix_api_set_username           (MatrixApi      *self,
-                                                  const char     *username);
-const char    *matrix_api_get_login_username     (MatrixApi      *self);
-void           matrix_api_set_login_username     (MatrixApi      *self,
-                                                  const char     *username);
-
-const char    *matrix_api_get_password           (MatrixApi      *self);
-void           matrix_api_set_password           (MatrixApi      *self,
-                                                  const char     *password);
-
-const char    *matrix_api_get_homeserver         (MatrixApi      *self);
-void           matrix_api_set_homeserver         (MatrixApi      *self,
-                                                  const char     *homeserver);
-const char    *matrix_api_get_device_id          (MatrixApi      *self);
-const char    *matrix_api_get_access_token       (MatrixApi      *self);
-void           matrix_api_set_access_token       (MatrixApi      *self,
-                                                  const char     *access_token,
-                                                  const char     *device_id);
-const char    *matrix_api_get_next_batch         (MatrixApi      *self);
-void           matrix_api_set_next_batch         (MatrixApi      *self,
-                                                  const char     *next_batch);
-void          matrix_api_set_sync_callback       (MatrixApi      *self,
-                                                  MatrixCallback  callback,
-                                                  gpointer        object);
-gboolean      matrix_api_is_sync                 (MatrixApi      *self);
-void          matrix_api_start_sync              (MatrixApi      *self);
-void          matrix_api_stop_sync               (MatrixApi      *self);
-void          matrix_api_set_upload_key          (MatrixApi      *self,
-                                                  char           *key);
-void          matrix_api_set_typing              (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  gboolean        is_typing);
-void          matrix_api_get_room_state_async    (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonArray    *matrix_api_get_room_state_finish   (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void          matrix_api_get_room_users_async    (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonObject   *matrix_api_get_room_users_finish   (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void          matrix_api_get_room_name_async     (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonObject   *matrix_api_get_room_name_finish    (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void          matrix_api_get_room_encryption_async  (MatrixApi   *self,
-                                                     const char  *room_id,
-                                                     GAsyncReadyCallback callback,
-                                                     gpointer     user_data);
-char         *matrix_api_get_room_encryption_finish (MatrixApi   *self,
-                                                     GAsyncResult *result,
-                                                     GError     **error);
-void          matrix_api_set_room_encryption_async  (MatrixApi   *self,
-                                                     const char  *room_id,
-                                                     GAsyncReadyCallback callback,
-                                                     gpointer     user_data);
-gboolean      matrix_api_set_room_encryption_finish (MatrixApi   *self,
-                                                     GAsyncResult *result,
-                                                     GError     **error);
-
-void          matrix_api_get_members_async       (MatrixApi      *self,
-                                                  const char *room_id,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonObject   *matrix_api_get_members_finish      (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_load_prev_batch_async    (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  char           *prev_batch,
-                                                  char           *last_batch,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonObject   *matrix_api_load_prev_batch_finish  (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void          matrix_api_query_keys_async        (MatrixApi      *self,
-                                                  GListModel     *members_list,
-                                                  const char     *token,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonObject   *matrix_api_query_keys_finish       (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void          matrix_api_claim_keys_async        (MatrixApi      *self,
-                                                  GListModel     *member_list,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-JsonObject   *matrix_api_claim_keys_finish       (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void           matrix_api_get_file_async         (MatrixApi      *self,
-                                                  ChattyMessage  *message,
-                                                  ChattyFileInfo *file,
-                                                  GCancellable   *cancellable,
-                                                  GFileProgressCallback progress_callback,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_get_file_finish          (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void          matrix_api_send_file_async         (MatrixApi      *self,
-                                                  ChattyChat     *chat,
-                                                  const char     *file_name,
-                                                  GCancellable   *cancellable,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_send_file_finish         (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_send_message_async       (MatrixApi      *self,
-                                                  ChattyChat     *chat,
-                                                  const char     *room_id,
-                                                  ChattyMessage  *message,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_send_message_finish      (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_set_read_marker_async    (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  ChattyMessage  *message,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean    matrix_api_set_read_marker_finish    (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_upload_group_keys_async  (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  GListModel     *member_list,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_upload_group_keys_finish (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_leave_chat_async         (MatrixApi      *self,
-                                                  const char     *room_id,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_leave_chat_finish        (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_get_user_info_async      (MatrixApi      *self,
-                                                  const char     *user_id,
-                                                  GCancellable   *cancellable,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_get_user_info_finish     (MatrixApi      *self,
-                                                  char         **name,
-                                                  char         **avatar_url,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_set_name_async           (MatrixApi      *self,
-                                                  const char     *name,
-                                                  GCancellable   *cancellable,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_set_name_finish          (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_set_user_avatar_async    (MatrixApi      *self,
-                                                  const char     *file_name,
-                                                  GCancellable   *cancellable,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_set_user_avatar_finish   (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_get_3pid_async           (MatrixApi      *self,
-                                                  GCancellable   *cancellable,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_get_3pid_finish          (MatrixApi      *self,
-                                                  GPtrArray     **emails,
-                                                  GPtrArray     **phones,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-void         matrix_api_delete_3pid_async        (MatrixApi      *self,
-                                                  const char     *value,
-                                                  ChattyIdType    type,
-                                                  GCancellable   *cancellable,
-                                                  GAsyncReadyCallback callback,
-                                                  gpointer        user_data);
-gboolean     matrix_api_delete_3pid_finish       (MatrixApi      *self,
-                                                  GAsyncResult   *result,
-                                                  GError        **error);
-
-G_END_DECLS
diff --git a/src/matrix/matrix-db.c b/src/matrix/matrix-db.c
deleted file mode 100644
index d0e57319929fa6484fbde49f445c5c6fa6e567f8..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-db.c
+++ /dev/null
@@ -1,1320 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-db.c
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-/* This is heavily based on chatty-history */
-
-#define G_LOG_DOMAIN "matrix-db"
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
-
-#include <glib.h>
-#include <fcntl.h>
-#include <sqlite3.h>
-
-#include "chatty-utils.h"
-#include "matrix-enc.h"
-#include "matrix-db.h"
-
-struct _MatrixDb
-{
-  GObject      parent_instance;
-
-  GAsyncQueue *queue;
-  GThread     *worker_thread;
-  sqlite3     *db;
-};
-
-/*
- * MatrixDb->db should never be accessed nor modified in main thread
- * except for checking if it’s %NULL.  Any operation should be done only
- * in @worker_thread.  Don't reuse the same #MatrixDb once closed.
- */
-
-typedef void (*MatrixDbCallback) (MatrixDb *self,
-                                  GTask *task);
-
-G_DEFINE_TYPE (MatrixDb, matrix_db, G_TYPE_OBJECT)
-
-static void
-warn_if_sql_error (int         status,
-                   const char *message)
-{
-  if (status == SQLITE_OK || status == SQLITE_ROW || status == SQLITE_DONE)
-    return;
-
-  g_warning ("Error %s. errno: %d, message: %s", message, status, sqlite3_errstr (status));
-}
-
-static void
-matrix_bind_text (sqlite3_stmt *statement,
-                   guint         position,
-                   const char   *bind_value,
-                   const char   *message)
-{
-  guint status;
-
-  status = sqlite3_bind_text (statement, position, bind_value, -1, SQLITE_TRANSIENT);
-  warn_if_sql_error (status, message);
-}
-
-static void
-matrix_bind_int (sqlite3_stmt *statement,
-                  guint         position,
-                  int           bind_value,
-                  const char   *message)
-{
-  guint status;
-
-  status = sqlite3_bind_int (statement, position, bind_value);
-  warn_if_sql_error (status, message);
-}
-
-static int
-matrix_db_create_schema (MatrixDb *self,
-                         GTask    *task)
-{
-  const char *sql;
-  char *error = NULL;
-  int status;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  sql = "BEGIN TRANSACTION;"
-
-    "CREATE TABLE IF NOT EXISTS devices ("
-    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
-    "device TEXT NOT NULL UNIQUE);"
-
-    "CREATE TABLE IF NOT EXISTS users ("
-    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
-    "username TEXT NOT NULL, "
-    "device_id INTEGER REFERENCES devices(id), "
-    "UNIQUE (username, device_id));"
-
-    "CREATE TABLE IF NOT EXISTS accounts ("
-    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
-    "user_id INTEGER NOT NULL REFERENCES users(id), "
-    "next_batch TEXT, "
-    "pickle TEXT, "
-    "enabled INTEGER DEFAULT 0, "
-    "UNIQUE (user_id));"
-
-    "CREATE TABLE IF NOT EXISTS rooms ("
-    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
-    "account_id INTEGER NOT NULL REFERENCES accounts(id), "
-    "room_name TEXT NOT NULL, "
-    "prev_batch TEXT, "
-    "UNIQUE (account_id, room_name));"
-
-    "CREATE TABLE IF NOT EXISTS encryption_keys ("
-    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
-    "file_url TEXT NOT NULL, "
-    "file_sha256 TEXT, "
-    /* Initialization vector: iv in JSON */
-    "iv TEXT NOT NULL, "
-    /* v in JSON */
-    "version INT DEFAULT 2 NOT NULL, "
-    /* alg in JSON */
-    "algorithm INT NOT NULL, "
-    /* k in JSON */
-    "key TEXT NOT NULL, "
-    /* kty in JSON */
-    "type INT NOT NULL, "
-    /* ext in JSON */
-    "extractable INT DEFAULT 1 NOT NULL, "
-    "UNIQUE (file_url));"
-
-    /* TODO: Someday */
-    "CREATE TABLE IF NOT EXISTS session ("
-    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
-    "account_id INTEGER NOT NULL REFERENCES accounts(id), "
-    "sender_key TEXT NOT NULL, "
-    "session_id TEXT NOT NULL, "
-    "type INTEGER NOT NULL, "
-    "pickle TEXT NOT NULL, "
-    "time INT, "
-    "UNIQUE (account_id, sender_key, session_id));"
-
-    "COMMIT;";
-
-  status = sqlite3_exec (self->db, sql, NULL, NULL, &error);
-
-  if (status != SQLITE_OK) {
-    g_task_return_new_error (task,
-                             G_IO_ERROR,
-                             G_IO_ERROR_FAILED,
-                             "Error creating chatty_im table. errno: %d, desc: %s. %s",
-                             status, sqlite3_errmsg (self->db), error);
-    return status;
-  }
-
-  return status;
-}
-
-
-static void
-matrix_open_db (MatrixDb *self,
-                GTask    *task)
-{
-  const char *dir, *file_name;
-  g_autofree char *db_path = NULL;
-  sqlite3 *db;
-  int status;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (!self->db);
-
-  dir = g_object_get_data (G_OBJECT (task), "dir");
-  file_name = g_object_get_data (G_OBJECT (task), "file-name");
-  g_assert (dir && *dir);
-  g_assert (file_name && *file_name);
-
-  g_mkdir_with_parents (dir, S_IRWXU);
-  db_path = g_build_filename (dir, file_name, NULL);
-
-  status = sqlite3_open (db_path, &db);
-
-  if (status == SQLITE_OK) {
-    self->db = db;
-    status = matrix_db_create_schema (self, task);
-
-    if (status == SQLITE_OK)
-      g_task_return_boolean (task, TRUE);
-  } else {
-    g_task_return_new_error (task,
-                             G_IO_ERROR,
-                             G_IO_ERROR_FAILED,
-                             "Database could not be opened. errno: %d, desc: %s",
-                             status, sqlite3_errmsg (db));
-    sqlite3_close (db);
-  }
-}
-
-static void
-matrix_close_db (MatrixDb *self,
-                 GTask    *task)
-{
-  sqlite3 *db;
-  int status;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  db = self->db;
-  self->db = NULL;
-  status = sqlite3_close (db);
-
-  if (status == SQLITE_OK) {
-    /*
-     * We can’t know when will @self associated with the task will
-     * be unref.  So matrix_db_get_default() called immediately
-     * after this may return the @self that is yet to be free.  But
-     * as the worker_thread is exited after closing the database, any
-     * actions with the same @self will not execute, and so the tasks
-     * will take ∞ time to complete.
-     *
-     * So Instead of relying on GObject to free the object, Let’s
-     * explicitly run dispose
-     */
-    g_object_run_dispose (G_OBJECT (self));
-    g_debug ("Database closed successfully");
-    g_task_return_boolean (task, TRUE);
-  } else {
-    g_task_return_new_error (task,
-                             G_IO_ERROR,
-                             G_IO_ERROR_FAILED,
-                             "Database could not be closed. errno: %d, desc: %s",
-                             status, sqlite3_errmsg (db));
-  }
-}
-
-static void
-matrix_db_save_account (MatrixDb *self,
-                        GTask    *task)
-{
-  sqlite3_stmt *stmt;
-  const char *device, *pickle, *username, *batch;
-  int status, device_id = 0, user_id = 0;
-  gboolean enabled;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  batch = g_object_get_data (G_OBJECT (task), "batch");
-  pickle = g_object_get_data (G_OBJECT (task), "pickle");
-  device = g_object_get_data (G_OBJECT (task), "device");
-  enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled"));
-  username = g_object_get_data (G_OBJECT (task), "username");
-
-  if (device) {
-    sqlite3_prepare_v2 (self->db,
-                        "INSERT OR IGNORE INTO devices(device) VALUES(?1)",
-                        -1, &stmt, NULL);
-
-    matrix_bind_text (stmt, 1, device, "binding when updating device");
-    sqlite3_step (stmt);
-    sqlite3_finalize (stmt);
-
-    sqlite3_prepare_v2 (self->db,
-                        "SELECT id FROM devices WHERE device=?",
-                        -1, &stmt, NULL);
-    matrix_bind_text (stmt, 1, device, "binding when getting device id");
-
-    if (sqlite3_step (stmt) == SQLITE_ROW)
-      device_id = sqlite3_column_int (stmt, 0);
-    sqlite3_finalize (stmt);
-  }
-
-
-  /* Find user id with no device id set and have an associated account */
-  sqlite3_prepare_v2 (self->db,
-                      "SELECT users.id FROM users "
-                      "INNER JOIN accounts ON user_id=users.id "
-                      "WHERE username=?1 AND device_id IS NULL LIMIT 1",
-                      -1, &stmt, NULL);
-  matrix_bind_text (stmt, 1, username, "binding when getting user");
-
-  if (sqlite3_step (stmt) == SQLITE_ROW)
-    user_id = sqlite3_column_int (stmt, 0);
-
-  if (user_id) {
-    sqlite3_prepare_v2 (self->db,
-                        "UPDATE users SET device_id=? WHERE users.id=?",
-                        -1, &stmt, NULL);
-    if (device_id)
-      matrix_bind_int (stmt, 1, device_id, "binding when updating existing user");
-    matrix_bind_int (stmt, 2, user_id, "binding when updating existing user");
-    user_id = 0;
-  } else {
-    sqlite3_prepare_v2 (self->db,
-                        "INSERT OR IGNORE INTO users(username,device_id) VALUES(?1,?2)",
-                        -1, &stmt, NULL);
-    matrix_bind_text (stmt, 1, username, "binding when updating user");
-    if (device_id)
-      matrix_bind_int (stmt, 2, device_id, "binding when updating user");
-  }
-
-  sqlite3_step (stmt);
-  sqlite3_finalize (stmt);
-
-  sqlite3_prepare_v2 (self->db,
-                      "SELECT id FROM users WHERE username=?1 AND (device_id=?2 "
-                      "OR (device_id IS NULL AND ?2 IS NULL)) LIMIT 1",
-                      -1, &stmt, NULL);
-  matrix_bind_text (stmt, 1, username, "binding when getting user id");
-  if (device_id)
-    matrix_bind_int (stmt, 2, device_id, "binding when getting user id");
-
-  if (sqlite3_step (stmt) == SQLITE_ROW)
-    user_id = sqlite3_column_int (stmt, 0);
-  sqlite3_finalize (stmt);
-
-  sqlite3_prepare_v2 (self->db,
-                      "INSERT INTO accounts(user_id,pickle,next_batch,enabled) "
-                      "VALUES(?1,?2,?3,?4) "
-                      "ON CONFLICT(user_id) "
-                      "DO UPDATE SET pickle=?2, next_batch=?3, enabled=?4",
-                      -1, &stmt, NULL);
-
-  matrix_bind_int (stmt, 1, user_id, "binding when updating account");
-  if (pickle && *pickle)
-    matrix_bind_text (stmt, 2, pickle, "binding when updating account");
-  matrix_bind_text (stmt, 3, batch, "binding when updating account");
-  matrix_bind_int (stmt, 4, enabled, "binding when updating account");
-
-  status = sqlite3_step (stmt);
-  sqlite3_finalize (stmt);
-
-  if (status == SQLITE_DONE)
-    g_task_return_boolean (task, TRUE);
-  else
-    g_task_return_new_error (task,
-                             G_IO_ERROR,
-                             G_IO_ERROR_FAILED,
-                             "Error saving account. errno: %d, desc: %s",
-                             status, sqlite3_errmsg (self->db));
-}
-
-/* Find the account id of matching @username and @device_id,
- * The item should already be in the db, otherwise it's an
- * error.
- */
-static int
-matrix_db_get_account_id (MatrixDb   *self,
-                          GTask      *task,
-                          const char *username,
-                          const char *device_name)
-{
-  sqlite3_stmt *stmt;
-  const char *error;
-  int status;
-
-  g_assert (username && *username);
-  g_assert (device_name && *device_name);
-
-  sqlite3_prepare_v2 (self->db,
-                      "SELECT accounts.id FROM accounts "
-                      "INNER JOIN users ON username=? "
-                      "INNER JOIN devices ON device_id=users.device_id AND devices.device=? "
-                      "WHERE accounts.user_id=users.id",
-                      -1, &stmt, NULL);
-  matrix_bind_text (stmt, 1, username, "binding when getting account id");
-  matrix_bind_text (stmt, 2, device_name, "binding when getting account id");
-
-  status = sqlite3_step (stmt);
-  if (status == SQLITE_ROW)
-    return sqlite3_column_int (stmt, 0);
-
-  if (status == SQLITE_DONE)
-    error = "Account not found in db";
-  else
-    error = sqlite3_errmsg (self->db);
-
-  g_task_return_new_error (task,
-                           G_IO_ERROR,
-                           G_IO_ERROR_FAILED,
-                           "Couldn't find user %s. error: %s",
-                           username, error);
-  return 0;
-}
-
-static void
-matrix_db_load_account (MatrixDb *self,
-                        GTask    *task)
-{
-  sqlite3_stmt *stmt;
-  char *username, *device_id;
-  int status;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  username = g_object_get_data (G_OBJECT (task), "username");
-  device_id = g_object_get_data (G_OBJECT (task), "device");
-
-  status = sqlite3_prepare_v2 (self->db,
-                               "SELECT enabled,pickle,next_batch,device "
-                               "FROM accounts "
-                               "INNER JOIN users "
-                               "ON users.username=?1 AND users.id=user_id "
-                               "LEFT JOIN devices "
-                               "ON device_id=devices.id AND devices.device=?2 "
-                               "WHERE (?2 is NULL AND device is NULL) OR (?2 is NOT NULL AND device=?2)",
-                               -1, &stmt, NULL);
-  matrix_bind_text (stmt, 1, username, "binding when loading account");
-  matrix_bind_text (stmt, 2, device_id, "binding when loading account");
-
-  status = sqlite3_step (stmt);
-
-  if (status == SQLITE_ROW) {
-    GObject *object = G_OBJECT (task);
-
-    g_object_set_data (object, "enabled", GINT_TO_POINTER (sqlite3_column_int (stmt, 0)));
-    g_object_set_data_full (object, "pickle", g_strdup ((char *)sqlite3_column_text (stmt, 1)), g_free);
-    g_object_set_data_full (object, "batch", g_strdup ((char *)sqlite3_column_text (stmt, 2)), g_free);
-    g_object_set_data_full (object, "device", g_strdup ((char *)sqlite3_column_text (stmt, 3)), g_free);
-  }
-
-  sqlite3_finalize (stmt);
-  g_task_return_boolean (task, status == SQLITE_ROW);
-}
-
-static void
-matrix_db_save_room (MatrixDb *self,
-                     GTask    *task)
-{
-  sqlite3_stmt *stmt;
-  const char *username, *room_name, *batch, *account_device;
-  int status, account_id;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  batch = g_object_get_data (G_OBJECT (task), "prev-batch");
-  username = g_object_get_data (G_OBJECT (task), "account-id");
-  room_name = g_object_get_data (G_OBJECT (task), "room");
-  account_device = g_object_get_data (G_OBJECT (task), "account-device");
-
-  account_id = matrix_db_get_account_id (self, task, username, account_device);
-
-  if (!account_id)
-    return;
-
-  sqlite3_prepare_v2 (self->db,
-                      "INSERT INTO rooms(account_id,room_name,prev_batch) "
-                      "VALUES(?1,?2,?3) "
-                      "ON CONFLICT(account_id, room_name) "
-                      "DO UPDATE SET prev_batch=?3",
-                      -1, &stmt, NULL);
-  matrix_bind_int (stmt, 1, account_id, "binding when saving room");
-  matrix_bind_text (stmt, 2, room_name, "binding when saving room");
-  matrix_bind_text (stmt, 3, batch, "binding when saving room");
-
-  status = sqlite3_step (stmt);
-  sqlite3_finalize (stmt);
-  g_task_return_boolean (task, status == SQLITE_DONE);
-}
-
-static void
-matrix_db_load_room (MatrixDb *self,
-                     GTask    *task)
-{
-  const char *username, *room_name, *account_device;
-  sqlite3_stmt *stmt;
-  int account_id;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  username = g_object_get_data (G_OBJECT (task), "account-id");
-  room_name = g_object_get_data (G_OBJECT (task), "room");
-  account_device = g_object_get_data (G_OBJECT (task), "account-device");
-
-  account_id = matrix_db_get_account_id (self, task, username, account_device);
-
-  if (!account_id)
-    return;
-
-  sqlite3_prepare_v2 (self->db,
-                      "SELECT prev_batch FROM rooms "
-                      "WHERE account_id=? AND room_name=? ",
-                      -1, &stmt, NULL);
-
-  matrix_bind_int (stmt, 1, account_id, "binding when loading room");
-  matrix_bind_text (stmt, 2, room_name, "binding when loading room");
-
-  if (sqlite3_step (stmt) == SQLITE_ROW)
-    g_task_return_pointer (task, g_strdup ((char *)sqlite3_column_text (stmt, 0)), g_free);
-  else
-    g_task_return_pointer (task, NULL, NULL);
-
-  sqlite3_finalize (stmt);
-}
-
-static void
-matrix_db_delete_account (MatrixDb *self,
-                          GTask    *task)
-{
-  sqlite3_stmt *stmt;
-  char *username;
-  int status;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  username = g_object_get_data (G_OBJECT (task), "username");
-
-  status = sqlite3_prepare_v2 (self->db,
-                               "DELETE FROM accounts "
-                               "WHERE accounts.id IN ("
-                               "SELECT users.id FROM users "
-                               "WHERE users.username=?)",
-                               -1, &stmt, NULL);
-  matrix_bind_text (stmt, 1, username, "binding when loading account");
-
-  status = sqlite3_step (stmt);
-  sqlite3_finalize (stmt);
-
-  g_task_return_boolean (task, status == SQLITE_ROW);
-}
-
-static void
-matrix_db_add_session (MatrixDb *self,
-                       GTask    *task)
-{
-  sqlite3_stmt *stmt;
-  const char *username, *account_device, *session_id, *sender_key, *pickle;
-  MatrixSessionType type;
-  int status, account_id;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type"));
-
-  pickle = g_object_get_data (G_OBJECT (task), "pickle");
-  username = g_object_get_data (G_OBJECT (task), "account-id");
-  session_id = g_object_get_data (G_OBJECT (task), "session-id");
-  sender_key = g_object_get_data (G_OBJECT (task), "sender-key");
-  account_device = g_object_get_data (G_OBJECT (task), "account-device");
-
-  account_id = matrix_db_get_account_id (self, task, username, account_device);
-
-  if (!account_id)
-    return;
-
-  status = sqlite3_prepare_v2 (self->db,
-                               "INSERT INTO session(account_id,sender_key,session_id,type,pickle) "
-                               "VALUES(?1,?2,?3,?4,?5)",
-                               -1, &stmt, NULL);
-
-  matrix_bind_int (stmt, 1, account_id, "binding when adding session");
-  matrix_bind_text (stmt, 2, sender_key, "binding when adding session");
-  matrix_bind_text (stmt, 3, session_id, "binding when adding session");
-  matrix_bind_int (stmt, 4, type, "binding when adding session");
-  matrix_bind_text (stmt, 5, pickle, "binding when adding session");
-
-  status = sqlite3_step (stmt);
-  sqlite3_finalize (stmt);
-  g_task_return_boolean (task, status == SQLITE_DONE);
-}
-
-static void
-matrix_db_save_file_url (MatrixDb *self,
-                         GTask    *task)
-{
-  ChattyFileInfo *file;
-  MatrixFileEncInfo *enc;
-  sqlite3_stmt *stmt;
-  int status, version, type, extractable, algorithm;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (G_IS_TASK (task));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (self->db);
-
-  file = g_object_get_data (G_OBJECT (task), "file");
-  g_return_if_fail (file && file->user_data);
-  enc = file->user_data;
-
-  type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type"));
-  version = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "version"));
-  algorithm = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "algorithm"));
-  extractable = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "extractable"));
-
-  status = sqlite3_prepare_v2 (self->db,
-                               "INSERT INTO encryption_keys(file_url,file_sha256,"
-                               "iv,version,algorithm,key,type,extractable) "
-                               "VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
-                               -1, &stmt, NULL);
-
-  matrix_bind_text (stmt, 1, file->url, "binding when adding file url");
-  matrix_bind_text (stmt, 2, enc->sha256_base64, "binding when adding file url");
-  matrix_bind_text (stmt, 3, enc->aes_iv_base64, "binding when adding file url");
-  matrix_bind_int (stmt, 4, version, "binding when adding file url");
-  matrix_bind_int (stmt, 5, algorithm, "binding when adding file url");
-  matrix_bind_text (stmt, 6, enc->aes_key_base64, "binding when adding file url");
-  matrix_bind_int (stmt, 7, type, "binding when adding file url");
-  matrix_bind_int (stmt, 8, extractable, "binding when adding file url");
-
-  status = sqlite3_step (stmt);
-  sqlite3_finalize (stmt);
-
-  g_task_return_boolean (task, status == SQLITE_DONE);
-}
-
-static void
-db_lookup_session (MatrixDb *self,
-                   GTask    *task)
-{
-  sqlite3_stmt *stmt;
-  const char *username, *account_device, *session_id, *sender_key;
-  char *pickle = NULL;
-  MatrixSessionType type;
-  int status, account_id;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (G_IS_TASK (task));
-
-  type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type"));
-
-  pickle = g_object_get_data (G_OBJECT (task), "pickle");
-  username = g_object_get_data (G_OBJECT (task), "account-id");
-  session_id = g_object_get_data (G_OBJECT (task), "session-id");
-  sender_key = g_object_get_data (G_OBJECT (task), "sender-key");
-  account_device = g_object_get_data (G_OBJECT (task), "account-device");
-
-  account_id = matrix_db_get_account_id (self, task, username, account_device);
-
-  if (!account_id)
-    return;
-
-  sqlite3_prepare_v2 (self->db,
-                      "SELECT pickle FROM session "
-                      "WHERE account_id=? AND sender_key=? AND session_id=? AND type=?",
-                      -1, &stmt, NULL);
-
-  matrix_bind_int (stmt, 1, account_id, "binding when adding session");
-  matrix_bind_text (stmt, 2, sender_key, "binding when looking up session");
-  matrix_bind_text (stmt, 3, session_id, "binding when looking up session");
-  matrix_bind_int (stmt, 4, type, "binding when looking up session");
-
-  status = sqlite3_step (stmt);
-
-  if (status == SQLITE_ROW)
-    pickle = g_strdup ((char *)sqlite3_column_text (stmt, 0));
-
-  sqlite3_finalize (stmt);
-  g_task_return_pointer (task, pickle, g_free);
-}
-
-static void
-history_get_olm_sessions (MatrixDb *self,
-                          GTask    *task)
-{
-  GPtrArray *sessions = NULL;
-  sqlite3_stmt *stmt;
-  int status;
-
-  g_assert (MATRIX_IS_DB (self));
-  g_assert (g_thread_self () == self->worker_thread);
-  g_assert (G_IS_TASK (task));
-
-  status = sqlite3_prepare_v2 (self->db,
-                               "SELECT sender,sender_key,session_pickle FROM olm_session "
-                               "ORDER BY id DESC LIMIT 100", -1, &stmt, NULL);
-
-  warn_if_sql_error (status, "getting olm sessions");
-
-  while (sqlite3_step (stmt) == SQLITE_ROW) {
-    MatrixDbData *data;
-
-    if (!sessions)
-      sessions = g_ptr_array_new_full (100, NULL);
-
-    data = g_new (MatrixDbData, 1);
-    data->sender = g_strdup ((char *)sqlite3_column_text (stmt, 0));
-    data->sender_key = g_strdup ((char *)sqlite3_column_text (stmt, 1));
-    data->session_pickle = g_strdup ((char *)sqlite3_column_text (stmt, 2));
-
-    g_ptr_array_insert (sessions, 0, data);
-  }
-
-  status = sqlite3_finalize (stmt);
-  warn_if_sql_error (status, "finalizing when getting olm sessions");
-
-  g_task_return_pointer (task, sessions, (GDestroyNotify)g_ptr_array_unref);
-}
-
-static gpointer
-matrix_db_worker (gpointer user_data)
-{
-  MatrixDb *self = user_data;
-  GTask *task;
-
-  g_assert (MATRIX_IS_DB (self));
-
-  while ((task = g_async_queue_pop (self->queue))) {
-    MatrixDbCallback callback;
-
-    g_assert (task);
-    callback = g_task_get_task_data (task);
-    callback (self, task);
-    g_object_unref (task);
-
-    if (callback == matrix_close_db)
-      break;
-  }
-
-  return NULL;
-}
-
-static void
-ma_finish_cb (GObject      *object,
-              GAsyncResult *result,
-              gpointer      user_data)
-{
-  g_autoptr(GError) error = NULL;
-
-  g_task_propagate_boolean (G_TASK (result), &error);
-
-  if (error)
-    g_warning ("Error: %s", error->message);
-
-  g_task_return_boolean (G_TASK (user_data), !error);
-}
-
-static void
-matrix_db_close (MatrixDb *self)
-{
-  g_autoptr(GTask) task = NULL;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-
-  if (!self->db)
-    return;
-
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  matrix_db_close_async (self, ma_finish_cb, task);
-
-  /* Wait until the task is completed */
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-}
-
-static void
-matrix_db_dispose (GObject *object)
-{
-  MatrixDb *self = (MatrixDb *)object;
-
-  matrix_db_close (self);
-
-  g_clear_pointer (&self->worker_thread, g_thread_unref);
-
-  G_OBJECT_CLASS (matrix_db_parent_class)->dispose (object);
-}
-
-static void
-matrix_db_finalize (GObject *object)
-{
-  MatrixDb *self = (MatrixDb *)object;
-
-  if (self->db)
-    g_warning ("Database not closed");
-
-  g_clear_pointer (&self->queue, g_async_queue_unref);
-
-  G_OBJECT_CLASS (matrix_db_parent_class)->finalize (object);
-}
-
-static void
-matrix_db_class_init (MatrixDbClass *klass)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->dispose  = matrix_db_dispose;
-  object_class->finalize = matrix_db_finalize;
-}
-
-static void
-matrix_db_init (MatrixDb *self)
-{
-  self->queue = g_async_queue_new ();
-}
-
-MatrixDb *
-matrix_db_new (void)
-{
-  return g_object_new (MATRIX_TYPE_DB, NULL);
-}
-
-/**
- * matrix_db_open_async:
- * @self: a #MatrixDb
- * @dir: (transfer full): The database directory
- * @file_name: The file name of database
- * @callback: a #GAsyncReadyCallback, or %NULL
- * @user_data: closure data for @callback
- *
- * Open the database file @file_name from path @dir.
- * Complete with matrix_db_open_finish() to get
- * the result.
- */
-void
-matrix_db_open_async (MatrixDb            *self,
-                      char                *dir,
-                      const char          *file_name,
-                      GAsyncReadyCallback  callback,
-                      gpointer             user_data)
-{
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (dir || !*dir);
-  g_return_if_fail (file_name || !*file_name);
-
-  if (self->db) {
-    g_warning ("A DataBase is already open");
-    return;
-  }
-
-  if (!self->worker_thread)
-    self->worker_thread = g_thread_new ("matrix-db-worker",
-                                        matrix_db_worker,
-                                        self);
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_open_async);
-  g_task_set_task_data (task, matrix_open_db, NULL);
-  g_object_set_data_full (G_OBJECT (task), "dir", dir, g_free);
-  g_object_set_data_full (G_OBJECT (task), "file-name", g_strdup (file_name), g_free);
-
-  g_async_queue_push (self->queue, task);
-}
-
-/**
- * matrix_db_open_finish:
- * @self: a #MatrixDb
- * @result: a #GAsyncResult provided to callback
- * @error: a location for a #GError or %NULL
- *
- * Completes opening a database started with
- * matrix_db_open_async().
- *
- * Returns: %TRUE if database was opened successfully.
- * %FALSE otherwise with @error set.
- */
-gboolean
-matrix_db_open_finish (MatrixDb      *self,
-                       GAsyncResult  *result,
-                       GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-/**
- * matrix_db_is_open:
- * @self: a #MatrixDb
- *
- * Get if the database is open or not
- *
- * Returns: %TRUE if a database is open.
- * %FALSE otherwise.
- */
-gboolean
-matrix_db_is_open (MatrixDb *self)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-
-  return !!self->db;
-}
-
-/**
- * matrix_db_close_async:
- * @self: a #MatrixDb
- * @callback: a #GAsyncReadyCallback, or %NULL
- * @user_data: closure data for @callback
- *
- * Close the database opened.
- * Complete with matrix_db_close_finish() to get
- * the result.
- */
-void
-matrix_db_close_async (MatrixDb            *self,
-                       GAsyncReadyCallback  callback,
-                       gpointer             user_data)
-{
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_close_async);
-  g_task_set_task_data (task, matrix_close_db, NULL);
-
-  g_async_queue_push (self->queue, task);
-}
-
-/**
- * matrix_db_open_finish:
- * @self: a #MatrixDb
- * @result: a #GAsyncResult provided to callback
- * @error: a location for a #GError or %NULL
- *
- * Completes closing a database started with
- * matrix_db_close_async().  @self is
- * g_object_unref() if closing succeeded.
- * So @self will be freed if you haven’t kept
- * your own reference on @self.
- *
- * Returns: %TRUE if database was closed successfully.
- * %FALSE otherwise with @error set.
- */
-gboolean
-matrix_db_close_finish (MatrixDb      *self,
-                        GAsyncResult  *result,
-                        GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_db_save_account_async (MatrixDb            *self,
-                              ChattyAccount       *account,
-                              gboolean             enabled,
-                              char                *pickle,
-                              const char          *device_id,
-                              const char          *next_batch,
-                              GAsyncReadyCallback  callback,
-                              gpointer             user_data)
-{
-  GObject *object;
-  GTask *task;
-  const char *username;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (CHATTY_IS_ACCOUNT (account));
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_save_account_async);
-  g_task_set_task_data (task, matrix_db_save_account, NULL);
-
-  object = G_OBJECT (task);
-  username = chatty_item_get_username (CHATTY_ITEM (account));
-
-  if (g_application_get_default ())
-    g_application_hold (g_application_get_default ());
-
-  if (!username || !*username) {
-    g_task_return_boolean (task, FALSE);
-    return;
-  }
-
-  g_object_set_data (object, "enabled", GINT_TO_POINTER (enabled));
-  g_object_set_data_full (object, "pickle", pickle, g_free);
-  g_object_set_data_full (object, "device", g_strdup (device_id), g_free);
-  g_object_set_data_full (object, "batch", g_strdup (next_batch), g_free);
-  g_object_set_data_full (object, "username", g_strdup (username), g_free);
-  g_object_set_data_full (object, "account", g_object_ref (account), g_object_unref);
-
-  g_async_queue_push (self->queue, task);
-}
-
-gboolean
-matrix_db_save_account_finish (MatrixDb      *self,
-                               GAsyncResult  *result,
-                               GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  if (g_application_get_default ())
-    g_application_release (g_application_get_default ());
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_db_load_account_async (MatrixDb            *self,
-                              ChattyAccount       *account,
-                              const char          *device_id,
-                              GAsyncReadyCallback  callback,
-                              gpointer             user_data)
-{
-  GTask *task;
-  const char *username;
-
-  g_return_if_fail (!device_id || *device_id);
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (CHATTY_IS_ACCOUNT (account));
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_load_account_async);
-  g_task_set_task_data (task, matrix_db_load_account, NULL);
-
-  username = chatty_item_get_username (CHATTY_ITEM (account));
-
-  if (!username || !*username) {
-    g_task_return_boolean (task, FALSE);
-    return;
-  }
-
-  g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device_id), g_free);
-  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
-  g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref);
-
-  g_async_queue_push (self->queue, task);
-}
-
-gboolean
-matrix_db_load_account_finish (MatrixDb      *self,
-                               GAsyncResult  *result,
-                               GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_db_save_room_async (MatrixDb            *self,
-                           ChattyAccount       *account,
-                           const char          *account_device,
-                           const char          *room_id,
-                           const char          *prev_batch,
-                           GAsyncReadyCallback  callback,
-                           gpointer             user_data)
-{
-  GTask *task;
-  const char *username;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (CHATTY_IS_ACCOUNT (account));
-  g_return_if_fail (room_id && *room_id == '!');
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_save_room_async);
-  g_task_set_task_data (task, matrix_db_save_room, NULL);
-
-  username = chatty_item_get_username (CHATTY_ITEM (account));
-  g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room_id), g_free);
-  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
-  g_object_set_data_full (G_OBJECT (task), "account-id", g_strdup (username), g_free);
-  g_object_set_data_full (G_OBJECT (task), "prev-batch", g_strdup (prev_batch), g_free);
-  g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref);
-  g_object_set_data_full (G_OBJECT (task), "account-device", g_strdup (account_device), g_free);
-
-  g_async_queue_push (self->queue, task);
-}
-
-gboolean
-matrix_db_save_room_finish (MatrixDb      *self,
-                            GAsyncResult  *result,
-                            GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_db_load_room_async (MatrixDb            *self,
-                           ChattyAccount       *account,
-                           const char          *account_device,
-                           const char          *room_id,
-                           GAsyncReadyCallback  callback,
-                           gpointer             user_data)
-{
-  GTask *task;
-  const char *username;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (CHATTY_IS_ACCOUNT (account));
-  g_return_if_fail (room_id && *room_id == '!');
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_load_room_async);
-  g_task_set_task_data (task, matrix_db_load_room, NULL);
-
-  username = chatty_item_get_username (CHATTY_ITEM (account));
-  g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room_id), g_free);
-  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
-  g_object_set_data_full (G_OBJECT (task), "account-id", g_strdup (username), g_free);
-  g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref);
-  g_object_set_data_full (G_OBJECT (task), "account-device", g_strdup (account_device), g_free);
-
-  g_async_queue_push (self->queue, task);
-}
-
-char *
-matrix_db_load_room_finish (MatrixDb      *self,
-                            GAsyncResult  *result,
-                            GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-void
-matrix_db_delete_account_async (MatrixDb            *self,
-                                ChattyAccount       *account,
-                                GAsyncReadyCallback  callback,
-                                gpointer             user_data)
-{
-  GTask *task;
-  const char *username;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (CHATTY_IS_ACCOUNT (account));
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_delete_account_async);
-  g_task_set_task_data (task, matrix_db_delete_account, NULL);
-
-  username = chatty_item_get_username (CHATTY_ITEM (account));
-  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
-  g_object_set_data_full (G_OBJECT (task), "account", g_object_ref (account), g_object_unref);
-
-  g_async_queue_push (self->queue, task);
-}
-
-gboolean
-matrix_db_delete_account_finish (MatrixDb      *self,
-                                 GAsyncResult  *result,
-                                 GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-void
-matrix_db_add_session_async (MatrixDb            *self,
-                             const char          *account_id,
-                             const char          *account_device,
-                             const char          *room_id,
-                             const char          *session_id,
-                             const char          *sender_key,
-                             char                *pickle,
-                             MatrixSessionType    type,
-                             GAsyncReadyCallback  callback,
-                             gpointer             user_data)
-{
-  GObject *object;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (account_id && *account_id);
-  g_return_if_fail (account_device && *account_device);
-  g_return_if_fail (room_id && *room_id);
-  g_return_if_fail (session_id && *session_id);
-  g_return_if_fail (sender_key && *sender_key);
-  g_return_if_fail (pickle && *pickle);
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_add_session_async);
-  g_task_set_task_data (task, matrix_db_add_session, NULL);
-  object = G_OBJECT (task);
-
-  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
-  g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free);
-  g_object_set_data_full (object, "room-id", g_strdup (room_id), g_free);
-  g_object_set_data_full (object, "session-id", g_strdup (session_id), g_free);
-  g_object_set_data_full (object, "sender-key", g_strdup (sender_key), g_free);
-  g_object_set_data_full (object, "pickle", pickle, g_free);
-  g_object_set_data (object, "type", GINT_TO_POINTER (type));
-
-  g_async_queue_push (self->queue, task);
-}
-
-void
-matrix_db_save_file_url_async (MatrixDb            *self,
-                               ChattyMessage       *message,
-                               ChattyFileInfo      *file,
-                               int                  version,
-                               int                  algorithm,
-                               int                  type,
-                               gboolean             extractable,
-                               GAsyncReadyCallback  callback,
-                               gpointer             user_data)
-{
-  GObject *object;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-  g_return_if_fail (version == 2);
-  g_return_if_fail (extractable);
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_save_file_url_async);
-  g_task_set_task_data (task, matrix_db_save_file_url, NULL);
-  object = G_OBJECT (task);
-
-  g_object_set_data (object, "file", file);
-  g_object_set_data_full (object, "message", g_object_ref (message), g_object_unref);
-
-  g_object_set_data (object, "type", GINT_TO_POINTER (type));
-  g_object_set_data (object, "version", GINT_TO_POINTER (version));
-  g_object_set_data (object, "algorithm", GINT_TO_POINTER (algorithm));
-  g_object_set_data (object, "extractable", GINT_TO_POINTER (extractable));
-
-  g_async_queue_push (self->queue, task);
-}
-
-char *
-matrix_db_lookup_session (MatrixDb          *self,
-                          const char        *account_id,
-                          const char        *account_device,
-                          const char        *session_id,
-                          const char        *sender_key,
-                          MatrixSessionType  type)
-{
-  g_autoptr(GTask) task = NULL;
-  g_autoptr(GError) error = NULL;
-  GObject *object;
-  char *pickle;
-
-  g_return_val_if_fail (MATRIX_IS_DB (self), NULL);
-  g_return_val_if_fail (account_id && *account_id, NULL);
-  g_return_val_if_fail (account_device && *account_device, NULL);
-  g_return_val_if_fail (session_id && *session_id, NULL);
-  g_return_val_if_fail (sender_key && *sender_key, NULL);
-
-  task = g_task_new (self, NULL, NULL, NULL);
-  g_object_ref (task);
-
-  g_task_set_source_tag (task, matrix_db_lookup_session);
-  g_task_set_task_data (task, db_lookup_session, NULL);
-  object = G_OBJECT (task);
-
-  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
-  g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free);
-  g_object_set_data_full (object, "session-id", g_strdup (session_id), g_free);
-  g_object_set_data_full (object, "sender-key", g_strdup (sender_key), g_free);
-  g_object_set_data (object, "type", GINT_TO_POINTER (type));
-
-  g_async_queue_push (self->queue, task);
-  g_assert (task);
-
-  /* Wait until the task is completed */
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-
-  pickle = g_task_propagate_pointer (task, &error);
-
-  if (error)
-    g_debug ("Error getting session: %s", error->message);
-
-  return pickle;
-}
-
-void
-matrix_db_get_olm_sessions_async (MatrixDb            *self,
-                                  GAsyncReadyCallback  callback,
-                                  gpointer             user_data)
-{
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_DB (self));
-
-  task = g_task_new (self, NULL, callback, user_data);
-  g_task_set_source_tag (task, matrix_db_get_olm_sessions_async);
-  g_task_set_task_data (task, history_get_olm_sessions, NULL);
-
-  g_async_queue_push (self->queue, task);
-}
-
-GPtrArray *
-matrix_db_get_olm_sessions_finish (MatrixDb      *self,
-                                   GAsyncResult  *result,
-                                   GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_DB (self), NULL);
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (!error || !*error, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
diff --git a/src/matrix/matrix-db.h b/src/matrix/matrix-db.h
deleted file mode 100644
index 8a0ed253f5c5e966878baf30315dc2923c75db7b..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-db.h
+++ /dev/null
@@ -1,165 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-db.h
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#pragma once
-
-#include <glib.h>
-
-#include "chatty-account.h"
-#include "chatty-message.h"
-
-G_BEGIN_DECLS
-
-typedef struct {
-  char *sender;
-  char *sender_key;
-  char *session_pickle;
-} MatrixDbData;
-
-/* These values shouldn’t be changed. They are used in DB */
-typedef enum {
-  SESSION_OLM_V1_IN      = 1,
-  SESSION_OLM_V1_OUT     = 2,
-  SESSION_MEGOLM_V1_IN   = 3,
-  SESSION_MEGOLM_V1_OUT  = 4,
-} MatrixSessionType;
-
-#define CHATTY_ALGORITHM_A256CTR 1
-
-#define CHATTY_KEY_TYPE_OCT      1
-
-#define MATRIX_TYPE_DB (matrix_db_get_type ())
-
-G_DECLARE_FINAL_TYPE (MatrixDb, matrix_db, MATRIX, DB, GObject)
-
-MatrixDb      *matrix_db_new                           (void);
-void           matrix_db_open_async                    (MatrixDb        *self,
-                                                        char            *dir,
-                                                        const char      *file_name,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_open_finish                   (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-gboolean       matrix_db_is_open                       (MatrixDb        *self);
-void           matrix_db_close_async                   (MatrixDb        *self,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer        user_data);
-gboolean       matrix_db_close_finish                  (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_save_account_async            (MatrixDb        *db,
-                                                        ChattyAccount   *account,
-                                                        gboolean         enabled,
-                                                        char            *pickle,
-                                                        const char      *device_id,
-                                                        const char      *next_batch,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_save_account_finish           (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_load_account_async            (MatrixDb        *db,
-                                                        ChattyAccount   *account,
-                                                        const char      *device_id,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_load_account_finish           (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_save_room_async               (MatrixDb        *self,
-                                                        ChattyAccount   *account,
-                                                        const char      *account_device,
-                                                        const char      *room_id,
-                                                        const char      *prev_batch,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_save_room_finish              (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_load_room_async               (MatrixDb        *self,
-                                                        ChattyAccount   *account,
-                                                        const char      *account_device,
-                                                        const char      *room_id,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-char          *matrix_db_load_room_finish              (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_delete_account_async          (MatrixDb        *self,
-                                                        ChattyAccount   *account,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_delete_account_finish         (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_save_file_url_async           (MatrixDb        *self,
-                                                        ChattyMessage   *message,
-                                                        ChattyFileInfo  *file,
-                                                        int              version,
-                                                        int              algorithm,
-                                                        int              type,
-                                                        gboolean         extractable,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_save_file_url_finish          (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_add_session_async             (MatrixDb        *self,
-                                                        const char      *account_id,
-                                                        const char      *account_device,
-                                                        const char      *room_id,
-                                                        const char      *session_id,
-                                                        const char      *sender_key,
-                                                        char            *pickle,
-                                                        MatrixSessionType type,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-gboolean       matrix_db_add_session_finish            (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-char          *matrix_db_lookup_session                (MatrixDb        *self,
-                                                        const char      *account_id,
-                                                        const char      *account_device,
-                                                        const char      *session_id,
-                                                        const char      *sender_key,
-                                                        MatrixSessionType type);
-void           matrix_db_get_olm_sessions_async        (MatrixDb        *self,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-GPtrArray     *matrix_db_get_olm_sessions_finish       (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_add_olm_session_async         (MatrixDb        *self,
-                                                        const char      *sender,
-                                                        const char      *sender_key,
-                                                        const char      *pickle,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-GPtrArray     *matrix_db_get_messages_finish           (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_get_group_in_sessions_async   (MatrixDb        *self,
-                                                        guint            limit,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-GPtrArray     *matrix_db_group_in_sessions_finish      (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-void           matrix_db_get_group_out_sessions_async  (MatrixDb        *self,
-                                                        guint            limit,
-                                                        GAsyncReadyCallback callback,
-                                                        gpointer         user_data);
-GPtrArray     *matrix_db_get_group_out_sessions_finish (MatrixDb        *self,
-                                                        GAsyncResult    *result,
-                                                        GError         **error);
-
-G_END_DECLS
diff --git a/src/matrix/matrix-enc.c b/src/matrix/matrix-enc.c
deleted file mode 100644
index 145c04c3fc6b9a3bcd6807ae0ac04dc4a16e77b6..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-enc.c
+++ /dev/null
@@ -1,1330 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-enc.c
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#define G_LOG_DOMAIN "chatty-matrix-enc"
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
-
-#include <json-glib/json-glib.h>
-#include <olm/olm.h>
-#include <sys/random.h>
-
-#include "chatty-settings.h"
-#include "chatty-ma-buddy.h"
-#include "matrix-utils.h"
-#include "matrix-db.h"
-#include "matrix-enc.h"
-#include "chatty-log.h"
-
-#define KEY_LABEL_SIZE    6
-#define STRING_ALLOCATION 512
-
-/**
- * SECTION: chatty-contact
- * @title: MatrixEnc
- * @short_description: An abstraction over #FolksIndividual
- * @include: "chatty-contact.h"
- */
-
-/*
- * Documentations:
- *   https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide
- *
- * Other:
- *  * We use g_malloc(size) instead of g_malloc(size * sizeof(type)) for all ‘char’
- *    and ‘[u]int8_t’, unless there is a possibility that the type can change.
- */
-struct _MatrixEnc
-{
-  GObject parent_instance;
-
-  OlmAccount *account;
-  OlmUtility *utility;
-  char       *pickle_key;
-
-  GHashTable *in_olm_sessions;
-  GHashTable *out_olm_sessions;
-  GHashTable *in_group_sessions;
-  GHashTable *out_group_sessions;
-
-  /* Use something better, like sqlite */
-  MatrixDb   *matrix_db;
-
-  char *user_id;
-  char *device_id;
-
-  char *curve_key; /* Public part of Curve25519 identity key */
-  char *ed_key;    /* Public part of Ed25519 fingerprint key */
-};
-
-G_DEFINE_TYPE (MatrixEnc, matrix_enc, G_TYPE_OBJECT)
-
-static void
-free_olm_session (gpointer data)
-{
-  olm_clear_session (data);
-  g_free (data);
-}
-
-static void
-free_in_group_session (gpointer data)
-{
-  olm_clear_inbound_group_session (data);
-  g_free (data);
-}
-
-static void
-free_out_group_session (gpointer data)
-{
-  olm_clear_outbound_group_session (data);
-  g_free (data);
-}
-
-static void
-free_all_details (MatrixEnc *self)
-{
-  if (self->account)
-    olm_clear_account (self->account);
-
-  g_clear_pointer (&self->account, g_free);
-  g_hash_table_remove_all (self->in_olm_sessions);
-  g_hash_table_remove_all (self->out_olm_sessions);
-  g_hash_table_remove_all (self->in_group_sessions);
-  g_hash_table_remove_all (self->out_group_sessions);
-}
-
-static char *
-ma_olm_encrypt (OlmSession *session,
-                const char *plain_text)
-{
-  g_autofree char *encrypted = NULL;
-  g_autofree void *random = NULL;
-  size_t length, rand_len;
-
-  g_assert (session);
-
-  if (!plain_text)
-    return NULL;
-
-  rand_len = olm_encrypt_random_length (session);
-  random = g_malloc (rand_len);
-  getrandom (random, rand_len, GRND_NONBLOCK);
-
-  length = olm_encrypt_message_length (session, strlen (plain_text));
-  encrypted = g_malloc (length + 1);
-  length = olm_encrypt (session, plain_text, strlen (plain_text),
-                        random, rand_len,
-                        encrypted, length);
-
-  if (length == olm_error ()) {
-    g_warning ("Error encrypting: %s", olm_session_last_error (session));
-
-    return NULL;
-  }
-  encrypted[length] = '\0';
-
-  return g_steal_pointer (&encrypted);
-}
-
-static OlmSession *
-ma_create_olm_out_session (MatrixEnc  *self,
-                           const char *curve_key,
-                           const char *one_time_key)
-{
-  g_autofree OlmSession *session = NULL;
-  g_autofree void *buffer = NULL;
-  size_t length, error;
-
-  g_assert (MATRIX_ENC (self));
-
-  if (!curve_key || !one_time_key)
-    return NULL;
-
-  session = g_malloc (olm_session_size ());
-  olm_session (session);
-
-  length = olm_create_outbound_session_random_length (session);
-  buffer = g_malloc (length);
-  getrandom (buffer, length, GRND_NONBLOCK);
-
-  error = olm_create_outbound_session (session,
-                                       self->account,
-                                       curve_key, strlen (curve_key),
-                                       one_time_key, strlen (one_time_key),
-                                       buffer, length);
-  olm_encrypt_message_type (session);
-  if (error == olm_error ()) {
-    g_warning ("Error creating outbound olm session: %s",
-               olm_session_last_error (session));
-    return NULL;
-  }
-
-  return g_steal_pointer (&session);
-}
-
-/*
- * matrix_enc_load_identity_keys:
- * @self: A #MatrixEnc
- *
- * Load the public part of Ed25519 fingerprint
- * key pair and Curve25519 identity key pair.
- */
-static void
-matrix_enc_load_identity_keys (MatrixEnc *self)
-{
-  g_autoptr(JsonParser) parser = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autofree char *key = NULL;
-  JsonObject *object;
-  JsonNode *node;
-  size_t length, err;
-
-  length = olm_account_identity_keys_length (self->account);
-  key = malloc (length + 1);
-  err = olm_account_identity_keys (self->account, key, length);
-  key[length] = '\0';
-
-  if (err == olm_error ()) {
-    g_warning ("error getting identity keys: %s", olm_account_last_error (self->account));
-    return;
-  }
-
-  parser = json_parser_new ();
-  json_parser_load_from_data (parser, key, length, &error);
-
-  if (error) {
-    g_warning ("error parsing keys: %s", error->message);
-    return;
-  }
-
-  node = json_parser_get_root (parser);
-  object = json_node_get_object (node);
-
-  g_free (self->curve_key);
-  g_free (self->ed_key);
-
-  self->curve_key = g_strdup (json_object_get_string_member (object, "curve25519"));
-  self->ed_key = g_strdup (json_object_get_string_member (object, "ed25519"));
-}
-
-static void
-create_new_details (MatrixEnc *self)
-{
-  g_autofree void *buffer = NULL;
-  size_t length, err;
-
-  g_assert (MATRIX_ENC (self));
-
-  CHATTY_TRACE_MSG ("Creating new encryption keys");
-
-  free_all_details (self);
-
-  self->account = g_malloc (olm_account_size ());
-  olm_account (self->account);
-
-  matrix_utils_free_buffer (self->pickle_key);
-  self->pickle_key = g_uuid_string_random ();
-
-  length = olm_create_account_random_length (self->account);
-  buffer = g_malloc (length);
-  getrandom (buffer, length, GRND_NONBLOCK);
-  err = olm_create_account (self->account, buffer, length);
-  if (err == olm_error ())
-    g_warning ("Error creating account: %s", olm_account_last_error (self->account));
-}
-
-static void
-matrix_enc_sign_json_object (MatrixEnc  *self,
-                             JsonObject *object)
-{
-  g_autoptr(GString) str = NULL;
-  g_autofree char *signature = NULL;
-  g_autofree char *label = NULL;
-  JsonObject *sign, *child;
-
-  g_assert (MATRIX_IS_ENC (self));
-  g_assert (object);
-
-  /* The JSON is in canonical form.  Required for signing */
-  /* https://matrix.org/docs/spec/appendices#signing-json */
-  str = matrix_utils_json_get_canonical (object, NULL);
-  signature = matrix_enc_sign_string (self, str->str, str->len);
-
-  sign = json_object_new ();
-  label = g_strconcat ("ed25519:", self->device_id, NULL);
-  json_object_set_string_member (sign, label, signature);
-
-  child = json_object_new ();
-  json_object_set_object_member (child, self->user_id, sign);
-  json_object_set_object_member (object, "signatures", child);
-}
-
-static void
-matrix_enc_finalize (GObject *object)
-{
-  MatrixEnc *self = (MatrixEnc *)object;
-
-  olm_clear_account (self->account);
-  g_free (self->account);
-
-  olm_clear_utility (self->utility);
-  g_free (self->utility);
-
-  g_hash_table_unref (self->in_olm_sessions);
-  g_hash_table_unref (self->out_olm_sessions);
-  g_hash_table_unref (self->in_group_sessions);
-  g_hash_table_unref (self->out_group_sessions);
-  g_free (self->user_id);
-  g_free (self->device_id);
-  matrix_utils_free_buffer (self->pickle_key);
-  matrix_utils_free_buffer (self->curve_key);
-  matrix_utils_free_buffer (self->ed_key);
-  g_clear_object (&self->matrix_db);
-
-  G_OBJECT_CLASS (matrix_enc_parent_class)->finalize (object);
-}
-
-
-static void
-matrix_enc_class_init (MatrixEncClass *klass)
-{
-  GObjectClass *object_class  = G_OBJECT_CLASS (klass);
-
-  object_class->finalize = matrix_enc_finalize;
-}
-
-
-static void
-matrix_enc_init (MatrixEnc *self)
-{
-  self->utility = g_malloc (olm_utility_size ());
-  olm_utility (self->utility);
-
-  self->in_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                 g_free, free_olm_session);
-  self->out_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                  g_free, free_olm_session);
-  self->in_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                   g_free, free_in_group_session);
-  self->out_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                                    g_free, free_out_group_session);
-}
-
-/**
- * matrix_enc_new:
- * @pickle: (nullable): The account pickle
- * @key: @pickle key, can be %NULL if @pickle is %NULL
- *
- * If @pickle is non-null, the olm account is created
- * using the pickled data.  Otherwise a new olm account
- * is created. If @pickle is non-null and invalid
- * %NULL is returned.
- *
- * For @self to be ready for use, the details of @self
- * should be set with matrix_enc_set_details().
- *
- * Also see matrix_enc_get_pickle().
- *
- * Returns: (transfer full) (nullable): A new #MatrixEnc.
- * Free with g_object_unref()
- */
-MatrixEnc *
-matrix_enc_new (gpointer    matrix_db,
-                const char *pickle,
-                const char *key)
-{
-  g_autoptr(MatrixEnc) self = NULL;
-
-  g_return_val_if_fail (!pickle || (*pickle && key && *key), NULL);
-
-  self = g_object_new (MATRIX_TYPE_ENC, NULL);
-  g_set_object (&self->matrix_db, matrix_db);
-
-  /* Deserialize the pickle to create the account */
-  if (pickle && *pickle) {
-    g_autofree char *duped = NULL;
-    size_t err;
-
-    self->pickle_key = g_strdup (key);
-    self->account = g_malloc (olm_account_size ());
-    olm_account (self->account);
-
-    duped = g_strdup (pickle);
-    err = olm_unpickle_account (self->account, key, strlen (key),
-                                duped, strlen (duped));
-
-    if (err == olm_error ()) {
-      g_warning ("Error account unpickle: %s", olm_account_last_error (self->account));
-      return NULL;
-    }
-  } else {
-    create_new_details (self);
-  }
-
-  matrix_enc_load_identity_keys (self);
-
-  return g_steal_pointer (&self);
-}
-
-/**
- * matrix_enc_set_details:
- * @self: A #MatrixEnc
- * @user_id: Fully qualified Matrix user ID
- * @device_id: The device id string
- *
- * Set user id and device id of @self.  @user_id
- * should be fully qualified Matrix user ID
- * (ie, @user:example.com)
- */
-void
-matrix_enc_set_details (MatrixEnc  *self,
-                        const char *user_id,
-                        const char *device_id)
-{
-  g_autofree char *old_user = NULL;
-  g_autofree char *old_device = NULL;
-
-  g_return_if_fail (MATRIX_IS_ENC (self));
-  g_return_if_fail (!user_id || *user_id == '@');
-
-  old_user = self->user_id;
-  old_device = self->device_id;
-
-  self->user_id = g_strdup (user_id);
-  self->device_id = g_strdup (device_id);
-
-  if (self->user_id && old_device &&
-      g_strcmp0 (device_id, old_device) == 0) {
-    create_new_details (self);
-    matrix_enc_load_identity_keys (self);
-  }
-}
-
-char *
-matrix_enc_get_account_pickle (MatrixEnc *self)
-{
-  g_autofree char *pickle = NULL;
-  size_t length, err;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-
-  length = olm_pickle_account_length (self->account);
-  pickle = malloc (length + 1);
-  err = olm_pickle_account (self->account, self->pickle_key,
-                            strlen (self->pickle_key), pickle, length);
-  pickle[length] = '\0';
-
-  if (err == olm_error ()) {
-    g_warning ("Error getting account pickle: %s", olm_account_last_error (self->account));
-
-    return NULL;
-  }
-
-  return g_steal_pointer (&pickle);
-}
-
-char *
-matrix_enc_get_pickle_key (MatrixEnc *self)
-{
-  g_return_val_if_fail (MATRIX_ENC (self), NULL);
-
-  return g_strdup (self->pickle_key);
-}
-
-/**
- * matrix_enc_sign_string:
- * @self: A #MatrixEnc
- * @str: A string to sign
- * @len: The length of @str, or -1
- *
- * Sign @str and return the signature.
- * Returns %NULL on error.
- *
- * Returns: (transfer full): The signature string.
- * Free with g_free()
- */
-char *
-matrix_enc_sign_string (MatrixEnc  *self,
-                        const char *str,
-                        size_t      len)
-{
-  char *signature;
-  size_t length, err;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-  g_return_val_if_fail (str, NULL);
-  g_return_val_if_fail (*str, NULL);
-
-  if (len == (size_t) -1)
-    len = strlen (str);
-
-  length = olm_account_signature_length (self->account);
-  signature = malloc (length + 1);
-  err = olm_account_sign (self->account, str, len, signature, length);
-  signature[length] = '\0';
-
-  if (err == olm_error ()) {
-    g_warning ("Error signing data: %s", olm_account_last_error (self->account));
-
-    return NULL;
-  }
-
-  return signature;
-}
-
-/**
- * matrix_enc_verify:
- * @self: A #MatrixEnc
- * @object: A #JsonObject
- * @matrix_id: A Fully qualified Matrix ID
- * @device_id: The device id string.
- * @ed_key: The ED25519 key of @matrix_id
- *
- * Verify if the content in @object is signed by
- * the user @matrix_id with device @device_id.
- *
- * This function may modify @object by removing
- * "signatures" and "unsigned" members.
- *
- * Returns; %TRUE if verification succeeded.  Or
- * %FALSE otherwise.
- */
-gboolean
-matrix_enc_verify (MatrixEnc  *self,
-                   JsonObject *object,
-                   const char *matrix_id,
-                   const char *device_id,
-                   const char *ed_key)
-{
-  JsonNode *signatures, *non_signed;
-  g_autoptr(GString) json_str = NULL;
-  g_autofree char *signature = NULL;
-  g_autofree char *key_name = NULL;
-  JsonObject *child;
-  size_t error;
-
-  if (!object)
-    return FALSE;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), FALSE);
-  g_return_val_if_fail (matrix_id && *matrix_id == '@', FALSE);
-  g_return_val_if_fail (device_id && *device_id, FALSE);
-  g_return_val_if_fail (ed_key && *ed_key, FALSE);
-
-  /* https://matrix.org/docs/spec/appendices#checking-for-a-signature */
-  key_name = g_strconcat ("ed25519:", device_id, NULL);
-  child = matrix_utils_json_object_get_object (object, "signatures");
-  child = matrix_utils_json_object_get_object (child, matrix_id);
-  signature = g_strdup (matrix_utils_json_object_get_string (child, key_name));
-
-  if (!signature)
-    return FALSE;
-
-  signatures = json_object_dup_member (object, "signatures");
-  non_signed = json_object_dup_member (object, "signatures");
-  json_object_remove_member (object, "signatures");
-  json_object_remove_member (object, "unsigned");
-
-  json_str = matrix_utils_json_get_canonical (object, NULL);
-
-  if (signatures)
-    json_object_set_member (object, "signatures", signatures);
-  if (non_signed)
-    json_object_set_member (object, "unsigned", non_signed);
-
-  error = olm_ed25519_verify (self->utility,
-                              ed_key, ED25519_SIZE,
-                              json_str->str, json_str->len,
-                              signature, strlen (signature));
-
-  /* XXX: the libolm documentation is not much clear on this */
-  if (error == olm_error ()) {
-    g_debug ("Error verifying signature: %s", olm_utility_last_error (self->utility));
-    return FALSE;
-  }
-
-  return TRUE;
-}
-
-/**
- * matrix_enc_max_one_time_keys:
- * @self: A #MatrixEnc
- *
- * Get the maximum number of one time keys Olm
- * library can handle.
- *
- * Returns: The number of maximum one-time keys.
- */
-size_t
-matrix_enc_max_one_time_keys (MatrixEnc *self)
-{
-  g_return_val_if_fail (MATRIX_IS_ENC (self), 0);
-
-  return olm_account_max_number_of_one_time_keys (self->account);
-}
-
-/**
- * matrix_enc_create_one_time_keys:
- * @self: A #MatrixEnc
- * @count: A non-zero number
- *
- * Generate @count number of curve25519 one time keys.
- * @count is capped to the half of what Olm library
- * can handle.
- *
- * Returns: The number of one-time keys generated.
- * It will be <= @count.
- */
-size_t
-matrix_enc_create_one_time_keys (MatrixEnc *self,
-                                 size_t     count)
-{
-  g_autofree void *buffer = NULL;
-  size_t length, err;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), 0);
-  g_return_val_if_fail (count, 0);
-
-  /* doc: The maximum number of active keys supported by libolm
-     is returned by olm_account_max_number_of_one_time_keys.
-     The client should try to maintain about half this number on the homeserver. */
-  count = MIN (count, olm_account_max_number_of_one_time_keys (self->account) / 2);
-
-  length = olm_account_generate_one_time_keys_random_length (self->account, count);
-  buffer = g_malloc (length);
-  getrandom (buffer, length, GRND_NONBLOCK);
-  err = olm_account_generate_one_time_keys (self->account, count, buffer, length);
-
-  if (err == olm_error ()) {
-    g_warning ("Error creating one time keys: %s", olm_account_last_error (self->account));
-
-    return 0;
-  }
-
-  return count;
-}
-
-/**
- * matrix_enc_publish_one_time_keys:
- * @self: A #MatrixEnc
- *
- * Mark current set of one-time keys as published
- */
-void
-matrix_enc_publish_one_time_keys (MatrixEnc *self)
-{
-
-  g_return_if_fail (MATRIX_IS_ENC (self));
-
-  olm_account_mark_keys_as_published (self->account);
-}
-
-/**
- * matrix_enc_get_one_time_keys:
- * @self: A #MatrixEnc
- *
- * Get public part of unpublished Curve25519 one-time keys in @self.
- *
- * The returned data is a JSON-formatted object with the single
- * property curve25519, which is itself an object mapping key id
- * to base64-encoded Curve25519 key. For example:
- *
- * {
- *     "curve25519": {
- *         "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
- *         "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
- *     }
- * }
- *
- * Returns: (nullable) (transfer full): The unpublished one time keys.
- * Free with g_free()
- */
-JsonObject *
-matrix_enc_get_one_time_keys (MatrixEnc *self)
-{
-  g_autoptr(JsonParser) parser = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autofree char *buffer = NULL;
-  size_t length, err;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-
-  length = olm_account_one_time_keys_length (self->account);
-  buffer = g_malloc (length + 1);
-  err = olm_account_one_time_keys (self->account, buffer, length);
-  buffer[length] = '\0';
-
-  if (err == olm_error ()) {
-    g_warning ("Error getting one time keys: %s", olm_account_last_error (self->account));
-
-    return NULL;
-  }
-
-  /* Return NULL if there are no keys */
-  if (g_str_equal (buffer, "{\"curve25519\":{}}"))
-    return NULL;
-
-  parser = json_parser_new ();
-  json_parser_load_from_data (parser, buffer, length, &error);
-
-  if (error) {
-    g_warning ("error parsing keys: %s", error->message);
-    return NULL;
-  }
-
-  return json_node_dup_object (json_parser_get_root (parser));
-}
-
-/**
- * matrix_enc_get_one_time_keys_json:
- * @self: A #MatrixEnc
- *
- * Get the signed Curve25519 one-time keys JSON.  The JSON shall
- * be in the following format:
- *
- * {
- *   "signed_curve25519:AAAAHg": {
- *     "key": "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs",
- *     "signatures": {
- *       "@alice:example.com": {
- *         "ed25519:JLAFKJWSCS": "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/VzDlnfVJ+9jok1Bw"
- *       }
- *     }
- *   },
- *   "signed_curve25519:AAAAHQ": {
- *     "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
- *     "signatures": {
- *       "@alice:example.com": {
- *         "ed25519:JLAFKJWSCS": "IQeCEPb9HFk217cU9kw9EOiusC6kMIkoIRnbnfOh5Oc63S1ghgyjShBGpu34blQomoalCyXWyhaaT3MrLZYQAA"
- *       }
- *     }
- *   }
- * }
- *
- * Returns: (transfer full): A JSON encoded string.
- * Free with g_free()
- */
-char *
-matrix_enc_get_one_time_keys_json (MatrixEnc *self)
-{
-  g_autoptr(JsonObject) object = NULL;
-  g_autoptr(JsonObject) root = NULL;
-  g_autoptr(GList) members = NULL;
-  JsonObject *keys, *child;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-
-  object = matrix_enc_get_one_time_keys (self);
-
-  if (!object)
-    return NULL;
-
-  keys = json_object_new ();
-  object = json_object_get_object_member (object, "curve25519");
-  members = json_object_get_members (object);
-
-  for (GList *item = members; item; item = item->next) {
-    g_autofree char *label = NULL;
-    const char *value;
-
-    child = json_object_new ();
-    value = json_object_get_string_member (object, item->data);
-    json_object_set_string_member (child, "key", value);
-    matrix_enc_sign_json_object (self, child);
-
-    label = g_strconcat ("signed_curve25519:", item->data, NULL);
-    json_object_set_object_member (keys, label, child);
-  }
-
-  root = json_object_new ();
-  json_object_set_object_member (root, "one_time_keys", keys);
-
-  return matrix_utils_json_object_to_string (root, FALSE);
-}
-
-/**
- * matrix_enc_get_one_time_keys:
- * @self: A #MatrixEnc
- *
- * Get the signed device key JSON.  The JSON shall
- * be in the following format:
- *
- * {
- *   "user_id": "@alice:example.com",
- *   "device_id": "JLAFKJWSCS",
- *   "algorithms": [
- *     "m.olm.curve25519-aes-sha256",
- *     "m.megolm.v1.aes-sha2"
- *   ],
- *   "keys": {
- *     "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
- *     "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
- *   },
- *   "signatures": {
- *     "@alice:example.com": {
- *       "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
- *     }
- *   }
- * }
- *
- * Returns: (nullable): A JSON encoded string.
- * Free with g_free()
- */
-char *
-matrix_enc_get_device_keys_json (MatrixEnc *self)
-{
-  g_autoptr(JsonObject) root = NULL;
-  JsonObject *keys, *device_keys;
-  JsonArray *array;
-  char *label;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-  g_return_val_if_fail (self->user_id, NULL);
-  g_return_val_if_fail (self->device_id, NULL);
-
-  device_keys = json_object_new ();
-  json_object_set_string_member (device_keys, "user_id", self->user_id);
-  json_object_set_string_member (device_keys, "device_id", self->device_id);
-
-  array = json_array_new ();
-  json_array_add_string_element (array, ALGORITHM_OLM);
-  json_array_add_string_element (array, ALGORITHM_MEGOLM);
-  json_object_set_array_member (device_keys, "algorithms", array);
-
-  keys = json_object_new ();
-
-  label = g_strconcat ("curve25519:", self->device_id, NULL);
-  json_object_set_string_member (keys, label, self->curve_key);
-  g_free (label);
-
-  label = g_strconcat ("ed25519:", self->device_id, NULL);
-  json_object_set_string_member (keys, label, self->ed_key);
-  g_free (label);
-
-  json_object_set_object_member (device_keys, "keys", keys);
-  matrix_enc_sign_json_object (self, device_keys);
-
-  root = json_object_new ();
-  json_object_set_object_member (root, "device_keys", device_keys);
-
-  return matrix_utils_json_object_to_string (root, FALSE);
-}
-
-static gboolean
-in_olm_matches (gpointer key,
-                gpointer value,
-                gpointer user_data)
-{
-  g_autofree char *body = NULL;
-  size_t match;
-
-  body = g_strdup (user_data);
-  match = olm_matches_inbound_session (value, body, strlen (body));
-
-  if (match == olm_error ()) {
-    g_warning ("Error matching inbound session: %s", olm_session_last_error (key));
-    return FALSE;
-  }
-
-  return match;
-}
-
-static void
-handle_m_room_key (MatrixEnc  *self,
-                   JsonObject *root,
-                   const char *sender_key)
-{
-  g_autofree OlmInboundGroupSession *session = NULL;
-  JsonObject *object;
-  const char *session_key, *session_id, *room_id;
-
-  g_assert (MATRIX_IS_ENC (self));
-  g_assert (root);
-
-  session = g_malloc (olm_inbound_group_session_size ());
-  olm_inbound_group_session (session);
-
-  object = matrix_utils_json_object_get_object (root, "content");
-  session_key = matrix_utils_json_object_get_string (object, "session_key");
-  session_id = matrix_utils_json_object_get_string (object, "session_id");
-  room_id = matrix_utils_json_object_get_string (object, "room_id");
-
-  if (session_key) {
-    size_t error;
-
-    error = olm_init_inbound_group_session (session, (gpointer)session_key,
-                                            strlen (session_key));
-    if (error == olm_error ())
-      g_warning ("Error creating group session from key: %s", olm_inbound_group_session_last_error (session));
-
-    if (!error) {
-      g_autofree char *pickle = NULL;
-      int length;
-
-      length = olm_pickle_inbound_group_session_length (session);
-      pickle = g_malloc (length + 1);
-      olm_pickle_inbound_group_session (session, self->pickle_key,
-                                        strlen (self->pickle_key),
-                                        pickle, length);
-      pickle[length] = '\0';
-      CHATTY_TRACE (room_id, "saving session, room id: ");
-      if (self->matrix_db)
-        matrix_db_add_session_async (self->matrix_db, self->user_id, self->device_id,
-                                     room_id, session_id, sender_key,
-                                     g_steal_pointer (&pickle),
-                                     SESSION_MEGOLM_V1_IN, NULL, NULL);
-      g_hash_table_insert (self->in_group_sessions, g_strdup (session_id),
-                           g_steal_pointer (&session));
-
-    }
-  }
-}
-
-void
-matrix_enc_handle_room_encrypted (MatrixEnc  *self,
-                                  JsonObject *object)
-{
-  const char *algorithm, *sender, *sender_key;
-  g_autofree char *plaintext = NULL;
-  g_autofree char *body = NULL;
-  g_autofree char *copy = NULL;
-  OlmSession *session;
-  size_t error, length;
-  int type;
-
-  g_return_if_fail (MATRIX_IS_ENC (self));
-  g_return_if_fail (object);
-
-  sender = matrix_utils_json_object_get_string (object, "sender");
-  object = matrix_utils_json_object_get_object (object, "content");
-  algorithm = matrix_utils_json_object_get_string (object, "algorithm");
-  sender_key = matrix_utils_json_object_get_string (object, "sender_key");
-
-  if (!algorithm || !sender_key || !sender)
-    g_return_if_reached ();
-
-  if (!g_str_equal (algorithm, ALGORITHM_MEGOLM) &&
-      !g_str_equal (algorithm, ALGORITHM_OLM))
-    g_return_if_reached ();
-
-  object = matrix_utils_json_object_get_object (object, "ciphertext");
-  object = matrix_utils_json_object_get_object (object, self->curve_key);
-
-  body = g_strdup (matrix_utils_json_object_get_string (object, "body"));
-  type = matrix_utils_json_object_get_int (object, "type");
-
-  if (!body)
-    return;
-
-  if (type == OLM_MESSAGE_TYPE_PRE_KEY) {
-    session = g_hash_table_find (self->in_olm_sessions, in_olm_matches, body);
-    CHATTY_TRACE_MSG ("message with pre key received, session exits: %d", !!session);
-
-    if (!session) {
-      g_autofree char *body_copy = NULL;
-      session = g_malloc (olm_session_size ());
-      olm_session (session);
-
-      body_copy = g_strdup (body);
-      error = olm_create_inbound_session_from (session, self->account,
-                                               sender_key, strlen (sender_key),
-                                               body_copy, strlen (body_copy));
-      if (error == olm_error ()) {
-        g_warning ("Error creating session: %s", olm_session_last_error (session));
-        free_olm_session (session);
-
-        return;
-      }
-
-      /* Remove old used keys */
-      error = olm_remove_one_time_keys (self->account, session);
-      if (error == olm_error ())
-        g_warning ("Error removing key: %s", olm_account_last_error (self->account));
-
-      g_hash_table_insert (self->in_olm_sessions, g_strdup (sender_key), session);
-    }
-  } else {
-    CHATTY_TRACE_MSG ("normal message received ");
-
-    session = g_hash_table_lookup (self->in_olm_sessions, sender_key);
-
-    if (!session)
-      session = g_hash_table_lookup (self->out_olm_sessions, sender_key);
-
-    if (!session) {
-      g_warning ("Couldn't find session for normal message");
-      return;
-    }
-  }
-
-  copy = g_strdup (body);
-  length = olm_decrypt_max_plaintext_length (session, type, copy, strlen (copy));
-
-  if (length == olm_error ()) {
-    g_warning ("Error getting max length: %s", olm_session_last_error (session));
-
-    return;
-  }
-
-  plaintext = g_malloc (length + 1);
-  length = olm_decrypt (session, type, body, strlen (body), plaintext, length);
-  if (length == olm_error ()) {
-    g_warning ("Error decrypt session: %s", olm_session_last_error (session));
-
-    return;
-  }
-
-  plaintext[length] = '\0';
-  if (plaintext) {
-    g_autoptr(JsonObject) content = NULL;
-    const char *message_type;
-
-    content = matrix_utils_string_to_json_object (plaintext);
-    message_type = matrix_utils_json_object_get_string (content, "type");
-
-    CHATTY_TRACE_MSG ("message decrypted. type: %s", message_type);
-
-    if (g_strcmp0 (sender, matrix_utils_json_object_get_string (content, "sender")) != 0) {
-      g_warning ("Sender mismatch in encrypted content");
-      return;
-    }
-
-    if (g_strcmp0 (message_type, "m.room_key") == 0)
-      handle_m_room_key (self, content, sender_key);
-  }
-}
-
-char *
-matrix_enc_handle_join_room_encrypted (MatrixEnc  *self,
-                                       const char *room_id,
-                                       JsonObject *object)
-{
-  OlmInboundGroupSession *session = NULL;
-  const char *sender_key;
-  const char *ciphertext, *session_id;
-  g_autofree char *plaintext = NULL;
-  char *body;
-  size_t length;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-  g_return_val_if_fail (object, NULL);
-
-  sender_key = matrix_utils_json_object_get_string (object, "sender_key");
-
-  ciphertext = matrix_utils_json_object_get_string (object, "ciphertext");
-  session_id = matrix_utils_json_object_get_string (object, "session_id");
-  g_return_val_if_fail (ciphertext, NULL);
-
-  if (session_id)
-    session = g_hash_table_lookup (self->in_group_sessions, session_id);
-
-  CHATTY_TRACE_MSG ("Got room encrypted. session exits: %d", !!session);
-
-  if (!session) {
-    g_autofree char *pickle = NULL;
-
-    if (self->matrix_db)
-      pickle = matrix_db_lookup_session (self->matrix_db, self->user_id,
-                                         self->device_id, session_id,
-                                         sender_key, SESSION_MEGOLM_V1_IN);
-    if (pickle) {
-      int err;
-      session = g_malloc (olm_inbound_group_session_size ());
-      err = olm_unpickle_inbound_group_session (session, self->pickle_key,
-                                                strlen (self->pickle_key),
-                                                pickle, strlen (pickle));
-      if (err == olm_error ()) {
-        g_debug ("Error in group unpickle: %s", olm_inbound_group_session_last_error (session));
-        g_free (session);
-        session = NULL;
-      } else {
-        g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), session);
-        CHATTY_TRACE_MSG ("Got session from matrix db");
-      }
-    }
-  }
-
-  if (!session)
-    return NULL;
-
-  g_return_val_if_fail (session, NULL);
-
-  body = g_strdup (ciphertext);
-  length = olm_group_decrypt_max_plaintext_length (session, (gpointer)body, strlen (body));
-  g_free (body);
-
-  plaintext = g_malloc (length + 1);
-  body = g_strdup (ciphertext);
-  length = olm_group_decrypt (session, (gpointer)body, strlen (body),
-                              (gpointer)plaintext, length, NULL);
-  g_free (body);
-
-  if (length == olm_error ()) {
-    g_warning ("Error decrypting: %s", olm_inbound_group_session_last_error (session));
-    return NULL;
-  }
-
-  plaintext[length] = '\0';
-
-  return g_steal_pointer (&plaintext);
-}
-
-JsonObject *
-matrix_enc_encrypt_for_chat (MatrixEnc  *self,
-                             const char *room_id,
-                             const char *message)
-{
-  OlmOutboundGroupSession *session;
-  g_autofree char *encrypted = NULL;
-  g_autofree char *session_id = NULL;
-  JsonObject *root;
-  size_t message_len, length;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-  g_return_val_if_fail (message && *message, NULL);
-
-  session = g_hash_table_lookup (self->out_group_sessions, room_id);
-  g_return_val_if_fail (session, NULL);
-
-  message_len = strlen (message);
-  length = olm_group_encrypt_message_length (session, message_len);
-  encrypted = g_malloc (length + 1);
-  length = olm_group_encrypt (session, (gpointer)message, message_len,
-                              (gpointer)encrypted, length);
-
-  if (length == olm_error ()) {
-    g_warning ("Error encryption: %s", olm_outbound_group_session_last_error (session));
-    return NULL;
-  }
-
-  encrypted[length] = '\0';
-
-  length = olm_outbound_group_session_id_length (session);
-  session_id = g_malloc (length + 1);
-  length = olm_outbound_group_session_id (session, (gpointer)session_id, length);
-  session_id[length] = '\0';
-
-  root = json_object_new ();
-  json_object_set_string_member (root, "algorithm", ALGORITHM_MEGOLM);
-  json_object_set_string_member (root, "sender_key", self->curve_key);
-  json_object_set_string_member (root, "ciphertext", encrypted);
-  json_object_set_string_member (root, "session_id", session_id);
-  json_object_set_string_member (root, "device_id", self->device_id);
-
-  return root;
-}
-
-JsonObject *
-matrix_enc_create_out_group_keys (MatrixEnc  *self,
-                                  const char *room_id,
-                                  GListModel *members_list)
-{
-  g_autofree OlmOutboundGroupSession *session = NULL;
-  g_autofree uint8_t *session_key = NULL;
-  g_autofree uint8_t *session_id = NULL;
-  g_autofree uint8_t *random = NULL;
-  JsonObject *root, *child;
-  BuddyDevice *device;
-  size_t length;
-  size_t error;
-
-  g_return_val_if_fail (MATRIX_IS_ENC (self), FALSE);
-
-  /* Return early if the chat has an existing outbound group session */
-  if (g_hash_table_contains (self->out_group_sessions, room_id))
-    return NULL;
-
-  /* Initialize session */
-  session = g_malloc (olm_outbound_group_session_size ());
-  olm_outbound_group_session (session);
-
-  /* Feed in random bits */
-  length = olm_init_outbound_group_session_random_length (session);
-  random = g_malloc (length);
-  getrandom (random, length, GRND_NONBLOCK);
-  error = olm_init_outbound_group_session (session, random, length);
-  if (error == olm_error ()) {
-    g_warning ("Error init out group session: %s", olm_outbound_group_session_last_error (session));
-
-    return NULL;
-  }
-
-  /* Get session id */
-  length = olm_outbound_group_session_id_length (session);
-  session_id = g_malloc (length + 1);
-  length = olm_outbound_group_session_id (session, session_id, length);
-  if (length == olm_error ()) {
-    g_warning ("Error decrypt session: %s", olm_outbound_group_session_last_error (session));
-
-    return NULL;
-  }
-  session_id[length] = '\0';
-
-  /* Get session key */
-  length = olm_outbound_group_session_key_length (session);
-  session_key = g_malloc (length + 1);
-  length = olm_outbound_group_session_key (session, session_key, length);
-  if (length == olm_error ()) {
-    g_warning ("Error getting session key: %s", olm_outbound_group_session_last_error (session));
-
-    return NULL;
-  }
-  session_key[length] = '\0';
-
-  root = json_object_new ();
-
-  /* https://matrix.org/docs/spec/client_server/r0.6.1#m-room-key */
-  for (guint i = 0; i < g_list_model_get_n_items (members_list); i++) {
-    g_autoptr(ChattyMaBuddy) buddy = NULL;
-    g_autoptr(GList) devices = NULL;
-    const char *curve_key/* , *ed_key */;
-    JsonObject *user;
-
-    buddy = g_list_model_get_item (members_list, i);
-    devices = chatty_ma_buddy_get_devices (buddy);
-
-    user = json_object_new ();
-    json_object_set_object_member (root, chatty_item_get_username (CHATTY_ITEM (buddy)), user);
-
-    for (GList *node = devices; node; node = node->next) {
-      OlmSession *olm_session = NULL;
-      g_autofree char *one_time_key = NULL;
-      JsonObject *content;
-
-      device = node->data;
-      curve_key = chatty_ma_device_get_curve_key (device);
-
-      one_time_key = chatty_ma_device_get_one_time_key (device);
-      olm_session = ma_create_olm_out_session (self, curve_key, one_time_key);
-
-      if (!one_time_key || !curve_key || !olm_session)
-        continue;
-
-      g_hash_table_insert (self->out_olm_sessions, g_strdup (curve_key), olm_session);
-
-      /* Create per device object */
-      child = json_object_new ();
-      json_object_set_object_member (user, chatty_ma_device_get_id (device), child);
-
-      json_object_set_string_member (child, "algorithm", ALGORITHM_OLM);
-      json_object_set_string_member (child, "sender_key", self->curve_key);
-      json_object_set_object_member (child, "ciphertext", json_object_new ());
-
-      content = json_object_new ();
-      child = json_object_get_object_member (child, "ciphertext");
-      g_assert (child);
-      json_object_set_object_member (child, curve_key, content);
-
-      /* Body to be encrypted */
-      {
-        g_autoptr(JsonObject) object = NULL;
-        g_autofree char *encrypted = NULL;
-        g_autofree char *data = NULL;
-
-        /* Create a json object with common data */
-        object = json_object_new ();
-        json_object_set_string_member (object, "type", "m.room_key");
-        json_object_set_string_member (object, "sender", self->user_id);
-        json_object_set_string_member (object, "sender_device", self->device_id);
-
-        child = json_object_new ();
-        json_object_set_string_member (child, "ed25519", self->ed_key);
-        json_object_set_object_member (object, "keys", child);
-
-        child = json_object_new ();
-        json_object_set_string_member (child, "algorithm", "m.megolm.v1.aes-sha2");
-        json_object_set_string_member (child, "room_id", room_id);
-        json_object_set_string_member (child, "session_id", (char *)session_id);
-        json_object_set_string_member (child, "session_key", (char *)session_key);
-        json_object_set_int_member (child, "chain_index", olm_outbound_group_session_message_index (session));
-        json_object_set_object_member (object, "content", child);
-
-        /* User specific data */
-        json_object_set_string_member (object, "recipient", chatty_item_get_username (CHATTY_ITEM (buddy)));
-
-        /* Device specific data */
-        child = json_object_new ();
-        json_object_set_string_member (child, "ed25519", chatty_ma_device_get_ed_key (device));
-        json_object_set_object_member (object, "recipient_keys", child);
-
-        /* Now encrypt the above JSON */
-        data = matrix_utils_json_object_to_string (object, FALSE);
-        encrypted = ma_olm_encrypt (olm_session, data);
-
-        /* Add the encrypted data as the content */
-        json_object_set_int_member (content, "type", olm_encrypt_message_type (olm_session));
-        json_object_set_string_member (content, "body", encrypted);
-      }
-    }
-  }
-
-  /*
-   * We should also create an inbound session with the same key so
-   * that we we'll be able to decrypt the messages we sent (when
-   * we receive them via sync requests)
-   */
-  {
-    OlmInboundGroupSession *in_session;
-
-    in_session = g_malloc (olm_inbound_group_session_size ());
-    olm_inbound_group_session (in_session);
-    olm_init_inbound_group_session (in_session, (gpointer)session_key,
-                                    strlen ((char *)session_key));
-    g_hash_table_insert (self->in_group_sessions,
-                         g_strdup ((char *)session_id), in_session);
-  }
-
-  g_hash_table_insert (self->out_group_sessions,
-                       g_strdup (room_id), g_steal_pointer (&session));
-
-  matrix_utils_clear ((char *)session_key, strlen ((char *)session_key));
-  matrix_utils_clear ((char *)session_id, strlen ((char *)session_id));
-
-  return root;
-}
-
-void
-matrix_file_enc_info_free (MatrixFileEncInfo *enc_info)
-{
-  if (!enc_info)
-    return;
-
-  matrix_utils_free_buffer (enc_info->aes_iv_base64);
-  matrix_utils_free_buffer (enc_info->aes_key_base64);
-  matrix_utils_free_buffer (enc_info->sha256_base64);
-
-  matrix_utils_clear ((char *)enc_info->aes_iv, enc_info->aes_iv_len);
-  matrix_utils_clear ((char *)enc_info->aes_key, enc_info->aes_key_len);
-  matrix_utils_clear ((char *)enc_info->sha256, enc_info->sha256_len);
-  g_free (enc_info->aes_iv);
-  g_free (enc_info->aes_key);
-  g_free (enc_info->sha256);
-
-  g_free (enc_info);
-}
-
-const char *
-matrix_enc_get_curve25519_key (MatrixEnc *self)
-{
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-
-  return self->curve_key;
-}
-
-const char *
-matrix_enc_get_ed25519_key (MatrixEnc *self)
-{
-  g_return_val_if_fail (MATRIX_IS_ENC (self), NULL);
-
-  return self->ed_key;
-}
diff --git a/src/matrix/matrix-enc.h b/src/matrix/matrix-enc.h
deleted file mode 100644
index 4acf1c514a252180e42ac1eed2438c79609b79f9..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-enc.h
+++ /dev/null
@@ -1,88 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-enc.h
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#pragma once
-
-#include <json-glib/json-glib.h>
-#include <glib-object.h>
-
-#include "chatty-chat.h"
-
-G_BEGIN_DECLS
-
-typedef struct _MatrixFileEncInfo MatrixFileEncInfo;
-
-struct _MatrixFileEncInfo {
-  guchar *aes_iv;
-  guchar *aes_key;
-  guchar *sha256;
-
-  char *aes_iv_base64;
-  char *aes_key_base64;
-  char *sha256_base64;
-
-  gsize aes_iv_len;
-  gsize aes_key_len;
-  gsize sha256_len;
-};
-
-#define ALGORITHM_MEGOLM  "m.megolm.v1.aes-sha2"
-#define ALGORITHM_OLM     "m.olm.v1.curve25519-aes-sha2"
-#define CURVE25519_SIZE   43    /* when base64 encoded */
-#define ED25519_SIZE      43    /* when base64 encoded */
-
-#define MATRIX_TYPE_ENC (matrix_enc_get_type ())
-
-G_DECLARE_FINAL_TYPE (MatrixEnc, matrix_enc, MATRIX, ENC, GObject)
-
-MatrixEnc     *matrix_enc_new                        (gpointer      matrix_db,
-                                                      const char   *pickle,
-                                                      const char   *key);
-void           matrix_enc_set_details                (MatrixEnc    *self,
-                                                      const char   *user_id,
-                                                      const char   *device_id);
-char          *matrix_enc_get_account_pickle         (MatrixEnc    *self);
-char          *matrix_enc_get_pickle_key             (MatrixEnc    *self);
-char          *matrix_enc_sign_string                (MatrixEnc    *self,
-                                                      const char   *str,
-                                                      size_t        len);
-gboolean       matrix_enc_verify                     (MatrixEnc    *self,
-                                                      JsonObject   *object,
-                                                      const char   *matrix_id,
-                                                      const char   *device_id,
-                                                      const char   *ed_key);
-size_t         matrix_enc_max_one_time_keys          (MatrixEnc    *self);
-size_t         matrix_enc_create_one_time_keys       (MatrixEnc    *self,
-                                                      size_t        count);
-void           matrix_enc_publish_one_time_keys      (MatrixEnc    *self);
-JsonObject    *matrix_enc_get_one_time_keys          (MatrixEnc    *self);
-char          *matrix_enc_get_one_time_keys_json     (MatrixEnc    *self);
-char          *matrix_enc_get_device_keys_json       (MatrixEnc    *self);
-void           matrix_enc_handle_room_encrypted      (MatrixEnc    *self,
-                                                      JsonObject   *object);
-char          *matrix_enc_handle_join_room_encrypted (MatrixEnc    *self,
-                                                      const char   *room_id,
-                                                      JsonObject   *object);
-JsonObject    *matrix_enc_encrypt_for_chat           (MatrixEnc    *self,
-                                                      const char   *room_id,
-                                                      const char   *message);
-JsonObject    *matrix_enc_create_out_group_keys      (MatrixEnc    *self,
-                                                      const char   *room_id,
-                                                      GListModel   *members_list);
-void           matrix_file_enc_info_free             (MatrixFileEncInfo *enc_info);
-
-G_DEFINE_AUTOPTR_CLEANUP_FUNC (MatrixFileEncInfo, matrix_file_enc_info_free)
-
-/* For tests */
-const char    *matrix_enc_get_curve25519_key     (MatrixEnc    *self);
-const char    *matrix_enc_get_ed25519_key        (MatrixEnc    *self);
-
-G_END_DECLS
diff --git a/src/matrix/matrix-enums.h b/src/matrix/matrix-enums.h
deleted file mode 100644
index ec11579641be5d2a59ac457cf6c0c0fe60f45cc4..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-enums.h
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-enums.h
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#pragma once
-
-
-/**
- * MatrixError:
- *
- * The Error returned by the Matrix Server
- * See https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
- * for details.
- */
-typedef enum {
-  M_FORBIDDEN = 1,
-  M_UNKNOWN_TOKEN,
-  M_MISSING_TOKEN,
-  M_BAD_JSON,
-  M_NOT_JSON,
-  M_NOT_FOUND,
-  M_LIMIT_EXCEEDED,
-  M_UNKNOWN,
-  M_UNRECOGNIZED,
-  M_UNAUTHORIZED,
-  M_USER_DEACTIVATED,
-  M_USER_IN_USE,
-  M_INVALID_USERNAME,
-  M_ROOM_IN_USE,
-  M_INVALID_ROOM_STATE,
-  M_THREEPID_IN_USE,
-  M_THREEPID_NOT_FOUND,
-  M_THREEPID_AUTH_FAILED,
-  M_THREEPID_DENIED,
-  M_SERVER_NOT_TRUSTED,
-  M_UNSUPPORTED_ROOM_VERSION,
-  M_INCOMPATIBLE_ROOM_VERSION,
-  M_BAD_STATE,
-  M_GUEST_ACCESS_FORBIDDEN,
-  M_CAPTCHA_NEEDED,
-  M_CAPTCHA_INVALID,
-  M_MISSING_PARAM,
-  M_INVALID_PARAM,
-  M_TOO_LARGE,
-  M_EXCLUSIVE,
-  M_RESOURCE_LIMIT_EXCEEDED,
-  M_CANNOT_LEAVE_SERVER_NOTICE_ROOM,
-
-  /* Local options */
-  M_BAD_PASSWORD,
-  M_NO_HOME_SERVER,
-  M_BAD_HOME_SERVER,
-} MatrixError;
-
-
-/*
- * MATRIX_BLUE_PILL and MATRIX_RED_PILL
- * are objects than actions
- */
-typedef enum {
-  /* When nothing real is happening */
-  MATRIX_BLUE_PILL,  /* For no/unknown command */
-  MATRIX_GET_HOMESERVER,
-  MATRIX_VERIFY_HOMESERVER,
-  MATRIX_PASSWORD_LOGIN,
-  MATRIX_ACCESS_TOKEN_LOGIN,
-  MATRIX_UPLOAD_KEY,
-  MATRIX_GET_JOINED_ROOMS,
-  MATRIX_SET_TYPING,
-  MATRIX_SEND_MESSAGE,
-  MATRIX_SEND_IMAGE,
-  MATRIX_SEND_VIDEO,
-  MATRIX_SEND_FILE,
-  /* sync: plugged into the Matrix from real world */
-  MATRIX_RED_PILL,
-} MatrixAction;
diff --git a/src/matrix/matrix-filter.json b/src/matrix/matrix-filter.json
deleted file mode 100644
index 1fa90c65bd4051eb3a34481e4d595840f6f151cd..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-filter.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
-  "room": {
-    "timeline": {
-      "limit": 20
-    },
-    "state": {
-      "lazy_load_members": true
-    }
-  }
-}
diff --git a/src/matrix/matrix-net.c b/src/matrix/matrix-net.c
deleted file mode 100644
index d5305cf5d867069051015a1a718b8316d46fc854..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-net.c
+++ /dev/null
@@ -1,610 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-api.c
- *
- * Copyright 2021 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#define G_LOG_DOMAIN "chatty-matrix-net"
-
-#ifdef HAVE_CONFIG_H
-# include "config.h"
-#endif
-
-#define GCRYPT_NO_DEPRECATED
-#include <gcrypt.h>
-#include <libsoup/soup.h>
-#include <json-glib/json-glib.h>
-
-#include "chatty-utils.h"
-#include "matrix-utils.h"
-#include "matrix-enums.h"
-#include "matrix-enc.h"
-#include "matrix-net.h"
-#include "chatty-log.h"
-
-/**
- * SECTION: matrix-net
- * @title: MatrixNet
- * @short_description: Matrix Network related methods
- * @include: "matrix-net.h"
- */
-
-#define MAX_CONNECTIONS     4
-
-struct _MatrixNet
-{
-  GObject         parent_instance;
-
-  SoupSession    *soup_session;
-  SoupSession    *file_session;
-  GCancellable   *cancellable;
-  char           *homeserver;
-  char           *access_token;
-};
-
-
-G_DEFINE_TYPE (MatrixNet, matrix_net, G_TYPE_OBJECT)
-
-static void
-net_download_stream_cb (GObject      *obj,
-                        GAsyncResult *result,
-                        gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  GCancellable *cancellable;
-  GOutputStream *out_stream = NULL;
-  GError *error = NULL;
-  char *buffer, *secret;
-  gsize n_written;
-  gssize n_read;
-
-  g_assert (G_IS_TASK (task));
-
-  n_read = g_input_stream_read_finish (G_INPUT_STREAM (obj), result, &error);
-
-  if (error) {
-    g_task_return_error (task, error);
-
-    return;
-  }
-
-  cancellable = g_task_get_cancellable (task);
-  buffer = g_task_get_task_data (task);
-  secret = g_object_get_data (user_data, "secret");
-  out_stream = g_object_get_data (user_data, "out-stream");
-  g_assert (out_stream);
-
-  if (secret) {
-    gcry_cipher_hd_t cipher_hd;
-    gcry_error_t err;
-
-    cipher_hd = g_object_get_data (user_data, "cipher");
-    g_assert (cipher_hd);
-
-    err = gcry_cipher_decrypt (cipher_hd, secret, n_read, buffer, n_read);
-    if (!err)
-      buffer = secret;
-  }
-
-  g_output_stream_write_all (out_stream, buffer, n_read, &n_written, NULL, NULL);
-  if (n_read == 0 || n_read == -1) {
-    g_output_stream_close (out_stream, cancellable, NULL);
-
-    if (n_read == 0) {
-      g_autoptr(GFile) parent = NULL;
-      GFile *out_file;
-      ChattyFileInfo *file;
-
-      file = g_object_get_data (user_data, "file");
-      out_file = g_object_get_data (user_data, "out-file");
-
-      /* We don't use absolute directory so that the path is user agnostic */
-      parent = g_file_new_build_filename (g_get_user_cache_dir (), "chatty", NULL);
-      file->path = g_file_get_relative_path (parent, out_file);
-    }
-
-    g_task_return_boolean (task, n_read == 0);
-
-    return;
-  }
-
-  buffer = g_task_get_task_data (task);
-  g_input_stream_read_async (G_INPUT_STREAM (obj), buffer, 1024 * 8, G_PRIORITY_DEFAULT, cancellable,
-                             net_download_stream_cb, g_steal_pointer (&task));
-}
-
-static void
-net_get_file_stream_cb  (GObject      *obj,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  MatrixNet *self;
-  g_autoptr(GTask) task = user_data;
-  GCancellable *cancellable;
-  ChattyMessage *message;
-  ChattyFileInfo *file;
-  GInputStream *stream;
-  GError *error = NULL;
-  char *buffer = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_NET (self));
-
-  stream = soup_session_send_finish (SOUP_SESSION (obj), result, &error);
-  file = g_object_get_data (user_data, "file");
-  message = g_object_get_data (user_data, "message");
-  cancellable = g_task_get_cancellable (task);
-
-  if (!error) {
-    GFileOutputStream *out_stream;
-    GFile *out_file;
-    gboolean is_thumbnail = FALSE;
-    g_autofree char *file_name = NULL;
-
-    if (message &&
-        chatty_message_get_preview (message) == file)
-      is_thumbnail = TRUE;
-
-    file_name = g_path_get_basename (file->url);
-
-    /* If @message is NULL, @file is an avatar image */
-    out_file = g_file_new_build_filename (g_get_user_cache_dir (), "chatty", "matrix",
-                                          message ? "files" : "avatars",
-                                          is_thumbnail ? "thumbnail" : "", file_name,
-                                          NULL);
-    out_stream = g_file_append_to (out_file, 0, cancellable, &error);
-    g_object_set_data_full (user_data, "out-file", out_file, g_object_unref);
-    g_object_set_data_full (user_data, "out-stream", out_stream, g_object_unref);
-  }
-
-  if (error) {
-    g_task_return_error (task, error);
-    return;
-  }
-
-  buffer = g_malloc (1024 * 8);
-  g_task_set_task_data (task, buffer, g_free);
-
-  if (message &&
-      chatty_message_get_encrypted (message) && file->user_data) {
-    MatrixFileEncInfo *key;
-    gcry_cipher_hd_t cipher_hd;
-    gcry_error_t err;
-
-    key = file->user_data;
-    err = gcry_cipher_open (&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0);
-
-    if (!err)
-      err = gcry_cipher_setkey (cipher_hd, key->aes_key, key->aes_key_len);
-
-    if (!err)
-      err = gcry_cipher_setctr (cipher_hd, key->aes_iv, key->aes_iv_len);
-
-    if (!err) {
-      char *secret = g_malloc (1024 * 8);
-      g_object_set_data_full (user_data, "secret", secret, g_free);
-      g_object_set_data_full (user_data, "cipher", cipher_hd,
-                              (GDestroyNotify)gcry_cipher_close);
-    }
-  }
-
-  g_input_stream_read_async (stream, buffer, 1024 * 8, G_PRIORITY_DEFAULT, cancellable,
-                             net_download_stream_cb, g_steal_pointer (&task));
-}
-
-static void
-net_load_from_stream_cb (GObject      *object,
-                         GAsyncResult *result,
-                         gpointer      user_data)
-{
-  MatrixNet *self;
-  JsonParser *parser = JSON_PARSER (object);
-  g_autoptr(GTask) task = user_data;
-  JsonNode *root = NULL;
-  GError *error = NULL;
-
-  g_assert (JSON_IS_PARSER (parser));
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_NET (self));
-
-  json_parser_load_from_stream_finish (parser, result, &error);
-
-  if (!error) {
-    root = json_parser_get_root (parser);
-    error = matrix_utils_json_node_get_error (root);
-  }
-
-  if (error) {
-    if (g_error_matches (error, MATRIX_ERROR, M_LIMIT_EXCEEDED) &&
-        root &&
-        JSON_NODE_HOLDS_OBJECT (root)) {
-      JsonObject *obj;
-      guint retry;
-
-      obj = json_node_get_object (root);
-      retry = matrix_utils_json_object_get_int (obj, "retry_after_ms");
-      g_object_set_data (G_OBJECT (task), "retry-after", GINT_TO_POINTER (retry));
-    } else {
-      CHATTY_DEBUG_MSG ("Error loading from stream: %s", error->message);
-    }
-
-    g_task_return_error (task, error);
-    return;
-  }
-
-  if (JSON_NODE_HOLDS_OBJECT (root))
-    g_task_return_pointer (task, json_node_dup_object (root),
-                           (GDestroyNotify)json_object_unref);
-  else if (JSON_NODE_HOLDS_ARRAY (root))
-    g_task_return_pointer (task, json_node_dup_array (root),
-                           (GDestroyNotify)json_array_unref);
-  else
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
-                             "Received invalid data");
-}
-
-static void
-session_send_cb (GObject      *object,
-                 GAsyncResult *result,
-                 gpointer      user_data)
-{
-  MatrixNet *self;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(GInputStream) stream = NULL;
-  g_autoptr(JsonParser) parser = NULL;
-  GCancellable *cancellable;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  self = g_task_get_source_object (task);
-  g_assert (MATRIX_IS_NET (self));
-
-  stream = soup_session_send_finish (self->soup_session, result, &error);
-
-  if (error) {
-    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
-      CHATTY_TRACE_MSG ("Error session send: %s", error->message);
-    g_task_return_error (task, error);
-    return;
-  }
-
-  cancellable = g_task_get_cancellable (task);
-  parser = json_parser_new ();
-  json_parser_load_from_stream_async (parser, stream, cancellable,
-                                      net_load_from_stream_cb,
-                                      g_steal_pointer (&task));
-}
-
-/*
- * queue_data:
- * @data: (transfer full)
- * @size: non-zero if @data is not %NULL
- * @task: (transfer full)
- */
-static void
-queue_data (MatrixNet  *self,
-            char       *data,
-            gsize       size,
-            const char *uri_path,
-            const char *method, /* interned */
-            GHashTable *query,
-            GTask      *task)
-{
-  g_autoptr(SoupMessage) message = NULL;
-  g_autoptr(SoupURI) uri = NULL;
-  GCancellable *cancellable;
-  SoupMessagePriority msg_priority;
-  int priority = 0;
-
-  g_assert (MATRIX_IS_NET (self));
-  g_assert (uri_path && *uri_path);
-  g_assert (method && *method);
-  g_return_if_fail (self->homeserver && *self->homeserver);
-
-  g_assert (method == SOUP_METHOD_GET ||
-            method == SOUP_METHOD_POST ||
-            method == SOUP_METHOD_PUT);
-
-  uri = soup_uri_new (self->homeserver);
-  soup_uri_set_path (uri, uri_path);
-
-  if (self->access_token) {
-    if (!query)
-      query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
-                                     (GDestroyNotify)matrix_utils_free_buffer);
-
-    g_hash_table_replace (query, g_strdup ("access_token"), g_strdup (self->access_token));
-    soup_uri_set_query_from_form (uri, query);
-    g_hash_table_unref (query);
-  }
-
-  message = soup_message_new_from_uri (method, uri);
-  soup_message_headers_append (message->request_headers, "Accept-Encoding", "gzip");
-
-  priority = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "priority"));
-
-  if (priority <= -2)
-    msg_priority = SOUP_MESSAGE_PRIORITY_VERY_LOW;
-  else if (priority == -1)
-    msg_priority = SOUP_MESSAGE_PRIORITY_LOW;
-  else if (priority == 1)
-    msg_priority = SOUP_MESSAGE_PRIORITY_HIGH;
-  else if (priority >= 2)
-    msg_priority = SOUP_MESSAGE_PRIORITY_VERY_HIGH;
-  else
-    msg_priority = SOUP_MESSAGE_PRIORITY_NORMAL;
-
-  soup_message_set_priority (message, msg_priority);
-
-  if (data)
-    soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, data, size);
-
-  cancellable = g_task_get_cancellable (task);
-  g_task_set_task_data (task, g_object_ref (message), g_object_unref);
-  soup_session_send_async (self->soup_session, message, cancellable,
-                           session_send_cb, task);
-}
-
-static void
-matrix_net_finalize (GObject *object)
-{
-  MatrixNet *self = (MatrixNet *)object;
-
-  if (self->cancellable)
-    g_cancellable_cancel (self->cancellable);
-
-  soup_session_abort (self->soup_session);
-  soup_session_abort (self->file_session);
-
-  g_clear_object (&self->cancellable);
-  g_clear_object (&self->soup_session);
-  g_clear_object (&self->file_session);
-
-  g_free (self->homeserver);
-
-  matrix_utils_free_buffer (self->access_token);
-  G_OBJECT_CLASS (matrix_net_parent_class)->finalize (object);
-}
-
-static void
-matrix_net_class_init (MatrixNetClass *klass)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->finalize = matrix_net_finalize;
-}
-
-static void
-matrix_net_init (MatrixNet *self)
-{
-  self->soup_session = g_object_new (SOUP_TYPE_SESSION,
-                                     "max-conns-per-host", MAX_CONNECTIONS,
-                                     NULL);
-  self->file_session = g_object_new (SOUP_TYPE_SESSION,
-                                     "max-conns-per-host", MAX_CONNECTIONS,
-                                     NULL);
-  self->cancellable = g_cancellable_new ();
-}
-
-MatrixNet *
-matrix_net_new (void)
-{
-  return g_object_new (MATRIX_TYPE_NET, NULL);
-}
-
-void
-matrix_net_set_homeserver (MatrixNet  *self,
-                           const char *homeserver)
-{
-  g_return_if_fail (MATRIX_IS_NET (self));
-  g_return_if_fail (homeserver && *homeserver);
-
-  g_free (self->homeserver);
-  self->homeserver = g_strdup (homeserver);
-}
-
-void
-matrix_net_set_access_token (MatrixNet  *self,
-                             const char *access_token)
-{
-  g_return_if_fail (MATRIX_IS_NET (self));
-
-  matrix_utils_free_buffer (self->access_token);
-  self->access_token = g_strdup (access_token);
-}
-
-/**
- * matrix_net_send_data_async:
- * @self: A #MatrixNet
- * @priority: The priority of request, 0 for default
- * @data: (nullable) (transfer full): The data to send
- * @size: The @data size in bytes
- * @uri_path: A string of the matrix uri path
- * @method: An interned string for GET, PUT, POST, etc.
- * @query: (nullable): A query to pass to internal #SoupURI
- * @cancellable: (nullable): A #GCancellable
- * @callback: The callback to run when completed
- * @user_data: user data for @callback
- *
- * Send a JSON data @object to the @uri_path endpoint.
- * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT
- * or %SOUP_METHOD_POST.
- * If @cancellable is %NULL, the internal cancellable
- * shall be used
- */
-void
-matrix_net_send_data_async (MatrixNet           *self,
-                            int                  priority,
-                            char                *data,
-                            gsize                size,
-                            const char          *uri_path,
-                            const char          *method, /* interned */
-                            GHashTable          *query,
-                            GCancellable        *cancellable,
-                            GAsyncReadyCallback  callback,
-                            gpointer             user_data)
-{
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_NET (self));
-  g_return_if_fail (uri_path && *uri_path);
-  g_return_if_fail (method && *method);
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-  g_return_if_fail (callback);
-  g_return_if_fail (self->homeserver && *self->homeserver);
-
-  if (data && *data)
-    g_return_if_fail (size);
-
-  if (!cancellable)
-    cancellable = self->cancellable;
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority));
-
-  queue_data (self, data, size, uri_path, method, query, task);
-}
-
-/**
- * matrix_net_send_json_async:
- * @self: A #MatrixNet
- * @priority: The priority of request, 0 for default
- * @object: (nullable) (transfer full): The data to send
- * @uri_path: A string of the matrix uri path
- * @method: An interned string for GET, PUT, POST, etc.
- * @query: (nullable): A query to pass to internal #SoupURI
- * @cancellable: (nullable): A #GCancellable
- * @callback: The callback to run when completed
- * @user_data: user data for @callback
- *
- * Send a JSON data @object to the @uri_path endpoint.
- * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT
- * or %SOUP_METHOD_POST.
- * If @cancellable is %NULL, the internal cancellable
- * shall be used
- */
-void
-matrix_net_send_json_async (MatrixNet           *self,
-                            int                  priority,
-                            JsonObject          *object,
-                            const char          *uri_path,
-                            const char          *method, /* interned */
-                            GHashTable          *query,
-                            GCancellable        *cancellable,
-                            GAsyncReadyCallback  callback,
-                            gpointer             user_data)
-{
-  GTask *task;
-  char *data = NULL;
-  gsize size = 0;
-
-  g_return_if_fail (MATRIX_IS_NET (self));
-  g_return_if_fail (uri_path && *uri_path);
-  g_return_if_fail (method && *method);
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-  g_return_if_fail (callback);
-  g_return_if_fail (self->homeserver && *self->homeserver);
-
-  if (object) {
-    data = matrix_utils_json_object_to_string (object, FALSE);
-    json_object_unref (object);
-  }
-
-  if (data && *data)
-    size = strlen (data);
-
-  if (!cancellable)
-    cancellable = self->cancellable;
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority));
-
-  queue_data (self, data, size, uri_path, method, query, task);
-}
-
-/**
- * matrix_net_get_file_async:
- * @self: A #MatrixNet
- * @message: (nullable) (transfer full): A #ChattyMessage
- * @file: A #ChattyFileInfo
- * @cancellable: (nullable): A #GCancellable
- * @progress_callback: (nullable): A #GFileProgressCallback
- * @callback: The callback to run when completed
- * @user_data: user data for @callback
- *
- * Download the file @file.  @file path shall be updated
- * after download is completed, and if @file is encrypted
- * and has keys to decrypt the file, the file shall be
- * stored decrypted.
- */
-void
-matrix_net_get_file_async (MatrixNet             *self,
-                           ChattyMessage         *message,
-                           ChattyFileInfo        *file,
-                           GCancellable          *cancellable,
-                           GFileProgressCallback  progress_callback,
-                           GAsyncReadyCallback    callback,
-                           gpointer               user_data)
-{
-  g_autofree char *url = NULL;
-  SoupMessage *msg;
-  GTask *task;
-
-  g_return_if_fail (MATRIX_IS_NET (self));
-  g_return_if_fail (!message || CHATTY_IS_MESSAGE (message));
-  g_return_if_fail (file && file->url);
-
-  if (message)
-    g_object_ref (message);
-
-  if (!cancellable)
-    cancellable = self->cancellable;
-
-  if (g_str_has_prefix (file->url, "mxc://")) {
-    const char *file_url;
-
-    file_url = file->url + strlen ("mxc://");
-    url = g_strconcat (self->homeserver,
-                       "/_matrix/media/r0/download/", file_url, NULL);
-  }
-
-  if (!url)
-    url = g_strdup (file->url);
-
-  msg = soup_message_new (SOUP_METHOD_GET, url);
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_object_set_data (G_OBJECT (task), "progress", progress_callback);
-  g_object_set_data (G_OBJECT (task), "file", file);
-  g_object_set_data_full (G_OBJECT (task), "msg", msg, g_object_unref);
-  g_object_set_data_full (G_OBJECT (task), "message", message, g_object_unref);
-
-  file->status = CHATTY_FILE_DOWNLOADING;
-  if (message)
-    chatty_message_emit_updated (message);
-
-  soup_session_send_async (self->file_session, msg, cancellable,
-                           net_get_file_stream_cb, task);
-}
-
-gboolean
-matrix_net_get_file_finish (MatrixNet     *self,
-                            GAsyncResult  *result,
-                            GError       **error)
-{
-  g_return_val_if_fail (MATRIX_IS_NET (self), FALSE);
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-  g_return_val_if_fail (!error || !*error, FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
diff --git a/src/matrix/matrix-net.h b/src/matrix/matrix-net.h
deleted file mode 100644
index a243f27b998fe5c26999d230055d255eb15245e7..0000000000000000000000000000000000000000
--- a/src/matrix/matrix-net.h
+++ /dev/null
@@ -1,59 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-net.h
- *
- * Copyright 2021 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#pragma once
-
-#include <glib-object.h>
-
-#include "chatty-message.h"
-
-G_BEGIN_DECLS
-
-#define MATRIX_TYPE_NET (matrix_net_get_type ())
-
-G_DECLARE_FINAL_TYPE (MatrixNet, matrix_net, MATRIX, NET, GObject)
-
-MatrixNet *matrix_net_new              (void);
-void       matrix_net_set_homeserver   (MatrixNet         *self,
-                                        const char        *homeserver);
-void       matrix_net_set_access_token (MatrixNet         *self,
-                                        const char        *access_token);
-void       matrix_net_send_data_async  (MatrixNet         *self,
-                                        int                priority,
-                                        char              *data,
-                                        gsize              size,
-                                        const char        *uri_path,
-                                        const char        *method, /* interned */
-                                        GHashTable        *query,
-                                        GCancellable      *cancellable,
-                                        GAsyncReadyCallback callback,
-                                        gpointer           user_data);
-void       matrix_net_send_json_async  (MatrixNet         *self,
-                                        int                priority,
-                                        JsonObject        *object,
-                                        const char        *uri_path,
-                                        const char        *method, /* interned */
-                                        GHashTable        *query,
-                                        GCancellable      *cancellable,
-                                        GAsyncReadyCallback callback,
-                                        gpointer           user_data);
-void       matrix_net_get_file_async   (MatrixNet         *self,
-                                        ChattyMessage     *message,
-                                        ChattyFileInfo    *file,
-                                        GCancellable      *cancellable,
-                                        GFileProgressCallback progress_callback,
-                                        GAsyncReadyCallback callback,
-                                        gpointer           user_data);
-gboolean   matrix_net_get_file_finish  (MatrixNet         *self,
-                                        GAsyncResult      *result,
-                                        GError           **error);
-
-G_END_DECLS
diff --git a/src/matrix/matrix-utils.c b/src/matrix/matrix-utils.c
index 8c7b62b038afde23f03e52c1431942efe5a27097..49331b97085c8cc83d5edb456191343b9e2de4f8 100644
--- a/src/matrix/matrix-utils.c
+++ b/src/matrix/matrix-utils.c
@@ -1,7 +1,7 @@
 /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* matrix-utils.c
  *
- * Copyright 2020 Purism SPC
+ * Copyright 2020, 2022 Purism SPC
  *
  * Author(s):
  *   Mohammed Sadiq <sadiq@sadiqpk.org>
@@ -16,868 +16,12 @@
 # include "config.h"
 #endif
 
-#define __STDC_WANT_LIB_EXT1__ 1
 #include <string.h>
-#include <glib/gi18n.h>
 
-#include "matrix-enums.h"
 #include "chatty-log.h"
 #include "chatty-utils.h"
 #include "matrix-utils.h"
 
-static const char *error_codes[] = {
-  "", /* Index 0 is reserved for no error */
-  "M_FORBIDDEN",
-  "M_UNKNOWN_TOKEN",
-  "M_MISSING_TOKEN",
-  "M_BAD_JSON",
-  "M_NOT_JSON",
-  "M_NOT_FOUND",
-  "M_LIMIT_EXCEEDED",
-  "M_UNKNOWN",
-  "M_UNRECOGNIZED",
-  "M_UNAUTHORIZED",
-  "M_USER_DEACTIVATED",
-  "M_USER_IN_USE",
-  "M_INVALID_USERNAME",
-  "M_ROOM_IN_USE",
-  "M_INVALID_ROOM_STATE",
-  "M_THREEPID_IN_USE",
-  "M_THREEPID_NOT_FOUND",
-  "M_THREEPID_AUTH_FAILED",
-  "M_THREEPID_DENIED",
-  "M_SERVER_NOT_TRUSTED",
-  "M_UNSUPPORTED_ROOM_VERSION",
-  "M_INCOMPATIBLE_ROOM_VERSION",
-  "M_BAD_STATE",
-  "M_GUEST_ACCESS_FORBIDDEN",
-  "M_CAPTCHA_NEEDED",
-  "M_CAPTCHA_INVALID",
-  "M_MISSING_PARAM",
-  "M_INVALID_PARAM",
-  "M_TOO_LARGE",
-  "M_EXCLUSIVE",
-  "M_RESOURCE_LIMIT_EXCEEDED",
-  "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM",
-};
-
-/**
- * matrix_error_quark:
- *
- * Get the Matrix Error Quark.
- *
- * Returns: a #GQuark.
- **/
-G_DEFINE_QUARK (matrix-error-quark, matrix_error)
-
-GError *
-matrix_utils_json_node_get_error (JsonNode *node)
-{
-  JsonObject *object = NULL;
-  const char *error, *err_code;
-
-  if (!node || (!JSON_NODE_HOLDS_OBJECT (node) && !JSON_NODE_HOLDS_ARRAY (node)))
-    return g_error_new (MATRIX_ERROR, M_NOT_JSON,
-                        "Not JSON Object");
-
-  /* Returned by /_matrix/client/r0/rooms/{roomId}/state */
-  if (JSON_NODE_HOLDS_ARRAY (node))
-    return NULL;
-
-  object = json_node_get_object (node);
-  err_code = matrix_utils_json_object_get_string (object, "errcode");
-
-  if (!err_code)
-    return NULL;
-
-  error = matrix_utils_json_object_get_string (object, "error");
-
-  if (!error)
-    error = "Unknown Error";
-
-  if (!g_str_has_prefix (err_code, "M_"))
-    return g_error_new (MATRIX_ERROR, M_UNKNOWN,
-                        "Invalid Error code");
-
-  for (guint i = 0; i < G_N_ELEMENTS (error_codes); i++)
-    if (g_str_equal (error_codes[i], err_code))
-      return g_error_new (MATRIX_ERROR, i, "%s", error);
-
-  return g_error_new (MATRIX_ERROR, M_UNKNOWN,
-                      "Unknown Error");
-}
-
-void
-matrix_utils_clear (char   *buffer,
-                    size_t  length)
-{
-  if (!buffer || length == 0)
-    return;
-
-  /* Brushing up your C: Note: we are not comparing with -1 */
-  if (length == -1)
-    length = strlen (buffer);
-
-#ifdef __STDC_LIB_EXT1__
-  memset_s (buffer, length, 0, length);
-#elif HAVE_EXPLICIT_BZERO
-  explicit_bzero (buffer, length);
-#else
-  volatile char *end = buffer + length;
-
-  while (buffer != end)
-    *(buffer++) = 0;
-#endif
-}
-
-void
-matrix_utils_free_buffer (char *buffer)
-{
-  matrix_utils_clear (buffer, -1);
-  g_free (buffer);
-}
-
-gboolean
-matrix_utils_username_is_complete (const char *username)
-{
-  if (!username || *username != '@')
-    return FALSE;
-
-  if (strchr (username, ':'))
-    return TRUE;
-
-  return FALSE;
-}
-
-const char *
-matrix_utils_get_url_from_username (const char *username)
-{
-  if (!chatty_utils_username_is_valid (username, CHATTY_PROTOCOL_MATRIX))
-    return NULL;
-
-  /* Return the string after ‘:’ */
-  return strchr (username, ':') + 1;
-}
-
-char *
-matrix_utils_json_object_to_string (JsonObject *object,
-                                    gboolean    prettify)
-{
-  g_autoptr(JsonNode) node = NULL;
-
-  g_return_val_if_fail (object, NULL);
-
-  node = json_node_new (JSON_NODE_OBJECT);
-  json_node_init_object (node, object);
-
-  return json_to_string (node, !!prettify);
-}
-
-static void utils_json_canonical_array (JsonArray *array,
-                                        GString   *out);
-static void
-utils_handle_node (JsonNode *node,
-                   GString  *out)
-{
-  GType type;
-
-  g_assert (node);
-  g_assert (out);
-
-  type = json_node_get_value_type (node);
-
-  if (type == JSON_TYPE_OBJECT)
-    matrix_utils_json_get_canonical (json_node_get_object (node), out);
-  else if (type == JSON_TYPE_ARRAY)
-    utils_json_canonical_array (json_node_get_array (node), out);
-  else if (type == G_TYPE_INVALID)
-    g_string_append (out, "null");
-  else if (type == G_TYPE_STRING)
-    g_string_append_printf (out, "\"%s\"", json_node_get_string (node));
-  else if (type == G_TYPE_INT64)
-    g_string_append_printf (out, "%" G_GINT64_FORMAT, json_node_get_int (node));
-  else if (type == G_TYPE_DOUBLE)
-    g_string_append_printf (out, "%f", json_node_get_double (node));
-  else if (type == G_TYPE_BOOLEAN)
-    g_string_append (out, json_node_get_boolean (node) ? "true" : "false");
-  else
-    g_return_if_reached ();
-}
-
-static void
-utils_json_canonical_array (JsonArray *array,
-                            GString   *out)
-{
-  g_autoptr(GList) elements = NULL;
-
-  g_assert (array);
-  g_assert (out);
-
-  g_string_append_c (out, '[');
-  elements = json_array_get_elements (array);
-
-  /* The order of array members shouldn’t be changed */
-  for (GList *item = elements; item; item = item->next) {
-    utils_handle_node (item->data, out);
-
-    if (item->next)
-      g_string_append_c (out, ',');
-  }
-
-  g_string_append_c (out, ']');
-}
-
-GString *
-matrix_utils_json_get_canonical (JsonObject *object,
-                                 GString    *out)
-{
-  g_autoptr(GList) members = NULL;
-
-  g_return_val_if_fail (object, NULL);
-
-  if (!out)
-    out = g_string_sized_new (BUFFER_SIZE);
-
-  g_string_append_c (out, '{');
-
-  members = json_object_get_members (object);
-  members = g_list_sort (members, (GCompareFunc)g_strcmp0);
-
-  for (GList *item = members; item; item = item->next) {
-    JsonNode *node;
-
-    g_string_append_printf (out, "\"%s\":", (char *)item->data);
-
-    node = json_object_get_member (object, item->data);
-    utils_handle_node (node, out);
-
-    if (item->next)
-      g_string_append_c (out, ',');
-  }
-
-  g_string_append_c (out, '}');
-
-  return out;
-}
-
-JsonObject *
-matrix_utils_string_to_json_object (const char *json_str)
-{
-  g_autoptr(JsonParser) parser = NULL;
-  JsonNode *node;
-
-  parser = json_parser_new ();
-  if (!json_parser_load_from_data (parser, json_str, -1, NULL))
-    return NULL;
-
-  node = json_parser_get_root (parser);
-
-  if (!JSON_NODE_HOLDS_OBJECT (node))
-    return NULL;
-
-  return json_node_dup_object (node);
-}
-
-gint64
-matrix_utils_json_object_get_int (JsonObject *object,
-                                  const char *member)
-{
-  JsonNode *node;
-
-  if (!object || !member || !*member)
-    return 0;
-
-  node = json_object_get_member (object, member);
-
-  if (node && JSON_NODE_HOLDS_VALUE (node))
-    return json_node_get_int (node);
-
-  return 0;
-}
-
-gboolean
-matrix_utils_json_object_get_bool (JsonObject *object,
-                                   const char *member)
-{
-  JsonNode *node;
-
-  if (!object || !member || !*member)
-    return FALSE;
-
-  node = json_object_get_member (object, member);
-
-  if (node && JSON_NODE_HOLDS_VALUE (node))
-    return json_node_get_boolean (node);
-
-  return FALSE;
-}
-
-const char *
-matrix_utils_json_object_get_string (JsonObject *object,
-                                     const char *member)
-{
-  JsonNode *node;
-
-  if (!object || !member || !*member)
-    return NULL;
-
-  node = json_object_get_member (object, member);
-
-  if (node && JSON_NODE_HOLDS_VALUE (node))
-    return json_node_get_string (node);
-
-  return NULL;
-}
-
-JsonObject *
-matrix_utils_json_object_get_object (JsonObject *object,
-                                     const char *member)
-{
-  JsonNode *node;
-
-  if (!object || !member || !*member)
-    return NULL;
-
-  node = json_object_get_member (object, member);
-
-  if (node && JSON_NODE_HOLDS_OBJECT (node))
-    return json_node_get_object (node);
-
-  return NULL;
-}
-
-JsonArray *
-matrix_utils_json_object_get_array (JsonObject *object,
-                                    const char *member)
-{
-  JsonNode *node;
-
-  if (!object || !member || !*member)
-    return NULL;
-
-  node = json_object_get_member (object, member);
-
-  if (node && JSON_NODE_HOLDS_ARRAY (node))
-    return json_node_get_array (node);
-
-  return NULL;
-}
-
-JsonObject *
-matrix_utils_get_message_json_object (SoupMessage *message,
-                                      const char  *member)
-{
-  g_autoptr(JsonParser) parser = NULL;
-  g_autoptr(SoupBuffer) buffer = NULL;
-  JsonObject *object = NULL;
-  gboolean is_json;
-
-  if (!message || !message->response_body)
-    return NULL;
-
-  buffer = soup_message_body_flatten (message->response_body);
-  parser = json_parser_new ();
-  is_json = json_parser_load_from_data (parser, buffer->data, buffer->length, NULL);
-
-  if (is_json) {
-    JsonNode *root;
-
-    root = json_parser_get_root (parser);
-
-    if (root && JSON_NODE_HOLDS_OBJECT (root))
-      object = json_node_get_object (root);
-
-    if (member && object)
-      object = json_object_get_object_member (object, member);
-  }
-
-  return object ? json_object_ref (object) : NULL;
-}
-
-static gboolean
-cancel_read_uri (gpointer user_data)
-{
-  g_autoptr(GTask) task = user_data;
-
-  g_assert (G_IS_TASK (task));
-
-  g_object_set_data (G_OBJECT (task), "timeout-id", 0);
-
-  /* XXX: Not thread safe? */
-  if (g_task_get_completed (task) || g_task_had_error (task))
-    return G_SOURCE_REMOVE;
-
-  g_task_set_task_data (task, GINT_TO_POINTER (TRUE), NULL);
-  g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
-                           "Request timeout");
-  g_cancellable_cancel (g_task_get_cancellable (task));
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-load_from_stream_cb (JsonParser   *parser,
-                     GAsyncResult *result,
-                     gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  GError *error = NULL;
-  gboolean timeout;
-
-  g_assert (JSON_IS_PARSER (parser));
-  g_assert (G_IS_TASK (task));
-
-  timeout = GPOINTER_TO_INT (g_task_get_task_data (task));
-
-  /* Task return is handled somewhere else */
-  if (timeout)
-    return;
-
-  if (json_parser_load_from_stream_finish (parser, result, &error))
-    g_task_return_pointer (task, json_parser_steal_root (parser),
-                           (GDestroyNotify)json_node_unref);
-  else
-    g_task_return_error (task, error);
-}
-
-static gboolean
-matrix_utils_handle_ssl_error (SoupMessage *message)
-{
-  GTlsCertificate *cert = NULL;
-  GApplication *app;
-  GtkWidget *dialog;
-  GtkWindow *window = NULL;
-  SoupURI *uri;
-  g_autofree char *msg = NULL;
-  const char *host;
-  GTlsCertificateFlags err_flags;
-  gboolean cancelled = FALSE;
-
-  if (!SOUP_IS_MESSAGE (message) ||
-      !soup_message_get_https_status (message, &cert, &err_flags) ||
-      !err_flags)
-    return cancelled;
-
-  app = g_application_get_default ();
-  if (app)
-    window = gtk_application_get_active_window (GTK_APPLICATION (app));
-
-  if (!window)
-    return cancelled;
-
-  uri = soup_message_get_uri (message);
-  host = soup_uri_get_host (uri);
-
-  switch (err_flags) {
-  case G_TLS_CERTIFICATE_UNKNOWN_CA:
-    if (g_tls_certificate_get_issuer (cert))
-      msg = g_strdup_printf (_("The certificate for ‘%s’ has unknown CA"), host);
-    else
-      msg = g_strdup_printf (_("The certificate for ‘%s’ is self-signed"), host);
-    break;
-
-  case G_TLS_CERTIFICATE_EXPIRED:
-    msg = g_strdup_printf (_("The certificate for ‘%s’ has expired"), host);
-    break;
-
-  case G_TLS_CERTIFICATE_REVOKED:
-    msg = g_strdup_printf (_("The certificate for ‘%s’ has been revoked"), host);
-    break;
-
-  case G_TLS_CERTIFICATE_BAD_IDENTITY:
-  case G_TLS_CERTIFICATE_NOT_ACTIVATED:
-  case G_TLS_CERTIFICATE_INSECURE:
-  case G_TLS_CERTIFICATE_GENERIC_ERROR:
-  case G_TLS_CERTIFICATE_VALIDATE_ALL:
-  default:
-    msg = g_strdup_printf (_("Error validating certificate for ‘%s’"), host);
-  }
-
-  dialog = gtk_message_dialog_new (window,
-                                   GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
-                                   GTK_MESSAGE_QUESTION,
-                                   GTK_BUTTONS_NONE,
-                                   "%s", msg);
-
-  /* XXX: This may not work, see https://gitlab.gnome.org/GNOME/glib-networking/-/issues/32 */
-  if (err_flags == G_TLS_CERTIFICATE_REVOKED)
-    gtk_dialog_add_buttons (GTK_DIALOG (dialog),
-                            _("Close"), GTK_RESPONSE_CLOSE,
-                            NULL);
-  else
-    gtk_dialog_add_buttons (GTK_DIALOG (dialog),
-                            _("Reject"), GTK_RESPONSE_REJECT,
-                            _("Accept"), GTK_RESPONSE_ACCEPT,
-                            NULL);
-
-  if (gtk_dialog_run (GTK_DIALOG (dialog)) != GTK_RESPONSE_ACCEPT)
-    cancelled = TRUE;
-
-  gtk_widget_destroy (dialog);
-
-  return cancelled;
-}
-
-static void
-uri_file_read_cb (GObject      *object,
-                  GAsyncResult *result,
-                  gpointer      user_data)
-{
-  SoupSession *session = (SoupSession *)object;
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(GInputStream) stream = NULL;
-  g_autoptr(JsonParser) parser = NULL;
-  GCancellable *cancellable;
-  SoupMessage *message;
-  GError *error = NULL;
-  gboolean has_timeout;
-  GTlsCertificateFlags err_flags;
-
-  g_assert (G_IS_TASK (task));
-  g_assert (SOUP_IS_SESSION (session));
-
-  stream = soup_session_send_finish (session, result, &error);
-  message = g_object_get_data (G_OBJECT (task), "message");
-  has_timeout = GPOINTER_TO_INT (g_task_get_task_data (task));
-
-  /* Task return is handled somewhere else */
-  if (has_timeout)
-    return;
-
-  if (error) {
-    g_task_return_error (task, error);
-    return;
-  }
-
-  soup_message_get_https_status (message, NULL, &err_flags);
-
-  if (message &&
-      soup_message_get_https_status (message, NULL, &err_flags) &&
-      err_flags) {
-    guint timeout_id, timeout;
-
-    timeout = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout"));
-    timeout_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout-id"));
-    g_clear_handle_id (&timeout_id, g_source_remove);
-    g_object_unref (task);
-
-    if (matrix_utils_handle_ssl_error (message)) {
-      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED,
-                               "Cancelled");
-      return;
-    }
-
-    timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task));
-    g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id));
-  }
-
-  cancellable = g_task_get_cancellable (task);
-  parser = json_parser_new ();
-  json_parser_load_from_stream_async (parser, stream, cancellable,
-                                      (GAsyncReadyCallback)load_from_stream_cb,
-                                      g_steal_pointer (&task));
-}
-
-static void
-message_network_event_cb (SoupMessage        *msg,
-                          GSocketClientEvent  event,
-                          GIOStream          *connection,
-                          gpointer            user_data)
-{
-  GSocketAddress *address;
-
-  /* We shall have a non %NULL @connection by %G_SOCKET_CLIENT_CONNECTING event */
-  if (event != G_SOCKET_CLIENT_CONNECTING)
-    return;
-
-  /* @connection is a #GSocketConnection */
-  address = g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (connection), NULL);
-  g_object_set_data_full (user_data, "address", address, g_object_unref);
-}
-
-void
-matrix_utils_read_uri_async (const char          *uri,
-                             guint                timeout,
-                             GCancellable        *cancellable,
-                             GAsyncReadyCallback  callback,
-                             gpointer             user_data)
-{
-  g_autoptr(SoupSession) session = NULL;
-  g_autoptr(SoupMessage) message = NULL;
-  g_autoptr(GCancellable) cancel = NULL;
-  g_autoptr(GTask) task = NULL;
-  guint timeout_id;
-
-  g_return_if_fail (uri && *uri);
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  if (cancellable)
-    cancel = g_object_ref (cancellable);
-  else
-    cancel = g_cancellable_new ();
-
-  task = g_task_new (NULL, cancel, callback, user_data);
-  /* if this changes to TRUE, we consider it has been timeout */
-  g_task_set_task_data (task, GINT_TO_POINTER (FALSE), NULL);
-  g_task_set_source_tag (task, matrix_utils_read_uri_async);
-
-  timeout = CLAMP (timeout, 5, 60);
-  timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task));
-  g_object_set_data (G_OBJECT (task), "timeout", GUINT_TO_POINTER (timeout));
-  g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id));
-
-  message = soup_message_new (SOUP_METHOD_GET, uri);
-  if (!message) {
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME,
-                             "%s is not a valid uri", uri);
-    return;
-  }
-
-  soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
-  g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), g_object_unref);
-
-  /* XXX: Switch to  */
-  g_signal_connect_object (message, "network-event",
-                           G_CALLBACK (message_network_event_cb), task,
-                           G_CONNECT_AFTER);
-  session = soup_session_new ();
-  g_object_set (G_OBJECT (session), SOUP_SESSION_SSL_STRICT, FALSE, NULL);
-
-  soup_session_send_async (session, message, cancel,
-                           uri_file_read_cb,
-                           g_steal_pointer (&task));
-}
-
-JsonNode *
-matrix_utils_read_uri_finish (GAsyncResult  *result,
-                              GError       **error)
-{
-  GTask *task = (GTask *)result;
-
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (g_task_get_source_tag (task) == matrix_utils_read_uri_async, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-static void
-get_homeserver_cb (GObject      *obj,
-                   GAsyncResult *result,
-                   gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonNode) root = NULL;
-  JsonObject *object = NULL;
-  const char *homeserver = NULL;
-  GError *error = NULL;
-
-  g_assert (G_IS_TASK (task));
-
-  root = matrix_utils_read_uri_finish (result, &error);
-
-  if (!root) {
-    g_task_return_error (task, error);
-    return;
-  }
-
-  g_object_set_data_full (G_OBJECT (task), "address",
-                          g_object_steal_data (G_OBJECT (result), "address"),
-                          g_object_unref);
-
-  if (JSON_NODE_HOLDS_OBJECT (root))
-    object = json_node_get_object (root);
-
-  if (object)
-    object = matrix_utils_json_object_get_object (object, "m.homeserver");
-
-  if (object)
-    homeserver = matrix_utils_json_object_get_string (object, "base_url");
-
-  if (!homeserver)
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
-                             "Got invalid response from server");
-  else
-    g_task_return_pointer (task, g_strdup (homeserver), g_free);
-}
-
-/**
- * matrix_utils_get_homeserver_async:
- * @username: A complete matrix username
- * @timeout: timeout in seconds
- * @cancellable: (nullable): A #GCancellable
- * @callback: The callback to run
- * @user_data: (nullable): The data passed to @callback
- *
- * Get homeserver from the given @username.  @userename
- * should be in complete form (eg: @user:example.org)
- *
- * @timeout is clamped between 5 and 60 seconds.
- *
- * This is a network operation and shall connect to the
- * network to fetch homeserver details.
- *
- * See https://matrix.org/docs/spec/client_server/r0.6.1#server-discovery
- */
-void
-matrix_utils_get_homeserver_async (const char          *username,
-                                   guint                timeout,
-                                   GCancellable        *cancellable,
-                                   GAsyncReadyCallback  callback,
-                                   gpointer             user_data)
-{
-  g_autoptr(GTask) task = NULL;
-  g_autofree char *uri = NULL;
-  const char *url;
-
-  g_return_if_fail (username);
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-  g_return_if_fail (callback);
-
-  task = g_task_new (NULL, cancellable, callback, user_data);
-  g_task_set_source_tag (task, matrix_utils_get_homeserver_async);
-
-  if (!chatty_utils_username_is_valid (username, CHATTY_PROTOCOL_MATRIX)) {
-    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME,
-                             "Username '%s' is not a complete matrix id", username);
-    return;
-  }
-
-  url = matrix_utils_get_url_from_username (username);
-  uri = g_strconcat ("https://", url, "/.well-known/matrix/client", NULL);
-
-  matrix_utils_read_uri_async (uri, timeout, cancellable,
-                               get_homeserver_cb, g_steal_pointer (&task));
-}
-
-/**
- * matrix_utils_get_homeserver_finish:
- * @result: A #GAsyncResult
- * @error: (optional): A #GError
- *
- * Finish call to matrix_utils_get_homeserver_async().
- *
- * Returns: (nullable) : The homeserver string or %NULL
- * on error.  Free with g_free().
- */
-char *
-matrix_utils_get_homeserver_finish (GAsyncResult  *result,
-                                    GError       **error)
-{
-  GTask *task = (GTask *)result;
-
-  g_return_val_if_fail (G_IS_TASK (result), NULL);
-  g_return_val_if_fail (g_task_get_source_tag (task) == matrix_utils_get_homeserver_async, NULL);
-
-  return g_task_propagate_pointer (G_TASK (result), error);
-}
-
-static void
-api_get_version_cb (GObject      *obj,
-                    GAsyncResult *result,
-                    gpointer      user_data)
-{
-  g_autoptr(GTask) task = user_data;
-  g_autoptr(JsonNode) root = NULL;
-  JsonObject *object = NULL;
-  JsonArray *array = NULL;
-  GError *error = NULL;
-  const char *server;
-  gboolean valid;
-
-  g_assert (G_IS_TASK (task));
-
-  server = g_task_get_task_data (task);
-  root = matrix_utils_read_uri_finish (result, &error);
-
-  if (!error && root)
-    error = matrix_utils_json_node_get_error (root);
-
-  if (!root ||
-      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
-      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT)) {
-    if (error)
-      g_task_return_error (task, error);
-    else
-      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
-                               "Failed to get version for server '%s'", server);
-    return;
-  }
-
-  g_object_set_data_full (G_OBJECT (task), "address",
-                          g_object_steal_data (G_OBJECT (result), "address"),
-                          g_object_unref);
-
-  object = json_node_get_object (root);
-  array = matrix_utils_json_object_get_array (object, "versions");
-  valid = FALSE;
-
-  if (array) {
-    g_autoptr(GString) versions = NULL;
-    guint length;
-
-    versions = g_string_new ("");
-    length = json_array_get_length (array);
-
-    for (guint i = 0; i < length; i++) {
-      const char *version;
-
-      version = json_array_get_string_element (array, i);
-      g_string_append_printf (versions, " %s", version);
-
-      /* We have tested only with r0.6.x and r0.5.0 */
-      if (g_str_has_prefix (version, "r0.5.") ||
-          g_str_has_prefix (version, "r0.6."))
-        valid = TRUE;
-    }
-
-    g_log (G_LOG_DOMAIN, CHATTY_LOG_LEVEL_TRACE,
-           "'%s' has versions:%s, valid: %s",
-           server, versions->str, CHATTY_LOG_BOOL (valid));
-  }
-
-  g_task_return_boolean (task, valid);
-}
-
-void
-matrix_utils_verify_homeserver_async (const char          *server,
-                                      guint                timeout,
-                                      GCancellable        *cancellable,
-                                      GAsyncReadyCallback  callback,
-                                      gpointer             user_data)
-{
-  g_autoptr(GTask) task = NULL;
-  g_autofree char *uri = NULL;
-
-  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
-  g_return_if_fail (callback);
-
-  task = g_task_new (NULL, cancellable, callback, user_data);
-  g_task_set_task_data (task, g_strdup (server), g_free);
-  g_task_set_source_tag (task, matrix_utils_verify_homeserver_async);
-
-  if (!server || !*server ||
-      !g_str_has_prefix (server, "http")) {
-    g_task_return_new_error (task, G_IO_ERROR,
-                             G_IO_ERROR_INVALID_DATA,
-                             "URI '%s' is invalid", server);
-    return;
-  }
-
-  uri = g_strconcat (server, "/_matrix/client/versions", NULL);
-  matrix_utils_read_uri_async (uri, timeout, cancellable,
-                               api_get_version_cb,
-                               g_steal_pointer (&task));
-}
-
-gboolean
-matrix_utils_verify_homeserver_finish (GAsyncResult *result,
-                                       GError       **error)
-{
-  g_return_val_if_fail (G_IS_TASK (result), FALSE);
-
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
 static void
 pixbuf_load_stream_cb (GObject      *object,
                        GAsyncResult *result,
diff --git a/src/matrix/matrix-utils.h b/src/matrix/matrix-utils.h
index 4ad1e1acafe8254cb1e5a6276a1284a008f64637..77a05943c2efd40e2a90b43cc10574b88be3977a 100644
--- a/src/matrix/matrix-utils.h
+++ b/src/matrix/matrix-utils.h
@@ -13,58 +13,9 @@
 
 #include <gdk-pixbuf/gdk-pixbuf.h>
 #include <glib-object.h>
-#include <json-glib/json-glib.h>
-#include <libsoup/soup.h>
 
-#define MATRIX_ERROR (matrix_error_quark ())
+#include "cm-enums.h"
 
-GQuark        matrix_error_quark                    (void);
-GError       *matrix_utils_json_node_get_error      (JsonNode      *node);
-void          matrix_utils_clear                    (char          *buffer,
-                                                     size_t         length);
-void          matrix_utils_free_buffer              (char          *buffer);
-gboolean      matrix_utils_username_is_complete     (const char    *username);
-const char   *matrix_utils_get_url_from_username    (const char    *username);
-char         *matrix_utils_json_object_to_string    (JsonObject    *object,
-                                                     gboolean       prettify);
-GString      *matrix_utils_json_get_canonical       (JsonObject    *object,
-                                                     GString       *out);
-JsonObject   *matrix_utils_string_to_json_object    (const char    *json_str);
-gint64        matrix_utils_json_object_get_int      (JsonObject    *object,
-                                                     const char    *member);
-gboolean      matrix_utils_json_object_get_bool     (JsonObject    *object,
-                                                     const char    *member);
-const char   *matrix_utils_json_object_get_string   (JsonObject    *object,
-                                                     const char    *member);
-JsonObject   *matrix_utils_json_object_get_object   (JsonObject    *object,
-                                                     const char    *member);
-JsonArray    *matrix_utils_json_object_get_array    (JsonObject    *object,
-                                                     const char    *member);
-
-JsonObject   *matrix_utils_get_message_json_object  (SoupMessage   *message,
-                                                     const char    *member);
-
-void          matrix_utils_read_uri_async           (const char    *uri,
-                                                     guint          timeout,
-                                                     GCancellable  *cancellable,
-                                                     GAsyncReadyCallback callback,
-                                                     gpointer       user_data);
-JsonNode     *matrix_utils_read_uri_finish          (GAsyncResult  *result,
-                                                     GError       **error);
-void          matrix_utils_get_homeserver_async     (const char    *username,
-                                                     guint          timeout,
-                                                     GCancellable  *cancellable,
-                                                     GAsyncReadyCallback callback,
-                                                     gpointer       user_data);
-void          matrix_utils_verify_homeserver_async  (const char    *server,
-                                                     guint          timeout,
-                                                     GCancellable  *cancellable,
-                                                     GAsyncReadyCallback callback,
-                                                     gpointer       user_data);
-gboolean      matrix_utils_verify_homeserver_finish (GAsyncResult  *result,
-                                                     GError       **error);
-char         *matrix_utils_get_homeserver_finish    (GAsyncResult  *result,
-                                                     GError       **error);
 void          matrix_utils_get_pixbuf_async         (const char    *file,
                                                      GCancellable  *cancellable,
                                                      GAsyncReadyCallback callback,
diff --git a/src/matrix/meson.build b/src/matrix/meson.build
index 18d77c3e7112f5ccf187fed469107c636a08c685..e14688cb42d6b4b50e770d7fa04ebc7885c3390f 100644
--- a/src/matrix/meson.build
+++ b/src/matrix/meson.build
@@ -2,19 +2,9 @@ src_inc += include_directories('.')
 
 libsrc += files([
   'chatty-matrix.c',
-  'matrix-api.c',
-  'matrix-enc.c',
-  'matrix-db.c',
-  'matrix-net.c',
   'matrix-utils.c',
   'chatty-ma-account.c',
   'chatty-ma-buddy.c',
   'chatty-ma-chat.c',
+  'chatty-ma-key-chat.c',
 ])
-
-chatty_deps += [
-  dependency('libgcrypt'),
-  dependency('libsoup-2.4'),
-  dependency('json-glib-1.0'),
-  libolm_dep,
-]
diff --git a/src/meson.build b/src/meson.build
index a133d679783284254b118fcfe4674cc2bda55096..ad89a67cb04fc64a9df3d9a13193f34f40c584e9 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -68,19 +68,21 @@ ui_files = files (
   'ui/chatty-contact-row.ui',
   # FIXME: Testing this fails in CI,
   # but works fine locally
-  # 'ui/chatty-info-dialog.ui',
-  # 'ui/chatty-dialog-join-muc.ui',
+  'ui/chatty-info-dialog.ui',
+  'ui/chatty-dialog-join-muc.ui',
+  # Can't test as this requires chatty-contact-list,
   # 'ui/chatty-dialog-new-chat.ui',
   'ui/chatty-fp-row.ui',
   'ui/chatty-list-row.ui',
   'ui/chatty-message-row.ui',
   # FIXME
-  # 'ui/chatty-pp-account-details.ui',
-  # 'ui/chatty-ma-chat-info.ui',
-  # 'ui/chatty-pp-user-info.ui',
-  # 'ui/chatty-mm-chat-info.ui',
-  # 'ui/chatty-settings-dialog.ui',
-  # We can't test with GdTaggedEntry
+  'ui/chatty-pp-account-details.ui',
+  'ui/chatty-ma-chat-info.ui',
+  'ui/chatty-pp-chat-info.ui',
+  'ui/chatty-mm-chat-info.ui',
+  'ui/chatty-settings-dialog.ui',
+  # Can't test as this requires chatty-chat-list,
+  # which is not in libsrc
   # 'ui/chatty-window.ui',
 )
 
@@ -90,6 +92,10 @@ chatty_sources += [
   'chatty-application.c',
   'chatty-chat-list.c',
   'chatty-contact-list.c',
+  'chatty-header-bar.c',
+  'chatty-main-view.c',
+  'chatty-invite-view.c',
+  'chatty-verification-view.c',
   'chatty-window.c',
   'dialogs/chatty-info-dialog.c',
   'dialogs/chatty-settings-dialog.c',
@@ -101,7 +107,7 @@ chatty_sources += [
 libphonenumber_dep = cc.find_library('phonenumber', required: true)
 
 chatty_deps += [
-  dependency('gio-2.0', version: '>= 2.50'),
+  dependency('gio-2.0', version: '>= 2.66'),
   dependency('gtk+-3.0', version: '>= 3.22'),
   libgd_dep,
   dependency('libsecret-1'),
@@ -116,6 +122,7 @@ chatty_deps += [
   libfeedback_dep,
   libm_dep,
   libphonenumber_dep,
+  libcmatrix_dep,
 ]
 
 gnome = import('gnome')
@@ -125,16 +132,24 @@ resources = gnome.compile_resources('chatty-resources',
   c_name: 'chatty'
 )
 
-libchatty = both_libraries(
-  'chatty', libsrc, resources,
+libchatty_shared = shared_library(
+  'chatty', [libsrc, 'library.c'],
+  resources,
   include_directories: src_inc,
   install: false,
   dependencies: chatty_deps,
 )
 
+libchatty_static = static_library(
+  'chatty', libsrc,
+  resources,
+  include_directories: src_inc,
+  dependencies: chatty_deps,
+)
+
 gtk_builder_tool = find_program('gtk-builder-tool', required: false)
 if gtk_builder_tool.found()
-  preload_env = 'LD_PRELOAD=@0@:libhandy-1.so'.format(libchatty.get_shared_lib().full_path())
+  preload_env = 'LD_PRELOAD=libhandy-1.so:@0@'.format(libchatty_shared.full_path())
   foreach file: ui_files
     test('Validate @0@'.format(file), gtk_builder_tool,
          env: [preload_env],
@@ -145,7 +160,7 @@ endif
 executable('chatty', chatty_sources, resources,
   include_directories: src_inc,
   dependencies: chatty_deps,
-  link_with: libchatty.get_static_lib(),
+  link_with: libchatty_static,
   install: true,
   install_rpath: purple_plugdir,
 )
diff --git a/src/mm/chatty-mm-account.c b/src/mm/chatty-mm-account.c
index 978265986a02aeb19c96c2445b8b194191bb6ce1..6e9ee3d628469a21e92fab4c53721a93459269e3 100644
--- a/src/mm/chatty-mm-account.c
+++ b/src/mm/chatty-mm-account.c
@@ -178,9 +178,11 @@ create_sorted_numbers (const char *numbers,
   /* Make the array bigger so that we can assure it's NULL terminated */
   g_ptr_array_set_size (sorted, sorted->len + 1);
 
-  for (guint i = 0; i < sorted->len - 1; i++) {
+  for (guint i = 0; i < sorted->len - 1;) {
     if (g_strcmp0 (sorted->pdata[i], sorted->pdata[i + 1]) == 0)
       g_ptr_array_remove_index (sorted, i);
+    else
+      i++;
   }
 
   return g_strjoinv (",", (char **)sorted->pdata);
diff --git a/src/mm/chatty-mmsd.c b/src/mm/chatty-mmsd.c
index b5f0347a390593dcdff86249f096511d96fc8808..195aa0e93b874e9739a7c8303a7b2ab4284d514e 100644
--- a/src/mm/chatty-mmsd.c
+++ b/src/mm/chatty-mmsd.c
@@ -1508,6 +1508,93 @@ chatty_mmsd_service_added_cb (ChattyMmsd *self,
   chatty_mmsd_connect_to_service (self, parameters);
 }
 
+enum mms_tx_rx_error {
+  MMS_TX_RX_ERROR_UNKNOWN,
+  MMS_TX_RX_ERROR_DNS,
+  MMS_TX_RX_ERROR_HTTP
+};
+
+static void
+chatty_mmsd_service_send_error_cb (ChattyMmsd *self,
+                                   GVariant   *parameters)
+{
+  g_autoptr(GVariant) error_props = NULL;
+  g_autofree char *recipientlist = NULL;
+  g_autofree char *mms_path = NULL;
+  g_autofree char *sender = NULL;
+  g_autofree char *host = NULL;
+  unsigned int error_type;
+  mms_payload *payload;
+  ChattyMessage *message;
+  GVariantDict dict;
+
+  g_variant_get (parameters, "(@a{?*})", &error_props);
+  g_variant_dict_init (&dict, error_props);
+
+  if (!g_variant_dict_lookup (&dict, "ErrorType", "u", &error_type))
+    error_type = MMS_TX_RX_ERROR_UNKNOWN;
+
+  g_variant_dict_lookup (&dict, "Host", "s", &host);
+  g_variant_dict_lookup (&dict, "MessagePath", "s", &mms_path);
+
+  g_warning ("Error sending MMS of type %u, host %s, Chatty message uid %s",
+             error_type,
+             host,
+             mms_path);
+
+  payload = g_hash_table_lookup (self->mms_hash_table, mms_path);
+
+  if (payload == NULL) {
+     g_debug ("MMS not found. Was it already deleted?");
+     return;
+  }
+
+  message = payload->message;
+  sender = g_strdup (payload->sender);
+  recipientlist = g_strdup (payload->chat);
+  g_return_if_fail (recipientlist && *recipientlist);
+
+  chatty_message_set_status (payload->message,
+                             CHATTY_STATUS_SENDING_FAILED,
+                             0);
+
+  if (!chatty_mm_account_recieve_mms_cb (self->mm_account,
+                                         message,
+                                         sender,
+                                         recipientlist)) {
+     g_debug ("Message was deleted!");
+     return;
+  }
+  /* TODO: Have a user notification that the send failed */
+
+}
+
+static void
+chatty_mmsd_service_receive_error_cb (ChattyMmsd *self,
+                                      GVariant   *parameters)
+{
+  g_autoptr(GVariant) error_props = NULL;
+  g_autofree char *mms_from = NULL;
+  g_autofree char *host = NULL;
+  unsigned int error_type;
+  GVariantDict dict;
+
+  g_variant_get (parameters, "(@a{?*})", &error_props);
+  g_variant_dict_init (&dict, error_props);
+
+  if (!g_variant_dict_lookup (&dict, "ErrorType", "u", &error_type))
+    error_type = MMS_TX_RX_ERROR_UNKNOWN;
+
+  g_variant_dict_lookup (&dict, "Host", "s", &host);
+  g_variant_dict_lookup (&dict, "From", "s", &mms_from);
+
+  /* TODO: Notify user of issue */
+  g_warning ("Error receiving MMS of type %u, host %s, from %s",
+             error_type,
+             host,
+             mms_from);
+}
+
 static void
 chatty_mmsd_service_removed_cb (ChattyMmsd *self,
                                 GVariant   *parameters)
@@ -1717,6 +1804,14 @@ chatty_mmsd_signal_emitted_cb (GDBusConnection *connection,
            g_strcmp0 (interface_name, MMSD_MANAGER_INTERFACE) == 0 &&
            g_strcmp0 (object_path, MMSD_PATH) == 0)
     chatty_mmsd_service_removed_cb (self, parameters);
+  else if (g_strcmp0 (signal_name, "MessageSendError") == 0 &&
+           g_strcmp0 (interface_name, MMSD_SERVICE_INTERFACE) == 0 &&
+           g_strcmp0 (object_path, MMSD_MODEMMANAGER_PATH) == 0)
+    chatty_mmsd_service_send_error_cb (self, parameters);
+  else if (g_strcmp0 (signal_name, "MessageReceiveError") == 0 &&
+           g_strcmp0 (interface_name, MMSD_SERVICE_INTERFACE) == 0 &&
+           g_strcmp0 (object_path, MMSD_MODEMMANAGER_PATH) == 0)
+    chatty_mmsd_service_receive_error_cb (self, parameters);
   else if (g_strcmp0 (signal_name, "MessageAdded") == 0 &&
            g_strcmp0 (interface_name, MMSD_SERVICE_INTERFACE) == 0 &&
            g_strcmp0 (object_path, MMSD_MODEMMANAGER_PATH) == 0)
diff --git a/src/mm/chatty-sms-uri.c b/src/mm/chatty-sms-uri.c
index 6259702d11aa22274de643106e0ca146d7c14668..f8330dc2dc6201b7651a54deb64f6f5a3f8e0ac6 100644
--- a/src/mm/chatty-sms-uri.c
+++ b/src/mm/chatty-sms-uri.c
@@ -86,9 +86,11 @@ chatty_sms_parse_numbers (ChattySmsUri *self)
   /* Make the array bigger so that we can assure it's NULL terminated */
   g_ptr_array_set_size (array, array->len + 1);
 
-  for (guint i = 0; i < array->len - 1; i++) {
+  for (guint i = 0; i < array->len - 1;) {
     if (g_strcmp0 (array->pdata[i], array->pdata[i + 1]) == 0)
       g_ptr_array_remove_index (array, i);
+    else
+      i++;
   }
 
   self->numbers_str = g_strjoinv (",", (char **)array->pdata);
diff --git a/src/purple/chatty-purple.c b/src/purple/chatty-purple.c
index 43657e9a156883045d32f019ca200f73f11818e7..46e240ce02499ae68434faa2b34d9af0138529a4 100644
--- a/src/purple/chatty-purple.c
+++ b/src/purple/chatty-purple.c
@@ -503,8 +503,7 @@ purple_account_added_cb (PurpleAccount *pp_account,
   protocol_id = purple_account_get_protocol_id (pp_account);
 
   /* We handles matrix accounts native. */
-  if (chatty_settings_get_experimental_features (chatty_settings_get_default ()) &&
-      g_strcmp0 (protocol_id, "prpl-matrix") == 0) {
+  if (g_strcmp0 (protocol_id, "prpl-matrix") == 0) {
     return;
   }
 
@@ -1725,10 +1724,8 @@ chatty_purple_load_plugins (ChattyPurple *self)
   chatty_xeps_init ();
   settings = chatty_settings_get_default ();
 
-  if (chatty_settings_get_experimental_features (settings))
-    chatty_purple_unload_plugin (purple_plugins_find_with_id ("prpl-matrix"));
-
-  /* We now have native SMS */
+  /* We now have native Matrix and SMS */
+  chatty_purple_unload_plugin (purple_plugins_find_with_id ("prpl-matrix"));
   chatty_purple_unload_plugin (purple_plugins_find_with_id ("prpl-mm-sms"));
 
   g_signal_connect_object (settings, "notify::message-carbons",
diff --git a/src/purple/meson.build b/src/purple/meson.build
index adab23478a4dec96f9ecbb11f1d2e6accb4c2b69..68fc29db378f14b73eb8b3fd197e7aabd2461fc4 100644
--- a/src/purple/meson.build
+++ b/src/purple/meson.build
@@ -4,7 +4,7 @@ if (not purple_dep.found())
   subdir_done()
 endif
 
-purple_plugdir = purple_dep.get_pkgconfig_variable('plugindir')
+purple_plugdir = purple_dep.get_variable(pkgconfig: 'plugindir')
 jabber = meson.get_compiler('c').find_library('jabber', dirs: purple_plugdir)
 jabber_incdir = include_directories('xeps/prpl/jabber')
 src_inc += jabber_incdir
diff --git a/src/ui/chatty-chat-view.ui b/src/ui/chatty-chat-view.ui
index 61dd53cf4114d00edf30fc86705b28a71d6201de..d4ce7b755eecb232863cbaa3a3c3435416ca24cf 100644
--- a/src/ui/chatty-chat-view.ui
+++ b/src/ui/chatty-chat-view.ui
@@ -227,12 +227,6 @@
       </object>
     </child>
 
-    <child>
-      <object class="GtkBox" id="empty_view">
-        <property name="visible">True</property>
-      </object>
-    </child>
-
   </template>
 
   <object class="GtkTextBuffer" id="message_input_buffer">
diff --git a/src/ui/chatty-header-bar.ui b/src/ui/chatty-header-bar.ui
new file mode 100644
index 0000000000000000000000000000000000000000..b8fb9da3087e013e474cfbc793f829ed7f07ab20
--- /dev/null
+++ b/src/ui/chatty-header-bar.ui
@@ -0,0 +1,407 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ChattyHeaderBar" parent="GtkBox">
+    <property name="orientation">vertical</property>
+
+    <child>
+      <object class="HdyLeaflet" id="leaflet">
+        <property name="visible">1</property>
+        <property name="vexpand">0</property>
+
+        <child>
+          <object class="HdyHeaderBar" id="main_header_bar">
+            <property name="visible">1</property>
+            <property name="show-close-button">1</property>
+            <property name="title" translatable="yes">Chats</property>
+
+            <child>
+              <object class="GtkButton" id="back_button">
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <signal name="clicked" handler="header_back_clicked_cb" swapped="yes"/>
+                <child internal-child="accessible">
+                  <object class="AtkObject">
+                    <property name="accessible-name" translatable="yes">Back</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">go-previous-symbolic</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkMenuButton" id="add_chat_button">
+                <property name="visible">1</property>
+                <property name="sensitive">0</property>
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <property name="popover">new_message_popover</property>
+                <child internal-child="accessible">
+                  <object class="AtkObject">
+                    <property name="accessible-name" translatable="yes">Add Chat</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">list-add-symbolic</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkMenuButton">
+                <property name="visible" bind-source="add_chat_button" bind-property="visible" bind-flags="sync-create"/>
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <property name="popover">main_menu_popover</property>
+                <child internal-child="accessible">
+                  <object class="AtkObject">
+                    <property name="accessible-name" translatable="yes">Menu</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">open-menu-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+
+            <child>
+              <object class="GtkToggleButton" id="search_button">
+                <property name="visible">1</property>
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <child internal-child="accessible">
+                  <object class="AtkObject">
+                    <property name="accessible-name" translatable="yes">Search</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">system-search-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+
+          </object>
+          <packing>
+            <!-- The name should match the name for the sidebar in main window -->
+            <!-- so that we can bind them by name -->
+            <property name="name">sidebar</property>
+          </packing>
+        </child>
+
+        <child>
+          <object class="GtkSeparator">
+            <property name="visible">1</property>
+            <style>
+              <class name="sidebar"/>
+            </style>
+          </object>
+        </child>
+
+        <child>
+          <object class="HdyHeaderBar" id="content_header_bar">
+            <property name="visible">1</property>
+            <property name="expand">1</property>
+            <property name="show-close-button">1</property>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">1</property>
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <property name="visible" bind-source="leaflet" bind-property="folded" bind-flags="sync-create"/>
+                <signal name="clicked" handler="header_back_clicked_cb" swapped="yes"/>
+                <child internal-child="accessible">
+                  <object class="AtkObject">
+                    <property name="accessible-name" translatable="yes">Back</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">go-previous-symbolic</property>
+                    <property name="icon-size">1</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack-type">start</property>
+              </packing>
+            </child>
+            <child type="title">
+              <object class="GtkBox">
+                <property name="visible">1</property>
+                <property name="orientation">horizontal</property>
+                <property name="halign">center</property>
+                <property name="valign">center</property>
+                <child>
+                  <object class="ChattyAvatar" id="content_avatar">
+                    <property name="halign">center</property>
+                    <property name="size">28</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="content_title">
+                    <property name="visible">1</property>
+                    <property name="margin-start">6</property>
+                    <property name="halign">center</property>
+                    <property name="ellipsize">end</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkMenuButton" id="content_menu_button">
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <property name="popover">content_menu_popover</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">view-more-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">0</property>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="call_button">
+                <property name="visible">0</property>
+                <property name="can-focus">0</property>
+                <property name="receives-default">0</property>
+                <property name="action-name">win.call-user</property>
+                <child internal-child="accessible">
+                  <object class="AtkObject">
+                    <property name="accessible-name" translatable="yes">Call</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">1</property>
+                    <property name="icon-name">call-start-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">1</property>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <!-- The name should match the name for the content widget in main window -->
+            <!-- so that we can bind them by name -->
+            <property name="name">content</property>
+          </packing>
+        </child>
+
+      </object>
+    </child>
+
+  </template>
+
+  <object class="GtkPopoverMenu" id="main_menu_popover">
+    <child>
+      <object class="GtkBox">
+        <property name="visible">1</property>
+        <property name="margin">12</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">1</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes" context="show archived chat list when clicked">Archived</property>
+            <property name="action-name">win.show-archived</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">1</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Preferences</property>
+            <property name="action-name">win.show-settings</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">1</property>
+            <property name="text" translatable="yes">Keyboard _Shortcuts</property>
+            <property name="action-name">win.show-help-overlay</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">1</property>
+            <property name="text" translatable="yes">Help</property>
+	    <property name="action-name">app.help</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">1</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">About Chats</property>
+	    <property name="action-name">app.about</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+
+  <object class="GtkPopoverMenu" id="new_message_popover">
+    <property name="can-focus">0</property>
+    <property name="relative-to">add_chat_button</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">1</property>
+        <property name="margin">12</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton" id="new_chat_button">
+            <property name="visible">1</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">New Message…</property>
+            <property name="action-name">win.new-chat</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="new_sms_mms_button">
+            <property name="visible">1</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">New SMS/MMS Message…</property>
+            <property name="action-name">win.new-sms-mms</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="new_group_chat_button">
+            <property name="visible">1</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">New Group Message…</property>
+            <property name="action-name">win.new-group-chat</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+
+  <object class="GtkPopoverMenu" id="content_menu_popover">
+    <child>
+      <object class="GtkBox">
+        <property name="visible">1</property>
+        <property name="margin">12</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">1</property>
+            <property name="active">0</property>
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Chat Details</property>
+	    <property name="action-name">win.show-chat-details</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSeparator">
+            <property name="visible">1</property>
+            <property name="margin-top">6</property>
+            <property name="margin-bottom">6</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="leave_button">
+            <property name="visible">1</property>
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Leave Chat</property>
+	    <property name="action-name">win.leave-chat</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="block_button">
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Block Contact</property>
+	    <property name="action-name">win.block-chat</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="unblock_button">
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Unblock Contact</property>
+	    <property name="action-name">win.unblock-chat</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="archive_button">
+            <property name="visible">1</property>
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Archive chat</property>
+	    <property name="action-name">win.archive-chat</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="unarchive_button">
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Unarchive chat</property>
+	    <property name="action-name">win.unarchive-chat</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="delete_button">
+            <property name="visible">1</property>
+            <property name="can-focus">0</property>
+            <property name="receives-default">0</property>
+            <property name="text" translatable="yes">Delete Chat</property>
+	    <property name="action-name">win.delete-chat</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+
+  <object class="GtkSizeGroup" id="sidebar_group">
+    <widgets>
+      <widget name="main_header_bar"/>
+    </widgets>
+  </object>
+
+  <object class="GtkSizeGroup" id="content_group">
+    <widgets>
+      <widget name="content_header_bar"/>
+    </widgets>
+  </object>
+
+  <object class="HdyHeaderGroup" id="header_group">
+    <headerbars>
+      <headerbar name="main_header_bar"/>
+      <headerbar name="content_header_bar"/>
+    </headerbars>
+  </object>
+
+</interface>
diff --git a/src/ui/chatty-invite-view.ui b/src/ui/chatty-invite-view.ui
new file mode 100644
index 0000000000000000000000000000000000000000..634409491a4e0439c76bca629da35385e3723852
--- /dev/null
+++ b/src/ui/chatty-invite-view.ui
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ChattyInviteView" parent="GtkBox">
+
+    <child>
+      <object class="HdyClamp">
+        <property name="visible">1</property>
+        <property name="hexpand">1</property>
+        <property name="margin-top">18</property>
+        <property name="margin-bottom">18</property>
+        <property name="margin-start">18</property>
+        <property name="margin-end">18</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+
+        <child>
+          <object class="GtkBox">
+            <property name="visible">1</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">18</property>
+
+            <child>
+              <object class="GtkLabel" id="invite_title">
+                <property name="visible">1</property>
+                <property name="wrap">1</property>
+                <property name="justify">center</property>
+                <style>
+                  <class name="large-title"/>
+                </style>
+              </object>
+            </child>
+
+            <child>
+              <object class="ChattyAvatar" id="chat_avatar">
+                <property name="visible">1</property>
+                <property name="width-request">96</property>
+                <property name="height-request">96</property>
+              </object>
+            </child>
+
+            <!-- todo: when we have inviter details -->
+            <child>
+              <object class="GtkLabel" id="invite_subtitle">
+                <property name="visible">0</property>
+                <property name="wrap">1</property>
+                <property name="justify">center</property>
+                <style>
+                  <class name="heading"/>
+                </style>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkButton" id="accept_button">
+                <property name="visible">1</property>
+                <signal name="clicked" handler="invite_accept_clicked_cb" swapped="yes"/>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+
+                <!-- todo: Use GtkCenterBox when porting to GTK4 -->
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">6</property>
+                    <property name="halign">center</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">1</property>
+                        <property name="use-underline">1</property>
+                        <property name="label" translatable="yes">_Accept</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner" id="accept_spinner">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkButton" id="reject_button">
+                <property name="visible">1</property>
+                <property name="sensitive" bind-source="accept_button" bind-property="sensitive" bind-flags="sync-create"/>
+                <signal name="clicked" handler="invite_reject_clicked_cb" swapped="yes"/>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">6</property>
+                    <property name="halign">center</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">1</property>
+                        <property name="use-underline">1</property>
+                        <property name="label" translatable="yes">_Reject</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner" id="reject_spinner">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+          </object>
+        </child>
+
+      </object> <!-- ./HdyClamp -->
+    </child>
+
+  </template>
+</interface>
diff --git a/src/ui/chatty-main-view.ui b/src/ui/chatty-main-view.ui
new file mode 100644
index 0000000000000000000000000000000000000000..3f961c04be70757c27cff20068fdeb7da5950bfb
--- /dev/null
+++ b/src/ui/chatty-main-view.ui
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ChattyMainView" parent="GtkBox">
+
+    <child>
+      <object class="GtkStack" id="main_stack">
+        <property name="visible">1</property>
+
+        <child>
+          <object class="ChattyChatView" id="content_view">
+            <property name="visible">1</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="ChattyInviteView" id="invite_view">
+            <property name="visible">1</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="ChattyVerificationView" id="verification_view">
+            <property name="visible">1</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkBox" id="empty_view">
+            <property name="visible">1</property>
+          </object>
+        </child>
+
+      </object>
+    </child>
+
+  </template>
+</interface>
diff --git a/src/ui/chatty-settings-dialog.ui b/src/ui/chatty-settings-dialog.ui
index 953f2164207fa5aab609658d35fdeece0b108e22..d88fea6e58e421bc55942094e43e05155be79042 100644
--- a/src/ui/chatty-settings-dialog.ui
+++ b/src/ui/chatty-settings-dialog.ui
@@ -60,7 +60,7 @@
                 <property name="label" translatable="yes">_Add</property>
                 <signal name="clicked" handler="chatty_settings_add_clicked_cb" swapped="yes" />
                 <style>
-                  <class name="default" />
+                  <class name="suggested-action" />
                 </style>
               </object>
               <packing>
diff --git a/src/ui/chatty-verification-view.ui b/src/ui/chatty-verification-view.ui
new file mode 100644
index 0000000000000000000000000000000000000000..8ba214adabf7a286b6d4ea017a4980cb0236df54
--- /dev/null
+++ b/src/ui/chatty-verification-view.ui
@@ -0,0 +1,550 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ChattyVerificationView" parent="GtkBox">
+
+    <child>
+      <object class="HdyClamp">
+        <property name="visible">1</property>
+        <property name="hexpand">1</property>
+        <property name="margin-top">18</property>
+        <property name="margin-bottom">18</property>
+        <property name="margin-start">18</property>
+        <property name="margin-end">18</property>
+        <property name="halign">center</property>
+        <property name="valign">center</property>
+
+        <child>
+          <object class="GtkBox">
+            <property name="visible">1</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">18</property>
+
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">1</property>
+                <property name="wrap">1</property>
+                <property name="justify">center</property>
+                <property name="label" translatable="yes">Incoming Verification Request</property>
+                <style>
+                  <class name="large-title"/>
+                </style>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkImage">
+                <property name="visible">1</property>
+                <property name="icon-name">system-lock-screen-symbolic</property>
+                <property name="pixel-size">96</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">1</property>
+                <property name="wrap">1</property>
+                <property name="xalign">0.0</property>
+                <property name="label" translatable="yes">Verify this user to mark them as trusted.</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkBox">
+                <property name="visible">1</property>
+                <property name="spacing">12</property>
+                <child>
+                  <object class="ChattyAvatar" id="user_avatar">
+                    <property name="visible">1</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">3</property>
+                    <child>
+                      <object class="GtkLabel" id="name_label">
+                        <property name="visible">1</property>
+                        <property name="xalign">0.0</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="username_label">
+                        <property name="visible">1</property>
+                        <property name="xalign">0.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkLabel">
+                <property name="visible">1</property>
+                <property name="wrap">1</property>
+                <property name="xalign">0.0</property>
+                <property name="label" translatable="yes">Verifying this user will mark their session as trusted, and also mark your session as trusted to them.</property>
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkButton" id="continue_button">
+                <property name="visible">1</property>
+                <signal name="clicked" handler="verification_continue_clicked_cb" swapped="yes"/>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+
+                <!-- todo: Use GtkCenterBox when porting to GTK4 -->
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">6</property>
+                    <property name="hexpand">1</property>
+                    <child type="center">
+                      <object class="GtkLabel">
+                        <property name="visible">1</property>
+                        <property name="use-underline">1</property>
+                        <property name="label" translatable="yes">C_ontinue</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">1</property>
+                        <property name="icon-name">go-next-symbolic</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner" id="continue_spinner">
+                        <property name="visible">1</property>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkButton" id="cancel_button">
+                <property name="visible">1</property>
+                <property name="sensitive" bind-source="continue_button" bind-property="sensitive" bind-flags="sync-create"/>
+                <signal name="clicked" handler="verification_cancel_clicked_cb" swapped="yes"/>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">6</property>
+                    <property name="halign">center</property>
+                    <child type="center">
+                      <object class="GtkLabel">
+                        <property name="visible">1</property>
+                        <property name="use-underline">1</property>
+                        <property name="label" translatable="yes">_Cancel</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner" id="cancel_spinner">
+                        <property name="visible">1</property>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+          </object>
+        </child>
+
+      </object> <!-- ./HdyClamp -->
+    </child>
+
+  </template>
+
+  <object class="GtkDialog" id="verification_dialog">
+    <property name="visible">0</property>
+    <property name="modal">1</property>
+    <property name="use-header-bar">1</property>
+
+    <child internal-child="headerbar">
+      <object class="GtkHeaderBar">
+        <property name="visible">1</property>
+        <property name="show-close-button">0</property>
+        <child>
+          <object class="GtkButton">
+            <property name="label" translatable="yes">_Cancel</property>
+            <property name="visible">1</property>
+            <property name="use-underline">1</property>
+            <property name="valign">center</property>
+            <signal name="clicked" handler="verification_cancel_clicked_cb" swapped="yes"/>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="verification_type_button">
+            <property name="visible">1</property>
+            <property name="valign">center</property>
+            <signal name="clicked" handler="verification_type_clicked_cb" swapped="yes"/>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <property name="visible">1</property>
+        <property name="margin-start">12</property>
+        <property name="margin-end">12</property>
+        <property name="margin-top">12</property>
+        <property name="margin-bottom">12</property>
+        <property name="spacing">32</property>
+        <property name="halign">center</property>
+
+        <child>
+          <object class="GtkStack" id="content_stack">
+            <property name="visible">1</property>
+            <signal name="notify::visible-child" handler="verification_content_child_changed_cb" swapped="yes"/>
+
+            <child>
+              <object class="GtkGrid" id="emoji_content">
+                <property name="visible">1</property>
+                <property name="row-spacing">9</property>
+                <property name="column-spacing">3</property>
+                <property name="column-homogeneous">1</property>
+                <property name="row-homogeneous">1</property>
+
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">1</property>
+                    <property name="xalign">0.0</property>
+                    <property name="yalign">0.0</property>
+                    <property name="wrap">1</property>
+                    <property name="max-width-chars">40</property>
+                    <property name="label" translatable="yes">Verify this user by confirming the following emoji appears on their screen</property>
+                  </object>
+                  <packing>
+                    <property name="top-attach">0</property>
+                    <property name="left-attach">0</property>
+                    <property name="width">8</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji1_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji1_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">1</property>
+                    <property name="left-attach">0</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji2_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji2_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">1</property>
+                    <property name="left-attach">2</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji3_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji3_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">1</property>
+                    <property name="left-attach">4</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji4_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji4_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">1</property>
+                    <property name="left-attach">6</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji5_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji5_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">2</property>
+                    <property name="left-attach">1</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji6_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji6_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">2</property>
+                    <property name="left-attach">3</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="spacing">3</property>
+                    <property name="orientation">vertical</property>
+                    <child>
+                      <object class="GtkLabel" id="emoji7_label">
+                        <property name="visible">1</property>
+                        <style>
+                          <class name="large-title"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="emoji7_title">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="top-attach">2</property>
+                    <property name="left-attach">5</property>
+                    <property name="width">2</property>
+                  </packing>
+                </child>
+
+              </object>
+            </child>
+
+            <child>
+              <object class="GtkBox" id="decimal_content">
+                <property name="visible">1</property>
+                <property name="orientation">vertical</property>
+                <property name="spacing">32</property>
+
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">1</property>
+                    <property name="xalign">0.0</property>
+                    <property name="yalign">0.0</property>
+                    <property name="wrap">1</property>
+                    <property name="max-width-chars">40</property>
+                    <property name="label" translatable="yes">Verify this user by confirming the following number appears on their screen.</property>
+                  </object>
+                </child>
+
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">1</property>
+                    <property name="vexpand">1</property>
+                    <property name="halign">center</property>
+                    <property name="spacing">24</property>
+                    <style>
+                     <class name="large-title"/>
+                    </style>
+
+                    <child>
+                      <object class="GtkLabel" id="decimal1_label">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="decimal2_label">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="decimal3_label">
+                        <property name="visible">1</property>
+                      </object>
+                    </child>
+
+                  </object>
+                </child>
+
+              </object>
+            </child>
+
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkLabel">
+            <property name="visible">1</property>
+            <property name="xalign">0.0</property>
+            <property name="wrap">1</property>
+            <property name="max-width-chars">40</property>
+            <property name="label" translatable="yes">To be secure, do this in person or use a trusted way to communicate.</property>
+          </object>
+        </child>
+
+        <child>
+          <object class="GtkBox">
+            <property name="visible">1</property>
+            <property name="hexpand">1</property>
+            <property name="spacing">18</property>
+            <child>
+              <object class="GtkButton" id="no_match_button">
+                <property name="visible">1</property>
+                <property name="use-underline">1</property>
+                <property name="label" translatable="yes" context="verb, which means 'both items are not the same'">_Don’t Match</property>
+                <signal name="clicked" handler="verification_cancel_clicked_cb" swapped="yes"/>
+                <style>
+                  <class name="destructive-action"/>
+                </style>
+              </object>
+              <packing>
+                <property name="pack-type">start</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="match_button">
+                <property name="visible">1</property>
+                <property name="use-underline">1</property>
+                <property name="label" translatable="yes" context="verb, which means 'both items are the same'">_Match</property>
+                <signal name="clicked" handler="verification_match_clicked_cb" swapped="yes"/>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+
+      </object>
+    </child>
+
+  </object>
+
+  <object class="GtkSizeGroup">
+    <widgets>
+      <widget name="no_match_button"/>
+      <widget name="match_button"/>
+    </widgets>
+  </object>
+
+</interface>
diff --git a/src/ui/chatty-window.ui b/src/ui/chatty-window.ui
index 1b757fd3cf5b49b33165e53f1f9bdc95d0b6f950..6a3076cf4e5f29d3d357f170629f54dbf1b01479 100644
--- a/src/ui/chatty-window.ui
+++ b/src/ui/chatty-window.ui
@@ -1,245 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <requires lib="gtk+" version="3.16"/>
-  <object class="GtkPopoverMenu" id="header_view_chat_list_popover">
-    <property name="can_focus">False</property>
-    <child>
-      <object class="GtkBox">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="margin">12</property>
-        <property name="orientation">vertical</property>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes" context="show archived chat list when clicked">Archived</property>
-            <signal name="clicked" handler="chatty_window_show_archived" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Preferences</property>
-            <signal name="clicked" handler="chatty_window_show_settings_dialog" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">1</property>
-            <property name="text" translatable="yes">Keyboard _Shortcuts</property>
-            <property name="action-name">win.show-help-overlay</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="text" translatable="yes">Help</property>
-	    <property name="action-name">app.help</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">About Chats</property>
-            <signal name="clicked" handler="chatty_window_show_about_dialog" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-      </object>
-      <packing>
-        <property name="submenu">main</property>
-      </packing>
-    </child>
-  </object>
-  <object class="GtkPopoverMenu" id="header_chat_list_new_msg_popover">
-    <property name="can_focus">False</property>
-    <property name="relative-to">header_add_chat_button</property>
-    <child>
-      <object class="GtkBox">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="margin">12</property>
-        <property name="orientation">vertical</property>
-        <child>
-          <object class="GtkModelButton" id="menu_new_message_button">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">New Message…</property>
-            <signal name="clicked" handler="window_new_message_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="menu_new_sms_mms_message_button">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">New SMS/MMS Message…</property>
-            <signal name="clicked" handler="window_new_sms_mms_message_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="menu_new_group_message_button">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">New Group Message…</property>
-            <signal name="clicked" handler="window_new_muc_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-      </object>
-      <packing>
-        <property name="submenu">main</property>
-      </packing>
-    </child>
-  </object>
-  <object class="GtkPopoverMenu" id="header_view_message_list_popover">
-    <property name="can_focus">False</property>
-    <child>
-      <object class="GtkBox">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="margin">12</property>
-        <property name="orientation">vertical</property>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="active">False</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Chat Details</property>
-            <signal name="clicked" handler="window_show_chat_info_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkSeparator" id="separator_menu_msg_view2">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="margin_top">6</property>
-            <property name="margin_bottom">6</property>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="leave_button">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Leave Chat</property>
-            <signal name="clicked" handler="window_leave_chat_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="block_button">
-            <property name="visible">False</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Block Contact</property>
-            <signal name="clicked" handler="window_block_contact_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="unblock_button">
-            <property name="visible">False</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Unblock Contact</property>
-            <signal name="clicked" handler="window_unblock_contact_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="archive_button">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Archive chat</property>
-            <signal name="clicked" handler="window_archive_chat_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="unarchive_button">
-            <property name="visible">False</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Unarchive chat</property>
-            <signal name="clicked" handler="window_unarchive_chat_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="delete_button">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="receives_default">False</property>
-            <property name="text" translatable="yes">Delete Chat</property>
-            <signal name="clicked" handler="window_delete_buddy_clicked_cb" swapped="yes"/>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-          </packing>
-        </child>
-      </object>
-      <packing>
-        <property name="submenu">main</property>
-      </packing>
-    </child>
-  </object>
   <template class="ChattyWindow" parent="HdyApplicationWindow">
     <property name="can_focus">False</property>
     <property name="default_width">360</property>
@@ -251,205 +12,11 @@
       <object class="GtkBox">
         <property name="visible">1</property>
         <property name="orientation">vertical</property>
+
         <child>
-          <object class="HdyLeaflet" id="header_box">
-            <property name="visible">True</property>
-            <property name="visible-child-name" bind-source="content_box" bind-property="visible-child-name" bind-flags="sync-create"/>
-            <property name="vexpand">0</property>
-            <signal name="notify::folded" handler="notify_fold_cb" after="yes" swapped="yes"/>
-            <child>
-              <object class="HdyHeaderBar" id="header_bar">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="show_close_button">True</property>
-                <property name="title" translatable="yes">Chats</property>
-                <child>
-                  <object class="GtkButton" id="header_back_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <signal name="clicked" handler="window_show_unarchived_clicked_cb" swapped="yes"/>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">go-previous-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkMenuButton" id="header_add_chat_button">
-                    <property name="visible">True</property>
-                    <property name="sensitive">False</property>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <property name="popover">header_chat_list_new_msg_popover</property>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">list-add-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkMenuButton">
-                    <property name="visible" bind-source="header_add_chat_button" bind-property="visible" bind-flags="sync-create"/>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <property name="popover">header_view_chat_list_popover</property>
-                    <accelerator key="F10" signal="clicked" />
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">open-menu-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkToggleButton" id="search_button">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <property name="active" bind-source="chats_search_bar" bind-property="search-mode-enabled" bind-flags="sync-create|bidirectional"/>
-                    <accelerator key="f" modifiers="primary" signal="clicked" />
-                    <signal name="toggled" handler="window_search_toggled_cb" swapped="yes"/>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">system-search-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                  </packing>
-                </child>
-              </object>
-              <packing>
-                <property name="name">sidebar</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkSeparator">
-                <property name="visible">True</property>
-                <style>
-                  <class name="sidebar"/>
-                </style>
-              </object>
-            </child>
-            <child>
-              <object class="HdyHeaderBar" id="sub_header_bar">
-                <property name="visible">True</property>
-                <property name="expand">True</property>
-                <property name="show_close_button">True</property>
-                <child>
-                  <object class="GtkButton">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <property name="visible" bind-source="header_box" bind-property="folded" bind-flags="sync-create"/>
-                    <signal name="clicked" handler="window_back_clicked_cb" swapped="yes"/>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">go-previous-symbolic</property>
-                        <property name="icon_size">1</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">start</property>
-                  </packing>
-                </child>
-                <child type="title">
-                  <object class="GtkBox">
-                    <property name="visible">True</property>
-                    <property name="orientation">horizontal</property>
-                    <property name="can_focus">False</property>
-                    <property name="halign">center</property>
-                    <property name="valign">center</property>
-                    <child>
-                      <object class="ChattyAvatar" id="sub_header_icon">
-                        <property name="can_focus">False</property>
-                        <property name="halign">center</property>
-                        <property name="size">28</property>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkLabel" id="sub_header_label">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="margin-start">6</property>
-                        <property name="halign">center</property>
-                        <property name="ellipsize">end</property>
-                        <attributes>
-                          <attribute name="weight" value="bold"/>
-                        </attributes>
-                      </object>
-                      <packing>
-                        <property name="expand">False</property>
-                        <property name="fill">True</property>
-                      </packing>
-                    </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkMenuButton" id="header_sub_menu_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">False</property>
-                    <property name="receives_default">False</property>
-                    <property name="popover">header_view_message_list_popover</property>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">view-more-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="position">0</property>
-                    <property name="pack_type">end</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkButton" id="call_button">
-                    <property name="visible">False</property>
-                    <property name="can-focus">False</property>
-                    <property name="receives-default">False</property>
-                    <signal name="clicked" handler="window_call_button_clicked_cb" swapped="yes"/>
-                    <child internal-child="accessible">
-                      <object class="AtkObject">
-                        <property name="accessible-name" translatable="yes">Call</property>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="icon-name">call-start-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="position">1</property>
-                    <property name="pack_type">end</property>
-                  </packing>
-                </child>
-              </object>
-              <packing>
-                <property name="name">content</property>
-              </packing>
-            </child>
+          <object class="ChattyHeaderBar" id="header_bar">
+            <property name="visible">1</property>
+            <signal name="back-clicked" handler="window_back_clicked_cb" swapped="yes"/>
           </object>
         </child>
 
@@ -460,6 +27,7 @@
             <property name="transition-type">slide</property>
             <property name="can-swipe-back">True</property>
             <signal name="notify::visible-child-name" handler="window_content_box_changed" swapped="yes"/>
+            <signal name="notify::folded" handler="notify_fold_cb" after="yes" swapped="yes"/>
             <child>
               <object class="GtkBox" id="sidebar">
                 <property name="visible">True</property>
@@ -470,6 +38,7 @@
                   <object class="HdySearchBar" id="chats_search_bar">
                     <property name="visible">True</property>
                     <property name="hexpand">False</property>
+                    <signal name="notify::search-mode-enabled" handler="window_search_enable_changed_cb" swapped="yes"/>
                     <child>
                       <object class="GtkBox">
                         <property name="visible">True</property>
@@ -527,7 +96,7 @@
             </child>
 
             <child>
-              <object class="ChattyChatView" id="chat_view">
+              <object class="ChattyMainView" id="content_view">
                 <property name="visible">True</property>
                 <property name="width_request">300</property>
               </object>
@@ -557,25 +126,4 @@
     </child>
   </object>
 
-  <object class="GtkSizeGroup" id="start_pane_size_group">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="header_bar"/>
-      <widget name="sidebar"/>
-    </widgets>
-  </object>
-  <object class="GtkSizeGroup" id="end_pane_size_group">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="sub_header_bar"/>
-      <widget name="chat_view"/>
-    </widgets>
-  </object>
-  <object class="HdyHeaderGroup" id="header_group">
-    <property name="decorate-all" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/>
-    <headerbars>
-      <headerbar name="header_bar"/>
-      <headerbar name="sub_header_bar"/>
-    </headerbars>
-  </object>
 </interface>
diff --git a/subprojects/libcmatrix.wrap b/subprojects/libcmatrix.wrap
new file mode 100644
index 0000000000000000000000000000000000000000..8e785abc2af6bb9e55d925eb7d35b49f5528d299
--- /dev/null
+++ b/subprojects/libcmatrix.wrap
@@ -0,0 +1,4 @@
+[wrap-git]
+directory=libcmatrix
+url=https://source.puri.sm/Librem5/libcmatrix
+revision=main
diff --git a/subprojects/libcmatrix/.gitignore b/subprojects/libcmatrix/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..fb74cb679ece330d5d19aa84fdb7b7ab0261da4e
--- /dev/null
+++ b/subprojects/libcmatrix/.gitignore
@@ -0,0 +1,12 @@
+_build
+*.swp
+*~
+\#*#
+.\#*
+build
+.buildconfig
+.flatpak-builder
+
+GPATH
+GTAGS
+GRTAGS
\ No newline at end of file
diff --git a/subprojects/libcmatrix/.gitlab-ci.yml b/subprojects/libcmatrix/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b4464b3b6d48cdfbe99d5135107b8023f7516167
--- /dev/null
+++ b/subprojects/libcmatrix/.gitlab-ci.yml
@@ -0,0 +1,96 @@
+stages:
+  - build
+  - deploy
+
+build-soup2:
+  tags:
+    - librem5
+  image: debian:bullseye
+  stage: build
+  variables:
+    CCACHE_DIR: "${CI_PROJECT_DIR}/_ccache"
+    MESON_ARGS: "--buildtype=debugoptimized"
+  before_script:
+    - apt-get update -qq && apt-get install -qq ccache meson gcovr gcc
+      libsoup2.4-dev libsqlite3-dev libjson-glib-dev libgcrypt20-dev
+      libolm-dev libsecret-1-dev
+  script:
+    - meson ${MESON_ARGS} -Db_coverage=true _build
+    - ccache --zero-stats
+    - meson test -C _build
+    - ccache --show-stats
+    - mkdir -p _build/meson-logs/coveragereport
+    - ninja -C _build coverage-html
+  coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/'
+  cache:
+    key: build-cmatrix-soup2
+    paths:
+      - _ccache/
+  artifacts:
+    when: always
+    paths:
+      - _build
+
+build-soup3:
+  tags:
+    - librem5:arm64
+  image: debian:bookworm
+  stage: build
+  variables:
+    CCACHE_DIR: "${CI_PROJECT_DIR}/_ccache"
+    MESON_ARGS: "--buildtype=debugoptimized"
+  before_script:
+    - apt-get update -qq && apt-get install -qq ccache meson gcovr gcc
+      libsoup2.4-dev libsqlite3-dev libjson-glib-dev libgcrypt20-dev
+      libsoup-3.0-dev libolm-dev libsecret-1-dev
+  script:
+    - meson ${MESON_ARGS} -Db_coverage=true _build
+    - ccache --zero-stats
+    - meson test -C _build
+    - ccache --show-stats
+    - mkdir -p _build/meson-logs/coveragereport
+    - ninja -C _build coverage-html
+  coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/'
+  cache:
+    key: build-cmatrix-soup3
+    paths:
+      - _ccache/
+  artifacts:
+    when: always
+    paths:
+      - _build
+
+doc:
+  tags:
+    - librem5:arm64
+  image: debian:bookworm
+  stage: build
+  before_script:
+    - apt-get update -qq && apt-get install -qq ccache meson gcovr gcc
+      libsoup2.4-dev libsqlite3-dev libjson-glib-dev libgcrypt20-dev
+      libolm-dev gi-docgen libgirepository1.0-dev libsecret-1-dev
+      libsoup-3.0-dev
+  variables:
+    BUILD_OPTS: >-
+      -Dgtk_doc=true
+  script:
+    - meson ${BUILD_OPTS} _build
+    - ninja -C _build
+    - mv _build/doc/libcmatrix-0 _doc/
+  artifacts:
+    paths:
+      - _doc/
+
+pages:
+  tags:
+    - librem5
+  image: busybox:1
+  stage: deploy
+  script:
+    - mkdir public
+    - mv _build/meson-logs/coveragereport ${CI_PROJECT_DIR}/public/coverage
+  artifacts:
+    paths:
+      - public
+  only:
+    - main
diff --git a/subprojects/libcmatrix/COPYING b/subprojects/libcmatrix/COPYING
new file mode 100644
index 0000000000000000000000000000000000000000..4362b49151d7b34ef83b3067a8f9c9f877d72a0e
--- /dev/null
+++ b/subprojects/libcmatrix/COPYING
@@ -0,0 +1,502 @@
+                  GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL.  It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+  This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it.  You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+  When we speak of free software, we are referring to freedom of use,
+not price.  Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+  To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights.  These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+  For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you.  You must make sure that they, too, receive or can get the source
+code.  If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it.  And you must show them these terms so they know their rights.
+
+  We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+  To protect each distributor, we want to make it very clear that
+there is no warranty for the free library.  Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+  Finally, software patents pose a constant threat to the existence of
+any free program.  We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder.  Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+  Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License.  This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License.  We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+  When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library.  The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom.  The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+  We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License.  It also provides other free software developers Less
+of an advantage over competing non-free programs.  These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries.  However, the Lesser license provides advantages in certain
+special circumstances.
+
+  For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard.  To achieve this, non-free programs must be
+allowed to use the library.  A more frequent case is that a free
+library does the same job as widely used non-free libraries.  In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+  In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software.  For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+  Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.  Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library".  The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+                  GNU LESSER GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+  A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+  The "Library", below, refers to any such software library or work
+which has been distributed under these terms.  A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language.  (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+  "Source code" for a work means the preferred form of the work for
+making modifications to it.  For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+  Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it).  Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+  1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+  You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+  2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) The modified work must itself be a software library.
+
+    b) You must cause the files modified to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    c) You must cause the whole of the work to be licensed at no
+    charge to all third parties under the terms of this License.
+
+    d) If a facility in the modified Library refers to a function or a
+    table of data to be supplied by an application program that uses
+    the facility, other than as an argument passed when the facility
+    is invoked, then you must make a good faith effort to ensure that,
+    in the event an application does not supply such function or
+    table, the facility still operates, and performs whatever part of
+    its purpose remains meaningful.
+
+    (For example, a function in a library to compute square roots has
+    a purpose that is entirely well-defined independent of the
+    application.  Therefore, Subsection 2d requires that any
+    application-supplied function or table used by this function must
+    be optional: if the application does not supply it, the square
+    root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library.  To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License.  (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.)  Do not make any other change in
+these notices.
+
+  Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+  This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+  4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+  If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library".  Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+  However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library".  The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+  When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library.  The
+threshold for this to be true is not precisely defined by law.
+
+  If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work.  (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+  Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+  6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+  You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License.  You must supply a copy of this License.  If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License.  Also, you must do one
+of these things:
+
+    a) Accompany the work with the complete corresponding
+    machine-readable source code for the Library including whatever
+    changes were used in the work (which must be distributed under
+    Sections 1 and 2 above); and, if the work is an executable linked
+    with the Library, with the complete machine-readable "work that
+    uses the Library", as object code and/or source code, so that the
+    user can modify the Library and then relink to produce a modified
+    executable containing the modified Library.  (It is understood
+    that the user who changes the contents of definitions files in the
+    Library will not necessarily be able to recompile the application
+    to use the modified definitions.)
+
+    b) Use a suitable shared library mechanism for linking with the
+    Library.  A suitable mechanism is one that (1) uses at run time a
+    copy of the library already present on the user's computer system,
+    rather than copying library functions into the executable, and (2)
+    will operate properly with a modified version of the library, if
+    the user installs one, as long as the modified version is
+    interface-compatible with the version that the work was made with.
+
+    c) Accompany the work with a written offer, valid for at
+    least three years, to give the same user the materials
+    specified in Subsection 6a, above, for a charge no more
+    than the cost of performing this distribution.
+
+    d) If distribution of the work is made by offering access to copy
+    from a designated place, offer equivalent access to copy the above
+    specified materials from the same place.
+
+    e) Verify that the user has already received a copy of these
+    materials or that you have already sent this user a copy.
+
+  For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it.  However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+  It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system.  Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+  7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+    a) Accompany the combined library with a copy of the same work
+    based on the Library, uncombined with any other library
+    facilities.  This must be distributed under the terms of the
+    Sections above.
+
+    b) Give prominent notice with the combined library of the fact
+    that part of it is a work based on the Library, and explaining
+    where to find the accompanying uncombined form of the same work.
+
+  8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License.  Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License.  However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+  9. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Library or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+  10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+  11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded.  In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+  13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation.  If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+  14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission.  For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this.  Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+                            NO WARRANTY
+
+  15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU.  SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+           How to Apply These Terms to Your New Libraries
+
+  If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change.  You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+  To apply these terms, attach the following notices to the library.  It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the library's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This library is free software; you can redistribute it and/or
+    modify it under the terms of the GNU Lesser General Public
+    License as published by the Free Software Foundation; either
+    version 2.1 of the License, or (at your option) any later version.
+
+    This library is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+    Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public
+    License along with this library; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the
+  library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+  <signature of Ty Coon>, 1 April 1990
+  Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/subprojects/libcmatrix/README.md b/subprojects/libcmatrix/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..170d2b43bfc2166e6dedbc64c6a8c37f2862978c
--- /dev/null
+++ b/subprojects/libcmatrix/README.md
@@ -0,0 +1,60 @@
+<div align="center">
+  <a href="https://puri.sm">
+    <img src="https://path/to/image/raw/master/data/icons/icon.png" width="150" />
+  </a>
+  <br>
+
+  <a href="https://puri.sm"><b>libcmatrix</b></a>
+  <br>
+
+  A matrix protocol library writting in GObjectified C
+  <br>
+
+  <a href="https://source.puri.sm/Librem5/libcmatrix/pipelines"><img
+     src="https://source.puri.sm/Librem5/libcmatrix/badges/main/pipeline.svg" /></a>
+  <a href="https://source.puri.sm/Librem5/libcmatrix/coverage"><img
+     src="https://source.puri.sm/Librem5/libcmatrix/badges/main/coverage.svg" /></a>
+</div>
+
+---
+
+libcmatrix is a [Matrix][matrix] client library written in
+GObjectified C library.
+
+You could use the library if you are writing a matrix client
+in C.
+
+libcmatrix requires GObject and Gio, and the library assumes
+that a glib main event loop is running (All GTK apps have one)
+
+libcmatrix handles all E2EE transparently.  The messages/events
+are not stored and the client should store them for chat history,
+which may change in the future.
+
+## Dependencies
+   - glib >= 2.66
+   - gio >= 2.66
+   - libsoup-2
+   - ligbcrypt
+   - libolm3
+   - libsqlite3 >= 3.34
+
+Source Repository: [GitLab][gitlab]
+
+Issues and Feature Requests: [GitLab][issues]
+
+## Getting started
+
+   See `examples` directory for examples on how to use the library.
+   You shall have to use libcmatrix as a subproject.  Currently
+   libcmatrix provides no API nor ABI guarantee until it's stable
+   enough.
+
+
+<!-- Links referenced elsewhere -->
+<!-- To be updated -->
+[matrix]: https://matrix.org
+[home]: https://puri.sm
+[coverage]: https://source.puri.sm/Librem5/libcmatrix/coverage
+[gitlab]: https://source.puri.sm/Librem5/libcmatrix/
+[issues]: https://source.puri.sm/Librem5/libcmatrix/issues
diff --git a/subprojects/libcmatrix/doc/build-howto.md b/subprojects/libcmatrix/doc/build-howto.md
new file mode 100644
index 0000000000000000000000000000000000000000..f277fd8a09db8813c574b357358e78585f004674
--- /dev/null
+++ b/subprojects/libcmatrix/doc/build-howto.md
@@ -0,0 +1,32 @@
+Title: Compiling with libcmatrix
+Slug: building
+
+# Compiling with libcmatrix
+
+If you need to build libcmatrix, get the source from
+[here](https://source.puri.sm/Librem5/libcmatrix/) and see the `README.md` file.
+
+## Bundling the library
+
+libcmatrix is not meant to be used as a shared library. It should be embedded in your source
+tree as a git submodule instead:
+
+```
+git submodule add https://source.puri.sm/Librem5/libcmatrix.git subprojects/libcmatrix
+```
+
+Add this to your `meson.build`:
+
+```meson
+libcmatrix = subproject('libcmatrix',
+  default_options: [
+    'package_name=' + meson.project_name(),
+    'package_version=' + meson.project_version(),
+    'pkgdatadir=' + pkgdatadir,
+    'pkglibdir=' + pkglibdir,
+    'examples=false',
+    'gtk_doc=false',
+    'tests=false',
+  ])
+libcmatrix_dep = libcmatrix.get_variable('libcmatrix_dep')
+```
diff --git a/subprojects/libcmatrix/doc/libcmatrix.toml.in b/subprojects/libcmatrix/doc/libcmatrix.toml.in
new file mode 100644
index 0000000000000000000000000000000000000000..da9dbdd6d7e10ab1e928eabc1146f62c253b9ba2
--- /dev/null
+++ b/subprojects/libcmatrix/doc/libcmatrix.toml.in
@@ -0,0 +1,34 @@
+[library]
+version = "@VERSION@"
+description = "A matrix client client library written in GObjectified C"
+authors = "Purism SPC"
+license = "LGPL-2.1-or-later"
+browse_url = "https://source.puri.sm/Librem5/libcmatrix"
+repository_url = "https://source.puri.sm/Librem5/libcmatrix.git"
+website_url = "https://source.puri.sm/Librem5/libcmatrix"
+dependencies = [
+  "GObject-2.0",
+]
+devhelp = true
+search_index = true
+
+[dependencies."GObject-2.0"]
+name = "GObject"
+description = "The base type system library"
+docs_url = "https://developer.gnome.org/gobject/stable"
+
+[theme]
+name = "basic"
+show_index_summary = true
+show_class_hierarchy = true
+
+[source-location]
+# The base URL for the web UI
+base_url = "https://source.puri.sm/Librem5/libcmatrix/-/blob/main/"
+# The format for links, using "filename" and "line" for the format
+file_format = "{filename}#L{line}"
+
+[extra]
+content_files = [
+  "build-howto.md",
+]
diff --git a/subprojects/libcmatrix/doc/meson.build b/subprojects/libcmatrix/doc/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..4819fb16cface1d6a408fa1f8bab6bc6e445a482
--- /dev/null
+++ b/subprojects/libcmatrix/doc/meson.build
@@ -0,0 +1,45 @@
+if get_option('gtk_doc')
+
+expand_content_md_files = [
+  'build-howto.md',
+]
+
+toml_data = configuration_data()
+toml_data.set('VERSION', meson.project_version())
+
+libcmatrix_toml = configure_file(
+  input: 'libcmatrix.toml.in',
+  output: 'libcmatrix.toml',
+  configuration: toml_data
+)
+
+dependency('gi-docgen', version: '>= 2021.1',
+           fallback: ['gi-docgen', 'dummy_dep'],
+           native: true,
+           required: get_option('gtk_doc'))
+
+gidocgen = find_program('gi-docgen')
+
+docs_dir = datadir / 'doc'
+
+custom_target('libcmatrix-doc',
+  input: [ libcmatrix_toml, libcmatrix_gir[0] ],
+  output: 'libcmatrix-0',
+  command: [
+    gidocgen,
+    'generate',
+    '--quiet',
+    '--add-include-path=@0@'.format(meson.current_build_dir() / '../../src'),
+    '--config=@INPUT0@',
+    '--output-dir=@OUTPUT@',
+    '--no-namespace-dir',
+    '--content-dir=@0@'.format(meson.current_source_dir()),
+    '@INPUT1@',
+  ],
+  depend_files: [ expand_content_md_files ],
+  build_by_default: true,
+  install: true,
+  install_dir: docs_dir,
+)
+
+endif
diff --git a/subprojects/libcmatrix/examples/meson.build b/subprojects/libcmatrix/examples/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..99d402b1133158d169d55d0ab307a627f58bfca1
--- /dev/null
+++ b/subprojects/libcmatrix/examples/meson.build
@@ -0,0 +1,13 @@
+example_items = [
+  'simple-client',
+]
+
+foreach item: example_items
+  executable(
+    item,
+    item + '.c',
+    include_directories: src_inc,
+    link_with: cmatrix_lib,
+    dependencies: cmatrix_deps,
+  )
+endforeach
diff --git a/subprojects/libcmatrix/examples/simple-client.c b/subprojects/libcmatrix/examples/simple-client.c
new file mode 100644
index 0000000000000000000000000000000000000000..8546eda4ba0cf9f72139361d5a3e7e50bd9ddba5
--- /dev/null
+++ b/subprojects/libcmatrix/examples/simple-client.c
@@ -0,0 +1,194 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* client.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later OR CC0-1.0
+ */
+
+#include <stdio.h>
+
+#ifndef CMATRIX_USE_EXPERIMENTAL_API
+# define CMATRIX_USE_EXPERIMENTAL_API
+#endif /* CMATRIX_USE_EXPERIMENTAL_API */
+#include "cmatrix.h"
+
+CmMatrix *matrix;
+CmClient *client;
+
+static void
+simple_account_sync_cb (gpointer   object,
+                        CmClient  *cm_client,
+                        CmRoom    *room,
+                        GPtrArray *events,
+                        GError    *err)
+{
+  puts ("\n\n\n");
+
+  if (room && events)
+    {
+      for (guint i = 0; i < events->len; i++)
+        {
+          gpointer event;
+
+          event = events->pdata[i];
+          g_warning ("here: %d", cm_event_get_m_type (event));
+
+          if (CM_IS_ROOM_MESSAGE_EVENT (event) &&
+              cm_room_message_event_get_msg_type (event))
+            {
+              g_warning ("text message: %s", cm_room_message_event_get_body (event));
+            }
+        }
+    }
+
+  if (err)
+    g_warning ("client error: %s", err->message);
+
+  puts ("\n\n\n");
+}
+
+static void
+simple_matrix_save_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+
+  cm_matrix_save_client_finish (matrix, result, &error);
+
+  if (error)
+    g_warning ("Error saving client: %s", error->message);
+
+  /* Now, enable the client, and the client will start to sync, executing the callback on events */
+  cm_client_set_enabled (client, TRUE);
+}
+
+static void
+simple_get_homeserver_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+  const char *server;
+  char homeserver[255];
+
+  server = cm_client_get_homeserver_finish (client, result, &error);
+
+  /* It's okay to not able to get homeserver from username */
+  if (error)
+    g_message ("Failed to guess/verify homeserver: %s", error->message);
+  else if (server)
+    g_warning ("autofetched homeserver: %s", server);
+
+  /* Not having a homeserver set means we failed to guess it from provided login id */
+  /* So ask user for one. */
+  while (!cm_client_get_homeserver (client))
+    {
+      printf ("input your Matrix homeserver address: ");
+      scanf ("%s", homeserver);
+      if (!cm_client_set_homeserver (client, homeserver))
+        g_warning ("'%s' is not a valid homeserver uri (did you forget to "
+                   "prefix with 'https://')", homeserver);
+    }
+
+  /* The sync callback runs for every /sync response and other interesting events (like wrong password error) */
+  cm_client_set_sync_callback (client,
+                               simple_account_sync_cb,
+                               NULL, NULL);
+  cm_matrix_save_client_async (matrix, client,
+                               simple_matrix_save_cb, NULL);
+
+}
+
+static void
+simple_joined_rooms_changed_cb (GListModel *list,
+                                guint       position,
+                                guint       removed,
+                                guint       added,
+                                gpointer    user_data)
+{
+  puts ("\n\n\n");
+
+  g_warning ("joined rooms changed");
+  g_warning ("total number of items: %u", g_list_model_get_n_items (list));
+
+  for (guint i = 0; i < g_list_model_get_n_items (list); i++)
+    {
+      g_autoptr(CmRoom) room = NULL;
+
+      room = g_list_model_get_item (list, i);
+
+      g_warning ("room name: %s, room id: %s",
+                 cm_room_get_name (room),
+                 cm_room_get_id (room));
+    }
+
+  puts ("\n\n\n");
+}
+
+static void
+simple_matrix_open_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+  char username[255], password[255];
+  GListModel *joined_rooms;
+  CmAccount *account;
+
+  if (!cm_matrix_open_finish (matrix, result, &error))
+    g_error ("Error opening db: %s", error->message);
+
+  printf ("input your Matrix username: ");
+  scanf ("%s", username);
+  printf ("input your Matrix password: ");
+  scanf ("%s", password);
+  puts ("");
+
+  g_message ("username: %s, password: %s", username, password);
+
+  client = cm_matrix_client_new (matrix);
+  account = cm_client_get_account (client);
+  joined_rooms = cm_client_get_joined_rooms (client);
+  g_signal_connect_object (joined_rooms, "items-changed",
+                           G_CALLBACK (simple_joined_rooms_changed_cb), client,
+                           0);
+
+  if (!cm_account_set_login_id (account, username))
+    g_error ("'%s' isn't a valid username", username);
+  cm_client_set_password (client, password);
+  cm_client_set_device_name (client, "Example CMatrix");
+
+  /* try if we can get a valid homeserver from username */
+  cm_client_get_homeserver_async (client, NULL,
+                                  simple_get_homeserver_cb, NULL);
+}
+
+int
+main (void)
+{
+  GMainLoop *main_loop;
+  g_autofree char *db_dir = NULL;
+  g_autofree char *data_dir = NULL;
+  g_autofree char *cache_dir = NULL;
+
+  /* Initialize the library */
+  cm_init (TRUE);
+
+  /* Create a matrix object */
+  data_dir = g_build_filename (g_get_user_data_dir (), "CMatrix", "simple-client", NULL);
+  cache_dir = g_build_filename (g_get_user_cache_dir (), "CMatrix", "simple-client", NULL);
+  matrix = cm_matrix_new (data_dir, cache_dir, "com.example.CMatrix", FALSE);
+
+  /* Ask matrix to open/create the db which will be used to store keys, session data, etc. */
+  db_dir = g_build_filename (g_get_user_data_dir (), "CMatrix", "simple-client", NULL);
+  cm_matrix_open_async (matrix, db_dir, "matrix.db", NULL,
+                        simple_matrix_open_cb, NULL);
+
+  main_loop = g_main_loop_new (NULL, FALSE);
+  g_main_loop_run (main_loop);
+}
diff --git a/subprojects/libcmatrix/meson.build b/subprojects/libcmatrix/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..2c1dcaad4ba17b8d1ee3f478952a4bcabfb80d5b
--- /dev/null
+++ b/subprojects/libcmatrix/meson.build
@@ -0,0 +1,138 @@
+project('libcmatrix', 'c',
+          version: '0.0.0',
+          license: 'LGPL-2.1-or-later',
+    meson_version: '>= 0.56.0',
+  default_options: [ 'warning_level=2', 'buildtype=debugoptimized', 'c_std=gnu18' ],
+)
+
+gnome = import('gnome')
+i18n  = import('i18n')
+
+VERSION = meson.project_version()
+ABI_MAJOR = VERSION.split('.')[0]
+ABI_MINOR = VERSION.split('.')[1]
+ABI_MICRO = VERSION.split('.')[2]
+
+prefix = get_option('prefix')
+bindir = join_paths(prefix, get_option('bindir'))
+datadir = join_paths(prefix, get_option('datadir'))
+localedir = join_paths(prefix, get_option('localedir'))
+libdir = join_paths(prefix, get_option('libdir'))
+
+cc = meson.get_compiler('c')
+
+global_c_args = [
+  '-DCMATRIX_COMPILATION',
+]
+
+test_c_args = [
+  '-Wcast-align',
+  '-Wdate-time',
+  '-Wdeclaration-after-statement',
+  ['-Werror=format-security', '-Werror=format=2'],
+  '-Wendif-labels',
+  '-Werror=incompatible-pointer-types',
+  '-Werror=missing-declarations',
+  '-Werror=overflow',
+  '-Werror=return-type',
+  '-Werror=shift-count-overflow',
+  '-Werror=shift-overflow=2',
+  '-Werror=implicit-fallthrough=3',
+  '-Wfloat-equal',
+  '-Wformat-nonliteral',
+  '-Wformat-security',
+  '-Winit-self',
+  '-Wmaybe-uninitialized',
+  '-Wmissing-field-initializers',
+  '-Wmissing-include-dirs',
+  '-Wmissing-noreturn',
+  '-Wnested-externs',
+  '-Wno-strict-aliasing',
+  '-Wno-unused-parameter',
+  '-Wold-style-definition',
+  '-Wpointer-arith',
+  '-Wredundant-decls',
+  '-Wshadow',
+  '-Wsign-compare',
+  '-Wstrict-prototypes',
+  '-Wswitch-default',
+  '-Wswitch-enum',
+  '-Wtype-limits',
+  '-Wundef',
+  '-Wunused-function',
+]
+if get_option('buildtype') != 'plain'
+  test_c_args += '-fstack-protector-strong'
+endif
+
+foreach arg: test_c_args
+  if cc.has_multi_arguments(arg)
+    global_c_args += arg
+  endif
+endforeach
+add_project_arguments(
+  global_c_args,
+  language: 'c'
+)
+
+add_project_arguments([
+  '-DHAVE_CONFIG_H',
+  '-DCMATRIX_COMPILATION',
+  '-DCMATRIX_USE_EXPERIMENTAL_API',
+], language: 'c')
+
+config_h = configuration_data()
+config_h.set10('HAVE_EXPLICIT_BZERO', cc.has_function('explicit_bzero'))
+config_h.set_quoted('GETTEXT_PACKAGE', 'libcmatrix')
+config_h.set_quoted('LOCALEDIR', localedir)
+
+libolm_dep = cc.find_library('olm', required: true)
+
+if (cc.has_function('olm_account_unpublished_fallback_key', dependencies: libolm_dep))
+  config_h.set('OLM_ACCOUNT_PICKLE_V4', true)
+else
+  config_h.set('OLM_ACCOUNT_PICKLE_V4', false)
+endif
+
+if (cc.has_function('olm_pk_key_from_private', dependencies: libolm_dep))
+  config_h.set('HAVE_OLM3', true)
+else
+  config_h.set('HAVE_OLM2', true)
+endif
+
+configure_file(
+  output: 'config.h',
+  configuration: config_h,
+)
+
+root_inc = include_directories('.')
+src_inc = include_directories('src')
+
+gio_dep = dependency('gio-2.0', version: '>= 2.66')
+
+# libsoup-3 requires glib 2.69.1+, so if we have an older
+# glib, assume libsoup-2.4
+if gio_dep.version().version_compare('<= 2.69') or get_option('soup2')
+  soup_dep = dependency('libsoup-2.4', required: true)
+else
+  soup_dep = dependency('libsoup-3.0', required: true)
+endif
+
+cmatrix_deps = [
+  dependency('libgcrypt'),
+  dependency('libsecret-1'),
+  dependency('json-glib-1.0'),
+  dependency('sqlite3', version: '>=3.26.0'),
+  libolm_dep,
+  soup_dep,
+  gio_dep,
+  cc.find_library('m', required: false),
+]
+
+subdir('src')
+subdir('tests')
+subdir('doc')
+
+if (get_option('build-examples'))
+  subdir('examples')
+endif
diff --git a/subprojects/libcmatrix/meson_options.txt b/subprojects/libcmatrix/meson_options.txt
new file mode 100644
index 0000000000000000000000000000000000000000..ecc897db0d2f15e7b51c24dd52989344fae9d31e
--- /dev/null
+++ b/subprojects/libcmatrix/meson_options.txt
@@ -0,0 +1,10 @@
+# Subproject
+option('package_subdir', type: 'string',
+  description: 'Subdirectory to append to all installed files, for use as subproject'
+)
+option('build-examples', type: 'boolean', value: true, description : 'Build examples')
+option('soup2', type: 'boolean', value: false, description: 'Whether to build with libsoup2')
+
+option('gtk_doc',
+       type: 'boolean', value: false,
+       description: 'Whether to generate the API reference')
diff --git a/subprojects/libcmatrix/src/cm-client-private.h b/subprojects/libcmatrix/src/cm-client-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..828c763f8678242113beade0ad3b5cb9fe042aaf
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-client-private.h
@@ -0,0 +1,60 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-db-private.h"
+#include "cm-enc-private.h"
+#include "cm-net-private.h"
+#include "cm-types.h"
+#include "cm-client.h"
+
+G_BEGIN_DECLS
+
+void        cm_client_enable_as_in_store          (CmClient           *self);
+CmClient   *cm_client_new_from_secret             (gpointer            secret_retrievable,
+                                                   CmDb                *db);
+void        cm_client_save_secrets_async          (CmClient            *self,
+                                                   GAsyncReadyCallback  callback,
+                                                   gpointer             user_data);
+gboolean    cm_client_save_secrets_finish         (CmClient            *self,
+                                                   GAsyncResult        *result,
+                                                   GError             **error);
+void        cm_client_delete_secrets_async        (CmClient            *self,
+                                                   GAsyncReadyCallback  callback,
+                                                   gpointer             user_data);
+gboolean    cm_client_delete_secrets_finish       (CmClient            *self,
+                                                   GAsyncResult        *result,
+                                                   GError             **error);
+int         cm_client_pop_event_id                (CmClient            *self);
+CmDb       *cm_client_get_db                      (CmClient            *self);
+CmNet      *cm_client_get_net                     (CmClient            *self);
+CmEnc      *cm_client_get_enc                     (CmClient            *self);
+void        cm_client_set_db                      (CmClient            *self,
+                                                   CmDb                *db);
+const char *cm_client_get_filter_id               (CmClient            *self);
+void        cm_client_save                        (CmClient            *self);
+const char *cm_client_get_next_batch              (CmClient            *self);
+CmUserList *cm_client_get_user_list               (CmClient            *self);
+void          cm_client_get_file_async                (CmClient              *self,
+                                                       const char            *uri,
+                                                       GCancellable          *cancellable,
+                                                       GFileProgressCallback  progress_callback,
+                                                       gpointer               progress_user_data,
+                                                       GAsyncReadyCallback    callback,
+                                                       gpointer               user_data);
+GInputStream *cm_client_get_file_finish               (CmClient              *self,
+                                                       GAsyncResult          *result,
+                                                       GError               **error);
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-client.c b/subprojects/libcmatrix/src/cm-client.c
new file mode 100644
index 0000000000000000000000000000000000000000..d7f4225c807f75b538b4099529eaa91b7bd3933c
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-client.c
@@ -0,0 +1,3360 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-client.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-client"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+#include <libsecret/secret.h>
+#include <libsoup/soup.h>
+
+#include "cm-net-private.h"
+#include "cm-utils-private.h"
+#include "cm-common.h"
+#include "cm-db-private.h"
+#include "cm-olm-sas-private.h"
+#include "cm-enc-private.h"
+#include "cm-enums.h"
+#include "events/cm-event-private.h"
+#include "users/cm-room-member-private.h"
+#include "users/cm-user-private.h"
+#include "users/cm-user-list-private.h"
+#include "users/cm-account.h"
+#include "cm-room-private.h"
+#include "cm-room.h"
+#include "cm-secret-store-private.h"
+#include "cm-client-private.h"
+#include "cm-client.h"
+
+/**
+ * SECTION: cm-client
+ * @title: CmClient
+ * @short_description:
+ * @include: "cm-client.h"
+ */
+
+#define KEY_TIMEOUT         10000 /* milliseconds */
+#define URI_REQUEST_TIMEOUT 30    /* seconds */
+#define SYNC_TIMEOUT        30000 /* milliseconds */
+
+struct _CmClient
+{
+  GObject         parent_instance;
+
+  char           *homeserver;
+  char           *password;
+  char           *device_id;
+  char           *device_name;
+
+  CmAccount      *cm_account;
+  CmDb           *cm_db;
+  CmNet          *cm_net;
+  CmEnc          *cm_enc;
+  GSocketAddress *gaddress;
+
+  CmCallback      callback;
+  gpointer        cb_data;
+  GDestroyNotify  cb_destroy;
+
+  GCancellable   *cancellable;
+  char           *filter_id;
+  char           *next_batch;
+  char           *key;
+  char           *pickle_key;
+
+  CmUserList     *user_list;
+  /* direct_rooms are set on initial sync from 'account_data',
+   * which will then be moved to joined_rooms later */
+  GHashTable     *direct_rooms;
+  GListStore     *joined_rooms;
+  GListStore     *invited_rooms;
+
+  GListStore     *key_verifications;
+
+  /* for sending events, incremented for each event */
+  int             event_id;
+
+  guint           resync_id;
+
+  gboolean        db_migrated;
+  gboolean        room_list_loading;
+  gboolean        room_list_loaded;
+  gboolean        direct_room_list_loading;
+  gboolean        direct_room_list_loaded;
+
+  gboolean        db_loading;
+  gboolean        db_loaded;
+  gboolean        client_enabled;
+  gboolean        client_enabled_in_store;
+  gboolean        has_tried_connecting;
+  gboolean        is_logging_in;
+  /* Set if passsword is right/success using @access_token */
+  gboolean        login_success;
+  gboolean        is_sync;
+  gboolean        sync_failed;
+  gboolean        is_self_change;
+  gboolean        save_client_pending;
+  gboolean        save_secret_pending;
+  gboolean        is_saving_client;
+  gboolean        is_saving_secret;
+  gboolean        homeserver_verified;
+};
+
+G_DEFINE_TYPE (CmClient, cm_client, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_HOME_SERVER,
+  N_PROPS
+};
+
+enum {
+  STATUS_CHANGED,
+  ACCESS_TOKEN_CHANGED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties[N_PROPS];
+static guint signals[N_SIGNALS];
+
+const char *filter_json_str = "{ \"room\": { "
+  "  \"timeline\": { \"limit\": 20 }, "
+  "  \"state\": { \"lazy_load_members\": true } "
+  " }"
+  "}";
+
+static void     matrix_start_sync      (CmClient *self,
+                                        gpointer  tsk);
+static void     matrix_upload_key      (CmClient *self);
+static gboolean handle_matrix_glitches (CmClient *self,
+                                        GError    *error);
+
+static void
+cm_set_string_value (char       **strp,
+                     const char  *value)
+{
+  g_assert (strp);
+
+  g_free (*strp);
+  *strp = g_strdup (value);
+}
+
+static CmEvent *
+client_find_key_verification (CmClient *self,
+                              CmEvent  *event,
+                              gboolean  add_if_missing)
+{
+  CmOlmSas *olm_sas;
+  GListModel *model;
+  CmEventType type;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (CM_IS_EVENT (event));
+
+  type = cm_event_get_m_type (event);
+  model = G_LIST_MODEL (self->key_verifications);
+
+  g_assert (type >= CM_M_KEY_VERIFICATION_ACCEPT && type <= CM_M_KEY_VERIFICATION_START);
+
+  for (guint i = 0; i < g_list_model_get_n_items (model); i++)
+    {
+      g_autoptr(CmEvent) item = NULL;
+
+      item = g_list_model_get_item (model, i);
+      if (item == event)
+        return event;
+
+      olm_sas = g_object_get_data (G_OBJECT (item), "olm-sas");
+
+      if (cm_olm_sas_matches_event (olm_sas, event))
+        return item;
+    }
+
+  if (type != CM_M_KEY_VERIFICATION_START && type != CM_M_KEY_VERIFICATION_REQUEST)
+    return NULL;
+
+  olm_sas = cm_enc_get_sas_for_event (self->cm_enc, event);
+  cm_olm_sas_set_client (olm_sas, self);
+
+  if (add_if_missing)
+    g_list_store_append (self->key_verifications, event);
+
+  return event;
+}
+
+/*
+ * client_mark_for_save:
+ * @save_client: 1 for TRUE, 0 for FALSE, ignore otherwise
+ * @save_secret: 1 for TRUE, 0 for FALSE, ignore otherwise
+ */
+static void
+client_mark_for_save (CmClient *self,
+                      int       save_client,
+                      int       save_secret)
+{
+  g_assert (CM_IS_CLIENT (self));
+
+  /* always reset to 0 when asked, as this is done after saving */
+  if (save_client == 0)
+    self->save_client_pending = save_client;
+  if (save_secret == 0)
+    self->save_secret_pending = save_secret;
+
+  if (g_object_get_data (G_OBJECT (self), "no-save"))
+    return;
+
+  if (self->is_self_change)
+    goto end;
+
+  if (save_client == 1)
+    self->save_client_pending = save_client;
+  if (save_secret == 1)
+    self->save_secret_pending = save_secret;
+
+ end:
+  cm_client_save (self);
+}
+
+static void
+client_set_login_state (CmClient *self,
+                        gboolean  logging_in,
+                        gboolean  logged_in)
+{
+  g_assert (CM_IS_CLIENT (self));
+
+  /* We can't be in process of logging in and logged in the same time */
+  if (logging_in)
+    g_assert (!logged_in);
+
+  if (self->is_logging_in == logging_in &&
+      self->login_success == logged_in)
+    return;
+
+  self->is_logging_in = logging_in;
+  self->login_success = logged_in;
+  g_signal_emit (self, signals[STATUS_CHANGED], 0);
+}
+
+static void
+client_reset_state (CmClient *self)
+{
+  g_assert (CM_IS_CLIENT (self));
+
+  self->is_sync = FALSE;
+  g_clear_pointer (&self->next_batch, g_free);
+  g_clear_pointer (&self->key, g_free);
+  g_clear_pointer (&self->pickle_key, gcry_free);
+  g_clear_pointer (&self->filter_id, g_free);
+
+  g_hash_table_remove_all (self->direct_rooms);
+  g_list_store_remove_all (self->joined_rooms);
+  g_list_store_remove_all (self->invited_rooms);
+  cm_net_set_access_token (self->cm_net, NULL);
+  cm_enc_set_details (self->cm_enc, NULL, NULL);
+  client_set_login_state (self, FALSE, FALSE);
+}
+
+static void
+client_key_verification_done_cb (GObject      *obj,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  CmEvent *event;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  event = g_task_get_task_data (task);
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (CM_IS_EVENT (event));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Key verification %p cancel %s", self,
+           event, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Key verification cancel error: %s", self, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      cm_utils_remove_list_item (self->key_verifications, event);
+      g_object_set_data (G_OBJECT (event), "completed", GINT_TO_POINTER (TRUE));
+      g_signal_emit_by_name (event, "updated", 0);
+      g_task_return_boolean (task, TRUE);
+    }
+}
+
+static void
+cm_client_key_verification_done_async (CmClient            *self,
+                                       CmEvent             *verification_event,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  CmOlmSas *olm_sas = NULL;
+  CmEvent *event, *reply_event;
+  g_autoptr(GTask) task = NULL;
+  g_autofree char *uri = NULL;
+  JsonObject *root;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (CM_IS_EVENT (verification_event));
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  event = client_find_key_verification (self, verification_event, FALSE);
+  g_debug ("(%p) Key verification %p cancel", self, event);
+
+  if (event)
+    olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas");
+
+  if (!olm_sas)
+    {
+      g_debug ("(%p) Key verification %p cancel fail, not in progress", self, verification_event);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED,
+                               "Provided key verification request is not in progress");
+      return;
+    }
+
+  g_task_set_task_data (task, g_object_ref (event), g_object_unref);
+
+  reply_event = cm_olm_sas_get_done_event (olm_sas);
+  root = cm_event_get_json (reply_event);
+
+  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.done/%s",
+                         cm_event_get_txn_id (reply_event));
+  cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT,
+                          NULL, cancellable,
+                          client_key_verification_done_cb,
+                          g_steal_pointer (&task));
+}
+
+static void
+send_json_cb (GObject      *obj,
+              GAsyncResult *result,
+              gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (error)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  g_task_return_pointer (task, g_steal_pointer (&root), (GDestroyNotify)json_object_unref);
+}
+
+static gboolean
+schedule_resync (gpointer user_data)
+{
+  CmClient *self = user_data;
+  gboolean sync_now;
+
+  g_assert (CM_IS_CLIENT (self));
+  self->resync_id = 0;
+
+  sync_now = cm_client_can_connect (self);
+
+  if (sync_now)
+    matrix_start_sync (self, NULL);
+  else
+    self->resync_id = g_timeout_add_seconds (URI_REQUEST_TIMEOUT,
+                                             schedule_resync, self);
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+handle_matrix_glitches (CmClient *self,
+                        GError   *error)
+{
+  if (!error)
+    return FALSE;
+
+  if (g_error_matches (error, CM_ERROR, CM_ERROR_UNKNOWN_TOKEN) &&
+      self->password)
+    {
+      g_debug ("(%p) Handle glitch, unknown token", self);
+
+      client_reset_state (self);
+      cm_db_delete_client_async (self->cm_db, self, NULL, NULL);
+      matrix_start_sync (self, NULL);
+
+      return TRUE;
+    }
+
+  /*
+   * The G_RESOLVER_ERROR may be suggesting that the hostname is wrong, but we don't
+   * know if it's network/DNS/Proxy error. So keep retrying.
+   */
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NETWORK_UNREACHABLE) ||
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT) ||
+#if SOUP_MAJOR_VERSION == 2
+      (error->domain == SOUP_HTTP_ERROR &&
+       error->code <= SOUP_STATUS_TLS_FAILED &&
+       error->code > SOUP_STATUS_CANCELLED) ||
+#else
+      error->domain == SOUP_TLD_ERROR ||
+      error->domain == G_TLS_ERROR ||
+#endif
+      /* Should we handle connection_refused, or just keep it for localhost? */
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED) ||
+      error->domain == G_RESOLVER_ERROR ||
+      error->domain == JSON_PARSER_ERROR)
+    {
+      self->sync_failed = TRUE;
+      g_signal_emit (self, signals[STATUS_CHANGED], 0);
+
+      if (cm_client_can_connect (self))
+        {
+          CM_TRACE ("(%p) Handle glitch, network error", self);
+          g_clear_handle_id (&self->resync_id, g_source_remove);
+
+          self->resync_id = g_timeout_add_seconds (URI_REQUEST_TIMEOUT,
+                                                   schedule_resync, self);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+client_login_with_password_async (CmClient            *self,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GString) str = NULL;
+  JsonObject *object, *child;
+  GTask *task;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (cm_account_get_login_id (self->cm_account) ||
+            cm_user_get_id (CM_USER (self->cm_account)));
+  g_assert (self->homeserver_verified);
+  g_assert (self->password && *self->password);
+
+  str = g_string_new (NULL);
+  g_debug ("(%p) Logging in with '%s'", self,
+           cm_utils_anonymize (str, cm_account_get_login_id (self->cm_account)));
+
+  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */
+  object = json_object_new ();
+  json_object_set_string_member (object, "type", "m.login.password");
+  json_object_set_string_member (object, "password", self->password);
+  json_object_set_string_member (object, "initial_device_display_name", self->device_name ?: "CMatrix");
+
+  child = json_object_new ();
+
+  if (cm_utils_user_name_is_email (cm_account_get_login_id (self->cm_account)))
+    {
+      json_object_set_string_member (child, "type", "m.id.thirdparty");
+      json_object_set_string_member (child, "medium", "email");
+      json_object_set_string_member (child, "address", cm_account_get_login_id (self->cm_account));
+    }
+  else
+    {
+      json_object_set_string_member (child, "type", "m.id.user");
+      json_object_set_string_member (child, "user", cm_account_get_login_id (self->cm_account));
+    }
+
+  json_object_set_object_member (object, "identifier", child);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  cm_net_send_json_async (self->cm_net, 2, object,
+                          "/_matrix/client/r0/login", SOUP_METHOD_POST,
+                          NULL, cancellable, send_json_cb,
+                          task);
+}
+
+static void
+cm_upload_filter_cb (GObject      *obj,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Upload filter %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      if (!handle_matrix_glitches (self, error))
+        g_warning ("Error uploading filter: %s", error->message);
+
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  g_task_return_pointer (task, NULL, NULL);
+  self->filter_id = g_strdup (cm_utils_json_object_get_string (root, "filter_id"));
+  g_debug ("(%p) Upload filter, id: %s", self, self->filter_id);
+
+  client_set_login_state (self, FALSE, TRUE);
+
+  if (!self->filter_id)
+    self->filter_id = g_strdup ("");
+
+  client_mark_for_save (self, TRUE, -1);
+  cm_client_save (self);
+
+  matrix_start_sync (self, NULL);
+}
+
+static void
+matrix_upload_filter (CmClient *self,
+                      gpointer  tsk)
+{
+  g_autoptr(GTask) task = tsk;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+  g_autofree char *uri = NULL;
+  g_autoptr(JsonParser) parser = NULL;
+  JsonObject *filter = NULL;
+  JsonNode *root = NULL;
+
+  g_debug ("(%p) Upload filter", self);
+
+  parser = json_parser_new ();
+  json_parser_load_from_data (parser, filter_json_str, -1, &error);
+
+  if (error)
+    g_warning ("(%p) Error parsing filter file: %s", self, error->message);
+
+  if (!error)
+    root = json_parser_get_root (parser);
+
+  if (root)
+    filter = json_node_get_object (root);
+
+  if (error || !root || !filter)
+    {
+      g_warning ("(%p) Error getting filter file: %s", self, error ? error->message : "");
+
+      self->filter_id = g_strdup ("");
+      /* Even if we have error uploading filter, continue syncing */
+      matrix_start_sync (self, NULL);
+
+      return;
+    }
+
+  cancellable = g_task_get_cancellable (task);
+  uri = g_strconcat ("/_matrix/client/r0/user/", cm_user_get_id (CM_USER (self->cm_account)), "/filter", NULL);
+  cm_net_send_json_async (self->cm_net, 2, json_object_ref (filter),
+                          uri, SOUP_METHOD_POST,
+                          NULL, cancellable, cm_upload_filter_cb,
+                          g_steal_pointer (&task));
+}
+
+static void
+cm_client_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  CmClient *self = (CmClient *)object;
+
+  switch (prop_id)
+    {
+    case PROP_HOME_SERVER:
+      cm_client_set_homeserver (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+cm_client_finalize (GObject *object)
+{
+  CmClient *self = (CmClient *)object;
+
+  if (self->cancellable)
+    g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  g_clear_object (&self->cm_account);
+  g_clear_object (&self->cm_net);
+
+  g_list_store_remove_all (self->joined_rooms);
+  g_clear_object (&self->joined_rooms);
+
+  g_list_store_remove_all (self->key_verifications);
+  g_clear_object (&self->key_verifications);
+
+  g_hash_table_unref (self->direct_rooms);
+
+  g_free (self->homeserver);
+  g_free (self->device_id);
+  g_free (self->device_name);
+  gcry_free (self->password);
+  gcry_free (self->pickle_key);
+
+  G_OBJECT_CLASS (cm_client_parent_class)->finalize (object);
+}
+
+static void
+cm_client_class_init (CmClientClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = cm_client_set_property;
+  object_class->finalize = cm_client_finalize;
+
+  properties[PROP_HOME_SERVER] =
+    g_param_spec_string ("home-server",
+                         "Home Server",
+                         "Matrix Home Server",
+                         NULL,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * ChattyItem::status-changed:
+   * @self: a #ChattyItem
+   *
+   * status-changed signal is emitted when the client's
+   * login and sync status are changed.
+   */
+  signals [STATUS_CHANGED] =
+    g_signal_new ("status-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+cm_client_init (CmClient *self)
+{
+  self->cm_account = g_object_new (CM_TYPE_ACCOUNT, NULL);
+  cm_user_set_client (CM_USER (self->cm_account), self);
+
+  self->cm_net = cm_net_new ();
+  self->user_list = cm_user_list_new (self);
+  self->cancellable = g_cancellable_new ();
+  self->joined_rooms = g_list_store_new (CM_TYPE_ROOM);
+  self->invited_rooms = g_list_store_new (CM_TYPE_ROOM);
+  self->key_verifications = g_list_store_new (CM_TYPE_EVENT);
+  self->direct_rooms = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                              g_free, g_object_unref);
+}
+
+/**
+ * cm_client_new:
+ *
+ * Returns: (transfer full): A #CmClient
+ */
+CmClient *
+cm_client_new (void)
+{
+  return g_object_new (CM_TYPE_CLIENT, NULL);
+}
+
+static char *
+client_get_value (const char *str,
+                  const char *key)
+{
+  const char *start, *end;
+
+  if (!str || !*str)
+    return NULL;
+
+  g_assert (key && *key);
+
+  start = strstr (str, key);
+  if (start) {
+    start = start + strlen (key);
+    while (*start && *start++ != '"')
+      ;
+
+    end = start - 1;
+    do {
+      end++;
+      end = strchr (end, '"');
+    } while (end && *(end - 1) == '\\' && *(end - 2) != '\\');
+
+    if (end && end > start)
+      return g_strndup (start, end - start);
+  }
+
+  return NULL;
+}
+
+void
+cm_client_enable_as_in_store (CmClient *self)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  self->is_self_change = TRUE;
+
+  if (self->client_enabled_in_store)
+    cm_client_set_enabled (self, TRUE);
+
+  self->client_enabled_in_store = FALSE;
+  self->is_self_change = FALSE;
+}
+
+CmClient *
+cm_client_new_from_secret (gpointer  secret_retrievable,
+                           CmDb     *db)
+{
+  CmClient *self = NULL;
+  g_autoptr(GHashTable) attributes = NULL;
+  SecretRetrievable *item = secret_retrievable;
+  g_autoptr(SecretValue) value = NULL;
+  const char *homeserver, *credentials = NULL;
+  const char *username, *login_username;
+  char *password, *token, *device_id;
+  char *password_str = NULL, *token_str = NULL;
+  g_autofree char *enabled = NULL;
+
+  g_return_val_if_fail (SECRET_IS_RETRIEVABLE (item), NULL);
+  g_return_val_if_fail (CM_IS_DB (db), NULL);
+
+  value = secret_retrievable_retrieve_secret_sync (item, NULL, NULL);
+
+  if (value)
+    credentials = secret_value_get_text (value);
+
+  if (!credentials)
+    return NULL;
+
+  attributes = secret_retrievable_get_attributes (item);
+  login_username = g_hash_table_lookup (attributes, CM_USERNAME_ATTRIBUTE);
+  homeserver = g_hash_table_lookup (attributes, CM_SERVER_ATTRIBUTE);
+
+  device_id = client_get_value (credentials, "\"device-id\"");
+  username = client_get_value (credentials, "\"username\"");
+  password = client_get_value (credentials, "\"password\"");
+  enabled = client_get_value (credentials, "\"enabled\"");
+  token = client_get_value (credentials, "\"access-token\"");
+
+  if (token)
+    token_str = g_strcompress (token);
+
+  if (password)
+    password_str = g_strcompress (password);
+
+  self = g_object_new (CM_TYPE_CLIENT, NULL);
+  self->is_self_change = TRUE;
+  cm_client_set_db (self, db);
+  cm_client_set_homeserver (self, homeserver);
+  cm_account_set_login_id (self->cm_account, login_username);
+  cm_client_set_user_id (self, username);
+  cm_client_set_password (self, password_str);
+  cm_client_set_device_id (self, device_id);
+
+  if (g_strcmp0 (enabled, "true") == 0)
+    self->client_enabled_in_store = TRUE;
+
+  cm_client_set_access_token (self, token_str);
+
+  if (token && device_id) {
+    g_autofree char *pickle = NULL;
+
+    pickle = client_get_value (credentials, "\"pickle-key\"");
+    cm_client_set_pickle_key (self, pickle);
+  }
+
+  self->is_self_change = FALSE;
+
+  cm_utils_free_buffer (device_id);
+  cm_utils_free_buffer (password);
+  cm_utils_free_buffer (password_str);
+  cm_utils_free_buffer (token);
+  cm_utils_free_buffer (token_str);
+
+  return self;
+}
+static void
+save_secrets_cb (GObject      *object,
+                 GAsyncResult *result,
+                 gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean ret;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  ret = cm_secret_store_save_finish (result, &error);
+
+  if (error)
+    {
+      client_mark_for_save (self, -1, TRUE);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      g_task_return_boolean (task, ret);
+    }
+
+  /* Unset at the end so that the client won't initiate
+   * saving the secret immediately on error.
+   */
+  self->is_saving_secret = FALSE;
+}
+
+void
+cm_client_save_secrets_async (CmClient            *self,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  char *pickle_key = NULL;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  task = g_task_new (self, NULL, callback, user_data);
+
+  if (g_object_get_data (G_OBJECT (self), "no-save"))
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Secrets marked not to save");
+      return;
+    }
+
+  if (self->is_saving_secret)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
+                               "Secrets are already being saved");
+      return;
+    }
+
+  self->is_saving_secret = TRUE;
+  client_mark_for_save (self, -1, FALSE);
+
+  if (self->cm_enc)
+    pickle_key = cm_enc_get_pickle_key (self->cm_enc);
+
+  cm_secret_store_save_async (self,
+                              g_strdup (cm_client_get_access_token (self)),
+                              pickle_key, NULL,
+                              save_secrets_cb,
+                              g_steal_pointer (&task));
+}
+
+gboolean
+cm_client_save_secrets_finish (CmClient      *self,
+                               GAsyncResult  *result,
+                               GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+delete_secrets_cb (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean ret;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  ret = cm_secret_store_delete_finish (result, &error);
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      g_list_store_remove_all (self->joined_rooms);
+      g_task_return_boolean (task, ret);
+    }
+}
+
+void
+cm_client_delete_secrets_async (CmClient            *self,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  cm_client_set_enabled (self, FALSE);
+  cm_secret_store_delete_async (self, NULL,
+                                delete_secrets_cb, task);
+}
+
+gboolean
+cm_client_delete_secrets_finish (CmClient      *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+CmAccount *
+cm_client_get_account (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->cm_account;
+}
+
+static void
+db_load_client_cb (GObject      *obj,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  CmClient *self;
+  guint room_count = 0;
+  gboolean success;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  g_assert (!self->cm_enc);
+
+  self->db_loaded = TRUE;
+  self->db_loading = FALSE;
+
+  success = cm_db_load_client_finish (self->cm_db, result, &error);
+  g_debug ("(%p) Load db %s", self,
+           CM_LOG_SUCCESS (!error || g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)));
+
+  if (!success)
+    {
+      if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          g_autoptr(GString) str = NULL;
+
+          str = g_string_new (NULL);
+          g_warning ("(%p) Error loading client '%s': %s", self,
+                     cm_utils_anonymize (str, cm_user_get_id (CM_USER (self->cm_account))),
+                     error->message);
+        }
+
+      /* We can load further even if fail to load from db */
+      /* XXX: handle difference between if the user is missing from db and failed fetching data */
+      matrix_start_sync (self, g_steal_pointer (&task));
+      return;
+    }
+
+  if (g_object_get_data (G_OBJECT (result), "pickle") && self->pickle_key)
+    {
+      const char *pickle;
+
+      pickle = g_object_get_data (G_OBJECT (result), "pickle");
+      self->cm_enc = cm_enc_new (self->cm_db, pickle, self->pickle_key);
+    }
+
+  if (self->cm_enc)
+    cm_enc_set_details (self->cm_enc,
+                        cm_client_get_user_id (self),
+                        cm_client_get_device_id (self));
+  else
+    g_clear_pointer (&self->pickle_key, gcry_free);
+
+  if (g_object_get_data (G_OBJECT (result), "rooms"))
+    {
+      g_autoptr(GPtrArray) rooms = NULL;
+
+      rooms = g_object_steal_data (G_OBJECT (result), "rooms");
+      room_count = rooms->len;
+
+      for (guint i = 0; i < rooms->len; i++)
+        {
+          CmRoom *room = rooms->pdata[i];
+          cm_room_set_client (room, self);
+        }
+
+      g_list_store_splice (self->joined_rooms, 0, 0, rooms->pdata, rooms->len);
+    }
+
+  self->db_migrated = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (result), "db-migrated"));
+  self->filter_id = g_strdup (g_object_get_data (G_OBJECT (result), "filter-id"));
+  self->next_batch = g_strdup (g_object_get_data (G_OBJECT (result), "batch"));
+  g_debug ("(%p) Load db, added %u room(s), db migrated: %s, filter-id: %s",
+           self, room_count, CM_LOG_BOOL (self->db_migrated), self->filter_id);
+
+  matrix_start_sync (self, g_steal_pointer (&task));
+}
+
+/*
+ * cm_client_pop_event_id:
+ * @self: A #CmClient
+ *
+ * Get a number to be used as event suffix.
+ * The number is incremented per request,
+ * so you always get a different number.
+ *
+ * Returns: An integer
+ */
+int
+cm_client_pop_event_id (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), 0);
+
+  self->event_id++;
+
+  return self->event_id - 1;
+}
+
+/*
+ * cm_client_get_db:
+ * @self: A #CmClient
+ *
+ * Get the #CmDb associated with the client.
+ * Can be %NULL if client hasn't loaded yet.
+ *
+ * Returns: (transfer none): A #CmDb
+ */
+CmDb *
+cm_client_get_db (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->cm_db;
+}
+
+/*
+ * cm_client_get_net:
+ * @self: A #CmClient
+ *
+ * Get the #CmNet associated with the client
+ *
+ * Returns: (transfer none): A #CmNet
+ */
+CmNet *
+cm_client_get_net (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->cm_net;
+}
+
+/*
+ * cm_client_get_enc:
+ * @self: A #CmClient
+ *
+ * Get the #CmEnc associated with the client,
+ * Can be %NULL if not loaded/logged in yet.
+ *
+ * Returns: (transfer none): A #CmEnc
+ */
+CmEnc *
+cm_client_get_enc (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->cm_enc;
+}
+
+/*
+ * cm_client_set_db:
+ * @self: A #CmClient
+ * @db: A #CmDb
+ *
+ * Set the db object for the client.
+ * There is at most one db object which
+ * is shared for every client.
+ *
+ * This should be set only after user ID
+ * and device ID is set.
+ *
+ * @self shall load the client info immediately
+ * after db is set and enable if the account
+ * if that's how it's set in db.
+ */
+void
+cm_client_set_db (CmClient *self,
+                  CmDb     *db)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (CM_IS_DB (db));
+
+  if (self->cm_db)
+    return;
+
+  self->cm_db = g_object_ref (db);
+}
+
+const char *
+cm_client_get_filter_id (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  if (self->filter_id && *self->filter_id)
+    return self->filter_id;
+
+  return NULL;
+}
+
+/**
+ * cm_client_set_enabled:
+ * @self: A #CmClient
+ * @enable: Whether to enable client
+ *
+ * Whether to enable the client and begin
+ * sync with the server.
+ */
+void
+cm_client_set_enabled (CmClient *self,
+                       gboolean  enable)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  if (enable)
+    g_return_if_fail (self->cm_db);
+
+  enable = !!enable;
+
+  if (self->client_enabled == enable)
+    return;
+
+  g_debug ("(%p) Set enable to %s", self, CM_LOG_BOOL (enable));
+
+  self->client_enabled = enable;
+  g_signal_emit (self, signals[STATUS_CHANGED], 0);
+
+  if (self->client_enabled)
+    {
+      matrix_start_sync (self, NULL);
+    }
+  else
+    {
+      cm_client_stop_sync (self);
+    }
+
+  client_mark_for_save (self, TRUE, TRUE);
+  cm_client_save (self);
+}
+
+static void
+db_save_cb (GObject      *object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+  g_autoptr(GError) error = NULL;
+  gboolean status;
+
+  status = cm_db_save_client_finish (self->cm_db, result, &error);
+  self->is_saving_client = FALSE;
+
+  if (error || !status)
+    self->save_client_pending = TRUE;
+
+  if (error)
+    g_warning ("Error saving to db: %s", error->message);
+
+  /* If settings changed when we were saving the current settings, repeat. */
+  cm_client_save (self);
+}
+
+/*
+ * cm_client_set_enabled:
+ * @self: A #CmClient
+ * @force: Whether to force saving to db
+ *
+ * Save the changes to associated CmDb.
+ * Set @force to %TRUE to force saving to
+ * db even when no changes are made
+ */
+void
+cm_client_save (CmClient *self)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  if (g_object_get_data (G_OBJECT (self), "no-save"))
+    return;
+
+  if (!cm_account_get_login_id (self->cm_account) &&
+      !cm_user_get_id (CM_USER (self->cm_account)))
+    return;
+
+  if (self->save_client_pending && !self->is_saving_client &&
+      cm_client_get_device_id (self))
+    {
+      char *pickle = NULL;
+
+      self->is_saving_client = TRUE;
+      self->save_client_pending = FALSE;
+
+      if (self->cm_enc)
+        pickle = cm_enc_get_pickle (self->cm_enc);
+
+      cm_db_save_client_async (self->cm_db, self, pickle,
+                               db_save_cb,
+                               g_object_ref (self));
+    }
+
+  if (self->save_secret_pending && !self->is_saving_secret)
+    cm_client_save_secrets_async (self, NULL, NULL);
+}
+
+/**
+ * cm_client_get_enabled:
+ * @self: A #CmClient
+ *
+ * Wheter @self has been enabled.
+ *
+ * Enabled client doesn't necessarily mean the client
+ * has logged in or is in sync with server.
+ * Also See cm_client_is_sync().
+ *
+ * Returns: %TRUE if @self has been enabled, %FALSE
+ * otherwise
+ */
+gboolean
+cm_client_get_enabled (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+
+  return self->client_enabled || self->client_enabled_in_store ||
+    GPOINTER_TO_INT (g_object_get_data (G_OBJECT (self), "enable"));
+}
+
+/**
+ * cm_client_set_sync_callback:
+ * @self: A #CmClient
+ * @callback: A #CmCallback
+ * @callback_data: A #GObject derived object for @callback user_data
+ * @callback_data_destroy: (nullable): The method to destroy @callback_data
+ *
+ * Set the sync callback which shall be executed for the
+ * events happening in @self.
+ *
+ * @callback_data_destroy() shall be executead only if @callback_data
+ * is not NULL.
+ */
+void
+cm_client_set_sync_callback (CmClient       *self,
+                             CmCallback      callback,
+                             gpointer        callback_data,
+                             GDestroyNotify  callback_data_destroy)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (callback);
+
+  if (self->cb_data &&
+      self->cb_destroy)
+    self->cb_destroy (self->cb_data);
+
+  self->callback = callback;
+  self->cb_data = callback_data;
+  self->cb_destroy = callback_data_destroy;
+}
+
+/**
+ * cm_client_set_user_id:
+ * @self: A #CmClient
+ * @matrix_user_id: A fully qualified matrix user ID
+ *
+ * Set the client matrix user ID.  This can be set only
+ * before the account has been enabled and never after.
+ *
+ * Returns: %TRUE if @matrix_user_id was successfully set,
+ * %FALSE otherwise.
+ */
+gboolean
+cm_client_set_user_id (CmClient   *self,
+                       const char *matrix_user_id)
+{
+  g_autoptr(GString) str = NULL;
+  GRefString *new_user_id = NULL;
+  g_autofree char *user_id = NULL;
+
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (!self->is_logging_in, FALSE);
+  g_return_val_if_fail (!self->login_success, FALSE);
+
+  if (!cm_utils_user_name_valid (matrix_user_id))
+    {
+      g_debug ("(%p) New user ID: '%s' %s. ID not valid", self,
+               matrix_user_id, CM_LOG_SUCCESS (FALSE));
+      return FALSE;
+    }
+
+  if (cm_user_get_id (CM_USER (self->cm_account)))
+    {
+      g_debug ("(%p) New user ID not set, a user id is already set", self);
+      return FALSE;
+    }
+
+  user_id = g_ascii_strdown (matrix_user_id, -1);
+  new_user_id = g_ref_string_new_intern (user_id);
+
+  cm_user_set_user_id (CM_USER (self->cm_account), new_user_id);
+  cm_user_list_set_account (self->user_list, self->cm_account);
+
+  str = g_string_new (NULL);
+  g_debug ("(%p) New user ID set: '%s'", self,
+           cm_utils_anonymize (str, matrix_user_id));
+
+  client_mark_for_save (self, TRUE, TRUE);
+
+  return TRUE;
+}
+
+/**
+ * cm_client_get_user_id:
+ * @self: A #CmClient
+ *
+ * Get the matrix user ID of the client @self.
+ * user ID may be available only after the
+ * login has succeeded and may return %NULL
+ * otherwise.
+ *
+ * Returns: (nullable) (transfer none): The matrix user ID of the client
+ */
+GRefString *
+cm_client_get_user_id (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return cm_user_get_id (CM_USER (self->cm_account));
+}
+
+/**
+ * cm_client_set_homeserver:
+ * @self: A #CmClient
+ * @homeserver: The homserver URL
+ *
+ * Set the matrix Home server URL for the client.
+ *
+ * Returns: %TRUE if @homeserver is valid,
+ * %FALSE otherwise.
+ */
+gboolean
+cm_client_set_homeserver (CmClient   *self,
+                          const char *homeserver)
+{
+#if SOUP_MAJOR_VERSION == 2
+  g_autoptr(SoupURI) uri = NULL;
+#else
+  g_autoptr(GUri) uri = NULL;
+#endif
+  GString *server;
+
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (!self->is_logging_in, FALSE);
+  g_return_val_if_fail (!self->login_success, FALSE);
+
+  if (!homeserver || !*homeserver)
+    return FALSE;
+
+  if (!g_str_has_prefix (homeserver, "http://") &&
+      !g_str_has_prefix (homeserver, "https://"))
+    return FALSE;
+
+  if (!cm_utils_home_server_valid (homeserver))
+    return FALSE;
+
+  server = g_string_new (homeserver);
+  if (g_str_has_suffix (server->str, "/"))
+    g_string_truncate (server, server->len - 1);
+
+  if (g_strcmp0 (self->homeserver, server->str) == 0)
+    {
+      g_string_free (server, TRUE);
+      return TRUE;
+    }
+
+  g_free (self->homeserver);
+  self->homeserver = g_string_free (server, FALSE);
+  cm_net_set_homeserver (self->cm_net, homeserver);
+  client_mark_for_save (self, TRUE, TRUE);
+
+  return TRUE;
+}
+
+/**
+ * cm_client_get_homeserver:
+ * @self: A #CmClient
+ *
+ * Get the Home server set for the client.
+ *
+ * Returns: (nullable): The Home server for the client
+ */
+const char *
+cm_client_get_homeserver (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  if (self->homeserver && *self->homeserver)
+    return self->homeserver;
+
+  return NULL;
+}
+
+/**
+ * cm_client_set_password:
+ * @self: A #CmClient
+ * @password: The password for the @self
+ *
+ * Set the password for the client.  You can't
+ * set the password once the client is enabled,
+ * except in the callback when the client failed
+ * to login due to wrong password.
+ */
+void
+cm_client_set_password (CmClient   *self,
+                        const char *password)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!self->is_logging_in);
+  g_return_if_fail (!self->login_success);
+  g_return_if_fail (!self->is_sync);
+
+  g_clear_pointer (&self->password, gcry_free);
+
+  if (password && *password)
+    {
+      self->password = gcry_malloc_secure (strlen (password) + 1);
+      strcpy (self->password, password);
+    }
+
+  client_mark_for_save (self, -1, TRUE);
+
+  if (self->has_tried_connecting &&
+      cm_client_get_enabled (self))
+    {
+      cm_client_stop_sync (self);
+      cm_client_start_sync (self);
+    }
+}
+
+/**
+ * cm_client_get_password:
+ * @self: A #CmClient
+ *
+ * Get the password set for the @self.
+ *
+ * Returns: The password string
+ */
+const char *
+cm_client_get_password (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->password;
+}
+
+/**
+ * cm_client_set_access_token:
+ * @self: A #CmClient
+ * @access_token: (nullable): The access token
+ *
+ * Set the access token to be used for sync.
+ * If set to %NULL, @self shall login using password
+ * And use the newly got access token for sync.
+ */
+void
+cm_client_set_access_token (CmClient   *self,
+                            const char *access_token)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!self->is_logging_in);
+  g_return_if_fail (!self->login_success);
+
+  cm_net_set_access_token (self->cm_net, access_token);
+}
+
+/**
+ * cm_client_get_access_token:
+ * @self: A #CmClient
+ *
+ * Get the access token of the client @self
+ *
+ * Returns: (nullable): The access token set
+ */
+const char *
+cm_client_get_access_token (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return cm_net_get_access_token (self->cm_net);
+}
+
+/*
+ * cm_client_get_next_batch:
+ * @self: A #CmClient
+ *
+ * Get the next batch of the client @self
+ * for the last /sync request
+ *
+ * Returns: (nullable): The next batch if set
+ */
+const char *
+cm_client_get_next_batch (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->next_batch;
+}
+
+CmUserList *
+cm_client_get_user_list (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->user_list;
+}
+
+/**
+ * cm_client_set_device_id:
+ * @self: A #CmClient
+ * @device_id: The device ID
+ *
+ * Set the device ID for @client.
+ */
+void
+cm_client_set_device_id (CmClient   *self,
+                         const char *device_id)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!self->is_logging_in);
+  g_return_if_fail (!self->login_success);
+
+  g_free (self->device_id);
+  self->device_id = g_strdup (device_id);
+}
+
+/**
+ * cm_client_get_device_id:
+ * @self: A #CmClient
+ *
+ * Get the device ID of the client @self.
+ * Device ID may be available only after
+ * a successful login.
+ *
+ * Returns: (nullable): The Device ID string
+ */
+const char *
+cm_client_get_device_id (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->device_id;
+}
+
+/**
+ * cm_client_set_device_name:
+ * @self: A #CmClient
+ * @device_name: The device name string
+ *
+ * Set the device name which shall be used as
+ * the human readable identifier for the device
+ */
+void
+cm_client_set_device_name (CmClient   *self,
+                           const char *device_name)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  g_free (self->device_name);
+  self->device_name = g_strdup (device_name);
+}
+
+/**
+ * cm_client_get_device_name:
+ * @self: A #CmClient
+ *
+ * Get the device name of the client @self
+ * as set with cm_client_set_device_name().
+ *
+ * Returns: (nullable): The Device name string
+ */
+const char *
+cm_client_get_device_name (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return self->device_name;
+}
+
+/**
+ * cm_client_set_pickle_key:
+ * @self: A #CmClient
+ * @pickle_key: The pickle key
+ *
+ * Set the account pickle key.  This key is used as
+ * the password to decrypt the encrypted pickle loaded
+ * from db, and so, this should be set before the db
+ * is set.
+ */
+void
+cm_client_set_pickle_key (CmClient   *self,
+                          const char *pickle_key)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!self->pickle_key);
+
+  if (pickle_key && *pickle_key)
+    {
+      self->pickle_key = gcry_malloc_secure (strlen (pickle_key) + 1);
+      strcpy (self->pickle_key, pickle_key);
+    }
+}
+
+/**
+ * cm_client_get_pickle_key:
+ * @self: A #CmClient
+ *
+ * Get the pickle password
+ */
+const char *
+cm_client_get_pickle_key (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  if (self->cm_enc)
+    return cm_enc_get_pickle_key (self->cm_enc);
+
+  return NULL;
+}
+
+/**
+ * cm_client_get_ed25519_key:
+ * @self: A #CmClient
+ *
+ * Get the public ed25519 key of own device.
+ */
+const char *
+cm_client_get_ed25519_key (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  if (self->cm_enc)
+    return cm_enc_get_ed25519_key (self->cm_enc);
+
+  return NULL;
+}
+
+static void
+client_join_room_by_id_cb (GObject      *obj,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_client_join_room_by_id_async (CmClient            *self,
+                                 const char          *room_id,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autofree char *uri = NULL;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (room_id && *room_id == '!');
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  uri = g_strconcat ("/_matrix/client/r0/join/", room_id, NULL);
+  cm_net_send_data_async (self->cm_net, 2, NULL, 0,
+                          uri, SOUP_METHOD_POST, NULL,
+                          cancellable,
+                          client_join_room_by_id_cb,
+                          task);
+}
+
+gboolean
+cm_client_join_room_by_id_finish (CmClient      *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+cm_client_get_homeserver_async (CmClient            *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  const char *user_id;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, cm_client_get_homeserver_async);
+  g_debug ("(%p) Get homeserver", self);
+
+  if (self->homeserver_verified && self->homeserver && *(self->homeserver))
+    {
+      g_debug ("(%p) Get homeserver already loaded", self);
+      g_task_return_pointer (task, self->homeserver, NULL);
+      return;
+    }
+
+  user_id = cm_user_get_id (CM_USER (self->cm_account));
+
+  if (!user_id)
+    user_id = cm_account_get_login_id (self->cm_account);
+
+  if (!cm_utils_user_name_valid (user_id))
+    user_id = NULL;
+
+  if (!user_id && !self->homeserver)
+    {
+      g_debug ("(%p) Get homeserver failed, no user id to guess", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "No user id present in client");
+      return;
+    }
+
+  matrix_start_sync (self, g_steal_pointer (&task));
+}
+
+const char *
+cm_client_get_homeserver_finish (CmClient      *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+verification_load_user_devices_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GPtrArray) users = NULL;
+  GError *error = NULL;
+
+  users = cm_user_list_load_devices_finish (CM_USER_LIST (object), result, &error);
+
+  if (error)
+    g_debug ("Load user devices error: %s", error->message);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, !users || users->len == 0);
+}
+
+static void
+client_key_verification_accept_cb (GObject      *obj,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  CmEvent *event;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  event = g_task_get_task_data (task);
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (CM_IS_EVENT (event));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Key verification %p accept %s", self,
+           event, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Key verification accept error: %s", self, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      g_autoptr(GPtrArray) users = NULL;
+      CmUser *user;
+
+      users = g_ptr_array_new_full (1, g_object_unref);
+      user = cm_event_get_sender (event);;
+      g_ptr_array_add (users, g_object_ref (user));
+
+      cm_user_list_load_devices_async (self->user_list, users,
+                                       verification_load_user_devices_cb,
+                                       g_steal_pointer (&task));
+    }
+}
+
+void
+cm_client_key_verification_continue_async (CmClient            *self,
+                                           CmEvent             *verification_event,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  CmEvent *event, *reply_event;
+  CmOlmSas *olm_sas = NULL;
+  g_autofree char *uri = NULL;
+  JsonObject *root;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (CM_IS_EVENT (verification_event));
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  event = client_find_key_verification (self, verification_event, FALSE);
+  g_debug ("(%p) Key verification %p continue", self, event);
+
+  if (event)
+    olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas");
+
+  if (!olm_sas)
+    {
+      g_debug ("(%p) Key verification %p continue fail, not in progress", self, verification_event);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED,
+                               "Provided key verification request is not in progress");
+      return;
+    }
+
+  g_task_set_task_data (task, g_object_ref (event), g_object_unref);
+
+  reply_event = cm_olm_sas_get_accept_event (olm_sas);
+  root = cm_event_get_json (reply_event);
+  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.accept/%s",
+                         cm_event_get_txn_id (reply_event));
+  cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT,
+                          NULL, cancellable,
+                          client_key_verification_accept_cb,
+                          g_steal_pointer (&task));
+}
+
+gboolean
+cm_client_key_verification_continue_finish (CmClient      *self,
+                                            GAsyncResult  *result,
+                                            GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+client_key_verification_match_cb (GObject      *obj,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  CmEvent *event;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  event = g_task_get_task_data (task);
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (CM_IS_EVENT (event));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Key verification %p match %s", self,
+           event, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Key verification match error: %s", self, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      g_task_return_boolean (task, TRUE);
+    }
+}
+
+void
+cm_client_key_verification_match_async (CmClient            *self,
+                                        CmEvent             *verification_event,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  CmEvent *event, *reply_event;
+  CmOlmSas *olm_sas = NULL;
+  g_autofree char *uri = NULL;
+  JsonObject *root;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (CM_IS_EVENT (verification_event));
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  event = client_find_key_verification (self, verification_event, FALSE);
+  g_debug ("(%p) Key verification %p match", self, event);
+
+  if (event)
+    olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas");
+
+  if (!olm_sas)
+    {
+      g_debug ("(%p) Key verification %p match fail, not in progress", self, verification_event);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED,
+                               "Provided key verification request is not in progress");
+      return;
+    }
+
+  g_task_set_task_data (task, g_object_ref (event), g_object_unref);
+
+  reply_event = cm_olm_sas_get_mac_event (olm_sas);
+  root = cm_event_get_json (reply_event);
+  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.mac/%s",
+                         cm_event_get_txn_id (reply_event));
+  cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT,
+                          NULL, cancellable,
+                          client_key_verification_match_cb,
+                          g_steal_pointer (&task));
+}
+
+gboolean
+cm_client_key_verification_match_finish (CmClient      *self,
+                                         GAsyncResult  *result,
+                                         GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+client_key_verification_cancel_cb (GObject      *obj,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  CmEvent *event;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  event = g_task_get_task_data (task);
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (CM_IS_EVENT (event));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Key verification %p cancel %s", self,
+           event, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Key verification cancel error: %s", self, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      cm_utils_remove_list_item (self->key_verifications, event);
+      g_task_return_boolean (task, TRUE);
+    }
+}
+
+void
+cm_client_key_verification_cancel_async (CmClient            *self,
+                                         CmEvent             *verification_event,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  CmOlmSas *olm_sas = NULL;
+  CmEvent *event, *reply_event;
+  g_autoptr(GTask) task = NULL;
+  g_autofree char *uri = NULL;
+  const char *cancel_code;
+  JsonObject *root;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (CM_IS_EVENT (verification_event));
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  event = client_find_key_verification (self, verification_event, FALSE);
+  g_debug ("(%p) Key verification %p cancel", self, event);
+
+  if (event)
+    olm_sas = g_object_get_data (G_OBJECT (event), "olm-sas");
+
+  if (!olm_sas)
+    {
+      g_debug ("(%p) Key verification %p cancel fail, not in progress", self, verification_event);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED,
+                               "Provided key verification request is not in progress");
+      return;
+    }
+
+  g_task_set_task_data (task, g_object_ref (event), g_object_unref);
+
+  cancel_code = cm_olm_sas_get_cancel_code (olm_sas);
+  reply_event = cm_olm_sas_get_cancel_event (olm_sas, cancel_code);
+  root = cm_event_get_json (reply_event);
+
+  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.cancel/%s",
+                         cm_event_get_txn_id (reply_event));
+  cm_net_send_json_async (self->cm_net, 0, root, uri, SOUP_METHOD_PUT,
+                          NULL, cancellable,
+                          client_key_verification_cancel_cb,
+                          g_steal_pointer (&task));
+}
+
+gboolean
+cm_client_key_verification_cancel_finish (CmClient      *self,
+                                          GAsyncResult  *result,
+                                          GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+client_verify_homeserver_cb (GObject      *obj,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  client_set_login_state (self, FALSE, FALSE);
+  self->homeserver_verified = cm_utils_verify_homeserver_finish (result, &error);
+  g_object_set_data (G_OBJECT (task), "action", "verify-homeserver");
+
+  g_debug ("(%p) Verify home server %s", self, CM_LOG_SUCCESS (!error));
+
+  /* Since GTask can't have timeout, We cancel the cancellable to fake timeout */
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT))
+    {
+      g_clear_object (&self->cancellable);
+      self->cancellable = g_cancellable_new ();
+    }
+
+  g_clear_object (&self->gaddress);
+  self->gaddress = g_object_steal_data (G_OBJECT (result), "address");
+
+  self->has_tried_connecting = TRUE;
+
+  if (g_task_get_source_tag (task) != cm_client_get_homeserver_async &&
+      handle_matrix_glitches (self, error))
+    return;
+
+  if (self->homeserver_verified)
+    {
+      if (g_task_get_source_tag (task) == cm_client_get_homeserver_async)
+        g_task_return_pointer (task, self->homeserver, NULL);
+      else
+        matrix_start_sync (self, g_steal_pointer (&task));
+    }
+  else
+    {
+      self->sync_failed = TRUE;
+
+      g_signal_emit (self, signals[STATUS_CHANGED], 0);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Failed to verify homeserver");
+      if (self->callback)
+        self->callback (self->cb_data, self, NULL, NULL, error);
+    }
+}
+
+static void
+client_password_login_cb (GObject      *obj,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(JsonObject) root = NULL;
+  JsonObject *object = NULL;
+  const char *value;
+  CmClient *self;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Login with password %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_autoptr(GString) str = NULL;
+      self->sync_failed = TRUE;
+
+      str = g_string_new (NULL);
+      g_debug ("(%p) Login %s, username: %s, error: %s", self, CM_LOG_SUCCESS (FALSE),
+               cm_utils_anonymize (str, cm_account_get_login_id (self->cm_account)),
+               error->message);
+      if (error->code == CM_ERROR_FORBIDDEN)
+        error->code = CM_ERROR_BAD_PASSWORD;
+
+      client_set_login_state (self, FALSE, FALSE);
+
+      if (!handle_matrix_glitches (self, error) && self->callback)
+        self->callback (self->cb_data, self, NULL, NULL, error);
+
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login */
+  value = cm_utils_json_object_get_string (root, "user_id");
+  self->is_logging_in = FALSE;
+  cm_client_set_user_id (self, value);
+  self->is_logging_in = TRUE;
+
+  value = cm_utils_json_object_get_string (root, "access_token");
+  cm_net_set_access_token (self->cm_net, value);
+
+  value = cm_utils_json_object_get_string (root, "device_id");
+  cm_set_string_value (&self->device_id, value);
+
+  object = cm_utils_json_object_get_object (root, "well_known");
+  object = cm_utils_json_object_get_object (object, "m.homeserver");
+  value = cm_utils_json_object_get_string (object, "base_url");
+  g_clear_object (&self->cm_enc);
+  self->cm_enc = cm_enc_new (self->cm_db, NULL, NULL);
+  cm_enc_set_details (self->cm_enc,
+                      cm_client_get_user_id (self),
+                      cm_client_get_device_id (self));
+  cm_set_string_value (&self->key, cm_enc_get_device_keys_json (self->cm_enc));
+  self->is_logging_in = FALSE;
+  cm_client_set_homeserver (self, value);
+  client_set_login_state (self, FALSE, !!cm_net_get_access_token (self->cm_net));
+  client_mark_for_save (self, TRUE, TRUE);
+  cm_client_save (self);
+
+  matrix_start_sync (self, g_steal_pointer (&task));
+}
+
+static gboolean
+room_matches_id (CmRoom     *room,
+                 const char *room_id)
+{
+  const char *item_room_id;
+
+  g_assert (CM_IS_ROOM (room));
+
+  item_room_id = cm_room_get_id (room);
+
+  if (g_strcmp0 (room_id, item_room_id) == 0)
+    return TRUE;
+
+  return FALSE;
+}
+
+static CmRoom *
+client_find_room (CmClient   *self,
+                  const char *room_id,
+                  GListStore *rooms)
+{
+  guint n_items;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (room_id && *room_id);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (rooms));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmRoom) room = NULL;
+
+      room = g_list_model_get_item (G_LIST_MODEL (rooms), i);
+      if (room_matches_id (room, room_id))
+        return room;
+    }
+
+  return NULL;
+}
+
+static void
+room_loaded_cb (GObject      *obj,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+  g_autoptr(GError) error = NULL;
+  CmRoom *room = CM_ROOM (obj);
+
+  cm_room_load_finish (room, result, &error);
+}
+
+static gboolean
+handle_one_time_keys (CmClient   *self,
+                      JsonObject *object)
+{
+  size_t count, limit;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!object)
+    return FALSE;
+
+  count = cm_utils_json_object_get_int (object, "signed_curve25519");
+  limit = cm_enc_max_one_time_keys (self->cm_enc) / 2;
+
+  /* If we don't have enough onetime keys add some */
+  if (count < limit)
+    {
+      /* First, get all already created keys */
+      if (!self->key)
+        self->key = cm_enc_get_one_time_keys_json (self->cm_enc);
+
+      /* If we got no keys, create new ones and get them */
+      if (!self->key)
+        {
+          g_debug ("(%p) Generating %" G_GSIZE_FORMAT " onetime keys", self, limit - count);
+          cm_enc_create_one_time_keys (self->cm_enc, limit - count);
+          self->key = cm_enc_get_one_time_keys_json (self->cm_enc);
+        }
+      matrix_upload_key (self);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+upload_key_cb (GObject      *obj,
+               GAsyncResult *result,
+               gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GError) error = NULL;
+  JsonObject *object = NULL;
+  g_autofree char *json_str = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (G_IS_TASK (result));
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Upload key %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      self->sync_failed = TRUE;
+      handle_matrix_glitches (self, error);
+      g_debug ("Error uploading key: %s", error->message);
+      return;
+    }
+
+  json_str = cm_utils_json_object_to_string (root, FALSE);
+  cm_enc_publish_one_time_keys (self->cm_enc);
+
+  object = cm_utils_json_object_get_object (root, "one_time_key_counts");
+
+  if (!handle_one_time_keys (self, object))
+    matrix_start_sync (self, NULL);
+}
+
+static void
+matrix_upload_key (CmClient *self)
+{
+  char *key;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (self->key);
+
+  key = g_steal_pointer (&self->key);
+
+  g_debug ("(%p) Upload key", self);
+  cm_net_send_data_async (self->cm_net, 2, key, strlen (key),
+                          "/_matrix/client/r0/keys/upload", SOUP_METHOD_POST,
+                          NULL, self->cancellable, upload_key_cb,
+                          g_object_ref (self));
+}
+
+/* Get the list of chat rooms marked as direct */
+static void
+parse_direct_rooms (CmClient   *self,
+                    JsonObject *root)
+{
+  g_autoptr(GList) user_ids = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  user_ids = json_object_get_members (root);
+
+  for (GList *user_id = user_ids; user_id; user_id = user_id->next)
+    {
+      JsonArray *array;
+      guint length = 0;
+
+      array = cm_utils_json_object_get_array (root, user_id->data);
+      if (array)
+        length = json_array_get_length (array);
+
+      for (guint i = 0; i < length; i++)
+        {
+          CmRoom *room;
+          const char *room_id;
+
+          room_id = json_array_get_string_element (array, i);
+
+          room = g_hash_table_lookup (self->direct_rooms, room_id);
+
+          if (!room)
+            room = client_find_room (self, room_id, self->joined_rooms);
+
+          if (room)
+            {
+              cm_room_set_generated_name (room, user_id->data);
+              cm_room_set_is_direct (room, TRUE);
+
+              continue;
+            }
+
+          room = cm_room_new (room_id);
+          cm_room_set_status (room, CM_STATUS_JOIN);
+          cm_room_set_client (room, self);
+          cm_room_set_is_direct (room, TRUE);
+          cm_room_set_generated_name (room, user_id->data);
+
+          /* This eats the ref on the new room */
+          g_hash_table_insert (self->direct_rooms, g_strdup (room_id), room);
+        }
+    }
+
+}
+
+static void
+handle_account_data (CmClient   *self,
+                     JsonObject *root)
+{
+  JsonObject *object, *content;
+  JsonArray *array;
+  guint length = 0;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  array = cm_utils_json_object_get_array (root, "events");
+  if (array)
+    length = json_array_get_length (array);
+
+  for (guint i = 0; i < length; i++)
+    {
+      const char *type;
+
+      object = json_array_get_object_element (array, i);
+      type = cm_utils_json_object_get_string (object, "type");
+
+      if (g_strcmp0 (type, "m.direct") != 0)
+        continue;
+
+      content = cm_utils_json_object_get_object (object, "content");
+
+      if (!content)
+        break;
+
+      parse_direct_rooms (self, content);
+    }
+}
+
+static void
+verification_send_key_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+}
+
+static void
+handle_to_device (CmClient   *self,
+                  JsonObject *root)
+{
+  JsonObject *object;
+  JsonArray *array;
+  guint length = 0;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  array = cm_utils_json_object_get_array (root, "events");
+  if (array)
+    length = json_array_get_length (array);
+
+  for (guint i = 0; i < length; i++)
+    {
+      g_autoptr(GPtrArray) events = NULL;
+      g_autoptr(CmEvent) event = NULL;
+      GRefString *user_id;
+      CmEventType type;
+
+      object = json_array_get_object_element (array, i);
+
+      event = cm_event_new_from_json (object, NULL);
+      user_id = cm_event_get_sender_id (event);
+      if (user_id)
+        {
+          CmUser *user;
+
+          user = cm_user_list_find_user (self->user_list, user_id, TRUE);
+          cm_event_set_sender (event, user);
+        }
+
+      type = cm_event_get_m_type (event);
+      events = g_ptr_array_new ();
+
+      if (type == CM_M_ROOM_ENCRYPTED)
+        {
+          cm_enc_handle_room_encrypted (self->cm_enc, object);
+        }
+      else if (type >= CM_M_KEY_VERIFICATION_ACCEPT &&
+               type <= CM_M_KEY_VERIFICATION_START)
+        {
+          CmEvent *key_event;
+          CmOlmSas *olm_sas;
+
+          /* Don't force add the event now. If the event has to be cancelled
+           * for any reason cancel it now and don't even report to the user */
+          key_event = client_find_key_verification (self, event, FALSE);
+          olm_sas = g_object_get_data (G_OBJECT (key_event ?: event), "olm-sas");
+
+          if (cm_olm_sas_get_cancel_code (olm_sas))
+            {
+              cm_client_key_verification_cancel_async (self, event, NULL, NULL, NULL);
+              return;
+            }
+
+          /* Now add the event if not already done so */
+          key_event = client_find_key_verification (self, event, TRUE);
+
+          if (key_event && type == CM_M_KEY_VERIFICATION_KEY)
+            {
+              /* The start/request event (which is `key_event`) shall not be the same as key event */
+              if (key_event != event)
+                {
+                  g_autofree char *uri = NULL;
+                  CmEvent *reply_event;
+                  JsonObject *obj;
+
+                  olm_sas = g_object_get_data (G_OBJECT (key_event), "olm-sas");
+                  reply_event = cm_olm_sas_get_key_event (olm_sas);
+
+                  obj = cm_event_get_json (reply_event);
+                  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.key.verification.key/%s",
+                                         cm_event_get_txn_id (reply_event));
+                  cm_net_send_json_async (self->cm_net, 0, obj, uri, SOUP_METHOD_PUT,
+                                          NULL, NULL,
+                                          verification_send_key_cb,
+                                          g_object_ref (self));
+                }
+            }
+
+          if (key_event && type == CM_M_KEY_VERIFICATION_MAC) {
+            CmDevice *device;
+
+            device = cm_olm_sas_get_device (olm_sas);
+            cm_db_update_device (self->cm_db, self, cm_event_get_sender (key_event), device);
+            cm_client_key_verification_done_async (self, key_event, NULL, NULL, NULL);
+          }
+        }
+    }
+}
+
+static void
+handle_room_join (CmClient   *self,
+                  JsonObject *root)
+{
+  g_autoptr(GList) joined_room_ids = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  joined_room_ids = json_object_get_members (root);
+
+  for (GList *room_id = joined_room_ids; room_id; room_id = room_id->next)
+    {
+      g_autoptr(GPtrArray) events = NULL;
+      CmRoom *room;
+      JsonObject *room_data;
+
+      room = client_find_room (self, room_id->data, self->joined_rooms);
+      room_data = cm_utils_json_object_get_object (root, room_id->data);
+
+      if (!room)
+        {
+          room = g_hash_table_lookup (self->direct_rooms, room_id->data);
+
+          if (room)
+            {
+              g_list_store_append (self->joined_rooms, room);
+              g_hash_table_remove (self->direct_rooms, room_id->data);
+            }
+          else
+            {
+              room = cm_room_new (room_id->data);
+              cm_room_set_status (room, CM_STATUS_JOIN);
+              cm_room_set_client (room, self);
+              g_list_store_append (self->joined_rooms, room);
+              g_object_unref (room);
+            }
+        }
+
+      cm_room_set_status (room, CM_STATUS_JOIN);
+      events = cm_room_set_data (room, room_data);
+      cm_db_add_room_events (self->cm_db, room, events, FALSE);
+
+      if (self->callback)
+        self->callback (self->cb_data, self, room, events, NULL);
+
+      cm_utils_remove_list_item (self->invited_rooms, room);
+
+      if (cm_room_get_replacement_room (room))
+        cm_utils_remove_list_item (self->joined_rooms, room);
+    }
+}
+
+static void
+handle_room_leave (CmClient   *self,
+                   JsonObject *root)
+{
+  g_autoptr(GList) left_room_ids = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  left_room_ids = json_object_get_members (root);
+
+  for (GList *room_id = left_room_ids; room_id; room_id = room_id->next)
+    {
+      g_autoptr(GPtrArray) events = NULL;
+      CmRoom *room;
+      JsonObject *room_data;
+
+      room = client_find_room (self, room_id->data, self->joined_rooms);
+      room_data = cm_utils_json_object_get_object (root, room_id->data);
+
+      if (!room)
+        continue;
+
+      events = cm_room_set_data (room, room_data);
+      cm_room_set_status (room, CM_STATUS_LEAVE);
+      cm_db_add_room_events (self->cm_db, room, events, FALSE);
+
+      if (self->callback)
+        self->callback (self->cb_data, self, room, events, NULL);
+
+      cm_utils_remove_list_item (self->joined_rooms, room);
+    }
+}
+
+static void
+handle_room_invite (CmClient   *self,
+                    JsonObject *root)
+{
+  g_autoptr(GList) invited_room_ids = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  invited_room_ids = json_object_get_members (root);
+
+  for (GList *room_id = invited_room_ids; room_id; room_id = room_id->next)
+    {
+      g_autoptr(GPtrArray) events = NULL;
+      CmRoom *room;
+      JsonObject *room_data;
+
+      room = client_find_room (self, room_id->data, self->invited_rooms);
+      room_data = cm_utils_json_object_get_object (root, room_id->data);
+
+      if (!room)
+        {
+          room = cm_room_new (room_id->data);
+          cm_room_set_status (room, CM_STATUS_INVITE);
+          cm_room_set_client (room, self);
+          g_list_store_append (self->joined_rooms, room);
+          g_object_unref (room);
+        }
+
+      events = cm_room_set_data (room, room_data);
+
+      if (events && events->len)
+        cm_db_add_room_events (self->cm_db, room, events, FALSE);
+
+      if (self->callback)
+        self->callback (self->cb_data, self, room, events, NULL);
+    }
+}
+
+static void
+handle_device_list (CmClient   *self,
+                    JsonObject *root)
+{
+  g_autoptr(GPtrArray) users = NULL;
+  guint n_items;
+
+  if (!root)
+    return;
+
+  users = g_ptr_array_new_with_free_func (g_object_unref);
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->joined_rooms));
+  cm_user_list_device_changed (self->user_list, root, users);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmRoom) room = NULL;
+
+      room = g_list_model_get_item (G_LIST_MODEL (self->joined_rooms), i);
+      cm_room_user_changed (room, users);
+    }
+
+  cm_db_mark_user_device_change (self->cm_db, self, users, TRUE, TRUE);
+}
+
+static void
+handle_red_pill (CmClient   *self,
+                 JsonObject *root)
+{
+  JsonObject *object;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  if (!root)
+    return;
+
+  handle_account_data (self, cm_utils_json_object_get_object (root, "account_data"));
+  handle_device_list (self, cm_utils_json_object_get_object (root, "device_lists"));
+  /* to_device should be handled first as it might contain keys to be used
+   * to decrypt following events */
+  handle_to_device (self, cm_utils_json_object_get_object (root, "to_device"));
+
+  object = cm_utils_json_object_get_object (root, "rooms");
+  handle_room_join (self, cm_utils_json_object_get_object (object, "join"));
+  handle_room_leave (self, cm_utils_json_object_get_object (object, "leave"));
+  handle_room_invite (self, cm_utils_json_object_get_object (object, "invite"));
+}
+
+static void
+matrix_take_red_pill_cb (GObject      *obj,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GError) error = NULL;
+  JsonObject *object = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (G_IS_TASK (result));
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (error)
+    {
+      self->sync_failed = TRUE;
+      client_set_login_state (self, FALSE, FALSE);
+      if (!handle_matrix_glitches (self, error))
+        self->callback (self->cb_data, self, NULL, NULL, error);
+      else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        g_debug ("Error syncing with time %s: %s", self->next_batch, error->message);
+      return;
+    }
+
+  client_set_login_state (self, FALSE, TRUE);
+
+  g_free (self->next_batch);
+  self->next_batch = g_strdup (cm_utils_json_object_get_string (root, "next_batch"));
+  client_mark_for_save (self, TRUE, -1);
+  cm_client_save (self);
+
+  {
+    g_autofree char *json_str = NULL;
+
+    json_str = cm_utils_json_object_to_string (root, FALSE);
+    handle_red_pill (self, root);
+
+    /* update variables only after the result is locally parsed  */
+    if (self->sync_failed || !self->is_sync)
+      {
+        self->sync_failed = FALSE;
+        self->is_sync = TRUE;
+        g_signal_emit (self, signals[STATUS_CHANGED], 0);
+      }
+  }
+
+  object = cm_utils_json_object_get_object (root, "device_one_time_keys_count");
+  if (handle_one_time_keys (self, object))
+    return;
+
+  /* Repeat */
+  matrix_start_sync (self, NULL);
+}
+
+static void
+matrix_take_red_pill (CmClient *self,
+                      gpointer  tsk)
+{
+  g_autoptr(GTask) task = tsk;
+  GCancellable *cancellable;
+  GHashTable *query;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+  if (self->login_success)
+    g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT));
+  else
+    g_hash_table_insert (query, g_strdup ("timeout"), g_strdup_printf ("%u", SYNC_TIMEOUT / 1000));
+
+  if (self->filter_id)
+    g_hash_table_insert (query, g_strdup ("filter"), g_strdup (self->filter_id));
+
+  if (self->next_batch)
+    g_hash_table_insert (query, g_strdup ("since"), g_strdup (self->next_batch));
+
+  cancellable = g_task_get_cancellable (task);
+  cm_net_send_json_async (self->cm_net, 2, NULL,
+                          "/_matrix/client/r0/sync", SOUP_METHOD_GET,
+                          query, cancellable, matrix_take_red_pill_cb,
+                          g_object_ref (self));
+}
+
+static void
+client_get_homeserver_cb (GObject      *obj,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  g_autofree char *homeserver = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  homeserver = cm_utils_get_homeserver_finish (result, &error);
+  g_object_set_data (G_OBJECT (task), "action", "get-homeserver");
+  g_debug ("(%p) Get home server %s", self, CM_LOG_SUCCESS (!error));
+
+  client_set_login_state (self, FALSE, FALSE);
+
+  if (error)
+    {
+      self->sync_failed = TRUE;
+
+      g_debug ("(%p) Get home server error: %s", self, error->message);
+      if (g_task_get_source_tag (task) != cm_client_get_homeserver_async)
+        handle_matrix_glitches (self, error);
+      g_task_return_error (task, error);
+
+      return;
+    }
+
+  g_debug ("(%p) Got home server: %s", self, homeserver);
+  if (!homeserver)
+    {
+      self->sync_failed = TRUE;
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_NO_HOME_SERVER,
+                               "Couldn't fetch homeserver");
+      if (self->callback)
+        self->callback (self->cb_data, self, NULL, NULL, error);
+
+      return;
+    }
+
+  cm_client_set_homeserver (self, homeserver);
+
+  if (!self->homeserver)
+    {
+      self->sync_failed = TRUE;
+      g_debug ("(%p) Get home server: '%s' is invalid uri", self, homeserver);
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_BAD_HOME_SERVER,
+                               "'%s' is not a valid URI", homeserver);
+      return;
+    }
+
+  matrix_start_sync (self, g_steal_pointer (&task));
+}
+
+gboolean
+cm_client_get_logging_in (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+
+  return self->is_logging_in;
+}
+
+gboolean
+cm_client_get_logged_in (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+
+  return self->login_success;
+}
+
+static void
+get_joined_rooms_cb (GObject      *obj,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (G_IS_TASK (result));
+
+  self->room_list_loading = FALSE;
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+
+  g_debug ("(%p) Get joined rooms %s", self, CM_LOG_SUCCESS (!error));
+
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return;
+
+  if (handle_matrix_glitches (self, error))
+    return;
+
+  if (!error) {
+    JsonArray *array;
+    guint length = 0;
+
+    array = cm_utils_json_object_get_array (root, "joined_rooms");
+
+    if (array)
+      length = json_array_get_length (array);
+
+    g_debug ("(%p) Get joined rooms, count: %u", self, length);
+
+    for (guint i = 0; i < length; i++)
+      {
+        const char *room_id;
+        CmRoom *room;
+
+        room_id = json_array_get_string_element (array, i);
+        room = client_find_room (self, room_id, self->joined_rooms);
+
+        if (!room)
+          {
+            room = g_hash_table_lookup (self->direct_rooms, room_id);
+            if (room)
+              {
+                g_list_store_append (self->joined_rooms, room);
+                g_hash_table_remove (self->direct_rooms, room_id);
+              }
+          }
+
+        if (!room)
+          {
+            room = cm_room_new (room_id);
+            cm_room_set_status (room, CM_STATUS_JOIN);
+            cm_room_set_client (room, self);
+            g_list_store_append (self->joined_rooms, room);
+            g_object_unref (room);
+          }
+
+        cm_room_load_async (room, self->cancellable,
+                            room_loaded_cb,
+                            g_object_ref (self));
+      }
+
+    self->room_list_loaded = TRUE;
+    matrix_start_sync (self, NULL);
+  }
+}
+
+static void
+get_direct_rooms_cb (GObject      *obj,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  g_autoptr(CmClient) self = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+  g_assert (G_IS_TASK (result));
+
+  self->direct_room_list_loading = FALSE;
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Get direct rooms %s", self, CM_LOG_SUCCESS (!error));
+
+  if (root)
+    parse_direct_rooms (self, root);
+
+  if (root || !error)
+    self->direct_room_list_loaded = TRUE;
+
+  matrix_start_sync (self, NULL);
+}
+
+static void
+matrix_start_sync (CmClient *self,
+                   gpointer  tsk)
+{
+  g_autoptr(GTask) task = tsk;
+  GCancellable *cancellable = NULL;
+
+  g_assert (CM_IS_CLIENT (self));
+
+  self->sync_failed = FALSE;
+
+  if (!task)
+    {
+      task = g_task_new (self, self->cancellable, NULL, NULL);
+      cancellable = self->cancellable;
+    }
+
+  cancellable = g_task_get_cancellable (task);
+
+  if (self->db_loading || self->room_list_loading || self->direct_room_list_loading)
+    return;
+
+  if (!self->db_loaded)
+    {
+      self->db_loading = TRUE;
+      g_debug ("(%p) Load db", self);
+      cm_db_load_client_async (self->cm_db, self,
+                               cm_client_get_device_id (self),
+                               db_load_client_cb,
+                               g_steal_pointer (&task));
+    }
+  else if (!self->homeserver)
+    {
+      const char *user_id;
+
+      user_id = cm_user_get_id (CM_USER (self->cm_account));
+
+      if (!user_id)
+        user_id = cm_account_get_login_id (self->cm_account);
+
+      if (!cm_utils_home_server_valid (self->homeserver) &&
+          !cm_utils_user_name_valid (user_id))
+        {
+          g_warning ("(%p) Error: No Homeserver provided", self);
+
+          g_task_return_new_error (task, CM_ERROR, CM_ERROR_NO_HOME_SERVER,
+                                   "No Homeserver provided");
+          return;
+        }
+
+      client_set_login_state (self, TRUE, FALSE);
+      g_debug ("(%p) Getting homeserver", self);
+      cm_utils_get_homeserver_async (user_id, 30, cancellable,
+                                     client_get_homeserver_cb,
+                                     g_steal_pointer (&task));
+    }
+  else if (!self->homeserver_verified)
+    {
+      client_set_login_state (self, TRUE, FALSE);
+      g_debug ("(%p) Verify homeserver '%s'", self, self->homeserver);
+      cm_utils_verify_homeserver_async (self->homeserver, 30, cancellable,
+                                        client_verify_homeserver_cb,
+                                        g_steal_pointer (&task));
+    }
+  else if (!self->password && !cm_net_get_access_token (self->cm_net))
+    {
+      GError *error;
+
+      g_warning ("(%p) No password provided, nor access token", self);
+
+      error = g_error_new (CM_ERROR, CM_ERROR_BAD_PASSWORD, "No Password provided");
+
+      if (self->callback)
+        self->callback (self->cb_data, self, NULL, NULL, error);
+
+      g_task_return_error (task, error);
+    }
+  else if (!cm_net_get_access_token (self->cm_net) || !self->cm_enc)
+    {
+      g_assert (self->cm_db);
+      cm_net_set_access_token (self->cm_net, NULL);
+      client_set_login_state (self, TRUE, FALSE);
+      g_debug ("(%p) Login with password", self);
+      client_login_with_password_async (self, cancellable,
+                                        client_password_login_cb,
+                                        g_steal_pointer (&task));
+    }
+  else if (self->db_migrated && !self->direct_room_list_loaded)
+    {
+      g_autofree char *uri = NULL;
+
+      self->direct_room_list_loading = TRUE;
+
+      uri = g_strconcat ("/_matrix/client/r0/user/",
+                         cm_user_get_id (CM_USER (self->cm_account)),
+                         "/account_data/m.direct", NULL);
+      g_debug ("(%p) Get direct rooms", self);
+      cm_net_send_json_async (self->cm_net, 0, NULL, uri, SOUP_METHOD_GET,
+                              NULL, NULL, get_direct_rooms_cb,
+                              g_object_ref (self));
+    }
+  else if (self->db_migrated && !self->room_list_loaded)
+    {
+      self->room_list_loading = TRUE;
+      g_debug ("(%p) Get joined rooms", self);
+      cm_net_send_json_async (self->cm_net, 0, NULL,
+                              "/_matrix/client/r0/joined_rooms", SOUP_METHOD_GET,
+                              NULL, NULL, get_joined_rooms_cb,
+                              g_object_ref (self));
+
+    }
+  else if (!self->filter_id)
+    {
+      g_assert (self->cm_enc);
+      g_assert (self->callback);
+      client_set_login_state (self, TRUE, FALSE);
+      matrix_upload_filter (self, g_steal_pointer (&task));
+    }
+  else
+    {
+      g_assert (self->cm_db);
+      g_assert (self->callback);
+      matrix_take_red_pill (self, g_steal_pointer (&task));
+    }
+}
+
+/**
+ * cm_client_can_connect:
+ * @self: A #CmClient
+ *
+ * Check if @self can be connected to homeserver with current
+ * network state.  This function is a bit dumb: returning
+ * %TRUE shall not ensure that the @self is connectable.
+ * But if %FALSE is returned, @self shall not be
+ * able to connect.
+ *
+ * Returns: %TRUE if heuristics says @self can be connected.
+ * %FALSE otherwise.
+ */
+gboolean
+cm_client_can_connect (CmClient *self)
+{
+  GNetworkMonitor *nm;
+  GInetAddress *inet;
+
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+
+  /* If never tried, assume we can connect */
+  if (!self->has_tried_connecting)
+    return TRUE;
+
+  nm = g_network_monitor_get_default ();
+
+  if (!self->gaddress || !G_IS_INET_SOCKET_ADDRESS (self->gaddress))
+    goto end;
+
+  inet = g_inet_socket_address_get_address ((GInetSocketAddress *)self->gaddress);
+
+  if (g_inet_address_get_is_loopback (inet) ||
+      g_inet_address_get_is_site_local (inet))
+    return g_network_monitor_can_reach (nm, G_SOCKET_CONNECTABLE (self->gaddress), NULL, NULL);
+
+ end:
+  /* Distributions may advertise to have full network support event
+   * when connected only to local network, so this isn't always right */
+  return g_network_monitor_get_connectivity (nm) == G_NETWORK_CONNECTIVITY_FULL;
+}
+
+/**
+ * cm_client_start_sync:
+ * @self: A #CmClient
+ *
+ * Start sync with server.  If @self is already
+ * in sync or in the process to do so, this method
+ * simply returns.
+ */
+void
+cm_client_start_sync (CmClient *self)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (self->callback);
+
+  if (self->is_sync || self->is_logging_in)
+    return;
+
+  g_debug ("(%p) Start sync", self);
+  g_clear_handle_id (&self->resync_id, g_source_remove);
+  matrix_start_sync (self, NULL);
+}
+
+/**
+ * cm_client_is_sync:
+ * @self: A #CmClient
+ *
+ * Check whether the client @self is in
+ * sync with the server or not.
+ *
+ * Returns: %TRUE if in sync with server.
+ * %FALSE otherwise.
+ */
+gboolean
+cm_client_is_sync (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+
+  return cm_net_get_access_token (self->cm_net) &&
+    self->login_success &&
+    self->is_sync && !self->sync_failed;
+}
+
+/**
+ * cm_client_stop_sync:
+ * @self: A #CmClient
+ *
+ * Stop sync with server.
+ */
+void
+cm_client_stop_sync (CmClient *self)
+{
+  g_return_if_fail (CM_IS_CLIENT (self));
+
+  if (self->cancellable)
+    g_cancellable_cancel (self->cancellable);
+
+  self->is_sync = FALSE;
+  self->sync_failed = FALSE;
+  self->is_logging_in = FALSE;
+  self->login_success = FALSE;
+
+  g_clear_handle_id (&self->resync_id, g_source_remove);
+  g_clear_object (&self->cancellable);
+  self->cancellable = g_cancellable_new ();
+  g_debug ("(%p) Stop sync", self);
+
+  g_signal_emit (self, signals[STATUS_CHANGED], 0);
+}
+
+/**
+ * cm_client_get_joined_rooms:
+ * @self: A #CmClient
+ *
+ * Get the list of joined rooms with
+ * #CmRoom as the members.
+ *
+ * Returns: (transfer none): A #GListModel
+ */
+GListModel *
+cm_client_get_joined_rooms (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return G_LIST_MODEL (self->joined_rooms);
+}
+
+/**
+ * cm_client_get_invited_rooms:
+ * @self: A #CmClient
+ *
+ * Get the list of invited rooms with
+ * #CmRoom as the members.
+ *
+ * Returns: (transfer none): A #GListModel
+ */
+GListModel *
+cm_client_get_invited_rooms (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return G_LIST_MODEL (self->invited_rooms);
+}
+
+GListModel *
+cm_client_get_key_verifications (CmClient *self)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), NULL);
+
+  return G_LIST_MODEL (self->key_verifications);
+}
+
+static void
+get_file_cb (GObject      *object,
+             GAsyncResult *result,
+             gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  GInputStream *istream = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  istream = cm_net_get_file_finish (self->cm_net, result, &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_pointer (task, istream, g_object_unref);
+}
+
+static void
+find_file_enc_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  CmClient *self;
+  g_autoptr(GTask) task = user_data;
+  CmEncFileInfo *file_info;
+  GCancellable *cancellable;
+  char *uri;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_CLIENT (self));
+
+  file_info = cm_enc_find_file_enc_finish (self->cm_enc, result, NULL);
+
+  cancellable = g_task_get_cancellable (task);
+  uri = g_task_get_task_data (task);
+
+  cm_net_get_file_async (self->cm_net,
+                         uri, file_info, cancellable,
+                         get_file_cb,
+                         g_steal_pointer (&task));
+}
+
+void
+cm_client_get_file_async (CmClient              *self,
+                          const char            *uri,
+                          GCancellable          *cancellable,
+                          GFileProgressCallback  progress_callback,
+                          gpointer               progress_user_data,
+                          GAsyncReadyCallback    callback,
+                          gpointer               user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_CLIENT (self));
+  g_return_if_fail (uri && *uri);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_task_data (task, g_strdup (uri), g_free);
+
+  cm_enc_find_file_enc_async (self->cm_enc, uri,
+                              find_file_enc_cb, task);
+}
+
+GInputStream *
+cm_client_get_file_finish (CmClient      *self,
+                           GAsyncResult  *result,
+                           GError       **error)
+{
+  g_return_val_if_fail (CM_IS_CLIENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/cm-client.h b/subprojects/libcmatrix/src/cm-client.h
new file mode 100644
index 0000000000000000000000000000000000000000..c8cd55606f94f95fd14937f59ea826005bde9889
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-client.h
@@ -0,0 +1,113 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#include "cm-enums.h"
+#include "cm-types.h"
+#include "users/cm-account.h"
+
+#define CM_TYPE_CLIENT (cm_client_get_type ())
+G_DECLARE_FINAL_TYPE (CmClient, cm_client, CM, CLIENT, GObject)
+
+typedef void   (*CmCallback)                        (gpointer            object,
+                                                     CmClient           *self,
+                                                     CmRoom             *room,
+                                                     GPtrArray          *events,
+                                                     GError             *err);
+
+CmClient     *cm_client_new                           (void);
+CmAccount    *cm_client_get_account                   (CmClient            *self);
+void          cm_client_set_enabled                   (CmClient            *self,
+                                                       gboolean             enable);
+gboolean      cm_client_get_enabled                   (CmClient            *self);
+void          cm_client_set_sync_callback             (CmClient            *self,
+                                                       CmCallback           callback,
+                                                       gpointer             callback_data,
+                                                       GDestroyNotify       callback_data_destroy);
+gboolean      cm_client_set_user_id                   (CmClient            *self,
+                                                       const char          *matrix_user_id);
+GRefString   *cm_client_get_user_id                   (CmClient            *self);
+gboolean      cm_client_set_homeserver                (CmClient            *self,
+                                                       const char          *homeserver);
+const char   *cm_client_get_homeserver                (CmClient            *self);
+void          cm_client_set_password                  (CmClient            *self,
+                                                       const char          *password);
+const char   *cm_client_get_password                  (CmClient            *self);
+void          cm_client_set_access_token              (CmClient            *self,
+                                                       const char          *access_token);
+const char   *cm_client_get_access_token              (CmClient            *self);
+void          cm_client_set_device_id                 (CmClient            *self,
+                                                       const char          *device_id);
+const char   *cm_client_get_device_id                 (CmClient            *self);
+void          cm_client_set_device_name               (CmClient            *self,
+                                                       const char          *device_name);
+const char   *cm_client_get_device_name               (CmClient            *self);
+void          cm_client_set_pickle_key                (CmClient            *self,
+                                                       const char          *pickle_key);
+const char   *cm_client_get_pickle_key                (CmClient            *self);
+const char   *cm_client_get_ed25519_key               (CmClient            *self);
+
+void          cm_client_join_room_by_id_async         (CmClient            *self,
+                                                       const char          *room_id,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+gboolean      cm_client_join_room_by_id_finish        (CmClient            *self,
+                                                       GAsyncResult        *result,
+                                                       GError             **error);
+void          cm_client_get_homeserver_async          (CmClient            *self,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+const char   *cm_client_get_homeserver_finish         (CmClient            *self,
+                                                       GAsyncResult        *result,
+                                                       GError             **error);
+void          cm_client_key_verification_continue_async (CmClient            *self,
+                                                         CmEvent             *verification_event,
+                                                         GCancellable        *cancellable,
+                                                         GAsyncReadyCallback  callback,
+                                                          gpointer             user_data);
+gboolean      cm_client_key_verification_continue_finish (CmClient           *self,
+                                                          GAsyncResult       *result,
+                                                          GError            **error);
+void          cm_client_key_verification_cancel_async    (CmClient            *self,
+                                                          CmEvent             *verification_event,
+                                                          GCancellable        *cancellable,
+                                                          GAsyncReadyCallback  callback,
+                                                          gpointer             user_data);
+gboolean      cm_client_key_verification_cancel_finish   (CmClient           *self,
+                                                          GAsyncResult       *result,
+                                                          GError            **error);
+void          cm_client_key_verification_match_async     (CmClient            *self,
+                                                          CmEvent             *verification_event,
+                                                          GCancellable        *cancellable,
+                                                          GAsyncReadyCallback  callback,
+                                                          gpointer             user_data);
+gboolean       cm_client_key_verification_match_finish   (CmClient           *self,
+                                                          GAsyncResult       *result,
+                                                          GError            **error);
+gboolean      cm_client_can_connect                   (CmClient            *self);
+void          cm_client_start_sync                    (CmClient            *self);
+gboolean      cm_client_is_sync                       (CmClient            *self);
+void          cm_client_stop_sync                     (CmClient            *self);
+gboolean      cm_client_get_logging_in                (CmClient            *self);
+gboolean      cm_client_get_logged_in                 (CmClient            *self);
+GListModel   *cm_client_get_joined_rooms              (CmClient            *self);
+GListModel   *cm_client_get_invited_rooms             (CmClient            *self);
+GListModel   *cm_client_get_key_verifications         (CmClient            *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-common.c b/subprojects/libcmatrix/src/cm-common.c
new file mode 100644
index 0000000000000000000000000000000000000000..8583c23ce207681b68d598093de73dcc8835f179
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-common.c
@@ -0,0 +1,21 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-common.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "cm-common.h"
+
+/**
+ * matrix_error_quark:
+ *
+ * Get the Matrix Error Quark.
+ *
+ * Returns: a #GQuark.
+ **/
+G_DEFINE_QUARK (cm-error-quark, cm_error)
diff --git a/subprojects/libcmatrix/src/cm-common.h b/subprojects/libcmatrix/src/cm-common.h
new file mode 100644
index 0000000000000000000000000000000000000000..9549daf59191525d8877ad7713f53f03d4284f45
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-common.h
@@ -0,0 +1,17 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-common.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#define CM_ERROR (cm_error_quark ())
+GQuark      cm_error_quark                          (void);
diff --git a/subprojects/libcmatrix/src/cm-db-private.h b/subprojects/libcmatrix/src/cm-db-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..200b6fca2df47aa487a9f377cbef11ee2748a608
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-db-private.h
@@ -0,0 +1,151 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-db.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "cm-enc-private.h"
+#include "cm-client.h"
+#include "cm-device.h"
+#include "cm-room.h"
+
+G_BEGIN_DECLS
+
+/* These values shouldn’t be changed. They are used in DB */
+typedef enum {
+  SESSION_OLM_V1_IN      = 1,
+  SESSION_OLM_V1_OUT     = 2,
+  SESSION_MEGOLM_V1_IN   = 3,
+  SESSION_MEGOLM_V1_OUT  = 4,
+} CmSessionType;
+
+#define CMATRIX_ALGORITHM_A256CTR 1
+
+#define CMATRIX_KEY_TYPE_OCT      1
+
+#define CM_TYPE_DB (cm_db_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmDb, cm_db, CM, DB, GObject)
+
+CmDb          *cm_db_new                           (void);
+void           cm_db_open_async                    (CmDb                *self,
+                                                    char                *dir,
+                                                    const char          *file_name,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_open_finish                   (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+gboolean       cm_db_is_open                       (CmDb                *self);
+void           cm_db_close_async                   (CmDb                *self,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_close_finish                  (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void           cm_db_save_client_async             (CmDb                *db,
+                                                    CmClient            *client,
+                                                    char                *pickle,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_save_client_finish            (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void           cm_db_load_client_async             (CmDb                *db,
+                                                    CmClient            *client,
+                                                    const char          *device_id,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_load_client_finish            (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void           cm_db_save_room_async               (CmDb                *self,
+                                                    CmClient            *client,
+                                                    CmRoom              *room,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_save_room_finish              (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void           cm_db_delete_client_async           (CmDb                *self,
+                                                    CmClient            *client,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_delete_client_finish          (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void           cm_db_save_file_enc_async           (CmDb                *self,
+                                                    CmEncFileInfo       *file,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean       cm_db_save_file_enc_finish          (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void           cm_db_find_file_enc_async           (CmDb                *self,
+                                                    const char          *url,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+CmEncFileInfo *cm_db_find_file_enc_finish          (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+gboolean       cm_db_add_session                   (CmDb                *self,
+                                                    gpointer             session,
+                                                    char                *pickle);
+gpointer       cm_db_lookup_session                (CmDb                *self,
+                                                    const char          *account_id,
+                                                    const char          *account_device,
+                                                    const char          *session_id,
+                                                    const char          *sender_key,
+                                                    const char          *pickle_key,
+                                                    const char          *room_id,
+                                                    CmSessionType        type);
+void           cm_db_add_room_members              (CmDb                *self,
+                                                    CmRoom              *cm_room,
+                                                    GPtrArray           *members);
+void           cm_db_add_room_events               (CmDb                *self,
+                                                    CmRoom              *room,
+                                                    GPtrArray           *events,
+                                                    gboolean             prepend);
+void           cm_db_get_past_events_async         (CmDb                *self,
+                                                    CmRoom              *room,
+                                                    CmEvent             *from,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+GPtrArray     *cm_db_get_past_events_finish        (CmDb                *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+gpointer       cm_db_lookup_olm_session            (CmDb                *self,
+                                                    const char          *account_id,
+                                                    const char          *account_device,
+                                                    const char          *sender_curve25519_key,
+                                                    const char          *body,
+                                                    const char          *pickle_key,
+                                                    CmSessionType        type,
+                                                    size_t               message_type,
+                                                    char               **out_plain_text);
+void           cm_db_mark_user_device_change       (CmDb                *self,
+                                                    CmClient            *client,
+                                                    GPtrArray           *users,
+                                                    gboolean             outdated,
+                                                    gboolean             is_tracking);
+void           cm_db_update_user_devices           (CmDb                *self,
+                                                    CmClient            *client,
+                                                    CmUser              *user,
+                                                    GPtrArray           *added,
+                                                    GPtrArray           *removed,
+                                                    gboolean             force_add);
+void          cm_db_update_device                  (CmDb                *self,
+                                                    CmClient            *client,
+                                                    CmUser              *user,
+                                                    CmDevice            *device);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-db.c b/subprojects/libcmatrix/src/cm-db.c
new file mode 100644
index 0000000000000000000000000000000000000000..bc6a61e48d75054a24fdb398d2575c3294c0cbb5
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-db.c
@@ -0,0 +1,3612 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-db.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-db"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib.h>
+#include <fcntl.h>
+#include <sqlite3.h>
+
+#include "events/cm-event-private.h"
+#include "events/cm-room-event-private.h"
+#include "events/cm-room-message-event-private.h"
+#include "users/cm-user-private.h"
+#include "cm-device-private.h"
+#include "cm-enc-private.h"
+#include "cm-olm-private.h"
+#include "cm-client-private.h"
+#include "cm-room-private.h"
+#include "cm-utils-private.h"
+#include "cm-db-private.h"
+
+#define STRING(arg) STRING_VALUE(arg)
+#define STRING_VALUE(arg) #arg
+
+/* increment when DB changes */
+#define DB_VERSION 2
+
+struct _CmDb
+{
+  GObject      parent_instance;
+
+  GAsyncQueue *queue;
+  GThread     *worker_thread;
+  sqlite3     *db;
+  char        *db_path;
+};
+
+#define VERIFICATION_UNSET       0
+#define VERIFICATION_KNOWN       1
+#define VERIFICATION_VERIFIED    2
+#define VERIFICATION_BLACKLISTED 3
+#define VERIFICATION_IGNORED     4
+#define VERIFICATION_IS_SELF     5
+
+#define EVENT_NOT_ENCRYPTED       0
+#define EVENT_NOT_DECRYPTED       1
+#define EVENT_DECRYPTED           2
+/* We got m.room.encrypted, but with empty content */
+#define EVENT_MAY_BE_DECRYPTED    3
+
+/*
+ * CmDb->db should never be accessed nor modified in main thread
+ * except for checking if it’s %NULL.  Any operation should be done only
+ * in @worker_thread.  Don't reuse the same #CmDb once closed.
+ *
+ * Always copy data with g_object_set_data_full() or similar if the data can change
+ * (regardless of whether the data has changed or not), so as to avoid surprises
+ * with multi-thread stuff.
+ */
+
+typedef void (*CmDbCallback) (CmDb  *self,
+                              GTask *task);
+
+G_DEFINE_TYPE (CmDb, cm_db, G_TYPE_OBJECT)
+
+static void
+warn_if_sql_error (int         status,
+                   const char *message)
+{
+  if (status == SQLITE_OK || status == SQLITE_ROW || status == SQLITE_DONE)
+    return;
+
+  g_warning ("Error %s. errno: %d, message: %s", message, status, sqlite3_errstr (status));
+}
+
+static void
+matrix_bind_text (sqlite3_stmt *statement,
+                  guint         position,
+                  const char   *bind_value,
+                  const char   *message)
+{
+  guint status;
+
+  status = sqlite3_bind_text (statement, position, bind_value, -1, SQLITE_TRANSIENT);
+  warn_if_sql_error (status, message);
+}
+
+static void
+matrix_bind_int (sqlite3_stmt *statement,
+                 guint         position,
+                 gint64        bind_value,
+                 const char   *message)
+{
+  guint status;
+
+  status = sqlite3_bind_int64 (statement, position, bind_value);
+  warn_if_sql_error (status, message);
+}
+
+static int
+db_event_state_to_int (CmEventState state)
+{
+  switch (state)
+    {
+    case CM_EVENT_STATE_DRAFT:
+      return 1;
+
+    case CM_EVENT_STATE_WAITING:
+      return 2;
+
+    case CM_EVENT_STATE_SENDING:
+      return 3;
+
+    case CM_EVENT_STATE_SENDING_FAILED:
+      return 4;
+
+    case CM_EVENT_STATE_SENT:
+      return 5;
+
+    case CM_EVENT_STATE_RECEIVED:
+      return 6;
+
+    case CM_EVENT_STATE_UNKNOWN:
+    default:
+      return 0;
+    }
+
+  return 0;
+}
+
+static CmEventState
+db_event_state_from_int (int state)
+{
+  if (state == 0)
+    return CM_EVENT_STATE_UNKNOWN;
+
+  if (state == 1)
+    return CM_EVENT_STATE_DRAFT;
+
+  if (state == 2 || state == 3 || state == 4)
+    return CM_EVENT_STATE_SENDING_FAILED;
+
+  if (state == 5)
+    return CM_EVENT_STATE_SENT;
+
+  if (state == 6)
+    return CM_EVENT_STATE_RECEIVED;
+
+  g_return_val_if_reached (CM_EVENT_STATE_UNKNOWN);
+}
+
+static int
+db_event_get_decryption_value (CmEvent *event)
+{
+  gboolean encrypted = 0, decrypted = 0, has_content = 0;
+
+  if (!event)
+    return EVENT_NOT_ENCRYPTED;
+
+  g_assert (CM_IS_EVENT (event));
+
+  encrypted = cm_event_is_encrypted (event);
+  decrypted = cm_event_is_decrypted (event);
+  has_content = cm_event_has_encrypted_content (event);
+
+  if (encrypted)
+    {
+      if (has_content && decrypted)
+        return EVENT_DECRYPTED;
+
+      if (!has_content)
+        return EVENT_MAY_BE_DECRYPTED;
+
+      return EVENT_NOT_DECRYPTED;
+    }
+
+  return EVENT_NOT_ENCRYPTED;
+}
+
+static GPtrArray *
+db_get_past_room_events (CmDb   *self,
+                         CmRoom *cm_room,
+                         int     room_id,
+                         int     from_event_id,
+                         int     from_sorted_event_id,
+                         int     max_count)
+{
+  GPtrArray *events = NULL;
+  sqlite3_stmt *stmt;
+  gboolean skip = FALSE;
+
+  if (from_event_id)
+    skip = TRUE;
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT id,event_state,room_events.json_data FROM room_events "
+                      "WHERE room_id=? AND sorted_id <= ? "
+                      /* Limit to messages until chatty has better events support */
+                      "AND (event_type=? OR event_type=?)"
+                      "ORDER BY sorted_id DESC, id DESC LIMIT ?",
+                      -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, room_id, "binding when loading past events");
+  matrix_bind_int (stmt, 2, from_sorted_event_id, "binding when loading past events");
+  matrix_bind_int (stmt, 3, CM_M_ROOM_MESSAGE, "binding when loading past events");
+  matrix_bind_int (stmt, 4, CM_M_ROOM_ENCRYPTED, "binding when loading past events");
+  matrix_bind_int (stmt, 5, max_count, "binding when loading past events");
+
+  while (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      JsonObject *encrypted, *root;
+      g_autoptr(JsonObject) json = NULL;
+      CmRoomEvent *cm_event;
+      CmEventState state;
+
+      if (skip)
+        {
+          skip = FALSE;
+          continue;
+        }
+
+      json = cm_utils_string_to_json_object ((char *)sqlite3_column_text (stmt, 2));
+
+      if (!json)
+        continue;
+
+      root = cm_utils_json_object_get_object (json, "json");
+      encrypted = cm_utils_json_object_get_object (json, "encrypted");
+      cm_event = cm_room_event_new_from_json (cm_room, root, encrypted);
+      state = db_event_state_from_int (sqlite3_column_int (stmt, 1));
+      cm_event_set_state (CM_EVENT (cm_event), state);
+
+      if (!cm_event)
+        continue;
+
+      if (!events)
+        events = g_ptr_array_new_full (32, g_object_unref);
+
+      g_ptr_array_add (events, cm_event);
+    }
+
+  sqlite3_finalize (stmt);
+
+  return events;
+}
+
+/**
+   user_devices.json_data
+   - local
+   -  device_display_name
+   users.json_data
+   - local
+   -  name
+   -  avatar_url
+   -  status = unknown, known, blocked, verified,
+   accounts.json_data
+   - local
+   -  filter
+   -    id = text
+   -    version = int
+   rooms.json_data
+   - local
+   -  direct = bool
+   -  state = int (joined, invited, left, unknown)
+   -  name_loaded = bool
+   -  alias = string
+   -  draft = json object
+   -    m.text = string
+   -  encryption = string
+   -  rotation_period_ms = time_t (or may be use μs as provided by server?)
+   -  rotation_count_msgs = int (message count max for key change)
+   sessions.json_data
+   - local
+   -  key_added_date = time_t (or may be use μs as provided by server?)
+   room_members.json_data
+   - local
+   -  display_name TEXT (set if different from default name)
+   -  avatar_url TEXT (set id different from default avatar)
+*/
+static gboolean
+cm_db_create_schema (CmDb  *self,
+                     GTask *task)
+{
+  const char *sql;
+  char *error = NULL;
+  int status;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  sql = "PRAGMA user_version = " STRING (DB_VERSION) ";"
+
+    "CREATE TABLE IF NOT EXISTS users ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    /* v2 */
+    "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, "
+    /* Version 1: Unique */
+    /* v2: Remove Unique */
+    "username TEXT NOT NULL, "
+    /* v2 */
+    "tracking INTEGER NOT NULL DEFAULT 0, "
+    /* Version 1 */
+    "outdated INTEGER DEFAULT 1, "
+    /* Version 1 */
+    "json_data TEXT,"
+    /* v2 */
+    "UNIQUE (account_id, username));"
+
+    /* Version 1 */
+    "CREATE TABLE IF NOT EXISTS user_devices ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    "user_id INTEGER NOT NULL REFERENCES users(id), "
+    /* xxx: Move device to devices table along with
+     * curve25519_key and ed25519_key as the same
+     * can exist in several accounts
+     */
+    "device TEXT NOT NULL, "
+    "curve25519_key TEXT, "
+    "ed25519_key TEXT, "
+    "verification INTEGER DEFAULT 0, "
+    "json_data TEXT, "
+    "UNIQUE (user_id, device));"
+
+    "CREATE TABLE IF NOT EXISTS accounts ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    /* Version 1 */
+    "user_device_id INTEGER NOT NULL REFERENCES user_devices(id), "
+    "next_batch TEXT, "
+    "pickle TEXT, "
+    "enabled INTEGER DEFAULT 0, "
+    /* Version 1 */
+    "json_data TEXT, "
+    "UNIQUE (user_device_id));"
+
+    "CREATE TABLE IF NOT EXISTS rooms ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    /* v2: on delete cascade */
+    "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, "
+    "room_name TEXT NOT NULL, "
+    "prev_batch TEXT, "
+    /* Version 1 */
+    /* Set if the room has tombstone and got replaced by a different room */
+    "replacement_room_id INTEGER REFERENCES rooms(id),"
+    /* v2 */
+    "room_state INTEGER NOT NULL DEFAULT 0, "
+    /* Version 1 */
+    "json_data TEXT, "
+    "UNIQUE (account_id, room_name));"
+
+    /* v2 */
+    "CREATE TABLE IF NOT EXISTS room_members ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, "
+    "user_id INTEGER NOT NULL REFERENCES users(id), "
+    /* joined, invited, left (we set left instead of deleting as past messages may refer to user id) */
+    "user_state INTEGER NOT NULL DEFAULT 0, "
+    "json_data TEXT, "
+    "UNIQUE (room_id, user_id));"
+
+    /* v2 */
+    "CREATE TABLE IF NOT EXISTS room_events_cache ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, "
+    "sender_id INTEGER REFERENCES room_members(id), "
+    "event_uid TEXT NOT NULL, "
+    "origin_server_ts INTEGER, "
+    "json_data TEXT, "
+    "UNIQUE (room_id, event_uid));"
+
+    /* v2 */
+    "CREATE TABLE IF NOT EXISTS room_events ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    /* 'id' above only increments, 'sorted_id' increments
+     * or decrements in the order events has to be placed.
+     */
+    "sorted_id INTEGER NOT NULL, "
+    "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, "
+    "sender_id INTEGER NOT NULL REFERENCES room_members(id), "
+    "event_type INTEGER NOT NULL, "
+    "event_uid TEXT, "
+    "txnid TEXT, "
+    /* If set to 0, this event is a reply to some other, which is not yet in db */
+    "replaces_event_id INTEGER REFERENCES room_events(id), "
+    "replaces_event_cache_id INTEGER REFERENCES room_events_cache(id), "
+    /* This event has been replaced with the given event id */
+    /* This will likely point to the latest replacement id available */
+    "replaced_with_id INTEGER REFERENCES room_events(id), "
+    /* sending, sent, sending failed, */
+    "event_state INTEGER, "
+    "state_key TEXT, "
+    "origin_server_ts INTEGER NOT NULL, "
+    /* 0: not encrypted, 1: not decrypted 2: decrypted */
+    /* 3: may be decrypted, we got m.room.encrypted but without content */
+    "decryption INTEGER NOT NULL DEFAULT 0, "
+    /* direction int, encrypted int, verified int, txnid */
+    "json_data TEXT, "
+    "UNIQUE (room_id, event_uid));"
+
+    "CREATE TABLE IF NOT EXISTS encryption_keys ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    /* v2 */
+    "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, "
+    "file_url TEXT NOT NULL, "
+    "file_sha256 TEXT, "
+    /* Initialization vector: iv in JSON */
+    "iv TEXT NOT NULL, "
+    /* v in JSON */
+    "version INT DEFAULT 2 NOT NULL, "
+    /* alg in JSON */
+    "algorithm INT NOT NULL, "
+    /* k in JSON */
+    "key TEXT NOT NULL, "
+    /* kty in JSON */
+    "type INT NOT NULL, "
+    /* ext in JSON */
+    "extractable INT DEFAULT 1 NOT NULL, "
+    /* Version 1 */
+    "json_data TEXT, "
+    "UNIQUE (account_id, file_url));"
+
+    /* v2: Renamed to sessions from session */
+    "CREATE TABLE IF NOT EXISTS sessions ("
+    "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+    "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, "
+    "sender_key TEXT NOT NULL, "
+    "session_id TEXT NOT NULL, "
+    "type INTEGER NOT NULL, "
+    "pickle TEXT NOT NULL, "
+    "time INT, "
+    /* v2 */
+    "origin_server_ts INTEGER, "
+    /* Version 1 */
+    "room_id INTEGER REFERENCES rooms(id), "
+    /* v2 */
+    "chain_index INTEGER, "
+    /* v2 */
+    /* 0: usable, 1: rotated, 2: invalidated (on user/device removals) */
+    "session_state INTEGER NOT NULL DEFAULT 0, "
+    /* v1 */
+    "json_data TEXT, "
+    "UNIQUE (account_id, sender_key, session_id));"
+
+    /* v2 */
+    "CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid);"
+    "CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid);"
+    "CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device);"
+    "CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key);"
+    "CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid);"
+    "CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url);"
+    "CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key);"
+    "CREATE INDEX IF NOT EXISTS user_idx ON users (username);"
+
+    /* v2 */
+    "CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT "
+    "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL "
+    "BEGIN "
+    "UPDATE room_events SET replaced_with_id=NEW.id "
+    "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); "
+    "END;"
+
+    /* v2 */
+    "CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id "
+    "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL "
+    "BEGIN "
+    "UPDATE room_events SET replaced_with_id=NEW.id "
+    "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); "
+    "END;";
+
+  status = sqlite3_exec (self->db, sql, NULL, NULL, &error);
+
+  if (status != SQLITE_OK)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Error creating table. errno: %d, desc: %s. %s",
+                               status, sqlite3_errmsg (self->db), error);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static int
+cm_db_get_db_version (CmDb  *self,
+                      GTask *task)
+{
+  sqlite3_stmt *stmt;
+  int status, version = -1;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  sqlite3_prepare_v2 (self->db, "PRAGMA user_version;", -1, &stmt, NULL);
+  status = sqlite3_step (stmt);
+
+  if (status == SQLITE_ROW)
+    version = sqlite3_column_int (stmt, 0);
+  else
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_FAILED,
+                             "Couldn't get database version. error: %s",
+                             sqlite3_errmsg (self->db));
+  sqlite3_finalize (stmt);
+
+  return version;
+}
+
+static void
+cm_db_backup (CmDb *self)
+{
+  g_autoptr(GFile) backup_db = NULL;
+  g_autoptr(GFile) old_db = NULL;
+  g_autofree char *backup_name = NULL;
+  g_autofree char *time = NULL;
+  g_autoptr(GDateTime) date = NULL;
+  g_autoptr(GError) error = NULL;
+
+  date = g_date_time_new_now_local ();
+  time = g_date_time_format (date, "%Y-%m-%d-%H%M%S");
+  backup_name = g_strdup_printf ("%s.%s", self->db_path, time);
+  g_info ("Copying database for backup");
+
+  old_db = g_file_new_for_path (self->db_path);
+  backup_db = g_file_new_for_path (backup_name);
+  g_file_copy (old_db, backup_db, G_FILE_COPY_NONE, NULL, NULL, NULL, &error);
+  g_info ("Copying database success: %d", !error);
+
+  if (error &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+    g_error ("Error creating DB backup: %s", error->message);
+}
+
+static gboolean
+cm_db_migrate_db_v1 (CmDb  *self,
+                     GTask *task)
+{
+  char *error = NULL;
+  int status;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+
+  cm_db_backup (self);
+
+  status = sqlite3_exec (self->db,
+                         "CREATE TABLE IF NOT EXISTS tmp_users ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "username TEXT NOT NULL UNIQUE, "
+                         "outdated INTEGER DEFAULT 1, "
+                         "json_data TEXT "
+                         ");"
+
+                         "CREATE TABLE IF NOT EXISTS user_devices ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "user_id INTEGER NOT NULL REFERENCES users(id), "
+                         "device TEXT NOT NULL, "
+                         "curve25519_key TEXT, "
+                         "ed25519_key TEXT, "
+                         "verification INTEGER DEFAULT 0, "
+                         "json_data TEXT, "
+                         "UNIQUE (user_id, device));"
+
+                         "CREATE TABLE IF NOT EXISTS tmp_accounts ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "user_device_id INTEGER NOT NULL REFERENCES user_devices(id), "
+                         "next_batch TEXT, "
+                         "pickle TEXT, "
+                         "enabled INTEGER DEFAULT 0, "
+                         "json_data TEXT, "
+                         "UNIQUE (user_device_id));"
+
+                         "CREATE TABLE IF NOT EXISTS tmp_users ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "username TEXT NOT NULL UNIQUE, "
+                         "outdated INTEGER DEFAULT 1, "
+                         "json_data TEXT "
+                         ");"
+
+                         "INSERT OR IGNORE INTO tmp_users(username) "
+                         "SELECT DISTINCT username FROM users;"
+
+                         "INSERT OR IGNORE INTO user_devices(user_id,device) "
+                         "SELECT tmp_users.id,devices.device FROM tmp_users "
+                         "JOIN users ON users.username=tmp_users.username "
+                         "JOIN devices ON users.device_id=devices.id;"
+
+                         "INSERT OR IGNORE INTO tmp_accounts(user_device_id,next_batch,pickle,enabled) "
+                         "SELECT user_devices.id,next_batch,pickle,enabled FROM accounts "
+                         "JOIN users ON users.id=accounts.user_id "
+                         "JOIN devices ON users.device_id=devices.id "
+                         "JOIN user_devices ON user_devices.device=devices.device "
+                         "JOIN tmp_users ON user_devices.user_id=tmp_users.id "
+                         "AND tmp_users.username=users.username;"
+
+                         "UPDATE OR IGNORE session SET account_id=(SELECT tmp_accounts.id "
+                         "FROM tmp_accounts "
+                         "INNER JOIN accounts ON accounts.pickle=tmp_accounts.pickle "
+                         "AND session.account_id=accounts.id"
+                         ");"
+
+                         "UPDATE OR IGNORE rooms SET account_id=(SELECT tmp_accounts.id "
+                         "FROM tmp_accounts "
+                         "INNER JOIN accounts ON accounts.pickle=tmp_accounts.pickle "
+                         "AND rooms.account_id=accounts.id"
+                         ");"
+
+                         "DROP TABLE IF EXISTS users;"
+                         "DROP TABLE IF EXISTS accounts;"
+                         "DROP TABLE IF EXISTS devices;"
+
+                         "ALTER TABLE tmp_users RENAME TO users;"
+                         "ALTER TABLE tmp_accounts RENAME TO accounts;"
+
+                         "ALTER TABLE rooms ADD COLUMN replacement_room_id "
+                         "INTEGER REFERENCES rooms(id);"
+                         "ALTER TABLE rooms ADD COLUMN json_data TEXT;"
+
+                         "ALTER TABLE encryption_keys ADD COLUMN json_data TEXT;"
+
+                         "ALTER TABLE session ADD COLUMN room_id "
+                         "INTEGER REFERENCES rooms(id);"
+                         "ALTER TABLE session ADD COLUMN json_data TEXT;"
+
+                         "PRAGMA user_version = 1;",
+                         NULL, NULL, &error);
+
+  g_debug ("Migrating db to version 1, success: %d", !error);
+
+  if (status == SQLITE_OK || status == SQLITE_DONE)
+    return TRUE;
+
+  g_task_return_new_error (task,
+                           G_IO_ERROR,
+                           G_IO_ERROR_FAILED,
+                           "Couldn't migrate to new db. errno: %d. %s",
+                           status, error);
+  sqlite3_free (error);
+
+  return FALSE;
+}
+
+static gboolean
+cm_db_migrate_to_v2 (CmDb  *self,
+                     GTask *task)
+{
+  char *error = NULL;
+  int status;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+
+  cm_db_backup (self);
+
+  status = sqlite3_exec (self->db,
+                         "CREATE TABLE IF NOT EXISTS room_members ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, "
+                         "user_id INTEGER NOT NULL REFERENCES users(id), "
+                         /* joined, invited, left (we set left instead of deleting as past messages may refer to user id) */
+                         "user_state INTEGER NOT NULL DEFAULT 0, "
+                         "json_data TEXT, "
+                         "UNIQUE (room_id, user_id));"
+
+                         /* v2 */
+                         "CREATE TABLE IF NOT EXISTS room_events_cache ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, "
+                         "sender_id INTEGER REFERENCES room_members(id), "
+                         "event_uid TEXT NOT NULL, "
+                         "origin_server_ts INTEGER, "
+                         "json_data TEXT, "
+                         "UNIQUE (room_id, event_uid));"
+
+                         "CREATE TABLE IF NOT EXISTS room_events ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         /* 'id' above only increments, 'sorted_id' increments
+                          * or decrements in the order events has to be placed.
+                          */
+                         "sorted_id INTEGER NOT NULL, "
+                         "room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE, "
+                         "sender_id INTEGER NOT NULL REFERENCES room_members(id), "
+                         "event_type INTEGER NOT NULL, "
+                         "event_uid TEXT, "
+                         "txnid TEXT, "
+                         /* If set to 0, this event is a reply to some other, which is not yet in db */
+                         "replaces_event_id INTEGER REFERENCES room_events(id), "
+                         "replaces_event_cache_id INTEGER REFERENCES room_events_cache(id), "
+                         /* This event has been replaced with the given event id */
+                         /* This will likely point to the latest replacement id available */
+                         "replaced_with_id INTEGER REFERENCES room_events(id), "
+                         /* sending, sent, sending failed, */
+                         "event_state INTEGER, "
+                         "state_key TEXT, "
+                         "origin_server_ts INTEGER NOT NULL, "
+                         /* 0: not encrypted, 1: not decrypted 2: decrypted */
+                         /* 3: may be decrypted, we got m.room.encrypted but without content */
+                         "decryption INTEGER NOT NULL DEFAULT 0, "
+                         /* direction int, encrypted int, verified int, txnid */
+                         "json_data TEXT, "
+                         "UNIQUE (room_id, event_uid));"
+
+                         "CREATE TABLE IF NOT EXISTS tmp_encryption_keys ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         /* v2 */
+                         "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, "
+                         "file_url TEXT NOT NULL, "
+                         "file_sha256 TEXT, "
+                         /* Initialization vector: iv in JSON */
+                         "iv TEXT NOT NULL, "
+                         /* v in JSON */
+                         "version INT DEFAULT 2 NOT NULL, "
+                         /* alg in JSON */
+                         "algorithm INT NOT NULL, "
+                         /* k in JSON */
+                         "key TEXT NOT NULL, "
+                         /* kty in JSON */
+                         "type INT NOT NULL, "
+                         /* ext in JSON */
+                         "extractable INT DEFAULT 1 NOT NULL, "
+                         /* Version 1 */
+                         "json_data TEXT, "
+                         "UNIQUE (account_id, file_url));"
+
+                         "INSERT INTO tmp_encryption_keys(file_url,file_sha256,iv,version,algorithm,key,type,extractable) "
+                         "SELECT DISTINCT file_url,file_sha256,iv,version,algorithm,key,type,extractable FROM encryption_keys;"
+
+                         "CREATE TABLE IF NOT EXISTS tmp_users ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE, "
+                         "username TEXT NOT NULL, "
+                         "tracking INTEGER NOT NULL DEFAULT 0, "
+                         "outdated INTEGER DEFAULT 1, "
+                         "json_data TEXT, "
+                         "UNIQUE (account_id, username));"
+
+                         "INSERT INTO tmp_users(id,username) "
+                         "SELECT DISTINCT id,username FROM users;"
+
+                         "CREATE TABLE IF NOT EXISTS tmp_rooms ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         /* v2: on delete cascade */
+                         "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, "
+                         "room_name TEXT NOT NULL, "
+                         "prev_batch TEXT, "
+                         /* v1 */
+                         /* Set if the room has tombstone and got replaced by a different room */
+                         "replacement_room_id INTEGER REFERENCES rooms(id),"
+                         /* v2 */
+                         "room_state INTEGER NOT NULL DEFAULT 0, "
+                         /* v1 */
+                         "json_data TEXT, "
+                         "UNIQUE (account_id, room_name));"
+
+                         "INSERT OR IGNORE INTO tmp_rooms(id,account_id,room_name,prev_batch) "
+                         "SELECT DISTINCT id,account_id,room_name,prev_batch FROM rooms;"
+
+                         "CREATE TABLE IF NOT EXISTS tmp_sessions ("
+                         "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
+                         "account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE, "
+                         "sender_key TEXT NOT NULL, "
+                         "session_id TEXT NOT NULL, "
+                         "type INTEGER NOT NULL, "
+                         "pickle TEXT NOT NULL, "
+                         "time INT, "
+                         /* v2 */
+                         "origin_server_ts INTEGER, "
+                         /* Version 1 */
+                         "room_id INTEGER REFERENCES rooms(id), "
+                         /* v2 */
+                         "chain_index INTEGER, "
+                         /* v2 */
+                         /* 0: usable, 1: rotated, 2: invalidated (on user/device removals) */
+                         "session_state INTEGER NOT NULL DEFAULT 0, "
+                         /* v1 */
+                         "json_data TEXT, "
+                         "UNIQUE (account_id, sender_key, session_id));"
+
+                         "INSERT OR IGNORE INTO tmp_sessions(id,account_id,sender_key,session_id,type,pickle,time) "
+                         "SELECT DISTINCT id,account_id,sender_key,session_id,type,pickle,time FROM session;"
+
+                         "DROP TABLE IF EXISTS rooms;"
+                         "DROP TABLE IF EXISTS users;"
+                         "DROP TABLE IF EXISTS session;"
+                         "DROP TABLE IF EXISTS encryption_keys;"
+
+                         "ALTER TABLE tmp_rooms RENAME TO rooms;"
+                         "ALTER TABLE tmp_users RENAME TO users;"
+                         "ALTER TABLE tmp_sessions RENAME TO sessions;"
+                         "ALTER TABLE tmp_encryption_keys RENAME TO encryption_keys;"
+
+                         "CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid);"
+                         "CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid);"
+                         "CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device);"
+                         "CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key);"
+                         "CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid);"
+                         "CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url);"
+                         "CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key);"
+                         "CREATE INDEX IF NOT EXISTS user_idx ON users (username);"
+
+                         /* v2 */
+                         "CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT "
+                         "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL "
+                         "BEGIN "
+                         "UPDATE room_events SET replaced_with_id=NEW.id "
+                         "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); "
+                         "END;"
+
+                         /* v2 */
+                         "CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id "
+                         "ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL "
+                         "BEGIN "
+                         "UPDATE room_events SET replaced_with_id=NEW.id "
+                         "WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id); "
+                         "END;"
+
+                         "PRAGMA user_version = 2;",
+                         NULL, NULL, &error);
+
+  g_debug ("Migrating db to version 2, success: %d", !error);
+
+  if (status == SQLITE_OK || status == SQLITE_DONE)
+    return TRUE;
+
+  g_task_return_new_error (task,
+                           G_IO_ERROR,
+                           G_IO_ERROR_FAILED,
+                           "Couldn't migrate to new db. errno: %d. %s",
+                           status, error);
+  sqlite3_free (error);
+
+  return FALSE;
+}
+
+static gboolean
+cm_db_migrate (CmDb  *self,
+               GTask *task)
+{
+  int version;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  version = cm_db_get_db_version (self, task);
+
+  if (version == DB_VERSION)
+    return TRUE;
+
+  switch (version) {
+  case -1:  /* Error */
+    return FALSE;
+
+  case 0:
+    if (!cm_db_migrate_db_v1 (self, task))
+      return FALSE;
+    /* fallthrough */
+
+  case 1:
+    if (!cm_db_migrate_to_v2 (self, task))
+      return FALSE;
+    break;
+
+  default:
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_FAILED,
+                             "Failed to migrate from unknown version %d",
+                             version);
+    return FALSE;
+  }
+
+  return TRUE;
+}
+
+static int
+db_get_room_event_id (CmDb       *self,
+                      int         room_id,
+                      int        *out_sorted_id,
+                      const char *event)
+{
+  sqlite3_stmt *stmt;
+  int event_id = 0;
+
+  if (!room_id || !event || !*event)
+    return 0;
+
+  g_assert (CM_IS_DB (self));
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT id,sorted_id FROM room_events WHERE room_id=? "
+                      "AND event_uid=?",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when selecting event");
+  matrix_bind_text (stmt, 2, event, "binding when selecting event");
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      event_id = sqlite3_column_int (stmt, 0);
+
+      if (out_sorted_id)
+        *out_sorted_id = sqlite3_column_int (stmt, 1);
+    }
+
+  sqlite3_finalize (stmt);
+
+  return event_id;
+}
+
+static int
+db_get_room_cache_event_id (CmDb       *self,
+                            int         room_id,
+                            const char *event,
+                            gboolean    insert_if_missing)
+{
+  sqlite3_stmt *stmt;
+  int event_cache_id = 0;
+
+  if (!room_id || !event || !*event)
+    return 0;
+
+  g_assert (CM_IS_DB (self));
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT id FROM room_events_cache "
+                      "WHERE room_id=? AND event_uid=?",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when selecting cache event");
+  matrix_bind_text (stmt, 2, event, "binding when selecting cache event");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    event_cache_id = sqlite3_column_int (stmt, 0);
+  sqlite3_finalize (stmt);
+
+  if (event_cache_id || !insert_if_missing)
+    return event_cache_id;
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO room_events_cache (room_id,event_uid) VALUES(?1,?2)",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when adding cache event");
+  matrix_bind_text (stmt, 2, event, "binding when adding cache event");
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  event_cache_id = sqlite3_last_insert_rowid (self->db);
+
+  return event_cache_id;
+}
+
+static int
+db_get_first_room_event_id (CmDb *self,
+                            int   room_id,
+                            int  *out_sorted_id)
+{
+  sqlite3_stmt *stmt;
+  int event_id = 0;
+
+  g_assert (CM_IS_DB (self));
+
+  if (!room_id)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT id,sorted_id FROM room_events WHERE room_id=? "
+                      "ORDER BY sorted_id ASC LIMIT 1",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when selecting event");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      event_id = sqlite3_column_int (stmt, 0);
+      if (out_sorted_id)
+        *out_sorted_id = sqlite3_column_int (stmt, 1);
+    }
+
+  sqlite3_finalize (stmt);
+
+  return event_id;
+}
+
+/* xxx: Merge with above method with a proper name */
+static int
+db_get_last_room_event_id (CmDb *self,
+                           int   room_id,
+                           int  *out_sorted_id)
+{
+  sqlite3_stmt *stmt;
+  int event_id = 0;
+
+  g_assert (CM_IS_DB (self));
+
+  if (!room_id)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT id,sorted_id FROM room_events WHERE room_id=? "
+                      "ORDER BY sorted_id DESC LIMIT 1",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when selecting event");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      event_id = sqlite3_column_int (stmt, 0);
+      if (out_sorted_id)
+        *out_sorted_id = sqlite3_column_int (stmt, 1);
+    }
+
+  sqlite3_finalize (stmt);
+
+  return event_id;
+}
+
+static int
+matrix_db_get_user_id (CmDb       *self,
+                       int         account_id,
+                       const char *username,
+                       gboolean    insert_if_missing)
+{
+  const char *query;
+  sqlite3_stmt *stmt;
+  int user_id = 0;
+
+  if (!username || !*username)
+    return 0;
+
+  g_assert (CM_IS_DB (self));
+
+  if (account_id)
+    query = "SELECT id FROM users WHERE username=? AND account_id=?";
+  else
+    query = "SELECT id FROM users WHERE username=? AND account_id IS NULL";
+
+  sqlite3_prepare_v2 (self->db, query, -1, &stmt, NULL);
+  matrix_bind_text (stmt, 1, username, "binding when selecting user");
+  if (account_id)
+    matrix_bind_int (stmt, 2, account_id, "binding when selecting user");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    user_id = sqlite3_column_int (stmt, 0);
+  sqlite3_finalize (stmt);
+
+  if (user_id || !insert_if_missing)
+    return user_id;
+
+  if (account_id)
+    query = "INSERT INTO users(username,account_id) VALUES(?1,?2)";
+  else
+    query = "INSERT INTO users(username) VALUES(?1)";
+
+  sqlite3_prepare_v2 (self->db, query, -1, &stmt, NULL);
+  matrix_bind_text (stmt, 1, username, "binding when adding user");
+  if (account_id)
+    matrix_bind_int (stmt, 2, account_id, "binding when adding user");
+
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+  user_id = sqlite3_last_insert_rowid (self->db);
+
+  return user_id;
+}
+
+static int
+matrix_db_get_user_device_id (CmDb       *self,
+                              const char *username,
+                              const char *device,
+                              int        *out_user_id,
+                              gboolean    insert_if_missing,
+                              gboolean    is_self)
+{
+  sqlite3_stmt *stmt;
+  int user_id = 0, user_device_id = 0;
+
+  if (!username || !*username || !device || !*device)
+    return 0;
+
+  g_assert (CM_IS_DB (self));
+
+  user_id = matrix_db_get_user_id (self, 0, username, insert_if_missing);
+
+  if (out_user_id)
+    *out_user_id = user_id;
+
+  if (!user_id)
+    return 0;
+
+  if (device && *device)
+    {
+      sqlite3_prepare_v2 (self->db,
+                          "SELECT user_devices.id FROM user_devices "
+                          "WHERE user_id=?1 AND user_devices.device=?2",
+                          -1, &stmt, NULL);
+      matrix_bind_int (stmt, 1, user_id, "binding when getting user device");
+      matrix_bind_text (stmt, 2, device, "binding when getting user device");
+    }
+  else
+    {
+      sqlite3_prepare_v2 (self->db,
+                          "SELECT user_devices.id FROM user_devices "
+                          "WHERE user_id=?1 AND user_devices.device IS NULL LIMIT 1",
+                          -1, &stmt, NULL);
+      matrix_bind_int (stmt, 1, user_id, "binding when getting user device");
+    }
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    user_device_id = sqlite3_column_int (stmt, 0);
+  sqlite3_finalize (stmt);
+
+  if (user_device_id || !insert_if_missing || !device || !*device)
+    return user_device_id;
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO user_devices(user_id, device, verification) "
+                      "VALUES(?1, ?2, ?3)",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, user_id, "binding when adding user device");
+  matrix_bind_text (stmt, 2, device, "binding when adding user device");
+  if (is_self)
+    matrix_bind_int (stmt, 3, VERIFICATION_IS_SELF, "binding when adding user device");
+
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+  user_device_id = sqlite3_last_insert_rowid (self->db);
+
+  return user_device_id;
+}
+
+static int
+matrix_db_get_account_id (CmDb       *self,
+                          const char *username,
+                          const char *device,
+                          int        *out_user_device_id,
+                          gboolean    insert_if_missing)
+{
+  sqlite3_stmt *stmt;
+  int user_device_id = 0;
+
+  if (!username || !*username || !device || !*device)
+    return 0;
+
+  user_device_id = matrix_db_get_user_device_id (self, username, device, NULL, insert_if_missing, TRUE);
+
+  if (out_user_device_id)
+    *out_user_device_id = user_device_id;
+
+  if (!user_device_id)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT accounts.id FROM accounts "
+                      "WHERE user_device_id=?1;",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, user_device_id, "binding when getting account id");
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    return sqlite3_column_int (stmt, 0);
+
+  if (!insert_if_missing)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO accounts(user_device_id) "
+                      "VALUES(?1)",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, user_device_id, "binding when updating account");
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  return sqlite3_last_insert_rowid (self->db);
+}
+
+static int
+matrix_db_get_room_id (CmDb       *self,
+                       int         account_id,
+                       const char *room,
+                       gboolean    insert_if_missing)
+{
+  sqlite3_stmt *stmt;
+  int room_id = 0;
+
+  if (!room || !*room || !account_id)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT rooms.id FROM rooms "
+                      "WHERE account_id=? and room_name=?",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, account_id, "binding when getting room id");
+  matrix_bind_text (stmt, 2, room, "binding when getting room id");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      room_id = sqlite3_column_int (stmt, 0);
+      sqlite3_finalize (stmt);
+
+      return room_id;
+    }
+
+  if (!insert_if_missing)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO rooms(account_id,room_name) "
+                      "VALUES(?1,?2)",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, account_id, "binding when getting room id");
+  matrix_bind_text (stmt, 2, room, "binding when getting room id");
+
+  sqlite3_step (stmt);
+  room_id = sqlite3_last_insert_rowid (self->db);
+
+  sqlite3_finalize (stmt);
+
+  return room_id;
+}
+
+static int
+db_get_room_member_id (CmDb       *self,
+                       int         account_id,
+                       int         room_id,
+                       const char *member,
+                       int        *out_user_id,
+                       gboolean    insert_if_missing)
+{
+  sqlite3_stmt *stmt;
+  int member_id = 0, user_id = 0;
+
+  if (!member || !*member || !room_id)
+    return 0;
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT user_id,room_members.id FROM room_members "
+                      "INNER JOIN users ON users.username=? AND users.account_id=? "
+                      "WHERE room_id=?",
+                      -1, &stmt, NULL);
+  matrix_bind_text (stmt, 1, member, "binding when getting room member id");
+  matrix_bind_int (stmt, 2, account_id, "binding when getting room member id");
+  matrix_bind_int (stmt, 3, room_id, "binding when getting room member id");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      if (out_user_id)
+        *out_user_id = sqlite3_column_int (stmt, 0);
+
+      member_id = sqlite3_column_int (stmt, 1);
+      sqlite3_finalize (stmt);
+
+      return member_id;
+    }
+
+  if (!insert_if_missing)
+    return 0;
+
+  user_id = matrix_db_get_user_id (self, account_id, member, insert_if_missing);
+  if (!user_id)
+    return 0;
+
+  if (out_user_id)
+    *out_user_id = user_id;
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO room_members(room_id,user_id) "
+                      "VALUES(?1,?2)",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when getting room member id");
+  matrix_bind_int (stmt, 2, user_id, "binding when getting room member id");
+
+  sqlite3_step (stmt);
+  member_id = sqlite3_last_insert_rowid (self->db);
+
+  sqlite3_finalize (stmt);
+
+  return member_id;
+}
+
+static void
+cm_db_update_user (CmDb   *self,
+                   int     user_id,
+                   CmUser *user)
+{
+  g_autofree char *json_str = NULL;
+  sqlite3_stmt *stmt;
+  JsonObject *json;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (CM_IS_USER (user));
+
+  if (!user_id)
+    return;
+
+  json = cm_user_generate_json (user);
+  if (!json)
+    return;
+
+  json_str = cm_utils_json_object_to_string (json, FALSE);
+
+  sqlite3_prepare_v2 (self->db,
+                      "UPDATE users SET json_data=?1 "
+                      "WHERE id=?2",
+                      -1, &stmt, NULL);
+
+  matrix_bind_text (stmt, 1, json_str, "binding when updating user");
+  matrix_bind_int (stmt, 2, user_id, "binding when updating user");
+
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+}
+
+static void
+cm_db_update_user_device (CmDb     *self,
+                          int       user_id,
+                          CmDevice *device)
+{
+  const char *curve25519_key, *ed25519_key;
+  g_autofree char *json_str = NULL;
+  sqlite3_stmt *stmt;
+  JsonObject *json;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (CM_IS_DEVICE (device));
+
+  if (!user_id)
+    return;
+
+  curve25519_key = cm_device_get_curve_key (device);
+  ed25519_key = cm_device_get_ed_key (device);
+  json = cm_device_get_json (device);
+  if (json)
+    json_str = cm_utils_json_object_to_string (json, FALSE);
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO user_devices(user_id,device,"
+                      "curve25519_key,ed25519_key,json_data)"
+                      "VALUES(?1,?2,?3,?4,?5) "
+                      "ON CONFLICT(user_id,device) DO UPDATE SET "
+                      "curve25519_key=?3, ed25519_key=?4, json_data=?5",
+                      -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, user_id, "binding when updating user device");
+  matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding when updating user device");
+  if (curve25519_key && *curve25519_key)
+    matrix_bind_text (stmt, 3, curve25519_key, "binding when updating user device");
+  if (ed25519_key && *ed25519_key)
+    matrix_bind_text (stmt, 4, ed25519_key, "binding when updating user device");
+  matrix_bind_text (stmt, 5, json_str, "binding when updating user device");
+
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+}
+
+static void
+matrix_open_db (CmDb  *self,
+                GTask *task)
+{
+  const char *dir, *file_name;
+  sqlite3 *db;
+  int status;
+  gboolean db_exists;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (!self->db);
+
+  dir = g_object_get_data (G_OBJECT (task), "dir");
+  file_name = g_object_get_data (G_OBJECT (task), "file-name");
+  g_assert (dir && *dir);
+  g_assert (file_name && *file_name);
+
+  g_mkdir_with_parents (dir, S_IRWXU);
+  self->db_path = g_build_filename (dir, file_name, NULL);
+
+  db_exists = g_file_test (self->db_path, G_FILE_TEST_EXISTS);
+  status = sqlite3_open (self->db_path, &db);
+
+  if (status == SQLITE_OK) {
+    self->db = db;
+
+    sqlite3_exec (self->db, "PRAGMA foreign_keys = OFF;", NULL, NULL, NULL);
+    sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+    if (db_exists) {
+      if (!cm_db_migrate (self, task))
+        {
+          sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+          return;
+        }
+    } else {
+      if (!cm_db_create_schema (self, task))
+        {
+          sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+          return;
+        }
+    }
+
+    sqlite3_exec (self->db, "PRAGMA foreign_keys = ON;", NULL, NULL, NULL);
+    sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+    g_task_return_boolean (task, TRUE);
+  } else {
+    g_task_return_boolean (task, FALSE);
+    sqlite3_close (db);
+  }
+}
+
+static void
+matrix_close_db (CmDb  *self,
+                 GTask *task)
+{
+  sqlite3 *db;
+  int status;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  db = self->db;
+  self->db = NULL;
+  status = sqlite3_close (db);
+
+  if (status == SQLITE_OK)
+    {
+      /*
+       * We can’t know when will @self associated with the task will
+       * be unref.  So cm_db_get_default() called immediately
+       * after this may return the @self that is yet to be free.  But
+       * as the worker_thread is exited after closing the database, any
+       * actions with the same @self will not execute, and so the tasks
+       * will take ∞ time to complete.
+       *
+       * So Instead of relying on GObject to free the object, Let’s
+       * explicitly run dispose
+       */
+      g_object_run_dispose (G_OBJECT (self));
+      g_debug ("Database closed successfully");
+      g_task_return_boolean (task, TRUE);
+    }
+  else
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Database could not be closed. errno: %d, desc: %s",
+                               status, sqlite3_errmsg (db));
+    }
+}
+
+static void
+cm_db_save_client (CmDb  *self,
+                   GTask *task)
+{
+  const char *device, *pickle, *username, *batch, *filter;
+  g_autofree char *json_str = NULL;
+  JsonObject *root, *obj;
+  sqlite3_stmt *stmt;
+  int status, user_device_id = 0, account_id = 0;
+  gboolean enabled;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  batch = g_object_get_data (G_OBJECT (task), "batch");
+  pickle = g_object_get_data (G_OBJECT (task), "pickle");
+  device = g_object_get_data (G_OBJECT (task), "device");
+  enabled = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled"));
+  username = g_object_get_data (G_OBJECT (task), "username");
+  filter = g_object_get_data (G_OBJECT (task), "filter-id");
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, username, device, &user_device_id, TRUE);
+
+  if (!account_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Failed to add account to db");
+      return;
+    }
+
+  if (filter && *filter)
+    {
+      root = json_object_new ();
+      obj = json_object_new ();
+      json_object_set_object_member (root, "local", obj);
+
+      if (filter && *filter)
+        json_object_set_string_member (obj, "filter-id", filter);
+
+      json_str = cm_utils_json_object_to_string (root, FALSE);
+    }
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO accounts(user_device_id,pickle,"
+                      "next_batch,enabled,json_data) "
+                      "VALUES(?1,?2,?3,?4,?5) "
+                      "ON CONFLICT(user_device_id) "
+                      "DO UPDATE SET pickle=?2, next_batch=?3, enabled=?4, json_data=?5",
+                      -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, user_device_id, "binding when updating account");
+  if (pickle && *pickle)
+    matrix_bind_text (stmt, 2, pickle, "binding when updating account");
+  matrix_bind_text (stmt, 3, batch, "binding when updating account");
+  matrix_bind_int (stmt, 4, enabled, "binding when updating account");
+  matrix_bind_text (stmt, 5, json_str, "binding when updating account");
+
+  status = sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  if (status == SQLITE_DONE)
+    g_task_return_boolean (task, TRUE);
+  else
+    g_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_FAILED,
+                             "Error saving account. errno: %d, desc: %s",
+                             status, sqlite3_errmsg (self->db));
+}
+
+static int
+cm_db_get_room_id (CmDb       *self,
+                   GTask      *task,
+                   const char *room,
+                   int         account_id)
+{
+  sqlite3_stmt *stmt;
+  const char *error;
+  int status;
+
+  g_assert (room && *room);
+  g_assert (account_id);
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT rooms.id FROM rooms "
+                      "WHERE room_name=? and account_id=?",
+                      -1, &stmt, NULL);
+  matrix_bind_text (stmt, 1, room, "binding when getting room id");
+  matrix_bind_int (stmt, 2, account_id, "binding when getting room id");
+
+  status = sqlite3_step (stmt);
+  if (status == SQLITE_ROW)
+    return sqlite3_column_int (stmt, 0);
+
+  if (status == SQLITE_DONE)
+    error = "Room not found in db";
+  else
+    error = sqlite3_errmsg (self->db);
+
+  g_task_return_new_error (task,
+                           G_IO_ERROR,
+                           G_IO_ERROR_FAILED,
+                           "Couldn't find room %s. error: %s",
+                           room, error);
+  return 0;
+}
+
+static GPtrArray *
+cm_db_get_rooms (CmDb       *self,
+                 int         account_id,
+                 const char *account_next_batch)
+{
+  g_autoptr(GPtrArray) rooms = NULL;
+  sqlite3_stmt *stmt;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (account_id);
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT id,room_name,prev_batch,json_data,room_state FROM rooms "
+                      "WHERE account_id=? AND replacement_room_id IS NULL "
+                      "AND room_state != ?",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, account_id, "binding when getting rooms");
+  matrix_bind_int (stmt, 2, CM_STATUS_LEAVE, "binding when getting rooms");
+
+  while (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      g_autoptr(GPtrArray) events = NULL;
+      char *room_name, *prev_batch, *json_str;
+      JsonObject *json = NULL;
+      CmRoom *room;
+      int room_id, event_id, sorted_event_id = 0;
+
+      if (!rooms)
+        rooms = g_ptr_array_new_full (32, g_object_unref);
+
+      room_id = sqlite3_column_int (stmt, 0);
+      room_name = (char *)sqlite3_column_text (stmt, 1);
+      prev_batch = (char *)sqlite3_column_text (stmt, 2);
+      json_str = (char *)sqlite3_column_text (stmt, 3);
+      json = cm_utils_string_to_json_object (json_str);
+
+      room = cm_room_new_from_json (room_name, json, NULL);
+      g_object_set_data (G_OBJECT (room), "-cm-room-id", GINT_TO_POINTER (room_id));
+      cm_room_set_prev_batch (room, prev_batch);
+      cm_room_set_status (room, sqlite3_column_int (stmt, 4));
+
+      event_id = db_get_last_room_event_id (self, room_id, &sorted_event_id);
+      if (event_id)
+        events = db_get_past_room_events (self, room, room_id, 0, sorted_event_id, 1);
+      else
+        cm_room_set_prev_batch (room, account_next_batch);
+
+      if (events)
+        cm_room_add_events (room, events, TRUE);
+
+      g_ptr_array_add (rooms, room);
+    }
+
+  sqlite3_finalize (stmt);
+
+  return g_steal_pointer (&rooms);
+}
+
+static void
+cm_db_load_client (CmDb  *self,
+                   GTask *task)
+{
+  sqlite3_stmt *stmt;
+  char *username, *device_id;
+  int status, account_id = 0;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  username = g_object_get_data (G_OBJECT (task), "username");
+  device_id = g_object_get_data (G_OBJECT (task), "device");
+
+  account_id = matrix_db_get_account_id (self, username, device_id, NULL, FALSE);
+
+  if (!account_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                               "Account not in db");
+      return;
+    }
+
+  status = sqlite3_prepare_v2 (self->db,
+                               "SELECT pickle,next_batch,json_data "
+                               "FROM accounts WHERE accounts.id=?",
+                               -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, account_id, "binding when loading account");
+  status = sqlite3_step (stmt);
+
+  if (status == SQLITE_ROW)
+    {
+      const char *filter;
+      GObject *object = G_OBJECT (task);
+      g_autoptr(JsonObject) json = NULL;
+      JsonObject *child;
+      GPtrArray *rooms;
+
+      g_object_set_data_full (object, "pickle", g_strdup ((char *)sqlite3_column_text (stmt, 0)), g_free);
+      g_object_set_data_full (object, "batch", g_strdup ((char *)sqlite3_column_text (stmt, 1)), g_free);
+
+      json = cm_utils_string_to_json_object ((char *)sqlite3_column_text (stmt, 2));
+      child = cm_utils_json_object_get_object (json, "local");
+      filter = cm_utils_json_object_get_string (child, "filter-id");
+
+      /* If we don't have json_data the db was just migrated from older version */
+      if (sqlite3_column_text (stmt, 2) == NULL)
+        g_object_set_data (object, "db-migrated", GINT_TO_POINTER (TRUE));
+
+      if (filter && *filter)
+        g_object_set_data_full (object, "filter-id", g_strdup (filter), g_free);
+
+      rooms = cm_db_get_rooms (self, account_id, (char *)sqlite3_column_text (stmt, 1));
+      g_object_set_data_full (object, "rooms", rooms, (GDestroyNotify)g_ptr_array_unref);
+    }
+
+  sqlite3_finalize (stmt);
+  g_task_return_boolean (task, status == SQLITE_ROW);
+}
+
+static void
+cm_db_save_room (CmDb  *self,
+                 GTask *task)
+{
+  CmRoom *room;
+  const char *username, *client_device, *prev_batch;
+  const char *replacement, *json = NULL;
+  sqlite3_stmt *stmt;
+  int account_id, room_id = 0, replacement_id = 0;
+  int room_status;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  username = g_object_get_data (G_OBJECT (task), "username");
+  client_device = g_object_get_data (G_OBJECT (task), "client-device");
+  room = g_object_get_data (G_OBJECT (task), "room");
+  json = g_object_get_data (G_OBJECT (task), "json");
+  prev_batch = g_object_get_data (G_OBJECT (task), "prev-batch");
+  replacement = g_object_get_data (G_OBJECT (task), "replacement");
+  room_status = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "status"));
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, username, client_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  if (replacement)
+    replacement_id = matrix_db_get_room_id (self, account_id,
+                                            replacement, TRUE);
+  room_id = matrix_db_get_room_id (self, account_id, cm_room_get_id (room), TRUE);
+
+  if (!room_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting room id");
+      return;
+    }
+
+  if (!cm_room_has_state_sync (room))
+    json = NULL;
+
+  sqlite3_prepare_v2 (self->db,
+                      "UPDATE rooms SET prev_batch=?1,json_data=?2, "
+                      "replacement_room_id=iif(?3 = 0, null, ?3),room_state=?4 "
+                      "WHERE id=?5",
+                      -1, &stmt, NULL);
+
+  matrix_bind_text (stmt, 1, prev_batch, "binding when saving room");
+  matrix_bind_text (stmt, 2, json, "binding when saving room");
+  matrix_bind_int (stmt, 3, replacement_id, "binding when saving room");
+  matrix_bind_int (stmt, 4, room_status, "binding when saving room");
+  matrix_bind_int (stmt, 5, room_id, "binding when saving room");
+
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+cm_db_delete_client (CmDb  *self,
+                     GTask *task)
+{
+  sqlite3_stmt *stmt;
+  char *username, *device_id;
+  int account_id;
+  int status;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  username = g_object_get_data (G_OBJECT (task), "username");
+  device_id = g_object_get_data (G_OBJECT (task), "device-id");
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, username, device_id, NULL, FALSE);
+
+  if (!account_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  status = sqlite3_prepare_v2 (self->db,
+                               "DELETE FROM sessions "
+                               "WHERE sessions.account_id=?1; ",
+                               -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, account_id, "binding when deleting account");
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  status = sqlite3_prepare_v2 (self->db,
+                               "DELETE FROM rooms "
+                               "WHERE rooms.account_id=?1; ",
+                               -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, account_id, "binding when deleting account");
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  status = sqlite3_prepare_v2 (self->db,
+                               "DELETE FROM accounts "
+                               "WHERE accounts.id=?1; ",
+                               -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, account_id, "binding when deleting account");
+
+  status = sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  g_task_return_boolean (task, status == SQLITE_ROW);
+}
+
+static void
+db_add_session (CmDb  *self,
+                GTask *task)
+{
+  CmOlm *session;
+  sqlite3_stmt *stmt;
+  const char *username, *account_device, *session_id, *sender_key, *pickle, *room;
+  CmSessionType type;
+  CmOlmState state;
+  int status, account_id, room_id = 0;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  session = g_object_get_data (G_OBJECT (task), "session");
+  g_assert (CM_IS_OLM (session));
+
+  room = cm_olm_get_room_id (session);
+  type = cm_olm_get_session_type (session);
+  username = cm_olm_get_account_id (session);
+  session_id = cm_olm_get_session_id (session);
+  sender_key = cm_olm_get_sender_key (session);
+  account_device = cm_olm_get_account_device (session);
+  pickle = g_object_get_data (G_OBJECT (task), "pickle");
+  state = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "state"));
+
+  account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  if (room)
+    room_id = cm_db_get_room_id (self, task, room, account_id);
+
+  status = sqlite3_prepare_v2 (self->db,
+                               /*                        1           2         3 */
+                               "INSERT INTO sessions(account_id,sender_key,session_id,"
+                               /* 4    5      6       7        8              9            10 */
+                               "type,pickle,room_id,time,session_state,origin_server_ts,json_data) "
+                               "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10) "
+                               "ON CONFLICT(account_id, sender_key, session_id) DO UPDATE SET "
+                               "pickle=?5, session_state=?8",
+                               -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, account_id, "binding when adding session");
+  matrix_bind_text (stmt, 2, sender_key, "binding when adding session");
+  matrix_bind_text (stmt, 3, session_id, "binding when adding session");
+  matrix_bind_int (stmt, 4, type, "binding when adding session");
+  matrix_bind_text (stmt, 5, pickle, "binding when adding session");
+  if (room_id)
+    matrix_bind_int (stmt, 6, room_id, "binding when adding session");
+  /* Save time in milliseconds */
+  matrix_bind_int (stmt, 7, time (NULL) * 1000, "binding when adding session");
+  matrix_bind_int (stmt, 8, state, "binding when adding session");
+
+  if (!g_object_get_data (G_OBJECT (session), "-cm-db-id"))
+  {
+    g_autofree char *json_str = NULL;
+    JsonObject *json, *child;
+
+    g_object_set_data (G_OBJECT (session), "-cm-db-id",
+                       GINT_TO_POINTER (sqlite3_last_insert_rowid (self->db)));
+    json = json_object_new ();
+    json_object_set_object_member (json, "local", json_object_new ());
+    child = cm_utils_json_object_get_object (json, "local");
+    json_object_set_string_member (child, "first_pickle", pickle);
+    json_str = cm_utils_json_object_to_string (json, FALSE);
+
+    matrix_bind_text (stmt, 10, json_str, "binding when adding session");
+  }
+
+  status = sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  if (status != SQLITE_DONE)
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                             "%s", sqlite3_errmsg (self->db));
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+static void
+cm_db_save_file_enc (CmDb  *self,
+                     GTask *task)
+{
+  CmEncFileInfo *file;
+  sqlite3_stmt *stmt;
+  int status, algorithm = 0, version = 0, type = 0;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  file = g_object_get_data (G_OBJECT (task), "file");
+  g_assert (file && file->mxc_uri);
+
+  if (g_strcmp0 (file->algorithm, "A256CTR") == 0)
+    algorithm = CMATRIX_ALGORITHM_A256CTR;
+
+  if (g_strcmp0 (file->kty, "oct") == 0)
+    type = CMATRIX_KEY_TYPE_OCT;
+
+  if (g_strcmp0 (file->version, "v2") == 0)
+    version = 2;
+
+  status = sqlite3_prepare_v2 (self->db,
+                               "INSERT INTO encryption_keys(file_url,file_sha256,"
+                               "iv,version,algorithm,key,type,extractable) "
+                               "VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
+                               -1, &stmt, NULL);
+
+  matrix_bind_text (stmt, 1, file->mxc_uri, "binding when adding file url");
+  matrix_bind_text (stmt, 2, file->sha256_base64, "binding when adding file url");
+  matrix_bind_text (stmt, 3, file->aes_iv_base64, "binding when adding file url");
+  matrix_bind_int (stmt, 4, version, "binding when adding file url");
+  matrix_bind_int (stmt, 5, algorithm, "binding when adding file url");
+  matrix_bind_text (stmt, 6, file->aes_key_base64, "binding when adding file url");
+  matrix_bind_int (stmt, 7, type, "binding when adding file url");
+  matrix_bind_int (stmt, 8, file->extractable, "binding when adding file url");
+
+  status = sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  g_task_return_boolean (task, status == SQLITE_DONE);
+}
+
+static void
+cm_db_find_file_enc (CmDb  *self,
+                     GTask *task)
+{
+  sqlite3_stmt *stmt;
+  CmEncFileInfo *file = NULL;
+  char *uri;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  uri = g_object_get_data (G_OBJECT (task), "uri");
+  g_assert (uri && *uri);
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT file_sha256,iv,key "
+                      "FROM encryption_keys WHERE file_url=?1",
+                      -1, &stmt, NULL);
+  matrix_bind_text (stmt, 1, uri, "binding when looking up file encryption");
+
+  if (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      file = g_new0 (CmEncFileInfo, 1);
+      file->mxc_uri = g_strdup (uri);
+      file->sha256_base64 = g_strdup ((char *)sqlite3_column_text (stmt, 0));
+      file->aes_iv_base64 = g_strdup ((char *)sqlite3_column_text (stmt, 1));
+      file->aes_key_base64 = g_strdup ((char *)sqlite3_column_text (stmt, 2));
+
+      if (!g_str_has_prefix (uri, "mxc://"))
+        g_clear_pointer (&file->mxc_uri, g_free);
+    }
+
+  g_task_return_pointer (task, file, cm_enc_file_info_free);
+}
+
+static void
+db_lookup_session (CmDb  *self,
+                   GTask *task)
+{
+  sqlite3_stmt *stmt;
+  const char *username, *account_device, *session_id, *sender_key;
+  char *pickle_key, *room;
+  CmOlm *session = NULL;
+  CmSessionType type;
+  int status, account_id, room_id = 0;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (G_IS_TASK (task));
+
+  type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type"));
+
+  room = g_object_get_data (G_OBJECT (task), "room-id");
+  username = g_object_get_data (G_OBJECT (task), "account-id");
+  session_id = g_object_get_data (G_OBJECT (task), "session-id");
+  sender_key = g_object_get_data (G_OBJECT (task), "sender-key");
+  pickle_key = g_object_get_data (G_OBJECT (task), "pickle-key");
+  account_device = g_object_get_data (G_OBJECT (task), "account-device");
+
+  account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  /* If no session-id is given, only MegOlm out session can be requested */
+  if (!session_id && type != SESSION_MEGOLM_V1_OUT)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Requested session without session id");
+      return;
+    }
+
+  if (room)
+    room_id =  matrix_db_get_room_id (self, account_id, room, FALSE);
+
+  if (session_id)
+    sqlite3_prepare_v2 (self->db,
+                        "SELECT id,pickle FROM sessions "
+                        "WHERE account_id=? AND sender_key=? AND type=? "
+                        "AND session_id=? AND session_state=0",
+                        -1, &stmt, NULL);
+  else
+    sqlite3_prepare_v2 (self->db,
+                        "SELECT id,pickle FROM sessions "
+                        "WHERE account_id=? AND sender_key=? AND type=?"
+                        "AND room_id=? AND session_state=0 "
+                        "ORDER BY id DESC LIMIT 1",
+                        -1, &stmt, NULL);
+
+
+  matrix_bind_int (stmt, 1, account_id, "binding when looking up session");
+  matrix_bind_text (stmt, 2, sender_key, "binding when looking up session");
+  matrix_bind_int (stmt, 3, type, "binding when looking up session");
+  if (session_id)
+    matrix_bind_text (stmt, 4, session_id, "binding when looking up session");
+  else if (room_id)
+    matrix_bind_int (stmt, 4, room_id, "binding when looking up session");
+
+  status = sqlite3_step (stmt);
+
+  if (status == SQLITE_ROW)
+    {
+      g_autofree char *pickle = NULL;
+      int id;
+
+      pickle = g_strdup ((const char *)sqlite3_column_text (stmt, 1));
+      session = cm_olm_new_from_pickle (pickle, pickle_key, sender_key, type);
+      id = sqlite3_column_int (stmt, 0);
+
+      if (session)
+        g_object_set_data (G_OBJECT (session), "-cm-db-id", GINT_TO_POINTER (id));
+    }
+
+  sqlite3_finalize (stmt);
+  g_task_return_pointer (task, session, g_object_unref);
+}
+
+static void
+db_lookup_olm_session (CmDb  *self,
+                       GTask *task)
+{
+  sqlite3_stmt *stmt;
+  const char *username, *account_device, *sender_curve_key, *body;
+  char *pickle_key, *plain_text = NULL;
+  gpointer session = NULL;
+  CmSessionType type;
+  size_t message_type;
+  int account_id;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (G_IS_TASK (task));
+
+  type = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "type"));
+  message_type = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "message-type"));
+
+  username = g_object_get_data (G_OBJECT (task), "account-id");
+  sender_curve_key = g_object_get_data (G_OBJECT (task), "sender-key");
+  account_device = g_object_get_data (G_OBJECT (task), "account-device");
+  body = g_object_get_data (G_OBJECT (task), "body");
+  pickle_key = g_strdup (g_object_get_data (G_OBJECT (task), "pickle-key"));
+
+  account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  sqlite3_prepare_v2 (self->db,
+                      "SELECT pickle FROM sessions "
+                      "WHERE account_id=? AND sender_key=? AND type=?",
+                      -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, account_id, "binding when looking up olm session");
+  matrix_bind_text (stmt, 2, sender_curve_key, "binding when looking up olm session");
+  matrix_bind_int (stmt, 3, type, "binding when looking up olm session");
+
+  while (sqlite3_step (stmt) == SQLITE_ROW)
+    {
+      char *pickle;
+
+      pickle = (char *)sqlite3_column_text (stmt, 0);
+      session = cm_olm_match_olm_session (body, strlen (body), message_type,
+                                          pickle, pickle_key, sender_curve_key,
+                                          type, &plain_text);
+
+      if (session)
+        break;
+    }
+
+  cm_utils_free_buffer (pickle_key);
+  sqlite3_finalize (stmt);
+
+  g_object_set_data_full (G_OBJECT (task), "plaintext", plain_text,
+                          (GDestroyNotify)cm_utils_free_buffer);
+  g_task_return_pointer (task, session, g_object_unref);
+}
+
+static void
+db_update_user_tracking (CmDb     *self,
+                         int       user_id,
+                         gboolean  outdated,
+                         gboolean  is_tracking)
+{
+  sqlite3_stmt *stmt;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (user_id);
+
+  sqlite3_prepare_v2 (self->db,
+                      "UPDATE users SET tracking=?1, outdated=?2 "
+                      "WHERE id=?3",
+                      -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, is_tracking, "binding add user device");
+  matrix_bind_int (stmt, 2, outdated, "binding add user device");
+  matrix_bind_int (stmt, 3, user_id, "binding add user device");
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+}
+
+static void
+db_mark_user_device_change (CmDb  *self,
+                            GTask *task)
+{
+  GPtrArray *users;
+  const char *username, *account_device;
+  gboolean outdated, is_tracking;
+  int account_id;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (G_IS_TASK (task));
+
+  is_tracking = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "tracking"));
+  outdated = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "outdated"));
+  username = g_object_get_data (G_OBJECT (task), "account-id");
+  users = g_object_get_data (G_OBJECT (task), "users");
+  account_device = g_object_get_data (G_OBJECT (task), "account-device");
+  g_assert (users && users->len);
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, username, account_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  for (guint i = 0; i < users->len; i++)
+    {
+      int user_id;
+
+      username = cm_user_get_id (users->pdata[i]);
+      user_id = matrix_db_get_user_id (self, account_id, username, TRUE);
+      db_update_user_tracking (self, user_id, outdated, is_tracking);
+    }
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+db_update_user_devices (CmDb  *self,
+                        GTask *task)
+{
+  const char *account_username, *account_device, *username;
+  GPtrArray *added, *removed;
+  int account_id, user_id, force_add;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (G_IS_TASK (task));
+
+  account_username = g_object_get_data (G_OBJECT (task), "account-id");
+  account_device = g_object_get_data (G_OBJECT (task), "account-device");
+  force_add = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "force-add"));
+  username = g_object_get_data (G_OBJECT (task), "username");
+  removed = g_object_get_data (G_OBJECT (task), "removed");
+  added = g_object_get_data (G_OBJECT (task), "added");
+  g_assert (added || removed);
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, account_username, account_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  user_id = matrix_db_get_user_id (self, account_id, username, force_add);
+
+  if (!user_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                               "User not in db");
+      return;
+    }
+
+  for (guint i = 0; added &&  i < added->len; i++)
+    {
+      sqlite3_stmt *stmt;
+      CmDevice *device;
+      gboolean verified;
+
+      device = added->pdata[i];
+      verified = cm_device_is_verified (device);
+      sqlite3_prepare_v2 (self->db,
+                          "INSERT INTO user_devices(user_id,device,curve25519_key,ed25519_key,verification) "
+                          "VALUES(?1,?2,?3,?4,?5) ON CONFLICT(user_id,device) DO UPDATE SET "
+                          "verification=?5",
+                          -1, &stmt, NULL);
+
+      matrix_bind_int (stmt, 1, user_id, "binding add user device");
+      matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding add user device");
+      matrix_bind_text (stmt, 3, cm_device_get_curve_key (device), "binding add user device");
+      matrix_bind_text (stmt, 4, cm_device_get_ed_key (device), "binding add user device");
+      if (verified)
+        matrix_bind_int (stmt, 5, VERIFICATION_VERIFIED, "binding add user device");
+
+      sqlite3_step (stmt);
+      sqlite3_finalize (stmt);
+    }
+
+  for (guint i = 0; removed &&  i < removed->len; i++)
+    {
+      sqlite3_stmt *stmt;
+      CmDevice *device;
+
+      device = removed->pdata[i];
+      sqlite3_prepare_v2 (self->db,
+                          "DELETE FROM user_devices WHERE user_id=?1 AND device=?2",
+                          -1, &stmt, NULL);
+
+      matrix_bind_int (stmt, 1, user_id, "binding add user device");
+      matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding add user device");
+      sqlite3_step (stmt);
+      sqlite3_finalize (stmt);
+    }
+
+  db_update_user_tracking (self, user_id, FALSE, TRUE);
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+db_update_user_device (CmDb  *self,
+                       GTask *task)
+{
+  const char *account_username, *account_device, *username;
+  sqlite3_stmt *stmt;
+  CmDevice *device;
+  int account_id, user_id;
+  gboolean verified;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (G_IS_TASK (task));
+
+  account_username = g_object_get_data (G_OBJECT (task), "account-id");
+  account_device = g_object_get_data (G_OBJECT (task), "account-device");
+  username = g_object_get_data (G_OBJECT (task), "username");
+  device = g_object_get_data (G_OBJECT (task), "device");
+  g_assert (device);
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, account_username, account_device, NULL, FALSE);
+
+  if (!account_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR,
+                               "Error getting account id");
+      return;
+    }
+
+  user_id = matrix_db_get_user_id (self, account_id, username, FALSE);
+
+  if (!user_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                               "User not in db");
+      return;
+    }
+
+  verified = cm_device_is_verified (device);
+
+  sqlite3_prepare_v2 (self->db,
+                      "INSERT INTO user_devices(user_id,device,curve25519_key,ed25519_key,verification) "
+                      "VALUES(?1,?2,?3,?4,?5) ON CONFLICT(user_id,device) DO UPDATE SET "
+                      "verification=?5",
+                      -1, &stmt, NULL);
+
+  matrix_bind_int (stmt, 1, user_id, "binding add user device");
+  matrix_bind_text (stmt, 2, cm_device_get_id (device), "binding add user device");
+  matrix_bind_text (stmt, 3, cm_device_get_curve_key (device), "binding add user device");
+  matrix_bind_text (stmt, 4, cm_device_get_ed_key (device), "binding add user device");
+  if (verified)
+    matrix_bind_int (stmt, 5, VERIFICATION_VERIFIED, "binding add user device");
+
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+cm_db_delete_event_with_txn_id (CmDb       *self,
+                                int         room_id,
+                                const char *txnid)
+{
+  sqlite3_stmt *stmt;
+
+  g_assert (CM_IS_DB (self));
+
+  if (!room_id || !txnid || !*txnid)
+    return;
+
+  sqlite3_prepare_v2 (self->db,
+                      "DELETE FROM room_events "
+                      "WHERE room_id=? AND txnid=? AND event_uid IS NULL",
+                      -1, &stmt, NULL);
+  matrix_bind_int (stmt, 1, room_id, "binding when deleting room event txnid");
+  matrix_bind_text (stmt, 2, txnid, "binding when deleting room event txnid");
+  sqlite3_step (stmt);
+  sqlite3_finalize (stmt);
+}
+
+static void
+db_add_room_members (CmDb  *self,
+                     GTask *task)
+{
+  const char *username, *device, *room;
+  GPtrArray *members;
+  int room_id, account_id;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  username = g_object_get_data (G_OBJECT (task), "username");
+  members = g_object_get_data (G_OBJECT (task), "members");
+  device = g_object_get_data (G_OBJECT (task), "device");
+  room = g_object_get_data (G_OBJECT (task), "room");
+  g_assert (members && members->len);
+
+  account_id = matrix_db_get_account_id (self, username, device, NULL, FALSE);
+  room_id = matrix_db_get_room_id (self, account_id, room, FALSE);
+
+  if (!room_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                               "Account or Room not found in db");
+      return;
+    }
+
+  /* todo: Look into sqlite transactions */
+  for (guint i = 0; i < members->len; i++)
+    {
+      CmUser *cm_user = members->pdata[i];
+      GListModel *devices;
+      guint n_items;
+      int member_id, user_id = 0;
+
+      username = cm_user_get_id (cm_user);
+      member_id = db_get_room_member_id (self, account_id, room_id, username, &user_id, TRUE);
+
+      if (!member_id)
+        continue;
+
+      cm_db_update_user (self, user_id, cm_user);
+      devices = cm_user_get_devices (cm_user);
+      n_items = g_list_model_get_n_items (devices);
+
+      for (guint j = 0; j < n_items; j++)
+        {
+          g_autoptr(CmDevice) cm_device = NULL;
+
+          cm_device = g_list_model_get_item (devices, j);
+          cm_db_update_user_device (self, user_id, cm_device);
+        }
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+db_add_room_events (CmDb  *self,
+                    GTask *task)
+{
+  const char *username, *device, *room;
+  sqlite3_stmt *stmt;
+  GPtrArray *events;
+  int room_id, account_id, sorted_event_id = 0, match_id = 0;
+  gboolean prepend;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  username = g_object_get_data (G_OBJECT (task), "username");
+  device = g_object_get_data (G_OBJECT (task), "device");
+  room = g_object_get_data (G_OBJECT (task), "room");
+  events = g_object_get_data (G_OBJECT (task), "events");
+  prepend = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "prepend"));
+  g_assert (events && events->len);
+
+  sqlite3_exec (self->db, "BEGIN TRANSACTION;", NULL, NULL, NULL);
+  account_id = matrix_db_get_account_id (self, username, device, NULL, FALSE);
+  room_id = matrix_db_get_room_id (self, account_id, room, FALSE);
+
+  if (prepend)
+    match_id = db_get_first_room_event_id (self, room_id, &sorted_event_id);
+  else
+    match_id = db_get_last_room_event_id (self, room_id, &sorted_event_id);
+
+  if (match_id)
+    prepend ? (--sorted_event_id) : (++sorted_event_id);
+
+  if (!room_id)
+    {
+      sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                               "Account or Room not found in db");
+      return;
+    }
+
+  for (guint i = 0; i < events->len; i++)
+    {
+      g_autoptr(JsonObject) encrypted = NULL;
+      g_autoptr(JsonObject) json_obj = NULL;
+      g_autoptr(JsonObject) json = NULL;
+      JsonObject *local = NULL;
+      CmEvent *event = events->pdata[i];
+      g_autofree char *json_str = NULL;
+      const char *sender;
+      int member_id, replaces_id, replaces_cache_id = 0;
+      int event_state, status;
+
+      json = cm_event_get_json (event);
+      encrypted = cm_event_get_encrypted_json (event);
+      sender = cm_event_get_sender_id (event);
+
+      member_id = db_get_room_member_id (self, account_id, room_id, sender, NULL, TRUE);
+
+      /* Delete existing ones as we add them below so that the sort order is right */
+      if (cm_event_get_txn_id (event))
+        cm_db_delete_event_with_txn_id (self, room_id, cm_event_get_txn_id (event));
+
+      if (!member_id)
+        continue;
+
+      replaces_id = db_get_room_event_id (self, room_id, NULL,
+                                          cm_event_get_replaces_id (event));
+      if (cm_event_get_replaces_id (event) && !replaces_id)
+        replaces_cache_id = db_get_room_cache_event_id (self, room_id,
+                                                        cm_event_get_replaces_id (event), TRUE);
+
+      json_obj = json_object_new ();
+      if (json)
+        json_object_set_object_member (json_obj, "json", g_steal_pointer (&json));
+      if (encrypted)
+        json_object_set_object_member (json_obj, "encrypted", g_steal_pointer (&encrypted));
+
+      if (cm_event_get_txn_id (event))
+        {
+          local = json_object_new ();
+          json_object_set_string_member (local, "txnid",
+                                         cm_event_get_txn_id (event));
+        }
+
+      if (local)
+        json_object_set_object_member (json_obj, "local", local);
+
+      json_str = cm_utils_json_object_to_string (json_obj, FALSE);
+      event_state = db_event_state_to_int (cm_event_get_state (event));
+
+      sqlite3_prepare_v2 (self->db,
+                          /*                          1       2         3 */
+                          "INSERT INTO room_events(sorted_id,room_id,sender_id,"
+                          /*   4           5      6          7                    8 */
+                          "event_type,event_uid,txnid,replaces_event_id,replaces_event_cache_id,"
+                          /*   9           10             11          12         13*/
+                          "event_state,state_key,origin_server_ts,decryption,json_data) "
+                          "VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)",
+                          -1, &stmt, NULL);
+      matrix_bind_int (stmt, 1, sorted_event_id, "binding when adding event");
+      matrix_bind_int (stmt, 2, room_id, "binding when adding event");
+      matrix_bind_int (stmt, 3, member_id, "binding when adding event");
+      matrix_bind_int (stmt, 4, cm_event_get_m_type (event), "binding when adding event");
+      matrix_bind_text (stmt, 5, cm_event_get_id (event), "binding when adding event");
+      if (cm_event_get_txn_id (event))
+        matrix_bind_text (stmt, 6, cm_event_get_txn_id (event), "binding when adding event");
+      if (replaces_id)
+        matrix_bind_int (stmt, 7, replaces_id, "binding when adding event");
+      if (replaces_cache_id)
+        matrix_bind_int (stmt, 8, replaces_cache_id, "binding when adding event");
+      matrix_bind_int (stmt, 9, event_state, "binding when adding event");
+      matrix_bind_text (stmt, 10, cm_event_get_state_key (event), "binding when adding event");
+      matrix_bind_int (stmt, 11, cm_event_get_time_stamp (event), "binding when adding event");
+      matrix_bind_int (stmt, 12, db_event_get_decryption_value (event), "binding when adding event");
+      matrix_bind_text (stmt, 13, json_str, "binding when adding event");
+      status = sqlite3_step (stmt);
+      sqlite3_finalize (stmt);
+
+      if (status == SQLITE_DONE)
+        {
+          int event_id, event_cache_id;
+
+          event_id = sqlite3_last_insert_rowid (self->db);
+          event_cache_id = db_get_room_cache_event_id (self, room_id,
+                                                     cm_event_get_id (event), FALSE);
+
+          if (event_id && event_cache_id)
+            {
+              sqlite3_prepare_v2 (self->db,
+                                  "UPDATE room_events SET replaces_event_id=?"
+                                  "WHERE replaces_event_cache_id=?;",
+                                  -1, &stmt, NULL);
+              matrix_bind_int (stmt, 1, event_id, "binding when adding event");
+              matrix_bind_int (stmt, 2, event_cache_id, "binding when adding event");
+              sqlite3_step (stmt);
+              sqlite3_finalize (stmt);
+            }
+        }
+      else
+        {
+          g_warning ("Failed to save event: %s, error: %s",
+                     cm_event_get_id (event),
+                     sqlite3_errmsg (self->db));
+        }
+
+      prepend ? (--sorted_event_id) : (++sorted_event_id);
+    }
+  sqlite3_exec (self->db, "END TRANSACTION;", NULL, NULL, NULL);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static void
+db_get_past_events (CmDb  *self,
+                    GTask *task)
+{
+  const char *username, *device, *room, *event;
+  GPtrArray *events = NULL;
+  CmRoom *cm_room;
+  int room_id, account_id, event_id, sorted_event_id = 0;
+
+  g_assert (CM_IS_DB (self));
+  g_assert (G_IS_TASK (task));
+  g_assert (g_thread_self () == self->worker_thread);
+  g_assert (self->db);
+
+  room = g_object_get_data (G_OBJECT (task), "room");
+  event = g_object_get_data (G_OBJECT (task), "event");
+  device = g_object_get_data (G_OBJECT (task), "device");
+  cm_room = g_object_get_data (G_OBJECT (task), "cm-room");
+  username = g_object_get_data (G_OBJECT (task), "username");
+
+  account_id = matrix_db_get_account_id (self, username, device, NULL, FALSE);
+  room_id = matrix_db_get_room_id (self, account_id, room, FALSE);
+
+  if (event)
+    event_id = db_get_room_event_id (self, room_id, &sorted_event_id, event);
+  else
+    event_id = db_get_last_room_event_id (self, room_id, &sorted_event_id);
+
+  if (!event_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
+                               "Couldn't find event in db");
+      return;
+    }
+
+  events = db_get_past_room_events (self, cm_room, room_id,
+                                    event ? event_id : 0,
+                                    sorted_event_id, 30);
+  g_task_return_pointer (task, events, (GDestroyNotify)g_ptr_array_unref);
+}
+
+static gpointer
+cm_db_worker (gpointer user_data)
+{
+  CmDb *self = user_data;
+  GTask *task;
+
+  g_assert (CM_IS_DB (self));
+
+  while ((task = g_async_queue_pop (self->queue)))
+    {
+      CmDbCallback callback;
+
+      g_assert (task);
+      callback = g_task_get_task_data (task);
+      callback (self, task);
+      g_object_unref (task);
+
+      if (callback == matrix_close_db)
+        break;
+    }
+
+  return NULL;
+}
+
+static void
+ma_finish_cb (GObject      *object,
+              GAsyncResult *result,
+              gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+
+  g_task_propagate_boolean (G_TASK (result), &error);
+
+  if (error)
+    g_warning ("Error: %s", error->message);
+
+  g_task_return_boolean (G_TASK (user_data), !error);
+}
+
+static void
+cm_db_close (CmDb *self)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (CM_IS_DB (self));
+
+  if (!self->db)
+    return;
+
+  task = g_task_new (NULL, NULL, NULL, NULL);
+  cm_db_close_async (self, ma_finish_cb, task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+}
+
+static void
+cm_db_dispose (GObject *object)
+{
+  CmDb *self = (CmDb *)object;
+
+  cm_db_close (self);
+
+  g_clear_pointer (&self->worker_thread, g_thread_unref);
+
+  G_OBJECT_CLASS (cm_db_parent_class)->dispose (object);
+}
+
+static void
+cm_db_finalize (GObject *object)
+{
+  CmDb *self = (CmDb *)object;
+
+  if (self->db)
+    g_warning ("Database not closed");
+
+  g_clear_pointer (&self->queue, g_async_queue_unref);
+  g_free (self->db_path);
+
+  G_OBJECT_CLASS (cm_db_parent_class)->finalize (object);
+}
+
+static void
+cm_db_class_init (CmDbClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose  = cm_db_dispose;
+  object_class->finalize = cm_db_finalize;
+}
+
+static void
+cm_db_init (CmDb *self)
+{
+  self->queue = g_async_queue_new ();
+}
+
+CmDb *
+cm_db_new (void)
+{
+  return g_object_new (CM_TYPE_DB, NULL);
+}
+
+/**
+ * cm_db_open_async:
+ * @self: a #CmDb
+ * @dir: (transfer full): The database directory
+ * @file_name: The file name of database
+ * @callback: a #GAsyncReadyCallback, or %NULL
+ * @user_data: closure data for @callback
+ *
+ * Open the database file @file_name from path @dir.
+ * Complete with cm_db_open_finish() to get
+ * the result.
+ */
+void
+cm_db_open_async (CmDb                *self,
+                  char                *dir,
+                  const char          *file_name,
+                  GAsyncReadyCallback  callback,
+                  gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (dir || !*dir);
+  g_return_if_fail (file_name || !*file_name);
+
+  if (self->db)
+    {
+      g_warning ("A DataBase is already open");
+      return;
+    }
+
+  if (!self->worker_thread)
+    self->worker_thread = g_thread_new ("matrix-db-worker",
+                                        cm_db_worker,
+                                        self);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_open_async);
+  g_task_set_task_data (task, matrix_open_db, NULL);
+  g_object_set_data_full (G_OBJECT (task), "dir", dir, g_free);
+  g_object_set_data_full (G_OBJECT (task), "file-name", g_strdup (file_name), g_free);
+
+  g_async_queue_push (self->queue, task);
+}
+
+/**
+ * cm_db_open_finish:
+ * @self: a #CmDb
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError or %NULL
+ *
+ * Completes opening a database started with
+ * cm_db_open_async().
+ *
+ * Returns: %TRUE if database was opened successfully.
+ * %FALSE otherwise with @error set.
+ */
+gboolean
+cm_db_open_finish (CmDb          *self,
+                   GAsyncResult  *result,
+                   GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/**
+ * cm_db_is_open:
+ * @self: a #CmDb
+ *
+ * Get if the database is open or not
+ *
+ * Returns: %TRUE if a database is open.
+ * %FALSE otherwise.
+ */
+gboolean
+cm_db_is_open (CmDb *self)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+
+  return !!self->db;
+}
+
+/**
+ * cm_db_close_async:
+ * @self: a #CmDb
+ * @callback: a #GAsyncReadyCallback, or %NULL
+ * @user_data: closure data for @callback
+ *
+ * Close the database opened.
+ * Complete with cm_db_close_finish() to get
+ * the result.
+ */
+void
+cm_db_close_async (CmDb                *self,
+                   GAsyncReadyCallback  callback,
+                   gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_close_async);
+  g_task_set_task_data (task, matrix_close_db, NULL);
+
+  g_async_queue_push (self->queue, task);
+}
+
+/**
+ * cm_db_close_finish:
+ * @self: a #CmDb
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError or %NULL
+ *
+ * Completes closing a database started with
+ * cm_db_close_async().  @self is
+ * g_object_unref() if closing succeeded.
+ * So @self will be freed if you haven’t kept
+ * your own reference on @self.
+ *
+ * Returns: %TRUE if database was closed successfully.
+ * %FALSE otherwise with @error set.
+ */
+gboolean
+cm_db_close_finish (CmDb          *self,
+                    GAsyncResult  *result,
+                    GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+cm_db_save_client_async (CmDb                *self,
+                         CmClient            *client,
+                         char                *pickle,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  GObject *object;
+  GTask *task;
+  const char *username;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_save_client_async);
+  g_task_set_task_data (task, cm_db_save_client, NULL);
+
+  object = G_OBJECT (task);
+  username = cm_client_get_user_id (client);
+
+  if (g_application_get_default ())
+    g_application_hold (g_application_get_default ());
+
+  if (!username || !*username || !cm_client_get_device_id (client))
+    {
+      g_task_return_boolean (task, FALSE);
+      return;
+    }
+
+  g_object_set_data (object, "enabled", GINT_TO_POINTER (cm_client_get_enabled (client)));
+  g_object_set_data_full (object, "pickle", pickle, g_free);
+  g_object_set_data_full (object, "device",
+                          g_strdup (cm_client_get_device_id (client)), g_free);
+  g_object_set_data_full (object, "batch",
+                          g_strdup (cm_client_get_next_batch (client)), g_free);
+  g_object_set_data_full (object, "username", g_strdup (username), g_free);
+  g_object_set_data_full (object, "account", g_object_ref (client), g_object_unref);
+  g_object_set_data_full (object, "filter-id",
+                          g_strdup (cm_client_get_filter_id (client)), g_free);
+
+  g_async_queue_push (self->queue, task);
+}
+
+gboolean
+cm_db_save_client_finish (CmDb          *self,
+                          GAsyncResult  *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  if (g_application_get_default ())
+    g_application_release (g_application_get_default ());
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+cm_db_load_client_async (CmDb                *self,
+                         CmClient            *client,
+                         const char          *device_id,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  GTask *task;
+  const char *username;
+
+  g_return_if_fail (!device_id || *device_id);
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_load_client_async);
+  g_task_set_task_data (task, cm_db_load_client, NULL);
+
+  username = cm_client_get_user_id (client);
+
+  if (!username || !*username || !device_id)
+    {
+      g_task_return_boolean (task, FALSE);
+      return;
+    }
+
+  g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device_id), g_free);
+  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
+  g_object_set_data_full (G_OBJECT (task), "client", g_object_ref (client), g_object_unref);
+
+  g_async_queue_push_front (self->queue, task);
+}
+
+gboolean
+cm_db_load_client_finish (CmDb          *self,
+                          GAsyncResult  *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+cm_db_save_room_async (CmDb                *self,
+                       CmClient            *client,
+                       CmRoom              *room,
+                       GAsyncReadyCallback  callback,
+                       gpointer             user_data)
+{
+  const char *username, *device_id, *prev_batch;
+  const char *replacement;
+  CmStatus room_status;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_save_room_async);
+  g_task_set_task_data (task, cm_db_save_room, NULL);
+
+  username = cm_client_get_user_id (client);
+  device_id = cm_client_get_device_id (client);
+  prev_batch = cm_room_get_prev_batch (room);
+  replacement = cm_room_get_replacement_room (room);
+  room_status = cm_room_get_status (room);
+
+  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
+  g_object_set_data_full (G_OBJECT (task), "room", g_object_ref (room), g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "json", cm_room_get_json (room), g_free);
+  g_object_set_data_full (G_OBJECT (task), "prev-batch", g_strdup (prev_batch), g_free);
+  g_object_set_data_full (G_OBJECT (task), "client", g_object_ref (client), g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "client-device", g_strdup (device_id), g_free);
+  g_object_set_data_full (G_OBJECT (task), "replacement", g_strdup (replacement), g_free);
+  g_object_set_data (G_OBJECT (task), "status", GINT_TO_POINTER (room_status));
+
+  g_async_queue_push (self->queue, task);
+}
+
+gboolean
+cm_db_save_room_finish (CmDb          *self,
+                        GAsyncResult  *result,
+                        GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+cm_db_delete_client_async (CmDb                *self,
+                           CmClient            *client,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  const char *username, *device_id;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_delete_client_async);
+  g_task_set_task_data (task, cm_db_delete_client, NULL);
+
+  username = cm_client_get_user_id (client);
+  device_id = cm_client_get_device_id (client);
+
+  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
+  g_object_set_data_full (G_OBJECT (task), "device-id", g_strdup (device_id), g_free);
+  g_object_set_data_full (G_OBJECT (task), "client", g_object_ref (client), g_object_unref);
+
+  g_async_queue_push (self->queue, task);
+}
+
+gboolean
+cm_db_delete_client_finish (CmDb          *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+cm_db_add_session (CmDb     *self,
+                   gpointer  session,
+                   char     *pickle)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GObject *object;
+  gboolean success;
+
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (CM_IS_OLM (session), FALSE);
+  g_return_val_if_fail (pickle && *pickle, FALSE);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+  g_task_set_source_tag (task, cm_db_add_session);
+  g_task_set_task_data (task, db_add_session, NULL);
+  object = G_OBJECT (task);
+
+  g_object_set_data_full (object, "session", g_object_ref (session), g_object_unref);
+  g_object_set_data_full (object, "pickle", pickle, g_free);
+  g_object_set_data (object, "state", GINT_TO_POINTER (cm_olm_get_state (session)));
+  if (cm_olm_get_session_type (session) == SESSION_MEGOLM_V1_OUT)
+    g_object_set_data (object, "chain-index", GINT_TO_POINTER (cm_olm_get_message_index (session)));
+
+  g_async_queue_push_front (self->queue, task);
+
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  success = g_task_propagate_boolean (task, &error);
+
+  if (error)
+    g_warning ("Failed to save olm session with id: %s, error: %s",
+               cm_olm_get_session_id (session), error->message);
+
+  return success;
+}
+
+void
+cm_db_save_file_enc_async (CmDb                *self,
+                           CmEncFileInfo       *file,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (file && file->mxc_uri);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_save_file_enc_async);
+  g_task_set_task_data (task, cm_db_save_file_enc, NULL);
+  g_object_set_data (G_OBJECT (task), "file", file);
+
+  g_async_queue_push (self->queue, task);
+}
+
+gboolean
+cm_db_save_file_enc_finish (CmDb          *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+cm_db_find_file_enc_async (CmDb                *self,
+                           const char          *uri,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (uri && *uri);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_source_tag (task, cm_db_find_file_enc_async);
+  g_task_set_task_data (task, cm_db_find_file_enc, NULL);
+
+  g_object_set_data_full (G_OBJECT (task), "uri", g_strdup (uri), g_free);
+
+  g_async_queue_push (self->queue, task);
+}
+
+CmEncFileInfo *
+cm_db_find_file_enc_finish (CmDb          *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+gpointer
+cm_db_lookup_session (CmDb          *self,
+                      const char    *account_id,
+                      const char    *account_device,
+                      const char    *session_id,
+                      const char    *sender_key,
+                      const char    *pickle_key,
+                      const char    *room_id,
+                      CmSessionType  type)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GObject *object;
+  gpointer session;
+
+  g_return_val_if_fail (CM_IS_DB (self), NULL);
+  g_return_val_if_fail (account_id && *account_id, NULL);
+  g_return_val_if_fail (account_device && *account_device, NULL);
+  g_return_val_if_fail (sender_key && *sender_key, NULL);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+
+  g_task_set_source_tag (task, cm_db_lookup_session);
+  g_task_set_task_data (task, db_lookup_session, NULL);
+  object = G_OBJECT (task);
+
+  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
+  g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free);
+  g_object_set_data_full (object, "session-id", g_strdup (session_id), g_free);
+  g_object_set_data_full (object, "sender-key", g_strdup (sender_key), g_free);
+  g_object_set_data_full (object, "room-id", g_strdup (room_id), g_free);
+  g_object_set_data_full (object, "pickle-key", g_strdup (pickle_key),
+                          (GDestroyNotify)cm_utils_free_buffer);
+  g_object_set_data (object, "type", GINT_TO_POINTER (type));
+  g_async_queue_push (self->queue, task);
+  g_assert (task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  session = g_task_propagate_pointer (task, &error);
+
+  if (error)
+    g_debug ("Error getting session: %s", error->message);
+
+  return session;
+}
+
+gpointer
+cm_db_lookup_olm_session (CmDb           *self,
+                          const char     *account_id,
+                          const char     *account_device,
+                          const char     *sender_curve25519_key,
+                          const char     *body,
+                          const char     *pickle_key,
+                          CmSessionType   type,
+                          size_t          message_type,
+                          char          **out_plain_text)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GObject *object;
+  char *pickle;
+
+  g_return_val_if_fail (CM_IS_DB (self), NULL);
+  g_return_val_if_fail (account_id && *account_id, NULL);
+  g_return_val_if_fail (account_device && *account_device, NULL);
+  g_return_val_if_fail (sender_curve25519_key && *sender_curve25519_key, NULL);
+  g_return_val_if_fail (body && *body, NULL);
+  g_return_val_if_fail (pickle_key && *pickle_key, NULL);
+  g_return_val_if_fail (out_plain_text, NULL);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+
+  g_task_set_source_tag (task, cm_db_lookup_olm_session);
+  g_task_set_task_data (task, db_lookup_olm_session, NULL);
+  object = G_OBJECT (task);
+
+  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
+  g_object_set_data_full (object, "account-device", g_strdup (account_device), g_free);
+  g_object_set_data_full (object, "sender-key", g_strdup (sender_curve25519_key), g_free);
+  g_object_set_data_full (object, "body", g_strdup (body), g_free);
+  g_object_set_data_full (object, "pickle-key", g_strdup (pickle_key),
+                          (GDestroyNotify)cm_utils_free_buffer);
+  g_object_set_data (object, "type", GINT_TO_POINTER (type));
+  g_object_set_data (object, "message-type", GUINT_TO_POINTER (message_type));
+
+  /* Push to end as we may have to match items inserted immediately before */
+  g_async_queue_push (self->queue, task);
+  g_assert (task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  pickle = g_task_propagate_pointer (task, &error);
+  *out_plain_text = g_object_steal_data (G_OBJECT (task), "plaintext");
+
+  if (error)
+    g_debug ("Error getting session: %s", error->message);
+
+  return pickle;
+}
+
+void
+cm_db_mark_user_device_change (CmDb      *self,
+                               CmClient  *client,
+                               GPtrArray *users,
+                               gboolean   outdated,
+                               gboolean   is_tracking)
+{
+  const char *account_id, *device;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GObject *object;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+  g_return_if_fail (users && users->len);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+
+  g_task_set_source_tag (task, cm_db_mark_user_device_change);
+  g_task_set_task_data (task, db_mark_user_device_change, NULL);
+  account_id = cm_client_get_user_id (client);
+  device = cm_client_get_device_id (client);
+  object = G_OBJECT (task);
+
+  g_debug ("Saving user device change: %p", users);
+  g_object_set_data_full (object, "users", g_ptr_array_ref (users),
+                          (GDestroyNotify)g_ptr_array_unref);
+  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
+  g_object_set_data_full (object, "account-device", g_strdup (device), g_free);
+  g_object_set_data (object, "tracking", GINT_TO_POINTER (is_tracking));
+  g_object_set_data (object, "outdated", GINT_TO_POINTER (outdated));
+
+  g_async_queue_push (self->queue, task);
+  g_assert (task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  g_task_propagate_pointer (task, &error);
+  g_debug ("Saving user device change %s", CM_LOG_SUCCESS (!error));
+
+  if (error)
+    g_debug ("Error marking user device changed: %s", error->message);
+}
+
+void
+cm_db_update_user_devices (CmDb       *self,
+                           CmClient   *client,
+                           CmUser     *user,
+                           GPtrArray  *added,
+                           GPtrArray  *removed,
+                           gboolean    force_add)
+{
+  const char *account_id, *device;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GObject *object;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+  g_return_if_fail (CM_IS_USER (user));
+  g_return_if_fail ((added && added->len) || (removed && removed->len));
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+
+  g_task_set_source_tag (task, cm_db_update_user_devices);
+  g_task_set_task_data (task, db_update_user_devices, NULL);
+  account_id = cm_client_get_user_id (client);
+  device = cm_client_get_device_id (client);
+  object = G_OBJECT (task);
+
+  if (g_application_get_default ())
+    g_application_hold (g_application_get_default ());
+
+  if (added)
+    g_ptr_array_ref (added);
+
+  if (removed)
+    g_ptr_array_ref (removed);
+
+  g_debug ("Updating user devices. user %p, added: %u, removed: %u",
+           user, added ? added->len : 0, removed ? removed->len : 0);
+  g_object_set_data_full (object, "client", g_object_ref (client), g_object_unref);
+  g_object_set_data_full (object, "user", g_object_ref (user), g_object_unref);
+  g_object_set_data_full (object, "added", added, (GDestroyNotify)g_ptr_array_unref);
+  g_object_set_data_full (object, "removed", removed, (GDestroyNotify)g_ptr_array_unref);
+  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
+  g_object_set_data_full (object, "username", g_strdup (cm_user_get_id (user)), g_free);
+  g_object_set_data_full (object, "account-device", g_strdup (device), g_free);
+  g_object_set_data (object, "force-add", GINT_TO_POINTER (force_add));
+
+  g_async_queue_push (self->queue, task);
+  g_assert (task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  if (g_application_get_default ())
+    g_application_release (g_application_get_default ());
+
+  g_task_propagate_boolean (task, &error);
+
+  g_debug ("Updating user devices. user %p %s", user, CM_LOG_SUCCESS (!error));
+
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    g_debug ("Updating user devices. user %p skipped, we are not tracking the user", user);
+  else if (error)
+    g_warning ("Error updating user devices, user %p: %s", user, error->message);
+}
+
+void
+cm_db_update_device (CmDb     *self,
+                     CmClient *client,
+                     CmUser   *user,
+                     CmDevice *device)
+{
+  const char *account_id, *device_id;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GObject *object;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+  g_return_if_fail (CM_IS_DEVICE (device));
+  g_return_if_fail (CM_IS_USER (user));
+  g_return_if_fail (cm_device_get_user (device) == user);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+
+  g_task_set_source_tag (task, cm_db_update_user_device);
+  g_task_set_task_data (task, db_update_user_device, NULL);
+  account_id = cm_client_get_user_id (client);
+  device_id = cm_client_get_device_id (client);
+  object = G_OBJECT (task);
+
+  if (g_application_get_default ())
+    g_application_hold (g_application_get_default ());
+
+  g_debug ("Updating device. user %p, device: %p ", user, device);
+  g_object_set_data_full (object, "client", g_object_ref (client), g_object_unref);
+  g_object_set_data_full (object, "user", g_object_ref (user), g_object_unref);
+  g_object_set_data_full (object, "device", g_object_ref (device), g_object_unref);
+  g_object_set_data_full (object, "account-id", g_strdup (account_id), g_free);
+  g_object_set_data_full (object, "username", g_strdup (cm_user_get_id (user)), g_free);
+  g_object_set_data_full (object, "account-device", g_strdup (device_id), g_free);
+
+  g_async_queue_push (self->queue, task);
+  g_assert (task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  if (g_application_get_default ())
+    g_application_release (g_application_get_default ());
+
+  g_task_propagate_boolean (task, &error);
+
+  g_debug ("Updating user device. user: %p, device: %p, %s", user, device, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    g_warning ("Error updating user devices, user: %p, device: %p: %s", user, device, error->message);
+}
+
+void
+cm_db_add_room_members (CmDb      *self,
+                        CmRoom    *cm_room,
+                        GPtrArray *members)
+{
+  const char *username, *device, *room;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  CmClient *client;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_ROOM (cm_room));
+
+  if (!members || !members->len)
+    return;
+
+  if (g_application_get_default ())
+    g_application_hold (g_application_get_default ());
+
+  client = cm_room_get_client (cm_room);
+  username = cm_client_get_user_id (client);
+  device = cm_client_get_device_id (client);
+  room = cm_room_get_id (cm_room);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+  g_object_ref (cm_room);
+  g_ptr_array_ref (members);
+  g_task_set_task_data (task, db_add_room_members, NULL);
+  g_object_set_data_full (G_OBJECT (task), "members", members, (GDestroyNotify)g_ptr_array_unref);
+  g_object_set_data_full (G_OBJECT (task), "cm-room", cm_room, g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room), g_free);
+  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
+  g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device), g_free);
+
+  g_async_queue_push (self->queue, task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  if (g_application_get_default ())
+    g_application_release (g_application_get_default ());
+
+  g_task_propagate_boolean (task, &error);
+
+  if (error)
+    g_warning ("Error saving room members: %s", error->message);
+}
+
+/*
+ * cm_db_add_room_events:
+ * @prepend: Whether the events should be
+ * added before or after already saved
+ * events. Set %TRUE to add before. %FALSE
+ * otherwise.
+ */
+void
+cm_db_add_room_events (CmDb      *self,
+                       CmRoom    *cm_room,
+                       GPtrArray *events,
+                       gboolean   prepend)
+{
+  const char *username, *device, *room;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  CmClient *client;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_ROOM (cm_room));
+
+  if (!events || !events->len)
+    return;
+
+  if (g_application_get_default ())
+    g_application_hold (g_application_get_default ());
+
+  client = cm_room_get_client (cm_room);
+  username = cm_client_get_user_id (client);
+  device = cm_client_get_device_id (client);
+  room = cm_room_get_id (cm_room);
+
+  task = g_task_new (self, NULL, NULL, NULL);
+  g_object_ref (task);
+  g_object_ref (cm_room);
+  g_ptr_array_ref (events);
+  g_task_set_task_data (task, db_add_room_events, NULL);
+  g_object_set_data_full (G_OBJECT (task), "events", events, (GDestroyNotify)g_ptr_array_unref);
+  g_object_set_data_full (G_OBJECT (task), "cm-room", cm_room, g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room), g_free);
+  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
+  g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device), g_free);
+  g_object_set_data (G_OBJECT (task), "prepend", GINT_TO_POINTER (!!prepend));
+
+  g_async_queue_push (self->queue, task);
+
+  /* Wait until the task is completed */
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  if (g_application_get_default ())
+    g_application_release (g_application_get_default ());
+
+  g_task_propagate_boolean (task, &error);
+
+  if (error)
+    g_debug ("Error getting session: %s", error->message);
+}
+
+void
+cm_db_get_past_events_async (CmDb                *self,
+                             CmRoom              *room,
+                             CmEvent             *from,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  const char *room_name, *username, *device, *event = NULL;
+  CmClient *client;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_DB (self));
+  g_return_if_fail (CM_IS_ROOM (room));
+  g_return_if_fail (!from || CM_IS_EVENT (from));
+
+  g_object_ref (room);
+  if (from)
+    {
+      g_object_ref (from);
+      event = cm_event_get_id (from);
+    }
+
+  client = cm_room_get_client (room);
+  room_name = cm_room_get_id (room);
+  username = cm_client_get_user_id (client);
+  device = cm_client_get_device_id (client);
+  g_return_if_fail (device && *device);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_object_set_data_full (G_OBJECT (task), "cm-room", room, g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "cm-event", from, g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "room", g_strdup (room_name), g_free);
+  g_object_set_data_full (G_OBJECT (task), "event", g_strdup (event), g_free);
+  g_object_set_data_full (G_OBJECT (task), "username", g_strdup (username), g_free);
+  g_object_set_data_full (G_OBJECT (task), "device", g_strdup (device), g_free);
+  g_task_set_source_tag (task, cm_db_get_past_events_async);
+  g_task_set_task_data (task, db_get_past_events, NULL);
+
+  g_async_queue_push (self->queue, task);
+}
+
+GPtrArray *
+cm_db_get_past_events_finish (CmDb          *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (CM_IS_DB (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/cm-device-private.h b/subprojects/libcmatrix/src/cm-device-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..3c6f7c5330a6ec41abed2f94030d84c1969cb92c
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-device-private.h
@@ -0,0 +1,32 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+#include "cm-types.h"
+#include "cm-device.h"
+
+G_BEGIN_DECLS
+
+CmDevice   *cm_device_new              (CmUser     *user,
+                                        CmClient   *client,
+                                        JsonObject *root);
+void        cm_device_set_verified     (CmDevice   *self,
+                                        gboolean    verified);
+gboolean    cm_device_is_verified      (CmDevice   *self);
+CmUser     *cm_device_get_user         (CmDevice   *self);
+JsonObject *cm_device_get_json         (CmDevice   *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-device.c b/subprojects/libcmatrix/src/cm-device.c
new file mode 100644
index 0000000000000000000000000000000000000000..c8df30afff0ed58a215814199f3082de27b836bd
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-device.c
@@ -0,0 +1,192 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-device"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-client.h"
+#include "cm-client-private.h"
+#include "cm-utils-private.h"
+#include "cm-enc-private.h"
+#include "cm-device-private.h"
+#include "cm-device.h"
+
+struct _CmDevice
+{
+  GObject   parent_instance;
+
+  CmClient *client;
+  JsonObject *json;
+  CmUser     *user;
+  char     *device_id;
+  char     *device_name;
+  char     *ed_key;
+  char     *curve_key;
+
+  gboolean meagolm_v1;
+  gboolean olm_v1;
+  gboolean signature_failed;
+  gboolean verified;
+};
+
+G_DEFINE_TYPE (CmDevice, cm_device, G_TYPE_OBJECT)
+
+static void
+cm_device_finalize (GObject *object)
+{
+  CmDevice *self = (CmDevice *)object;
+
+  g_clear_object (&self->client);
+  g_free (self->device_id);
+  g_free (self->device_name);
+  g_free (self->ed_key);
+  g_free (self->curve_key);
+  g_clear_pointer (&self->json, json_object_unref);
+
+  G_OBJECT_CLASS (cm_device_parent_class)->finalize (object);
+}
+
+static void
+cm_device_class_init (CmDeviceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_device_finalize;
+}
+
+static void
+cm_device_init (CmDevice *self)
+{
+}
+
+CmDevice *
+cm_device_new (CmUser     *user,
+               CmClient   *client,
+               JsonObject *root)
+{
+  JsonObject *object;
+  JsonArray *array;
+  CmDevice *self;
+  const char *text;
+  char *key_name;
+
+  g_return_val_if_fail (CM_IS_USER (user), NULL);
+  g_return_val_if_fail (CM_IS_CLIENT (client), NULL);
+  g_return_val_if_fail (root, NULL);
+
+  if (g_strcmp0 (cm_user_get_id (user),
+                 cm_utils_json_object_get_string (root, "user_id")) != 0)
+    g_return_val_if_reached (NULL);
+
+  self = g_object_new (CM_TYPE_DEVICE, NULL);
+  self->json = json_object_ref (root);
+  g_set_weak_pointer (&self->user, user);
+  self->client = g_object_ref (client);
+
+  text = cm_utils_json_object_get_string (root, "device_id");
+  self->device_id = g_strdup (text);
+  g_return_val_if_fail (text && *text, NULL);
+
+  object = cm_utils_json_object_get_object (root, "unsigned");
+  text = cm_utils_json_object_get_string (object, "device_display_name");
+  self->device_name = g_strdup (text);
+
+  key_name = g_strconcat ("ed25519:", self->device_id, NULL);
+  object = cm_utils_json_object_get_object (root, "keys");
+  text = cm_utils_json_object_get_string (object, key_name);
+  self->ed_key = g_strdup (text);
+  g_free (key_name);
+
+  if (!cm_enc_verify (cm_client_get_enc (self->client), root,
+                      cm_user_get_id (user),
+                      self->device_id, self->ed_key))
+    {
+      /* DEBUG */
+      g_warning ("Signature failed");
+      self->signature_failed = TRUE;
+      return self;
+    }
+
+  key_name = g_strconcat ("curve25519:", self->device_id, NULL);
+  object = cm_utils_json_object_get_object (root, "keys");
+  text = cm_utils_json_object_get_string (object, key_name);
+  self->curve_key = g_strdup (text);
+  g_free (key_name);
+
+  array = cm_utils_json_object_get_array (root, "algorithms");
+  for (guint i = 0; array && i < json_array_get_length (array); i++) {
+    const char *algorithm;
+
+    algorithm = json_array_get_string_element (array, i);
+    if (g_strcmp0 (algorithm, ALGORITHM_MEGOLM) == 0)
+      self->meagolm_v1 = TRUE;
+    else if (g_strcmp0 (algorithm, ALGORITHM_OLM) == 0)
+      self->olm_v1 = TRUE;
+  }
+
+  return self;
+}
+
+void
+cm_device_set_verified (CmDevice *self,
+                        gboolean  verified)
+{
+  g_return_if_fail (CM_IS_DEVICE (self));
+
+  self->verified = !!verified;
+}
+
+gboolean
+cm_device_is_verified (CmDevice *self)
+{
+  g_return_val_if_fail (CM_IS_DEVICE (self), FALSE);
+
+  return !self->signature_failed && self->verified;
+}
+
+JsonObject *
+cm_device_get_json (CmDevice *self)
+{
+  g_return_val_if_fail (CM_IS_DEVICE (self), NULL);
+
+  return self->json;
+}
+
+CmUser *
+cm_device_get_user (CmDevice *self)
+{
+  g_return_val_if_fail (CM_IS_DEVICE (self), NULL);
+
+  return self->user;
+}
+
+const char *
+cm_device_get_id (CmDevice *self)
+{
+  g_return_val_if_fail (CM_IS_DEVICE (self), NULL);
+
+  return self->device_id;
+}
+
+const char *
+cm_device_get_ed_key (CmDevice *self)
+{
+  g_return_val_if_fail (CM_IS_DEVICE (self), NULL);
+
+  return self->ed_key;
+}
+
+const char *
+cm_device_get_curve_key (CmDevice *self)
+{
+  g_return_val_if_fail (CM_IS_DEVICE (self), NULL);
+
+  return self->curve_key;
+}
diff --git a/subprojects/libcmatrix/src/cm-device.h b/subprojects/libcmatrix/src/cm-device.h
new file mode 100644
index 0000000000000000000000000000000000000000..488cf16791de72c00a5454c16a07a39103d05065
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-device.h
@@ -0,0 +1,27 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_DEVICE (cm_device_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmDevice, cm_device, CM, DEVICE, GObject)
+
+const char   *cm_device_get_id                  (CmDevice *self);
+const char   *cm_device_get_ed_key              (CmDevice *self);
+const char   *cm_device_get_curve_key           (CmDevice *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-enc-private.h b/subprojects/libcmatrix/src/cm-enc-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..9cb054a4f9ba270ed0fc538f0852cdb6bc3df2c1
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-enc-private.h
@@ -0,0 +1,113 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-enc.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+#include <json-glib/json-glib.h>
+#include <glib-object.h>
+
+#include "events/cm-event.h"
+#include "cm-types.h"
+
+G_BEGIN_DECLS
+
+typedef struct _CmEncFileInfo CmEncFileInfo;
+typedef void * cm_gcry_t;
+
+
+struct _CmEncFileInfo {
+  char *mxc_uri;
+
+  char *aes_iv_base64;
+  char *aes_key_base64;
+  char *sha256_base64;
+
+  char *algorithm;
+  char *version;
+  char *kty;
+
+  gboolean extractable;
+};
+
+#define ALGORITHM_MEGOLM  "m.megolm.v1.aes-sha2"
+#define ALGORITHM_OLM     "m.olm.v1.curve25519-aes-sha2"
+#define CURVE25519_SIZE   43    /* when base64 encoded */
+#define ED25519_SIZE      43    /* when base64 encoded */
+
+#define CM_TYPE_ENC (cm_enc_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmEnc, cm_enc, CM, ENC, GObject)
+
+CmEnc          *cm_enc_new                       (gpointer             matrix_db,
+                                                  const char          *pickle,
+                                                  const char          *key);
+gpointer       cm_enc_get_sas_for_event          (CmEnc               *self,
+                                                  CmEvent             *event);
+void           cm_enc_set_details                (CmEnc               *self,
+                                                  GRefString          *user_id,
+                                                  const char          *device_id);
+char          *cm_enc_get_pickle                 (CmEnc               *self);
+char          *cm_enc_get_pickle_key             (CmEnc               *self);
+char          *cm_enc_sign_string                (CmEnc               *self,
+                                                  const char          *str,
+                                                  size_t               len);
+gboolean       cm_enc_verify                     (CmEnc               *self,
+                                                  JsonObject          *object,
+                                                  const char          *matrix_id,
+                                                  const char          *device_id,
+                                                  const char          *ed_key);
+size_t         cm_enc_max_one_time_keys          (CmEnc               *self);
+size_t         cm_enc_create_one_time_keys       (CmEnc               *self,
+                                                  size_t               count);
+void           cm_enc_publish_one_time_keys      (CmEnc               *self);
+JsonObject    *cm_enc_get_one_time_keys          (CmEnc               *self);
+char          *cm_enc_get_one_time_keys_json     (CmEnc               *self);
+char          *cm_enc_get_device_keys_json       (CmEnc               *self);
+void           cm_enc_handle_room_encrypted      (CmEnc               *self,
+                                                  JsonObject          *object);
+char          *cm_enc_handle_join_room_encrypted (CmEnc               *self,
+                                                  CmRoom              *room,
+                                                  JsonObject          *object);
+JsonObject    *cm_enc_encrypt_for_chat           (CmEnc               *self,
+                                                  CmRoom              *room,
+                                                  const char          *message);
+JsonObject    *cm_enc_create_out_group_keys      (CmEnc               *self,
+                                                  CmRoom              *room,
+                                                  GPtrArray           *one_time_keys,
+                                                  gpointer            *out_session);
+gboolean       cm_enc_has_room_group_key         (CmEnc               *self,
+                                                  CmRoom              *room);
+void           cm_enc_set_room_group_key         (CmEnc               *self,
+                                                  CmRoom              *room,
+                                                  gpointer             out_session);
+void           cm_enc_rm_room_group_key          (CmEnc               *self,
+                                                  CmRoom              *room);
+void           cm_enc_find_file_enc_async        (CmEnc               *self,
+                                                  const char          *uri,
+                                                  GAsyncReadyCallback  callback,
+                                                  gpointer             user_data);
+CmEncFileInfo *cm_enc_find_file_enc_finish       (CmEnc               *self,
+                                                  GAsyncResult        *result,
+                                                  GError             **error);
+void           cm_enc_file_info_free             (gpointer             data);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (CmEncFileInfo, cm_enc_file_info_free)
+/* G_DEFINE_AUTOPTR_CLEANUP_FUNC (cm_gcry_t, gcry_free) */
+
+/* For tests */
+GRefString    *cm_enc_get_user_id            (CmEnc    *self);
+const char    *cm_enc_get_device_id          (CmEnc    *self);
+const char    *cm_enc_get_curve25519_key     (CmEnc    *self);
+const char    *cm_enc_get_ed25519_key        (CmEnc    *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-enc.c b/subprojects/libcmatrix/src/cm-enc.c
new file mode 100644
index 0000000000000000000000000000000000000000..e7e7b5986d920004c933110b74025b7a2c704141
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-enc.c
@@ -0,0 +1,1560 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-enc.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-enc"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <json-glib/json-glib.h>
+#include <olm/olm.h>
+#include <sys/random.h>
+
+#include "cm-utils-private.h"
+#include "users/cm-user-private.h"
+#include "users/cm-user-list-private.h"
+#include "users/cm-room-member-private.h"
+#include "cm-room-private.h"
+#include "cm-device.h"
+#include "cm-device-private.h"
+#include "cm-db-private.h"
+#include "cm-olm-private.h"
+#include "cm-olm-sas-private.h"
+#include "cm-enc-private.h"
+
+#define KEY_LABEL_SIZE    6
+#define STRING_ALLOCATION 512
+
+/*
+ * SECTION: cm-enc
+ * @title: CmEnc
+ * @short_description: An abstraction for E2EE
+ * @include: "cm-enc.h"
+ */
+
+/*
+ * Documentations:
+ *   https://matrix.org/docs/guides/end-to-end-encryption-implementation-guide
+ *
+ * Other:
+ *  * We use g_malloc(size) instead of g_malloc(size * sizeof(type)) for all ‘char’
+ *    and ‘[u]int8_t’, unless there is a possibility that the type can change.
+ */
+struct _CmEnc
+{
+  GObject parent_instance;
+
+  CmDb       *cm_db;
+
+  OlmAccount *account;
+  OlmUtility *utility;
+  char       *pickle_key;
+
+  /* FIXME: is there a way to limit hashtable entries to-say-1000,
+   * and when new items are added older ones are deleted?
+   * Or any other data structure with fast lookup?
+   */
+  GHashTable *enc_files;
+  GHashTable *in_olm_sessions;
+  GHashTable *out_olm_sessions;
+  GHashTable *in_group_sessions;
+  GHashTable *out_group_sessions;
+  GHashTable *out_group_room_session;
+
+  GRefString *user_id;
+  char *device_id;
+
+  char *curve_key; /* Public part of Curve25519 identity key */
+  char *ed_key;    /* Public part of Ed25519 fingerprint key */
+};
+
+G_DEFINE_TYPE (CmEnc, cm_enc, G_TYPE_OBJECT)
+
+static void
+free_all_details (CmEnc *self)
+{
+  if (self->account)
+    olm_clear_account (self->account);
+
+  g_clear_pointer (&self->account, g_free);
+  g_hash_table_remove_all (self->in_olm_sessions);
+  g_hash_table_remove_all (self->out_olm_sessions);
+  g_hash_table_remove_all (self->in_group_sessions);
+  g_hash_table_remove_all (self->out_group_sessions);
+  g_hash_table_remove_all (self->out_group_room_session);
+}
+
+static CmOlm *
+ma_enc_lookup_out_group_session (CmEnc       *self,
+                                 CmRoom      *room,
+                                 const char **out_session_id)
+{
+  const char *session_id;
+
+  g_assert (CM_IS_ENC (self));
+  g_assert (CM_IS_ROOM (room));
+
+  session_id = g_hash_table_lookup (self->out_group_room_session, room);
+  if (!session_id)
+    return NULL;
+
+  if (out_session_id)
+    *out_session_id = session_id;
+
+  return g_hash_table_lookup (self->out_group_sessions, session_id);
+}
+
+static CmOlm *
+ma_create_olm_out_session (CmEnc      *self,
+                           const char *curve_key,
+                           const char *one_time_key,
+                           const char *room_id)
+{
+  CmOlm *session;
+
+  g_assert (CM_ENC (self));
+
+  session = cm_olm_outbound_new (self->account, curve_key, one_time_key, room_id);
+
+  if (!session)
+    return NULL;
+
+  cm_olm_set_db (session, self->cm_db);
+  cm_olm_set_key (session, self->pickle_key);
+  cm_olm_set_sender_details (session, room_id, self->user_id);
+  cm_olm_set_account_details (session, self->user_id, self->device_id);
+  cm_olm_save (session);
+
+  return session;
+}
+
+/*
+ * cm_enc_load_identity_keys:
+ * @self: A #CmEnc
+ *
+ * Load the public part of Ed25519 fingerprint
+ * key pair and Curve25519 identity key pair.
+ */
+static gboolean
+cm_enc_load_identity_keys (CmEnc *self)
+{
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *key = NULL;
+  JsonObject *object;
+  JsonNode *node;
+  size_t length, err;
+
+  length = olm_account_identity_keys_length (self->account);
+  key = malloc (length + 1);
+  err = olm_account_identity_keys (self->account, key, length);
+  key[length] = '\0';
+
+  if (err == olm_error ())
+    {
+      g_warning ("error getting identity keys: %s", olm_account_last_error (self->account));
+      return FALSE;
+    }
+
+  parser = json_parser_new ();
+  json_parser_load_from_data (parser, key, length, &error);
+
+  if (error)
+    {
+      g_warning ("error parsing keys: %s", error->message);
+      return FALSE;
+    }
+
+  node = json_parser_get_root (parser);
+  object = json_node_get_object (node);
+
+  g_free (self->curve_key);
+  g_free (self->ed_key);
+
+  self->curve_key = g_strdup (json_object_get_string_member (object, "curve25519"));
+  self->ed_key = g_strdup (json_object_get_string_member (object, "ed25519"));
+
+  return TRUE;
+}
+
+static void
+create_new_details (CmEnc *self)
+{
+  char *pickle_key;
+  cm_gcry_t buffer;
+  size_t length, err;
+
+  g_assert (CM_ENC (self));
+
+  g_debug ("(%p) Creating new encryption keys", self);
+
+  free_all_details (self);
+
+  self->account = g_malloc (olm_account_size ());
+  olm_account (self->account);
+
+  gcry_free (self->pickle_key);
+  buffer = gcry_random_bytes_secure (64, GCRY_STRONG_RANDOM);
+  pickle_key = g_base64_encode (buffer, 64);
+  gcry_free (buffer);
+
+  /* Copy and free pickle_key as it's not an mlock() memory */
+  self->pickle_key = gcry_malloc_secure (strlen (pickle_key) + 1);
+  strcpy (self->pickle_key, pickle_key);
+  cm_utils_free_buffer (pickle_key);
+
+  length = olm_create_account_random_length (self->account);
+  if (length)
+    buffer = gcry_random_bytes (length, GCRY_STRONG_RANDOM);
+  err = olm_create_account (self->account, buffer, length);
+  gcry_free (buffer);
+  if (err == olm_error ())
+    g_warning ("Error creating account: %s", olm_account_last_error (self->account));
+}
+
+static void
+cm_enc_sign_json_object (CmEnc      *self,
+                         JsonObject *object)
+{
+  g_autoptr(GString) str = NULL;
+  g_autofree char *signature = NULL;
+  g_autofree char *label = NULL;
+  JsonObject *sign, *child;
+
+  g_assert (CM_IS_ENC (self));
+  g_assert (object);
+
+  /* The JSON is in canonical form.  Required for signing */
+  /* https://matrix.org/docs/spec/appendices#signing-json */
+  str = cm_utils_json_get_canonical (object, NULL);
+  signature = cm_enc_sign_string (self, str->str, str->len);
+
+  sign = json_object_new ();
+  label = g_strconcat ("ed25519:", self->device_id, NULL);
+  json_object_set_string_member (sign, label, signature);
+
+  child = json_object_new ();
+  json_object_set_object_member (child, self->user_id, sign);
+  json_object_set_object_member (object, "signatures", child);
+}
+
+static void
+cm_enc_finalize (GObject *object)
+{
+  CmEnc *self = (CmEnc *)object;
+
+  olm_clear_account (self->account);
+  g_free (self->account);
+
+  olm_clear_utility (self->utility);
+  g_free (self->utility);
+
+  g_hash_table_unref (self->enc_files);
+  g_hash_table_unref (self->in_olm_sessions);
+  g_hash_table_unref (self->out_olm_sessions);
+  g_hash_table_unref (self->in_group_sessions);
+  g_hash_table_unref (self->out_group_sessions);
+  g_hash_table_unref (self->out_group_room_session);
+
+  g_clear_pointer (&self->user_id, g_ref_string_release);
+  g_free (self->device_id);
+  gcry_free (self->pickle_key);
+  cm_utils_free_buffer (self->curve_key);
+  cm_utils_free_buffer (self->ed_key);
+  g_clear_object (&self->cm_db);
+
+  G_OBJECT_CLASS (cm_enc_parent_class)->finalize (object);
+}
+
+
+static void
+cm_enc_class_init (CmEncClass *klass)
+{
+  GObjectClass *object_class  = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_enc_finalize;
+}
+
+
+static void
+cm_enc_init (CmEnc *self)
+{
+  self->utility = g_malloc (olm_utility_size ());
+  olm_utility (self->utility);
+
+  self->enc_files = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                           g_free, cm_enc_file_info_free);
+  /* We use hashtable of hashtables, each value of hashtable indexed with
+     sender's curve25519 key */
+  /* in_olm_sessions = g_hash_table_new (g_str_hash, g_str_equal, */
+  /*                                     g_free, free_olm_session); */
+  self->in_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                                 (GDestroyNotify)g_hash_table_unref);
+  self->out_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                  g_free, g_object_unref);
+  self->in_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                   g_free, g_object_unref);
+  self->out_group_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                    g_free, g_object_unref);
+  self->out_group_room_session = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+                                                        g_object_unref, g_free);
+}
+
+/**
+ * cm_enc_new:
+ * @pickle: (nullable): The account pickle
+ * @key: @pickle key, can be %NULL if @pickle is %NULL
+ *
+ * If @pickle is non-null, the olm account is created
+ * using the pickled data.  Otherwise a new olm account
+ * is created. If @pickle is non-null and invalid
+ * %NULL is returned.
+ *
+ * For @self to be ready for use, the details of @self
+ * should be set with cm_enc_set_details().
+ *
+ * Also see cm_enc_get_pickle().
+ *
+ * Returns: (transfer full) (nullable): A new #CmEnc.
+ * Free with g_object_unref()
+ */
+CmEnc *
+cm_enc_new (gpointer    matrix_db,
+            const char *pickle,
+            const char *key)
+{
+  g_autoptr(CmEnc) self = NULL;
+
+  g_return_val_if_fail (!pickle || (*pickle && key && *key), NULL);
+
+  self = g_object_new (CM_TYPE_ENC, NULL);
+  g_set_object (&self->cm_db, matrix_db);
+
+  /* Deserialize the pickle to create the account */
+  if (pickle && *pickle)
+    {
+      g_autofree char *duped = NULL;
+      size_t err;
+
+      g_debug ("(%p) Create from pickle", self);
+      self->pickle_key = gcry_malloc_secure (strlen (pickle) + 1);
+      strcpy (self->pickle_key, key);
+      self->account = g_malloc (olm_account_size ());
+      olm_account (self->account);
+
+      duped = g_strdup (pickle);
+      err = olm_unpickle_account (self->account, key, strlen (key),
+                                  duped, strlen (duped));
+
+      if (err == olm_error ())
+        {
+          g_warning ("Error account unpickle: %s", olm_account_last_error (self->account));
+          return NULL;
+        }
+    }
+  else
+    {
+      create_new_details (self);
+    }
+
+  if (!cm_enc_load_identity_keys (self))
+    return NULL;
+
+  return g_steal_pointer (&self);
+}
+
+gpointer
+cm_enc_get_sas_for_event (CmEnc   *self,
+                          CmEvent *event)
+{
+  CmOlmSas *sas;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+  g_return_val_if_fail (CM_IS_EVENT (event), NULL);
+
+  sas = g_object_get_data (G_OBJECT (event), "olm-sas");
+
+  if (sas)
+    return sas;
+
+  sas = cm_olm_sas_new ();
+  cm_olm_sas_set_key_verification (sas, event);
+  g_object_set_data_full (G_OBJECT (event), "olm-sas", sas, g_object_unref);
+
+  return sas;
+}
+
+/**
+ * cm_enc_set_details:
+ * @self: A #CmEnc
+ * @user_id: (nullable): Fully qualified Matrix user ID
+ * @device_id: (nullable): The device id string
+ *
+ * Set user id and device id of @self.  @user_id
+ * should be fully qualified Matrix user ID
+ * (ie, @user:example.com)
+ */
+void
+cm_enc_set_details (CmEnc      *self,
+                    GRefString *user_id,
+                    const char *device_id)
+{
+  g_autoptr(GRefString) old_user = NULL;
+  g_autofree char *old_device = NULL;
+
+  g_return_if_fail (CM_IS_ENC (self));
+  g_return_if_fail (!user_id || *user_id == '@');
+
+  old_user = g_steal_pointer (&self->user_id);
+  old_device = self->device_id;
+
+  if (user_id)
+    self->user_id = g_ref_string_acquire (user_id);
+  self->device_id = g_strdup (device_id);
+
+  if (self->user_id && old_device &&
+      g_strcmp0 (device_id, old_device) == 0)
+    {
+      create_new_details (self);
+      cm_enc_load_identity_keys (self);
+    }
+}
+
+char *
+cm_enc_get_pickle (CmEnc *self)
+{
+  g_autofree char *pickle = NULL;
+  size_t length, err;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  length = olm_pickle_account_length (self->account);
+  pickle = malloc (length + 1);
+  err = olm_pickle_account (self->account, self->pickle_key,
+                            strlen (self->pickle_key), pickle, length);
+  pickle[length] = '\0';
+
+  if (err == olm_error ())
+    {
+      g_warning ("Error getting account pickle: %s", olm_account_last_error (self->account));
+
+      return NULL;
+    }
+
+  return g_steal_pointer (&pickle);
+}
+
+char *
+cm_enc_get_pickle_key (CmEnc *self)
+{
+  g_return_val_if_fail (CM_ENC (self), NULL);
+
+  return g_strdup (self->pickle_key);
+}
+
+/**
+ * cm_enc_sign_string:
+ * @self: A #CmEnc
+ * @str: A string to sign
+ * @len: The length of @str, or -1
+ *
+ * Sign @str and return the signature.
+ * Returns %NULL on error.
+ *
+ * Returns: (transfer full): The signature string.
+ * Free with g_free()
+ */
+char *
+cm_enc_sign_string (CmEnc  *self,
+                    const char *str,
+                    size_t      len)
+{
+  char *signature;
+  size_t length, err;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+  g_return_val_if_fail (str, NULL);
+  g_return_val_if_fail (*str, NULL);
+
+  if (len == (size_t) -1)
+    len = strlen (str);
+
+  length = olm_account_signature_length (self->account);
+  signature = malloc (length + 1);
+  err = olm_account_sign (self->account, str, len, signature, length);
+  signature[length] = '\0';
+
+  if (err == olm_error ())
+    {
+      g_warning ("Error signing data: %s", olm_account_last_error (self->account));
+
+      return NULL;
+    }
+
+  return signature;
+}
+
+/**
+ * cm_enc_verify:
+ * @self: A #CmEnc
+ * @object: A #JsonObject
+ * @matrix_id: A Fully qualified Matrix ID
+ * @device_id: The device id string.
+ * @ed_key: The ED25519 key of @matrix_id
+ *
+ * Verify if the content in @object is signed by
+ * the user @matrix_id with device @device_id.
+ *
+ * This function may modify @object by removing
+ * "signatures" and "unsigned" members.
+ *
+ * Returns; %TRUE if verification succeeded.  Or
+ * %FALSE otherwise.
+ */
+gboolean
+cm_enc_verify (CmEnc      *self,
+               JsonObject *object,
+               const char *matrix_id,
+               const char *device_id,
+               const char *ed_key)
+{
+  JsonNode *signatures, *non_signed;
+  g_autoptr(GString) json_str = NULL;
+  g_autofree char *signature = NULL;
+  g_autofree char *key_name = NULL;
+  JsonObject *child;
+  size_t error;
+
+  if (!object)
+    return FALSE;
+
+  g_return_val_if_fail (CM_IS_ENC (self), FALSE);
+  g_return_val_if_fail (matrix_id && *matrix_id == '@', FALSE);
+  g_return_val_if_fail (device_id && *device_id, FALSE);
+  g_return_val_if_fail (ed_key && *ed_key, FALSE);
+
+  /* https://matrix.org/docs/spec/appendices#checking-for-a-signature */
+  key_name = g_strconcat ("ed25519:", device_id, NULL);
+  child = cm_utils_json_object_get_object (object, "signatures");
+  child = cm_utils_json_object_get_object (child, matrix_id);
+  signature = g_strdup (cm_utils_json_object_get_string (child, key_name));
+
+  if (!signature)
+    return FALSE;
+
+  signatures = json_object_dup_member (object, "signatures");
+  non_signed = json_object_dup_member (object, "signatures");
+  /* Remove the non signed members before verification */
+  json_object_remove_member (object, "signatures");
+  json_object_remove_member (object, "unsigned");
+
+  json_str = cm_utils_json_get_canonical (object, NULL);
+
+  /* Revert the changes we made to the JSON object */
+  if (signatures)
+    json_object_set_member (object, "signatures", signatures);
+  if (non_signed)
+    json_object_set_member (object, "unsigned", non_signed);
+
+  error = olm_ed25519_verify (self->utility,
+                              ed_key, strlen (ed_key),
+                              json_str->str, json_str->len,
+                              signature, strlen (signature));
+
+  if (error == olm_error ())
+    {
+      g_debug ("Error verifying signature: %s", olm_utility_last_error (self->utility));
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+/**
+ * cm_enc_max_one_time_keys:
+ * @self: A #CmEnc
+ *
+ * Get the maximum number of one time keys Olm
+ * library can handle.
+ *
+ * Returns: The number of maximum one-time keys.
+ */
+size_t
+cm_enc_max_one_time_keys (CmEnc *self)
+{
+  g_return_val_if_fail (CM_IS_ENC (self), 0);
+
+  return olm_account_max_number_of_one_time_keys (self->account);
+}
+
+/**
+ * cm_enc_create_one_time_keys:
+ * @self: A #CmEnc
+ * @count: A non-zero number
+ *
+ * Generate @count number of curve25519 one time keys.
+ * @count is capped to the half of what Olm library
+ * can handle.
+ *
+ * Returns: The number of one-time keys generated.
+ * It will be <= @count.
+ */
+size_t
+cm_enc_create_one_time_keys (CmEnc  *self,
+                             size_t  count)
+{
+  cm_gcry_t buffer = NULL;
+  size_t length, err;
+
+  g_return_val_if_fail (CM_IS_ENC (self), 0);
+  g_return_val_if_fail (count, 0);
+
+  /* doc: The maximum number of active keys supported by libolm
+     is returned by olm_account_max_number_of_one_time_keys.
+     The client should try to maintain about half this number on the homeserver. */
+  count = MIN (count, olm_account_max_number_of_one_time_keys (self->account) / 2);
+
+  length = olm_account_generate_one_time_keys_random_length (self->account, count);
+  if (length)
+    buffer = gcry_random_bytes (length, GCRY_STRONG_RANDOM);
+  err = olm_account_generate_one_time_keys (self->account, count, buffer, length);
+
+  if (err == olm_error ())
+    {
+      g_warning ("Error creating one time keys: %s", olm_account_last_error (self->account));
+
+      return 0;
+    }
+
+  return count;
+}
+
+/**
+ * cm_enc_publish_one_time_keys:
+ * @self: A #CmEnc
+ *
+ * Mark current set of one-time keys as published,
+ * So that they won't be returned again when requested
+ * with cm_enc_get_one_time_keys() or so.
+ */
+void
+cm_enc_publish_one_time_keys (CmEnc *self)
+{
+  g_return_if_fail (CM_IS_ENC (self));
+
+  olm_account_mark_keys_as_published (self->account);
+}
+
+/**
+ * cm_enc_get_one_time_keys:
+ * @self: A #CmEnc
+ *
+ * Get public part of unpublished Curve25519 one-time keys in @self.
+ *
+ * The returned data is a JSON-formatted object with the single
+ * property curve25519, which is itself an object mapping key id
+ * to base64-encoded Curve25519 key. For example:
+ *
+ * {
+ *     "curve25519": {
+ *         "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
+ *         "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
+ *     }
+ * }
+ *
+ * Returns: (nullable) (transfer full): The unpublished one time keys.
+ * Free with g_free()
+ */
+JsonObject *
+cm_enc_get_one_time_keys (CmEnc *self)
+{
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *buffer = NULL;
+  size_t length, err;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  length = olm_account_one_time_keys_length (self->account);
+  buffer = g_malloc (length + 1);
+  err = olm_account_one_time_keys (self->account, buffer, length);
+  buffer[length] = '\0';
+
+  if (err == olm_error ())
+    {
+      g_warning ("Error getting one time keys: %s", olm_account_last_error (self->account));
+
+      return NULL;
+    }
+
+  /* Return NULL if there are no keys */
+  if (g_str_equal (buffer, "{\"curve25519\":{}}"))
+    return NULL;
+
+  parser = json_parser_new ();
+  json_parser_load_from_data (parser, buffer, length, &error);
+
+  if (error)
+    {
+      g_warning ("error parsing keys: %s", error->message);
+      return NULL;
+    }
+
+  return json_node_dup_object (json_parser_get_root (parser));
+}
+
+/**
+ * cm_enc_get_one_time_keys_json:
+ * @self: A #CmEnc
+ *
+ * Get the signed Curve25519 one-time keys JSON.  The JSON shall
+ * be in the following format:
+ *
+ * {
+ *   "signed_curve25519:AAAAHg": {
+ *     "key": "zKbLg+NrIjpnagy+pIY6uPL4ZwEG2v+8F9lmgsnlZzs",
+ *     "signatures": {
+ *       "@alice:example.com": {
+ *         "ed25519:JLAFKJWSCS": "FLWxXqGbwrb8SM3Y795eB6OA8bwBcoMZFXBqnTn58AYWZSqiD45tlBVcDa2L7RwdKXebW/VzDlnfVJ+9jok1Bw"
+ *       }
+ *     }
+ *   },
+ *   "signed_curve25519:AAAAHQ": {
+ *     "key": "j3fR3HemM16M7CWhoI4Sk5ZsdmdfQHsKL1xuSft6MSw",
+ *     "signatures": {
+ *       "@alice:example.com": {
+ *         "ed25519:JLAFKJWSCS": "IQeCEPb9HFk217cU9kw9EOiusC6kMIkoIRnbnfOh5Oc63S1ghgyjShBGpu34blQomoalCyXWyhaaT3MrLZYQAA"
+ *       }
+ *     }
+ *   }
+ * }
+ *
+ * Returns: (transfer full): A JSON encoded string.
+ * Free with g_free()
+ */
+char *
+cm_enc_get_one_time_keys_json (CmEnc *self)
+{
+  g_autoptr(JsonObject) object = NULL;
+  g_autoptr(JsonObject) root = NULL;
+  g_autoptr(GList) members = NULL;
+  JsonObject *keys, *child;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  object = cm_enc_get_one_time_keys (self);
+
+  if (!object)
+    return NULL;
+
+  keys = json_object_new ();
+  object = json_object_get_object_member (object, "curve25519");
+  members = json_object_get_members (object);
+
+  for (GList *item = members; item; item = item->next)
+    {
+      g_autofree char *label = NULL;
+      const char *value;
+
+      child = json_object_new ();
+      value = json_object_get_string_member (object, item->data);
+      json_object_set_string_member (child, "key", value);
+      cm_enc_sign_json_object (self, child);
+
+      label = g_strconcat ("signed_curve25519:", item->data, NULL);
+      json_object_set_object_member (keys, label, child);
+    }
+
+  root = json_object_new ();
+  json_object_set_object_member (root, "one_time_keys", keys);
+
+  return cm_utils_json_object_to_string (root, FALSE);
+}
+
+/**
+ * cm_enc_get_device_keys_json:
+ * @self: A #CmEnc
+ *
+ * Get the signed device key JSON.  The JSON shall
+ * be in the following format:
+ *
+ * {
+ *   "user_id": "@alice:example.com",
+ *   "device_id": "JLAFKJWSCS",
+ *   "algorithms": [
+ *     "m.olm.curve25519-aes-sha256",
+ *     "m.megolm.v1.aes-sha2"
+ *   ],
+ *   "keys": {
+ *     "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
+ *     "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
+ *   },
+ *   "signatures": {
+ *     "@alice:example.com": {
+ *       "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
+ *     }
+ *   }
+ * }
+ *
+ * Returns: (nullable): A JSON encoded string.
+ * Free with g_free()
+ */
+char *
+cm_enc_get_device_keys_json (CmEnc *self)
+{
+  g_autoptr(JsonObject) root = NULL;
+  JsonObject *keys, *device_keys;
+  JsonArray *array;
+  char *label;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+  g_return_val_if_fail (self->user_id, NULL);
+  g_return_val_if_fail (self->device_id, NULL);
+
+  device_keys = json_object_new ();
+  json_object_set_string_member (device_keys, "user_id", self->user_id);
+  json_object_set_string_member (device_keys, "device_id", self->device_id);
+
+  array = json_array_new ();
+  json_array_add_string_element (array, ALGORITHM_OLM);
+  json_array_add_string_element (array, ALGORITHM_MEGOLM);
+  json_object_set_array_member (device_keys, "algorithms", array);
+
+  keys = json_object_new ();
+
+  label = g_strconcat ("curve25519:", self->device_id, NULL);
+  json_object_set_string_member (keys, label, self->curve_key);
+  g_free (label);
+
+  label = g_strconcat ("ed25519:", self->device_id, NULL);
+  json_object_set_string_member (keys, label, self->ed_key);
+  g_free (label);
+
+  json_object_set_object_member (device_keys, "keys", keys);
+  cm_enc_sign_json_object (self, device_keys);
+
+  root = json_object_new ();
+  json_object_set_object_member (root, "device_keys", device_keys);
+
+  return cm_utils_json_object_to_string (root, FALSE);
+}
+
+static gboolean
+in_olm_matches (gpointer key,
+                gpointer value,
+                gpointer user_data)
+{
+  g_autofree char *body = NULL;
+  size_t match;
+
+  body = g_strdup (user_data);
+  match = olm_matches_inbound_session (value, body, strlen (body));
+
+  if (match == olm_error ())
+    g_warning ("Error matching inbound session: %s", olm_session_last_error (key));
+
+  return match == 1;
+}
+
+static void
+handle_m_room_key (CmEnc      *self,
+                   JsonObject *root,
+                   const char *sender_key)
+{
+  CmOlm *session;
+  JsonObject *object;
+  const char *session_key, *session_id, *room_id;
+
+  g_assert (CM_IS_ENC (self));
+  g_assert (root);
+
+  object = cm_utils_json_object_get_object (root, "content");
+  session_key = cm_utils_json_object_get_string (object, "session_key");
+  session_id = cm_utils_json_object_get_string (object, "session_id");
+  room_id = cm_utils_json_object_get_string (object, "room_id");
+
+  /* The documentation recommends to look if the session already exists */
+  if (!session_key ||
+      g_hash_table_lookup (self->in_group_sessions, session_id))
+    return;
+
+  session = cm_olm_in_group_new (session_key, sender_key, session_id);
+  g_debug ("(%p) Create new in group olm session %p", self, session);
+  cm_olm_set_sender_details (session, room_id, self->user_id);
+  cm_olm_set_account_details (session, self->user_id, self->device_id);
+  cm_olm_set_key (session, self->pickle_key);
+  cm_olm_set_db (session, self->cm_db);
+  cm_olm_save (session);
+  g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), session);
+}
+
+void
+cm_enc_handle_room_encrypted (CmEnc      *self,
+                              JsonObject *object)
+{
+  g_autoptr(GRefString) sender = NULL;
+  const char *algorithm, *sender_key;
+  g_autofree char *plaintext = NULL;
+  g_autofree char *body = NULL;
+  CmOlm *session = NULL;
+  size_t type;
+  gboolean force_save = FALSE;
+
+  g_return_if_fail (CM_IS_ENC (self));
+  g_return_if_fail (object);
+
+  if (cm_utils_json_object_get_string (object, "sender"))
+    sender = g_ref_string_new_intern (cm_utils_json_object_get_string (object, "sender"));
+  object = cm_utils_json_object_get_object (object, "content");
+  algorithm = cm_utils_json_object_get_string (object, "algorithm");
+  /* sender_key is the Curve25519 identity key of the sender */
+  sender_key = cm_utils_json_object_get_string (object, "sender_key");
+
+  if (!algorithm || !sender_key || !sender)
+    g_return_if_reached ();
+
+  if (!g_str_equal (algorithm, ALGORITHM_MEGOLM) &&
+      !g_str_equal (algorithm, ALGORITHM_OLM))
+    g_return_if_reached ();
+
+  object = cm_utils_json_object_get_object (object, "ciphertext");
+  object = cm_utils_json_object_get_object (object, self->curve_key);
+
+  body = g_strdup (cm_utils_json_object_get_string (object, "body"));
+  type = (size_t)cm_utils_json_object_get_int (object, "type");
+
+  if (!body)
+    return;
+
+  if (self->cm_db)
+    session = cm_db_lookup_olm_session (self->cm_db, self->user_id, self->device_id,
+                                        sender_key, body, self->pickle_key,
+                                        SESSION_OLM_V1_IN, type, &plaintext);
+
+  if (!session && type == OLM_MESSAGE_TYPE_MESSAGE)
+    session = cm_db_lookup_olm_session (self->cm_db, self->user_id, self->device_id,
+                                        sender_key, body, self->pickle_key,
+                                        SESSION_OLM_V1_OUT, type, &plaintext);
+
+  if (!session && type == OLM_MESSAGE_TYPE_PRE_KEY)
+    {
+      GHashTable *in_olm_sessions;
+
+      in_olm_sessions = g_hash_table_lookup (self->in_olm_sessions, sender_key);
+      if (in_olm_sessions)
+        session = g_hash_table_find (in_olm_sessions, in_olm_matches, body);
+      g_debug ("(%p) Message with pre-key received, has session: %p", self, session);
+
+      if (!session)
+        {
+          session = cm_olm_inbound_new (self->account, sender_key, body);
+          g_debug ("(%p) New inbound session created %p", self, session);
+          cm_olm_set_db (session, self->cm_db);
+          cm_olm_set_key (session, self->pickle_key);
+
+          force_save = TRUE;
+        }
+    }
+
+  g_debug ("(%p) Handle decrypted, session: %p", self, session);
+
+  if (!session)
+    return;
+
+  if (!plaintext)
+    plaintext = cm_olm_decrypt (session, type, body);
+
+  {
+    g_autoptr(JsonObject) content = NULL;
+    JsonObject *data;
+    const char *message_type;
+
+    content = cm_utils_string_to_json_object (plaintext);
+    message_type = cm_utils_json_object_get_string (content, "type");
+
+    g_debug ("(%p) Message decrypted. type: %s", self, message_type);
+
+    if (g_strcmp0 (sender, cm_utils_json_object_get_string (content, "sender")) != 0)
+      {
+        g_warning ("(%p) Sender mismatch in encrypted content", self);
+        return;
+      }
+
+    /* The content is not meant for us */
+    if (g_strcmp0 (self->user_id, cm_utils_json_object_get_string (content, "recipient")) != 0)
+      return;
+
+    data = cm_utils_json_object_get_object (content, "recipient_keys");
+    if (g_strcmp0 (self->ed_key, cm_utils_json_object_get_string (data, "ed25519")) != 0)
+      {
+        g_warning ("(%p) ed25519 in content doesn't match to ours", self);
+        return;
+      }
+
+    if (force_save)
+      {
+        GHashTable *in_olm_sessions;
+        const char *id, *room_id;
+
+        data = cm_utils_json_object_get_object (content, "content");
+        room_id = cm_utils_json_object_get_string (data, "room_id");
+        in_olm_sessions = g_hash_table_lookup (self->in_olm_sessions, sender_key);
+
+        if (!in_olm_sessions)
+          {
+            in_olm_sessions = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                     g_free, g_object_unref);
+            g_hash_table_insert (self->in_olm_sessions, g_strdup (sender_key),
+                                 in_olm_sessions);
+          }
+
+        g_debug ("(%p) Save in olm session %p", self, session);
+
+        id = cm_olm_get_session_id (session);
+        g_hash_table_insert (in_olm_sessions, g_strdup (id), session);
+        cm_olm_set_sender_details (session, room_id, sender);
+        cm_olm_set_account_details (session, self->user_id, self->device_id);
+        cm_olm_save (session);
+      }
+
+    if (g_strcmp0 (message_type, "m.room_key") == 0)
+      handle_m_room_key (self, content, sender_key);
+  }
+}
+
+static CmEncFileInfo *
+cm_enc_get_json_file_enc_info (JsonObject *root)
+{
+  g_autoptr(CmEncFileInfo) file = NULL;
+  JsonObject *child;
+
+  if (!root)
+    return NULL;
+
+  file = g_new0 (CmEncFileInfo, 1);
+  file->mxc_uri = g_strdup (cm_utils_json_object_get_string (root, "url"));
+  file->version = g_strdup (cm_utils_json_object_get_string (root, "v"));
+  file->aes_iv_base64 = g_strdup (cm_utils_json_object_get_string (root, "iv"));
+
+  child = cm_utils_json_object_get_object (root, "hashes");
+  file->sha256_base64 = g_strdup (cm_utils_json_object_get_string (child, "sha256"));
+
+  child = cm_utils_json_object_get_object (root, "key");
+  file->algorithm = g_strdup (cm_utils_json_object_get_string (child, "alg"));
+  file->extractable = cm_utils_json_object_get_bool (child, "ext");
+  file->kty = g_strdup (cm_utils_json_object_get_string (child, "kty"));
+  file->aes_key_base64 = g_strdup (cm_utils_json_object_get_string (child, "k"));
+
+  if (file->mxc_uri && g_str_has_prefix (file->mxc_uri, "mxc://") &&
+      file->aes_key_base64)
+    return g_steal_pointer (&file);
+
+  return NULL;
+}
+
+static void
+cm_enc_save_file_enc (CmEnc      *self,
+                      const char *json_str)
+{
+  g_autoptr(JsonObject) root = NULL;
+  CmEncFileInfo *file_info;
+  JsonObject *child;
+
+  g_assert (CM_IS_ENC (self));
+
+  root = cm_utils_string_to_json_object (json_str);
+
+  if (!root)
+    return;
+
+  child = cm_utils_json_object_get_object (root, "content");
+  child = cm_utils_json_object_get_object (child, "file");
+  file_info = cm_enc_get_json_file_enc_info (child);
+
+  if (file_info && file_info->mxc_uri &&
+      !g_hash_table_contains (self->enc_files, file_info->mxc_uri))
+    {
+      g_debug ("(%p) Save file keys", self);
+      g_hash_table_insert (self->enc_files, g_strdup (file_info->mxc_uri), file_info);
+      cm_db_save_file_enc_async (self->cm_db, file_info, NULL, NULL);
+    }
+
+  /* todo: handle encrypted thumbnails */
+}
+
+char *
+cm_enc_handle_join_room_encrypted (CmEnc      *self,
+                                   CmRoom     *room,
+                                   JsonObject *object)
+{
+  CmOlm *session = NULL;
+  const char *sender_key;
+  const char *ciphertext, *session_id;
+  g_autofree char *plaintext = NULL;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+  g_return_val_if_fail (object, NULL);
+
+  sender_key = cm_utils_json_object_get_string (object, "sender_key");
+
+  ciphertext = cm_utils_json_object_get_string (object, "ciphertext");
+  session_id = cm_utils_json_object_get_string (object, "session_id");
+
+  /* the ciphertext can be absent, eg: in redacted events */
+  if (!ciphertext)
+    return NULL;
+
+  if (session_id)
+    session = g_hash_table_lookup (self->in_group_sessions, session_id);
+
+  g_debug ("(%p) Got room encrypted, room: %p. session: %p", self, room, session);
+
+  if (!session && self->cm_db)
+    {
+      session = cm_db_lookup_session (self->cm_db, self->user_id,
+                                      self->device_id, session_id,
+                                      sender_key, self->pickle_key,
+                                      cm_room_get_id (room),
+                                      SESSION_MEGOLM_V1_IN);
+
+      g_debug ("(%p) Got in group session %p from matrix db", self, session);
+
+      if (session)
+        g_hash_table_insert (self->in_group_sessions, g_strdup (session_id), session);
+    }
+
+  if (!session)
+    return NULL;
+
+  g_return_val_if_fail (session, NULL);
+
+  plaintext = cm_olm_decrypt (session, 0, ciphertext);
+
+  if (strstr (plaintext, "\"key_ops\""))
+    cm_enc_save_file_enc (self, plaintext);
+
+  return g_steal_pointer (&plaintext);
+}
+
+JsonObject *
+cm_enc_encrypt_for_chat (CmEnc      *self,
+                         CmRoom     *room,
+                         const char *message)
+{
+  CmOlm *session;
+  g_autofree char *encrypted = NULL;
+  const char *session_id;
+  JsonObject *root;
+
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+  g_return_val_if_fail (CM_IS_ROOM (room), NULL);
+  g_return_val_if_fail (message && *message, NULL);
+
+  session = ma_enc_lookup_out_group_session (self, room, NULL);
+  g_return_val_if_fail (session, NULL);
+
+  encrypted = cm_olm_encrypt (session, message);
+  g_debug ("(%p) Enrypt for room %p, session: %p, chain-index: %zu",
+           self, room, session,
+           cm_olm_get_message_index (session));
+
+  cm_olm_update_validity (session,
+                          cm_room_get_encryption_msg_count (room),
+                          cm_room_get_encryption_rotation_time (room));
+  cm_olm_save (session);
+  session_id = cm_olm_get_session_id (session);
+
+  root = json_object_new ();
+  json_object_set_string_member (root, "algorithm", ALGORITHM_MEGOLM);
+  json_object_set_string_member (root, "sender_key", self->curve_key);
+  json_object_set_string_member (root, "ciphertext", encrypted);
+  json_object_set_string_member (root, "session_id", session_id);
+  json_object_set_string_member (root, "device_id", self->device_id);
+
+  return root;
+}
+
+JsonObject *
+cm_enc_create_out_group_keys (CmEnc      *self,
+                              CmRoom     *room,
+                              GPtrArray  *one_time_keys,
+                              gpointer   *out_session)
+{
+  CmOlm *session = NULL;
+  const char *session_key, *session_id;
+  JsonObject *root, *child;
+
+  g_return_val_if_fail (CM_IS_ENC (self), FALSE);
+  g_return_val_if_fail (CM_IS_ROOM (room), FALSE);
+  g_return_val_if_fail (one_time_keys && one_time_keys->len, NULL);
+  g_return_val_if_fail (out_session, NULL);
+
+  session = ma_enc_lookup_out_group_session (self, room, NULL);
+
+  if (!session)
+    {
+      session = cm_olm_out_group_new (self->curve_key);
+      cm_olm_set_account_details (session, self->user_id, self->device_id);
+      cm_olm_set_sender_details (session, cm_room_get_id (room), self->user_id);
+      cm_olm_set_key (session, self->pickle_key);
+      cm_olm_set_db (session, self->cm_db);
+      g_debug ("(%p) Create out group keys, room: %p, session: %p", self, room, session);
+    }
+
+  if (!session)
+    g_return_val_if_reached (NULL);
+
+  session_id = cm_olm_get_session_id (session);
+  session_key = cm_olm_get_session_key (session);
+  *out_session = session;
+
+  root = json_object_new ();
+
+  /* https://matrix.org/docs/spec/client_server/r0.6.1#m-room-key */
+  for (guint i = 0; i < one_time_keys->len; i++)
+    {
+      CmUser *member;
+      const char *curve_key;
+      JsonObject *user;
+      CmUserKey *key;
+
+      key = one_time_keys->pdata[i];
+      member = key->user;
+
+      user = json_object_new ();
+      json_object_set_object_member (root, cm_user_get_id (member), user);
+
+      for (guint j = 0; j < key->devices->len; j++)
+        {
+          CmDevice *device;
+          CmOlm *olm_session = NULL;
+          char *one_time_key = NULL;
+          JsonObject *content;
+
+          device = key->devices->pdata[j];
+          curve_key = cm_device_get_curve_key (device);
+
+          one_time_key = key->keys->pdata[j];
+          olm_session = ma_create_olm_out_session (self, curve_key, one_time_key,
+                                                   cm_room_get_id (room));
+
+          if (!one_time_key || !curve_key || !olm_session)
+            continue;
+
+          /* xxx: Do we want to store only the keys */
+          /* g_hash_table_insert (self->out_olm_sessions, g_strdup (curve_key), olm_session); */
+
+          /* Create per device object */
+          child = json_object_new ();
+          json_object_set_object_member (user, cm_device_get_id (device), child);
+
+          json_object_set_string_member (child, "algorithm", ALGORITHM_OLM);
+          json_object_set_string_member (child, "sender_key", self->curve_key);
+          json_object_set_object_member (child, "ciphertext", json_object_new ());
+
+          content = json_object_new ();
+          child = json_object_get_object_member (child, "ciphertext");
+          g_assert (child);
+          json_object_set_object_member (child, curve_key, content);
+
+          /* Body to be encrypted */
+          {
+            g_autoptr(JsonObject) object = NULL;
+            g_autofree char *encrypted = NULL;
+            g_autofree char *data = NULL;
+
+            /* Create a json object with common data */
+            object = json_object_new ();
+            json_object_set_string_member (object, "type", "m.room_key");
+            json_object_set_string_member (object, "sender", self->user_id);
+            json_object_set_string_member (object, "sender_device", self->device_id);
+
+            child = json_object_new ();
+            json_object_set_string_member (child, "ed25519", self->ed_key);
+            json_object_set_object_member (object, "keys", child);
+
+            child = json_object_new ();
+            json_object_set_string_member (child, "algorithm", "m.megolm.v1.aes-sha2");
+            json_object_set_string_member (child, "room_id", cm_room_get_id (room));
+            json_object_set_string_member (child, "session_id", session_id);
+            json_object_set_string_member (child, "session_key", session_key);
+            json_object_set_int_member (child, "chain_index", cm_olm_get_message_index (session));
+            json_object_set_object_member (object, "content", child);
+
+            /* User specific data */
+            json_object_set_string_member (object, "recipient", cm_user_get_id (member));
+
+            /* Device specific data */
+            child = json_object_new ();
+            json_object_set_string_member (child, "ed25519", cm_device_get_ed_key (device));
+            json_object_set_object_member (object, "recipient_keys", child);
+
+            /* Now encrypt the above JSON */
+            data = cm_utils_json_object_to_string (object, FALSE);
+            encrypted = cm_olm_encrypt (olm_session, data);
+
+            /* Add the encrypted data as the content */
+            json_object_set_int_member (content, "type", cm_olm_get_message_type (olm_session));
+            json_object_set_string_member (content, "body", encrypted);
+          }
+        }
+    }
+
+  return root;
+}
+
+/**
+ * cm_enc_has_room_group_key:
+ * @self: A #CmEnc
+ * @room_id: A matrix room id
+ *
+ * Check if any valid outgoing session is present
+ * for the given room @room_id.  This should be
+ * checked before sending each message as @self
+ * may rotate the key after certain count or time
+ *
+ * Returns: %TRUE if a valid group session is
+ * present.  %FALSE otherwise.
+ */
+gboolean
+cm_enc_has_room_group_key (CmEnc  *self,
+                           CmRoom *room)
+{
+  CmOlm *session;
+  const char *session_id = NULL;
+
+  g_return_val_if_fail (CM_IS_ENC (self), FALSE);
+  g_return_val_if_fail (CM_IS_ROOM (room), FALSE);
+
+  session = ma_enc_lookup_out_group_session (self, room, &session_id);
+
+    if (!session_id && self->cm_db &&
+        !g_object_get_data (G_OBJECT (room), "olm-checked"))
+      {
+        session = cm_db_lookup_session (self->cm_db, self->user_id,
+                                        self->device_id, NULL,
+                                        self->curve_key, self->pickle_key,
+                                        cm_room_get_id (room),
+                                        SESSION_MEGOLM_V1_OUT);
+
+        g_object_set_data (G_OBJECT (room), "olm-checked", GINT_TO_POINTER (TRUE));
+        g_debug ("(%p) Got out group session %p from matrix db", self, session);
+
+        if (session)
+          {
+            CmOlm *in_session;
+
+            cm_olm_set_db (session, self->cm_db);
+            cm_olm_set_sender_details (session, cm_room_get_id (room), self->user_id);
+            cm_olm_set_account_details (session, self->user_id, self->device_id);
+
+            session_id = cm_olm_get_session_id (session);
+
+            g_hash_table_insert (self->out_group_room_session,
+                                 g_object_ref (room), g_strdup (session_id));
+            g_hash_table_insert (self->out_group_sessions,
+                                 g_strdup (session_id), g_object_ref (session));
+
+            in_session = cm_olm_in_group_new_from_out (session, self->curve_key);
+            g_hash_table_insert (self->in_group_sessions,
+                                 g_strdup (session_id), in_session);
+          }
+      }
+
+  return !!session;
+}
+
+/**
+ * cm_enc_set_room_group_key:
+ * @self: A #CmEnc
+ * @room_id: The room id the session should be added to
+ * @out_session: A megolm #CmOlm
+ *
+ * Set the outgoing group encryption session for the room
+ * @room_id.  All future messages shall be encrypted with
+ * @out_session for the given room until it's invalidated
+ */
+void
+cm_enc_set_room_group_key (CmEnc    *self,
+                           CmRoom   *room,
+                           gpointer  out_session)
+{
+  CmOlm *in_session = NULL;
+  const char *session_id;
+
+  g_return_if_fail (CM_IS_ENC (self));
+  g_return_if_fail (CM_IS_ROOM (room));
+  g_return_if_fail (CM_IS_OLM (out_session));
+  g_return_if_fail (cm_olm_get_session_type (out_session) == SESSION_MEGOLM_V1_OUT);
+
+  if (ma_enc_lookup_out_group_session (self, room, NULL) == out_session)
+    return;
+
+  /* There should be no existing sessions for the room */
+  g_warn_if_fail (!g_hash_table_contains (self->out_group_room_session, room));
+
+  g_debug ("(%p) Set out group key, room: %p, session: %p", self, room, out_session);
+
+  session_id = cm_olm_get_session_id (out_session);
+  in_session = cm_olm_in_group_new_from_out (out_session, self->curve_key);
+  g_hash_table_insert (self->out_group_room_session,
+                       g_object_ref (room), g_strdup (session_id));
+  g_hash_table_insert (self->out_group_sessions,
+                       g_strdup (session_id), g_object_ref (out_session));
+  g_hash_table_insert (self->in_group_sessions,
+                       g_strdup (session_id), in_session);
+  cm_olm_save (out_session);
+  cm_olm_save (in_session);
+}
+
+/**
+ * cm_enc_rm_room_group_key:
+ * @self: A #CmEnc
+ * @room_id: The room id for which the session should be removed
+ *
+ * Invalidate any out group session for the given room.
+ * The in session pair of the same is not removed as it
+ * may be useful to decrypt yet to receive messages.
+ */
+void
+cm_enc_rm_room_group_key (CmEnc  *self,
+                          CmRoom *room)
+{
+  g_autoptr(CmOlm) session = NULL;
+  const char *session_id = NULL;
+
+  g_return_if_fail (CM_IS_ENC (self));
+  g_return_if_fail (CM_IS_ROOM (room));
+
+  session = ma_enc_lookup_out_group_session (self, room, &session_id);
+  g_debug ("(%p) Remove out group key, room: %p, session: %p", self, room, session);
+
+  if (session)
+    {
+      g_object_ref (session);
+      cm_olm_set_state (session, OLM_STATE_INVALIDATED);
+      g_hash_table_remove (self->out_group_sessions, session_id);
+      cm_olm_save (session);
+    }
+
+  g_hash_table_remove (self->out_group_room_session, room);
+}
+
+static void
+enc_find_file_enc_cb (GObject      *obj,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+  CmEnc *self;
+  g_autoptr(GTask) task = user_data;
+  CmEncFileInfo *file;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  file = cm_db_find_file_enc_finish (CM_DB (obj), result, NULL);
+  g_debug ("(%p) Find file key done, has key: %s", self, CM_LOG_SUCCESS (!!file));
+
+  g_task_return_pointer (task, file, cm_enc_file_info_free);
+}
+
+void
+cm_enc_find_file_enc_async (CmEnc               *self,
+                            const char          *uri,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  CmEncFileInfo *file;
+
+  g_return_if_fail (uri && *uri);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  file = g_hash_table_lookup (self->enc_files, uri);
+  g_debug ("(%p) Find file key", self);
+
+  if (file)
+    {
+      g_debug ("(%p) Find file key %s from cache", self, CM_LOG_SUCCESS (TRUE));
+      g_task_return_pointer (task, file, NULL);
+      return;
+    }
+
+  cm_db_find_file_enc_async (self->cm_db, uri,
+                             enc_find_file_enc_cb,
+                             g_steal_pointer (&task));
+}
+
+CmEncFileInfo *
+cm_enc_find_file_enc_finish (CmEnc         *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ENC (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+void
+cm_enc_file_info_free (gpointer data)
+{
+  CmEncFileInfo *file = data;
+
+  if (!file)
+    return;
+
+  cm_utils_free_buffer (file->aes_iv_base64);
+  cm_utils_free_buffer (file->aes_key_base64);
+  cm_utils_free_buffer (file->sha256_base64);
+
+  cm_utils_free_buffer (file->mxc_uri);
+  cm_utils_free_buffer (file->algorithm);
+  cm_utils_free_buffer (file->version);
+  cm_utils_free_buffer (file->kty);
+
+  g_free (file);
+}
+
+GRefString *
+cm_enc_get_user_id (CmEnc *self)
+{
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  return self->user_id;
+}
+
+const char *
+cm_enc_get_device_id (CmEnc *self)
+{
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  return self->device_id;
+}
+
+const char *
+cm_enc_get_curve25519_key (CmEnc *self)
+{
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  return self->curve_key;
+}
+
+const char *
+cm_enc_get_ed25519_key (CmEnc *self)
+{
+  g_return_val_if_fail (CM_IS_ENC (self), NULL);
+
+  return self->ed_key;
+}
diff --git a/subprojects/libcmatrix/src/cm-enums.h b/subprojects/libcmatrix/src/cm-enums.h
new file mode 100644
index 0000000000000000000000000000000000000000..431ff5e255cb95a120eb768271a3755a14a89cb6
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-enums.h
@@ -0,0 +1,174 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-enums.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+
+/**
+ * CmError:
+ *
+ * The Error returned by the Matrix Server
+ * See https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
+ * for details.
+ */
+/* The order and value of members here and the error strings
+ * in the array error_codes in cm-utils.c should match.
+ */
+typedef enum {
+  CM_ERROR_FORBIDDEN = 1,
+  CM_ERROR_UNKNOWN_TOKEN,
+  CM_ERROR_MISSING_TOKEN,
+  CM_ERROR_BAD_JSON,
+  CM_ERROR_NOT_JSON,
+  CM_ERROR_NOT_FOUND,
+  CM_ERROR_LIMIT_EXCEEDED,
+  CM_ERROR_UNKNOWN,
+  CM_ERROR_UNRECOGNIZED,
+  CM_ERROR_UNAUTHORIZED,
+  CM_ERROR_USER_DEACTIVATED,
+  CM_ERROR_USER_IN_USE,
+  CM_ERROR_INVALID_USERNAME,
+  CM_ERROR_ROOM_IN_USE,
+  CM_ERROR_INVALID_ROOM_STATE,
+  CM_ERROR_THREEPID_IN_USE,
+  CM_ERROR_THREEPID_NOT_FOUND,
+  CM_ERROR_THREEPID_AUTH_FAILED,
+  CM_ERROR_THREEPID_DENIED,
+  CM_ERROR_SERVER_NOT_TRUSTED,
+  CM_ERROR_UNSUPPORTED_ROOM_VERSION,
+  CM_ERROR_INCOMPATIBLE_ROOM_VERSION,
+  CM_ERROR_BAD_STATE,
+  CM_ERROR_GUEST_ACCESS_FORBIDDEN,
+  CM_ERROR_CAPTCHA_NEEDED,
+  CM_ERROR_CAPTCHA_INVALID,
+  CM_ERROR_MISSING_PARAM,
+  CM_ERROR_INVALID_PARAM,
+  CM_ERROR_TOO_LARGE,
+  CM_ERROR_EXCLUSIVE,
+  CM_ERROR_RESOURCE_LIMIT_EXCEEDED,
+  CM_ERROR_CANNOT_LEAVE_SERVER_NOTICE_ROOM,
+
+  /* Local errors */
+  CM_ERROR_BAD_PASSWORD,
+  CM_ERROR_NO_HOME_SERVER,
+  CM_ERROR_BAD_HOME_SERVER,
+  CM_ERROR_USER_DEVICE_CHANGED,
+} CmError;
+
+typedef enum {
+  CM_STATUS_UNKNOWN,
+  CM_STATUS_JOIN,
+  CM_STATUS_LEAVE,
+  CM_STATUS_INVITE,
+  CM_STATUS_BAN,
+  CM_STATUS_KNOCK
+} CmStatus;
+
+typedef enum {
+  CM_CONTENT_TYPE_UNKNOWN,
+  CM_CONTENT_TYPE_TEXT,
+  CM_CONTENT_TYPE_EMOTE,
+  CM_CONTENT_TYPE_NOTICE,
+  CM_CONTENT_TYPE_IMAGE,
+  CM_CONTENT_TYPE_FILE,
+  CM_CONTENT_TYPE_LOCATION,
+  CM_CONTENT_TYPE_AUDIO,
+  CM_CONTENT_TYPE_VIDEO,
+  CM_CONTENT_TYPE_SERVER_NOTICE,
+} CmContentType;
+
+/*
+ * The order of the enum items SHOULD NEVER
+ * be changed as they are used in database.
+ * New items should be appended to the end.
+ */
+typedef enum {
+  CM_M_UNKNOWN,
+  CM_M_CALL_ANSWER,
+  CM_M_CALL_ASSERTED_IDENTITY,
+  CM_M_CALL_ASSERTED_IDENTITY_PREFIX,
+  CM_M_CALL_CANDIDATES,
+  CM_M_CALL_HANGUP,
+  CM_M_CALL_INVITE,
+  CM_M_CALL_NEGOTIATE,
+  CM_M_CALL_REJECT,
+  CM_M_CALL_REPLACES,
+  CM_M_CALL_SELECT_ANSWER,
+  CM_M_DIRECT,
+  CM_M_DUMMY,
+  CM_M_FORWARDED_ROOM_KEY,
+  CM_M_FULLY_READ,
+  CM_M_IGNORED_USER_LIST,
+  CM_M_KEY_VERIFICATION_ACCEPT,
+  CM_M_KEY_VERIFICATION_CANCEL,
+  CM_M_KEY_VERIFICATION_DONE,
+  CM_M_KEY_VERIFICATION_KEY,
+  CM_M_KEY_VERIFICATION_MAC,
+  CM_M_KEY_VERIFICATION_READY,
+  CM_M_KEY_VERIFICATION_REQUEST,
+  CM_M_KEY_VERIFICATION_START,
+  CM_M_PRESENCE,
+  CM_M_PUSH_RULES,
+  CM_M_REACTION,
+  CM_M_RECEIPT,
+  CM_M_ROOM_ALIASES,
+  CM_M_ROOM_AVATAR,
+  CM_M_ROOM_BOT_OPTIONS,
+  CM_M_ROOM_CANONICAL_ALIAS,
+  CM_M_ROOM_CREATE,
+  CM_M_ROOM_ENCRYPTED,
+  CM_M_ROOM_ENCRYPTION,
+  CM_M_ROOM_GUEST_ACCESS,
+  CM_M_ROOM_HISTORY_VISIBILITY,
+  CM_M_ROOM_JOIN_RULES,
+  CM_M_ROOM_KEY,
+  CM_M_ROOM_KEY_REQUEST,
+  CM_M_ROOM_MEMBER,
+  CM_M_ROOM_MESSAGE,
+  CM_M_ROOM_MESSAGE_FEEDBACK,
+  CM_M_ROOM_NAME,
+  CM_M_ROOM_PINNED_EVENTS,
+  CM_M_ROOM_PLUMBING,
+  CM_M_ROOM_POWER_LEVELS,
+  CM_M_ROOM_REDACTION,
+  CM_M_ROOM_RELATED_GROUPS,
+  CM_M_ROOM_SERVER_ACL,
+  CM_M_ROOM_THIRD_PARTY_INVITE,
+  CM_M_ROOM_TOMBSTONE,
+  CM_M_ROOM_TOPIC,
+  CM_M_SECRET_REQUEST,
+  CM_M_SECRET_SEND,
+  CM_M_SECRET_STORAGE_DEFAULT_KEY,
+  CM_M_SPACE_CHILD,
+  CM_M_SPACE_PARENT,
+  CM_M_STICKER,
+  CM_M_TAG,
+  CM_M_TYPING,
+
+  /* Custom */
+  CM_M_USER_STATUS = 256,
+  CM_M_ROOM_INVITE,
+  CM_M_ROOM_BAN,
+  CM_M_ROOM_KICK,
+} CmEventType;
+
+typedef enum
+{
+  CM_EVENT_STATE_UNKNOWN,
+  CM_EVENT_STATE_DRAFT,
+  /* Messages that are queued to be sent */
+  CM_EVENT_STATE_WAITING,
+  /* When saving to db consider this as failed until sent? */
+  CM_EVENT_STATE_SENDING,
+  CM_EVENT_STATE_SENDING_FAILED,
+  CM_EVENT_STATE_SENT,
+  CM_EVENT_STATE_RECEIVED,
+} CmEventState;
diff --git a/subprojects/libcmatrix/src/cm-input-stream-private.h b/subprojects/libcmatrix/src/cm-input-stream-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..c8dc3fc27bc15eb829f60a4429e6508450bc44f3
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-input-stream-private.h
@@ -0,0 +1,37 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-input-stream-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+
+#include "cm-enc-private.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_INPUT_STREAM (cm_input_stream_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmInputStream, cm_input_stream, CM, INPUT_STREAM, GFilterInputStream)
+
+CmInputStream  *cm_input_stream_new                   (GInputStream        *base_stream);
+CmInputStream  *cm_input_stream_new_from_file         (GFile               *file,
+                                                       gboolean             encrypt,
+                                                       GCancellable        *cancellable,
+                                                       GError             **error);
+void            cm_input_stream_set_file_enc          (CmInputStream       *self,
+                                                       CmEncFileInfo       *file);
+void            cm_input_stream_set_encrypt           (CmInputStream       *self);
+char           *cm_input_stream_get_sha256            (CmInputStream       *self);
+const char     *cm_input_stream_get_content_type      (CmInputStream       *self);
+goffset         cm_input_stream_get_size              (CmInputStream       *self);
+JsonObject     *cm_input_stream_get_file_json         (CmInputStream       *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-input-stream.c b/subprojects/libcmatrix/src/cm-input-stream.c
new file mode 100644
index 0000000000000000000000000000000000000000..683740ba8363fbfa18b7658827e0b577c3cb9d98
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-input-stream.c
@@ -0,0 +1,426 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-input-stream.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-input-stream"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+
+#include "cm-utils-private.h"
+#include "cm-input-stream-private.h"
+
+struct _CmInputStream
+{
+  GFilterInputStream parent_instance;
+
+  gcry_cipher_hd_t   cipher_hd;
+
+  char              *aes_key_base64;
+  char              *aes_iv_base64;
+  char              *sha256_base64;
+
+  /* For files that will be used to upload */
+  GFile             *file;
+  GFileInfo         *file_info;
+  GChecksum         *checksum;
+  gboolean           checksum_complete;
+  gboolean           encrypt;
+
+  char              *buffer;
+  int                buffer_len;
+
+  gcry_error_t       gcr_error;
+};
+
+G_DEFINE_TYPE (CmInputStream, cm_input_stream, G_TYPE_FILTER_INPUT_STREAM)
+
+static char *
+value_to_unpadded_base64 (const guchar *data,
+                          gsize         data_len,
+                          gboolean      url_safe)
+{
+  char *base64;
+
+  g_assert (data);
+  g_assert (data_len);
+
+  base64 = g_base64_encode (data, data_len);
+  g_strdelimit (base64, "=", '\0');
+
+  if (url_safe)
+    {
+      g_strdelimit (base64, "/", '_');
+      g_strdelimit (base64, "+", '-');
+    }
+
+  return base64;
+}
+
+static void
+parse_base64_value (const char  *unpadded_base64,
+                    guchar     **out,
+                    gsize       *out_len)
+{
+  g_autofree char *base64 = NULL;
+  gsize len, padded_len;
+
+  g_assert (out);
+  g_assert (out_len);
+
+  if (!unpadded_base64)
+    return;
+
+  len = strlen (unpadded_base64);
+  /* base64 is always multiple of 4, so add space for padding */
+  if (len % 4)
+    padded_len = len + 4 - len % 4;
+  else
+    padded_len = len;
+  base64 = malloc (padded_len + 1);
+  strcpy (base64, unpadded_base64);
+  memset (base64 + len, '=', padded_len - len);
+  base64[padded_len] = '\0';
+
+  *out = g_base64_decode (base64, out_len);
+}
+
+static gssize
+cm_input_stream_read_fn (GInputStream  *stream,
+                         void          *buffer,
+                         gsize          count,
+                         GCancellable  *cancellable,
+                         GError       **error)
+{
+  CmInputStream *self = (CmInputStream *)stream;
+  gssize n_read = -1;
+
+  if (self->gcr_error)
+    goto end;
+
+  n_read = G_INPUT_STREAM_CLASS (cm_input_stream_parent_class)->read_fn (stream, buffer, count,
+                                                                         cancellable, error);
+
+  if (self->cipher_hd && n_read > 0)
+    {
+      /* We need sha256 checksums only for encrypted/to be encrypted files */
+      if (!self->checksum)
+        self->checksum = g_checksum_new (G_CHECKSUM_SHA256);
+
+      if (G_UNLIKELY (self->buffer_len < n_read))
+        {
+          self->buffer_len = MAX (n_read, 1024 * 8);
+          self->buffer = g_realloc (self->buffer, self->buffer_len);
+        }
+    }
+
+  /* Since it's CTR mode, the encrypted and decrypted always have the same size */
+  if (self->cipher_hd && n_read > 0)
+    {
+      if (self->encrypt)
+        {
+          self->gcr_error = gcry_cipher_encrypt (self->cipher_hd, self->buffer,
+                                                 n_read, buffer, n_read);
+          /* we are encrypting, calculate the checksum after encryption */
+          if (!self->gcr_error)
+            g_checksum_update (self->checksum, (gpointer)self->buffer, n_read);
+        }
+      else
+        {
+          /* we are decrypting, calculate the checksum before decryption */
+          g_checksum_update (self->checksum, buffer, n_read);
+          self->gcr_error = gcry_cipher_decrypt (self->cipher_hd, self->buffer,
+                                                 n_read, buffer, n_read);
+        }
+
+      if (!self->gcr_error)
+        memcpy (buffer, self->buffer, n_read);
+    }
+
+  if (!self->gcr_error && self->cipher_hd && n_read == 0)
+    self->checksum_complete = TRUE;
+
+ end:
+  if (self->gcr_error)
+    {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Failed decrypting buffer: %s", gcry_strerror (self->gcr_error));
+      return -1;
+    }
+
+  return n_read;
+}
+
+static void
+cm_input_stream_finalize (GObject *object)
+{
+  CmInputStream *self = (CmInputStream *)object;
+
+  if (self->cipher_hd)
+    gcry_cipher_close (self->cipher_hd);
+
+  if (self->checksum)
+    g_checksum_free (self->checksum);
+
+  g_free (self->buffer);
+
+  g_clear_object (&self->file);
+  g_clear_object (&self->file_info);
+
+  G_OBJECT_CLASS (cm_input_stream_parent_class)->finalize (object);
+}
+
+static void
+cm_input_stream_class_init (CmInputStreamClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GInputStreamClass *input_stream_class = G_INPUT_STREAM_CLASS (klass);
+
+  object_class->finalize = cm_input_stream_finalize;
+
+  input_stream_class->read_fn = cm_input_stream_read_fn;
+}
+
+static void
+cm_input_stream_init (CmInputStream *self)
+{
+}
+
+CmInputStream *
+cm_input_stream_new (GInputStream *base_stream)
+{
+  return g_object_new (CM_TYPE_INPUT_STREAM,
+                       "base-stream", base_stream,
+                       NULL);
+}
+
+CmInputStream *
+cm_input_stream_new_from_file (GFile         *file,
+                               gboolean       encrypt,
+                               GCancellable  *cancellable,
+                               GError       **error)
+{
+  CmInputStream *self;
+  GInputStream *stream;
+  GFileInfo *file_info;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
+
+  stream = G_INPUT_STREAM (g_file_read (file, cancellable, error));
+
+  if (!stream)
+    return NULL;
+
+  file_info = g_file_query_info (file,
+                                 G_FILE_ATTRIBUTE_STANDARD_SIZE","G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+                                 G_FILE_QUERY_INFO_NONE,
+                                 cancellable,
+                                 error);
+  if (!file_info)
+    return NULL;
+
+  self = cm_input_stream_new (stream);
+  self->file_info = file_info;
+  self->file = g_object_ref (file);
+  self->encrypt = !!encrypt;
+
+  if (encrypt)
+    cm_input_stream_set_encrypt (self);
+
+  return self;
+}
+
+void
+cm_input_stream_set_file_enc (CmInputStream *self,
+                              CmEncFileInfo *file)
+{
+  gcry_cipher_hd_t cipher_hd;
+  gsize len;
+
+  g_return_if_fail (CM_IS_INPUT_STREAM (self));
+
+  if (!file)
+    return;
+
+  g_return_if_fail (file->mxc_uri);
+  g_return_if_fail (file->aes_key_base64);
+  g_return_if_fail (!self->cipher_hd);
+
+  self->gcr_error = gcry_cipher_open (&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0);
+
+  if (!self->gcr_error)
+    self->cipher_hd = cipher_hd;
+
+  if (!self->gcr_error)
+    {
+      g_autofree char *key_base64 = NULL;
+      g_autofree guchar *key = NULL;
+
+      /* uses unpadded base64url */
+      key_base64 = g_strdup (file->aes_key_base64);
+      g_strdelimit (key_base64, "_", '/');
+      g_strdelimit (key_base64, "-", '+');
+      parse_base64_value (key_base64, &key, &len);
+      self->gcr_error = gcry_cipher_setkey (cipher_hd, key, len);
+
+      cm_utils_clear ((char *)key, len);
+      cm_utils_clear (key_base64, -1);
+    }
+
+  if (!self->gcr_error)
+    {
+      g_autofree char *iv_base64 = NULL;
+      g_autofree guchar *iv = NULL;
+
+      iv_base64 = g_strdup (file->aes_iv_base64);
+      /* uses unpadded base64 */
+      parse_base64_value (iv_base64, &iv, &len);
+      self->gcr_error = gcry_cipher_setctr (cipher_hd, iv, len);
+
+      cm_utils_clear ((char *)iv, len);
+      cm_utils_clear (iv_base64, -1);
+    }
+}
+
+void
+cm_input_stream_set_encrypt (CmInputStream *self)
+{
+  gcry_cipher_hd_t cipher_hd;
+
+  g_return_if_fail (CM_IS_INPUT_STREAM (self));
+  g_return_if_fail (!self->cipher_hd);
+
+  self->gcr_error = gcry_cipher_open (&cipher_hd, GCRY_CIPHER_AES256, GCRY_CIPHER_MODE_CTR, 0);
+
+  if (!self->gcr_error)
+    self->cipher_hd = cipher_hd;
+
+  if (!self->gcr_error)
+    {
+      g_autofree guchar *key = NULL;
+
+      key = gcry_random_bytes (32, GCRY_STRONG_RANDOM);
+      self->aes_key_base64 = value_to_unpadded_base64 (key, 32, TRUE);
+      self->gcr_error = gcry_cipher_setkey (cipher_hd, key, 32);
+
+      cm_utils_clear ((char *)key, 32);
+    }
+
+  if (!self->gcr_error)
+    {
+      g_autofree guchar *iv = NULL;
+
+      /* The first 8 bytes has to be random, and the rest (counter) has to be 0 */
+      iv = g_malloc0 (16);
+      gcry_randomize (iv, 8, GCRY_STRONG_RANDOM);
+      self->aes_iv_base64 = value_to_unpadded_base64 (iv, 16, FALSE);
+      self->gcr_error = gcry_cipher_setctr (cipher_hd, iv, 16);
+
+      cm_utils_clear ((char *)iv, 16);
+    }
+}
+
+char *
+cm_input_stream_get_sha256 (CmInputStream *self)
+{
+  guint8 *buffer;
+  gsize digest_len;
+
+  g_return_val_if_fail (CM_IS_INPUT_STREAM (self), NULL);
+
+  if (!self->checksum || !self->checksum_complete)
+    return NULL;
+
+  digest_len = g_checksum_type_get_length (G_CHECKSUM_SHA256);
+  buffer = g_malloc (digest_len);
+  g_checksum_get_digest (self->checksum, buffer, &digest_len);
+
+  return value_to_unpadded_base64 (buffer, digest_len, FALSE);
+}
+
+const char *
+cm_input_stream_get_content_type (CmInputStream *self)
+{
+  const char *content_type;
+
+  g_return_val_if_fail (CM_IS_INPUT_STREAM (self), NULL);
+
+  if (!self->file_info)
+    return NULL;
+
+  content_type = g_file_info_get_content_type (self->file_info);
+
+  if (content_type && !self->encrypt)
+    return content_type;
+
+  return "application/octect-stream";
+}
+
+goffset
+cm_input_stream_get_size (CmInputStream *self)
+{
+  g_return_val_if_fail (CM_IS_INPUT_STREAM (self), 0);
+
+  if (!self->file_info)
+    return 0;
+
+  return g_file_info_get_size (self->file_info);
+}
+
+JsonObject *
+cm_input_stream_get_file_json (CmInputStream *self)
+{
+  g_autofree char *sha256 = NULL;
+  JsonObject *root, *child;
+  JsonArray *array;
+  const char *url;
+
+  g_return_val_if_fail (CM_IS_INPUT_STREAM (self), NULL);
+
+  /* Return JSON only if the stream is used to encrypt and after file
+   * has been read completely
+   */
+  if (!self->encrypt || !self->checksum_complete || !self->cipher_hd)
+    return NULL;
+
+  /* The mxc url should have set somewhere else */
+  if (!g_object_get_data (G_OBJECT (self), "uri"))
+    return NULL;
+
+  url = g_object_get_data (G_OBJECT (self), "uri");
+  root = json_object_new ();
+  json_object_set_string_member (root, "v", "v2");
+  json_object_set_string_member (root, "url", url);
+  json_object_set_string_member (root, "iv", self->aes_iv_base64);
+
+  sha256 = cm_input_stream_get_sha256 (self);
+  child = json_object_new ();
+  json_object_set_string_member (child, "sha256", sha256);
+  json_object_set_object_member (root, "hashes", child);
+
+  array = json_array_new ();
+  json_array_add_string_element (array, "encrypt");
+  json_array_add_string_element (array, "decrypt");
+
+  child = json_object_new ();
+  json_object_set_array_member (child, "key_ops", array);
+  json_object_set_string_member (child, "alg", "A256CTR");
+  json_object_set_string_member (child, "kty", "oct");
+  json_object_set_string_member (child, "k", self->aes_key_base64);
+  json_object_set_boolean_member (child, "ext", TRUE);
+  json_object_set_object_member (root, "key", child);
+
+  return root;
+}
diff --git a/subprojects/libcmatrix/src/cm-matrix-private.h b/subprojects/libcmatrix/src/cm-matrix-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..664affcc402d0c1dacf7c74ee9f13d45710784f3
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-matrix-private.h
@@ -0,0 +1,26 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include "cm-matrix.h"
+#include "cm-db-private.h"
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+G_BEGIN_DECLS
+
+const char  *cm_matrix_get_data_dir   (void);
+const char  *cm_matrix_get_app_id     (void);
+
+/* To be used only for tests */
+CmDb *cm_matrix_get_db (CmMatrix *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-matrix.c b/subprojects/libcmatrix/src/cm-matrix.c
new file mode 100644
index 0000000000000000000000000000000000000000..9e2ec0d1648c39baea8181cc9162a4d527de3e60
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-matrix.c
@@ -0,0 +1,902 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-matrix"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+#include <libsecret/secret.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include "cm-db-private.h"
+#include "cm-utils-private.h"
+#include "cm-secret-store-private.h"
+#include "cm-client.h"
+#include "cm-client-private.h"
+#include "cm-matrix.h"
+#include "cm-matrix-private.h"
+
+/**
+ * SECTION: cm-matrix
+ * @title: CmMatrix
+ * @short_description:
+ * @include: "cm-matrix.h"
+ */
+
+struct _CmMatrix
+{
+  GObject parent_instance;
+
+  char *db_path;
+  char *db_name;
+
+  char *data_dir;
+  char *cache_dir;
+
+  CmDb *cm_db;
+
+  GListStore *clients_list;
+  GHashTable *clients_to_save;
+
+  guint    network_change_id;
+
+  gboolean secrets_loaded;
+  gboolean db_loaded;
+  gboolean is_opening;
+  gboolean loading_accounts;
+  gboolean disable_auto_login;
+};
+
+
+#define RECONNECT_TIMEOUT    500 /* milliseconds */
+
+char *cmatrix_data_dir, *cmatrix_app_id;
+
+G_DEFINE_TYPE (CmMatrix, cm_matrix, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_READY,
+  N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+static gboolean
+matrix_has_client (CmMatrix *self,
+                   CmClient *client,
+                   gboolean  check_pending)
+{
+  const char *login_name, *user_name;
+  CmAccount *account;
+  GListModel *model;
+  guint n_items;
+
+  g_assert (CM_IS_MATRIX (self));
+  g_assert (CM_IS_CLIENT (client));
+
+  model = G_LIST_MODEL (self->clients_list);
+  n_items = g_list_model_get_n_items (model);
+  account = cm_client_get_account (client);
+  user_name = cm_client_get_user_id (client);
+  login_name = cm_account_get_login_id (account);
+
+  /* For the time being, let's ignore the fact that the same username
+   * can exist in different homeservers
+   */
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmClient) item = NULL;
+      CmAccount *item_account;
+
+      item = g_list_model_get_item (model, i);
+      item_account = cm_client_get_account (item);
+
+      if (login_name &&
+          g_strcmp0 (login_name, cm_account_get_login_id (item_account)) == 0)
+        return TRUE;
+
+      if (user_name &&
+          g_strcmp0 (user_name, cm_client_get_user_id (item)) == 0)
+        return TRUE;
+
+      if (user_name &&
+          g_strcmp0 (user_name, cm_account_get_login_id (item_account)) == 0)
+        return TRUE;
+    }
+
+  if (check_pending)
+    return g_hash_table_contains (self->clients_to_save, login_name);
+
+  return FALSE;
+}
+
+static gboolean
+matrix_reconnect (gpointer user_data)
+{
+  CmMatrix *self = user_data;
+  GListModel *model;
+  guint n_items;
+
+  self->network_change_id = 0;
+
+  model = G_LIST_MODEL (self->clients_list);
+  n_items = g_list_model_get_n_items (model);
+
+  for (guint i = 0; i < n_items; i++) {
+    g_autoptr(CmClient) client = NULL;
+
+    client = g_list_model_get_item (model, i);
+
+    if (cm_client_can_connect (client) &&
+        cm_client_get_enabled (client))
+      cm_client_start_sync (client);
+    else
+      cm_client_stop_sync (client);
+  }
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+matrix_network_changed_cb (CmMatrix        *self,
+                           gboolean         network_available,
+                           GNetworkMonitor *network_monitor)
+{
+  g_assert (CM_IS_MATRIX (self));
+  g_assert (G_IS_NETWORK_MONITOR (network_monitor));
+
+  if (!cm_matrix_is_ready (self))
+    return;
+
+  g_clear_handle_id (&self->network_change_id, g_source_remove);
+  self->network_change_id = g_timeout_add (RECONNECT_TIMEOUT,
+                                           matrix_reconnect, self);
+}
+
+static void
+matrix_stop (CmMatrix *self)
+{
+  GListModel *model;
+  guint n_items;
+
+  g_assert (CM_IS_MATRIX (self));
+
+  model = G_LIST_MODEL (self->clients_list);
+  n_items = g_list_model_get_n_items (model);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmClient) client = NULL;
+
+      client = g_list_model_get_item (model, i);
+
+      cm_client_stop_sync (client);
+    }
+}
+
+static void
+cm_matrix_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  CmMatrix *self = (CmMatrix *)object;
+
+  switch (prop_id)
+    {
+    case PROP_READY:
+      g_value_set_boolean (value, cm_matrix_is_ready (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+cm_matrix_finalize (GObject *object)
+{
+  CmMatrix *self = (CmMatrix *)object;
+
+  g_clear_handle_id (&self->network_change_id, g_source_remove);
+
+  matrix_stop (self);
+  g_list_store_remove_all (self->clients_list);
+  g_clear_object (&self->clients_list);
+  g_hash_table_unref (self->clients_to_save);
+
+  g_free (self->db_path);
+  g_free (self->db_name);
+
+  g_free (self->data_dir);
+  g_free (self->cache_dir);
+
+  G_OBJECT_CLASS (cm_matrix_parent_class)->finalize (object);
+}
+
+static void
+cm_matrix_class_init (CmMatrixClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_matrix_finalize;
+  object_class->get_property = cm_matrix_get_property;
+
+  /**
+   * CmMatrix:ready:
+   *
+   * Whether matrix is enabled and usable
+   */
+  properties[PROP_READY] =
+    g_param_spec_boolean ("ready",
+                          "matrix is ready",
+                          "matrix is ready",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+cm_matrix_init (CmMatrix *self)
+{
+  if (!gcry_control (GCRYCTL_INITIALIZATION_FINISHED_P))
+    g_error ("libgcrypt has not been initialized, did you run cm_init()?");
+
+  self->clients_list = g_list_store_new (CM_TYPE_CLIENT);
+  self->clients_to_save = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                                 g_free, g_object_unref);
+
+  g_signal_connect_object (g_network_monitor_get_default (),
+                           "network-changed",
+                           G_CALLBACK (matrix_network_changed_cb), self,
+                           G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+}
+
+/**
+ * cm_matrix_new:
+ * @data_dir: The data directory
+ * @cache_dir: The cache directory
+ * @app_id: The app id string (unused)
+ * @disable_auto_login: Disable auto login
+ *
+ * Create a new #CmMatrix with the provided details
+ *
+ * @data_dir is used to store downloaded files,
+ * avatars, and thumbnails.  The content shall not
+ * be encrypted even if that was the case when
+ * received over the wire.
+ *
+ * @app_id should be a valid string when validated
+ * with g_application_id_is_valid()
+ *
+ * The same values should be provided every time
+ * #CmMatrix is created as these info are used
+ * to store data.
+ *
+ * Returns: (transfer full): A #CmMatrix
+ */
+/*
+ * @cache_dir may be used to store files temporarily
+ * when needed (eg: when resizing images)
+ */
+CmMatrix *
+cm_matrix_new (const char *data_dir,
+               const char *cache_dir,
+               const char *app_id,
+               gboolean    disable_auto_login)
+{
+  CmMatrix *self;
+  char *dir;
+
+  g_return_val_if_fail (data_dir && *data_dir, NULL);
+  g_return_val_if_fail (cache_dir && *cache_dir, NULL);
+  g_return_val_if_fail (g_application_id_is_valid (app_id), NULL);
+
+  self = g_object_new (CM_TYPE_MATRIX, NULL);
+  self->disable_auto_login = !!disable_auto_login;
+  self->data_dir = g_build_filename (data_dir, "cmatrix", NULL);
+  cmatrix_data_dir = g_strdup (self->data_dir);
+  cmatrix_app_id = g_strdup (app_id);
+  self->cache_dir = g_build_filename (cache_dir, "cmatrix", NULL);
+
+  dir = cm_utils_get_path_for_m_type (self->data_dir, CM_M_ROOM_MESSAGE, TRUE, NULL);
+  g_mkdir_with_parents (dir, S_IRWXU);
+  g_free (dir);
+
+  dir = cm_utils_get_path_for_m_type (self->data_dir, CM_M_ROOM_MEMBER, TRUE, NULL);
+  g_mkdir_with_parents (dir, S_IRWXU);
+  g_free (dir);
+
+  dir = cm_utils_get_path_for_m_type (self->data_dir, CM_M_ROOM_AVATAR, TRUE, NULL);
+  g_mkdir_with_parents (dir, S_IRWXU);
+  g_free (dir);
+
+  g_debug ("(%p) New matrix, data: %s, cache: %s", self, self->data_dir, self->cache_dir);
+
+  return self;
+}
+
+/**
+ * cm_init:
+ * @init_gcrypt: Whether to initialize gcrypt
+ *
+ * This function should be called to initialize the library.
+ * You may call this in main()
+ *
+ * If you don't initialize gcrypt, you should do it yourself
+ */
+void
+cm_init (gboolean init_gcrypt)
+{
+  /* Force HTTP1 as we have issues with HTTP/2 implementation in libsoup3
+   * Like https://gitlab.gnome.org/GNOME/libsoup/-/issues/302,
+   * https://gitlab.gnome.org/GNOME/libsoup/-/issues/296, etc */
+  /* todo: Remove once we have better HTTP/2 support */
+  g_setenv ("SOUP_FORCE_HTTP1", "1", FALSE);
+
+  if (init_gcrypt)
+    {
+      /* Version check should be the very first call because it
+         makes sure that important subsystems are initialized. */
+      if (!gcry_check_version (GCRYPT_VERSION))
+        {
+          g_critical ("libgcrypt version mismatch");
+          exit (2);
+        }
+      gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN);
+      gcry_control (GCRYCTL_INIT_SECMEM, 512 * 1024, 0);
+      gcry_control (GCRYCTL_RESUME_SECMEM_WARN);
+      gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
+    }
+}
+
+static void
+load_accounts_from_secrets (CmMatrix  *self,
+                            GPtrArray *accounts)
+{
+  g_assert (CM_IS_MATRIX (self));
+
+  if (!accounts || !accounts->len)
+    return;
+
+  g_debug ("(%p) Load %u account secrets", self, accounts->len);
+
+  g_assert (SECRET_IS_RETRIEVABLE (accounts->pdata[0]));
+
+  for (guint i = 0; i < accounts->len; i++)
+    {
+      g_autoptr(CmClient) client = NULL;
+
+      client = cm_client_new_from_secret (accounts->pdata[i], self->cm_db);
+      g_list_store_append (self->clients_list, client);
+      if (!self->disable_auto_login)
+        cm_client_enable_as_in_store (client);
+    }
+}
+
+static void
+db_open_cb (GObject      *obj,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  CmMatrix *self;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_MATRIX (self));
+
+  self->db_loaded = cm_db_open_finish (self->cm_db, result, &error);
+  self->is_opening = FALSE;
+  g_debug ("(%p) Open DB %s", self, CM_LOG_SUCCESS (self->db_loaded));
+
+  if (!self->db_loaded)
+    {
+      g_clear_object (&self->cm_db);
+      g_warning ("(%p) Open DB error: %s", self, error ? error->message : "");
+      g_task_return_error (task, error);
+      return;
+    }
+  else
+    {
+      GPtrArray *accounts;
+
+      accounts = g_task_get_task_data (task);
+      load_accounts_from_secrets (self, accounts);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_READY]);
+      g_task_return_boolean (task,  self->db_loaded && self->secrets_loaded);
+    }
+}
+
+static void
+matrix_store_load_cb (GObject      *object,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+  CmMatrix *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GPtrArray) accounts = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_MATRIX (self));
+
+  accounts = cm_secret_store_load_finish (result, &error);
+  self->is_opening = FALSE;
+  if (!error)
+    self->secrets_loaded = TRUE;
+
+  g_debug ("(%p) Load secrets %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_warning ("(%p) Load secrets error: %s", self, error->message);
+      g_task_return_error (task, error);
+      return;
+    }
+
+  if (self->db_loaded)
+    {
+      load_accounts_from_secrets (self, accounts);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_READY]);
+      g_task_return_boolean (task, TRUE);
+    }
+  else
+    {
+      self->is_opening = TRUE;
+      if (accounts)
+        g_task_set_task_data (task, g_steal_pointer (&accounts),
+                              (GDestroyNotify)g_ptr_array_unref);
+
+      self->cm_db = cm_db_new ();
+
+      g_debug ("(%p) Open DB", self);
+      cm_db_open_async (self->cm_db,
+                        g_strdup (self->db_path), self->db_name,
+                        db_open_cb, g_steal_pointer (&task));
+    }
+}
+
+/**
+ * cm_matrix_open_async:
+ * @db_path: The path where db is (to be) stored
+ * @db_name: The name of database
+ * @cancellable: (nullable): A #GCancellable
+ * @callback: The callback to run when ready
+ * @user_data: user data for @callback
+ *
+ * Open the matrix E2EE db which shall be used by clients
+ * when required.
+ *
+ * Run cm_matrix_open_finish() to get the result.
+ */
+void
+cm_matrix_open_async (CmMatrix            *self,
+                      const char          *db_path,
+                      const char          *db_name,
+                      GCancellable        *cancellable,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (CM_IS_MATRIX (self));
+  g_return_if_fail (db_path && *db_path);
+  g_return_if_fail (db_name && *db_name);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!cm_matrix_is_ready (self));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  if (self->is_opening)
+    {
+      g_debug ("(%p) Open matrix already in progress", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Opening db in progress");
+      return;
+    }
+
+  if (cm_matrix_is_ready (self))
+    {
+      g_debug ("(%p) Open matrix already succeeded", self);
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  self->is_opening = TRUE;
+
+  if (!self->db_path)
+    self->db_path = g_strdup (db_path);
+
+  if (!self->db_name)
+    self->db_name = g_strdup (db_name);
+
+  /* Don't load libsecret in tests as password request requires X11 */
+  if (g_test_initialized ())
+    self->secrets_loaded = TRUE;
+
+  if (!self->secrets_loaded)
+    {
+      g_debug ("(%p) Load secrets", self);
+      cm_secret_store_load_async (cancellable,
+                                  matrix_store_load_cb,
+                                  g_steal_pointer (&task));
+    }
+  else if (!self->db_loaded)
+    {
+      self->cm_db = cm_db_new ();
+
+      g_debug ("(%p) Open DB", self);
+      cm_db_open_async (self->cm_db, g_strdup (db_path), db_name,
+                        db_open_cb,
+                        g_steal_pointer (&task));
+    }
+  else
+    g_assert_not_reached ();
+}
+
+gboolean
+cm_matrix_open_finish (CmMatrix      *self,
+                       GAsyncResult  *result,
+                       GError       **error)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+cm_matrix_is_ready (CmMatrix *self)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+
+  return self->db_loaded || self->secrets_loaded;
+}
+
+GListModel *
+cm_matrix_get_clients_list (CmMatrix *self)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+
+  return G_LIST_MODEL (self->clients_list);
+}
+
+gboolean
+cm_matrix_has_client_with_id (CmMatrix   *self,
+                              const char *user_id)
+{
+  GListModel *model;
+  guint n_items;
+
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+  g_return_val_if_fail (user_id && *user_id, FALSE);
+
+  model = G_LIST_MODEL (self->clients_list);
+  n_items = g_list_model_get_n_items (model);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmClient) item = NULL;
+      CmAccount *item_account;
+
+      item = g_list_model_get_item (model, i);
+      item_account = cm_client_get_account (item);
+
+      if (g_strcmp0 (user_id, cm_account_get_login_id (item_account)) == 0)
+        return TRUE;
+
+      if (g_strcmp0 (user_id, cm_client_get_user_id (item)) == 0)
+        return TRUE;
+    }
+
+  return g_hash_table_contains (self->clients_to_save, user_id);
+}
+
+/**
+ * cm_matrix_client_new:
+ * @self: A #CmMatrix
+ *
+ * Create a new #CmClient.  It's an error
+ * to create a new client before opening
+ the db with cm_matrix_open_async()
+ *
+ * Returns: (transfer full): A #CmClient
+ */
+CmClient *
+cm_matrix_client_new (CmMatrix *self)
+{
+  CmClient *client;
+
+  g_return_val_if_fail (CM_IS_MATRIX (self), NULL);
+
+  if (!cm_matrix_is_ready (self))
+    g_error ("(%p) DB not open, See cm_matrix_open_async()", self);
+
+  client = g_object_new (CM_TYPE_CLIENT, NULL);
+  /* Mark the client as not to save automatically unless asked explicitly
+   * with cm_matrix_save_client_async() at least once.
+   */
+  g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (TRUE));
+  cm_client_set_db (client, self->cm_db);
+
+  g_debug ("(%p) New client %p created", self, client);
+
+  return client;
+}
+
+static void
+matrix_save_client_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  CmMatrix *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean ret;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_MATRIX (self));
+
+  ret = cm_client_save_secrets_finish (CM_CLIENT (object), result, &error);
+  g_debug ("(%p) Save client %p %s", self, object, CM_LOG_SUCCESS (self->db_loaded));
+
+  if (error)
+    {
+      g_warning ("(%p) Save client %p error: %s", self, object, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      CmAccount *account;
+
+      account = cm_client_get_account (CM_CLIENT (object));
+      g_list_store_append (self->clients_list, object);
+      g_hash_table_remove (self->clients_to_save,
+                           cm_account_get_login_id (account));
+      g_task_return_boolean (task, ret);
+    }
+
+  g_object_set_data (object, "enable", GINT_TO_POINTER (FALSE));
+}
+
+void
+cm_matrix_save_client_async (CmMatrix            *self,
+                             CmClient            *client,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  const char *login_id;
+  CmAccount *account;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_MATRIX (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  account = cm_client_get_account (client);
+  g_return_if_fail (cm_account_get_login_id (account));
+  /* user id is set after login, which should be set only by cmatrix */
+  g_return_if_fail (!cm_client_get_user_id (client));
+  g_return_if_fail (cm_client_get_homeserver (client));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (FALSE));
+  g_debug ("(%p) Save client %p", self, client);
+
+  if (matrix_has_client (self, client, TRUE))
+    {
+      g_debug ("(%p) Save client %p error, user exists", self, client);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_EXISTS,
+                               "User already exists");
+      return;
+    }
+
+  login_id = cm_account_get_login_id (account);
+  g_hash_table_insert (self->clients_to_save, g_strdup (login_id), g_object_ref (client));
+  g_object_set_data (G_OBJECT (client), "enable", GINT_TO_POINTER (TRUE));
+  cm_client_save_secrets_async (client,
+                                matrix_save_client_cb,
+                                task);
+}
+
+gboolean
+cm_matrix_save_client_finish (CmMatrix      *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+matrix_delete_client_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  CmMatrix *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean ret;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_MATRIX (self));
+
+  ret = cm_client_delete_secrets_finish (CM_CLIENT (object), result, &error);
+  g_debug ("(%p) Delete client %p %s", self, object, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_warning ("(%p) Delete client %p error: %s", self, object, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      CmClient *client;
+
+      client = g_task_get_task_data (task);
+      cm_utils_remove_list_item (self->clients_list, client);
+      g_task_return_boolean (task, ret);
+    }
+}
+
+void
+cm_matrix_delete_client_async (CmMatrix            *self,
+                               CmClient            *client,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_MATRIX (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  g_debug ("(%p) Delete client %p", self, client);
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_task_data (task, g_object_ref (client), g_object_unref);
+
+  cm_client_delete_secrets_async (client,
+                                  matrix_delete_client_cb,
+                                  task);
+}
+
+gboolean
+cm_matrix_delete_client_finish (CmMatrix      *self,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+CmDb *
+cm_matrix_get_db (CmMatrix *self)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), NULL);
+
+  return self->cm_db;
+}
+
+const char *
+cm_matrix_get_data_dir (void)
+{
+  return cmatrix_data_dir;
+}
+
+const char *
+cm_matrix_get_app_id (void)
+{
+  return cmatrix_app_id;
+}
+
+static void
+matrix_save_client (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  CmMatrix *self;
+  g_autoptr(GTask) task = user_data;
+  GPtrArray *clients;
+  CmClient *client;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_MATRIX (self));
+
+  clients = g_task_get_task_data (task);
+  client = (CmClient *)object;
+
+  if (client &&
+      cm_client_save_secrets_finish (client, result, NULL))
+    {
+      g_debug ("(%p) Save client %p done", self, CM_LOG_SUCCESS (TRUE));
+      g_list_store_append (self->clients_list, CM_CLIENT (object));
+      cm_client_enable_as_in_store (client);
+    }
+
+  if (!clients || !clients->len)
+    {
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  client = g_ptr_array_steal_index (clients, 0);
+  g_object_set_data_full (user_data, "client", client, g_object_unref);
+
+  g_debug ("(%p) Save client %p, %u left to save", self, client, clients->len);
+  cm_client_save_secrets_async (client,
+                                matrix_save_client,
+                                g_steal_pointer (&task));
+}
+
+void
+cm_matrix_add_clients_async (CmMatrix            *self,
+                             GPtrArray           *secrets,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  GPtrArray *clients;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_MATRIX (self));
+  g_return_if_fail (secrets && secrets->len);
+  g_return_if_fail (SECRET_IS_RETRIEVABLE (secrets->pdata[0]));
+  g_return_if_fail (cm_matrix_is_ready (self));
+
+  clients = g_ptr_array_new_full (secrets->len, g_object_unref);
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_task_data (task, clients, (GDestroyNotify)g_ptr_array_unref);
+
+  for (guint i = 0; i < secrets->len; i++)
+    {
+      SecretRetrievable *secret = secrets->pdata[i];
+      CmClient *client;
+
+      client = cm_client_new_from_secret (secret, self->cm_db);
+      if (client)
+        g_ptr_array_add (clients, client);
+      else
+        g_warning ("(%p) Failed to create client from secret", self);
+    }
+
+  g_debug ("(%p) Save clients, count: %u", self, secrets->len);
+  matrix_save_client (NULL, NULL, task);
+}
+
+gboolean
+cm_matrix_add_clients_finish (CmMatrix      *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (CM_IS_MATRIX (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/cm-matrix.h b/subprojects/libcmatrix/src/cm-matrix.h
new file mode 100644
index 0000000000000000000000000000000000000000..2e902b1fc3fa6c2fe33952f271de94cd07de1778
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-matrix.h
@@ -0,0 +1,63 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include "cm-client.h"
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_MATRIX (cm_matrix_get_type ())
+G_DECLARE_FINAL_TYPE (CmMatrix, cm_matrix, CM, MATRIX, GObject)
+
+CmMatrix   *cm_matrix_new                (const char          *data_dir,
+                                          const char          *cache_dir,
+                                          const char          *app_id,
+                                          gboolean             disable_auto_login);
+void        cm_init                      (gboolean             init_gcrypt);
+void        cm_matrix_open_async         (CmMatrix            *self,
+                                          const char          *db_path,
+                                          const char          *db_name,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data);
+gboolean    cm_matrix_open_finish        (CmMatrix            *self,
+                                          GAsyncResult        *result,
+                                          GError             **error);
+gboolean    cm_matrix_is_ready           (CmMatrix            *self);
+GListModel *cm_matrix_get_clients_list   (CmMatrix            *self);
+gboolean    cm_matrix_has_client_with_id (CmMatrix            *self,
+                                          const char          *user_id);
+CmClient   *cm_matrix_client_new         (CmMatrix            *self);
+void        cm_matrix_save_client_async    (CmMatrix            *self,
+                                            CmClient            *client,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+gboolean    cm_matrix_save_client_finish   (CmMatrix            *self,
+                                            GAsyncResult        *result,
+                                            GError             **error);
+void        cm_matrix_delete_client_async  (CmMatrix            *self,
+                                            CmClient            *client,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+gboolean    cm_matrix_delete_client_finish (CmMatrix            *self,
+                                            GAsyncResult        *result,
+                                            GError             **error);
+void        cm_matrix_add_clients_async    (CmMatrix            *self,
+                                            GPtrArray           *secrets,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+gboolean    cm_matrix_add_clients_finish   (CmMatrix            *self,
+                                            GAsyncResult        *result,
+                                            GError             **error);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-net-private.h b/subprojects/libcmatrix/src/cm-net-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..bce04964c417c55a7d81a09fdbedfa29e63ac2a7
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-net-private.h
@@ -0,0 +1,71 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-net-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <json-glib/json-glib.h>
+
+#include "cm-enc-private.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_NET (cm_net_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmNet, cm_net, CM, NET, GObject)
+
+CmNet         *cm_net_new                 (void);
+void           cm_net_set_homeserver      (CmNet                 *self,
+                                           const char            *homeserver);
+void           cm_net_set_access_token    (CmNet                 *self,
+                                           const char            *access_token);
+const char    *cm_net_get_access_token    (CmNet                 *self);
+void           cm_net_send_data_async     (CmNet                 *self,
+                                           int                    priority,
+                                           char                  *data,
+                                           gsize                  size,
+                                           const char            *uri_path,
+                                           const char            *method, /* interned */
+                                           GHashTable            *query,
+                                           GCancellable          *cancellable,
+                                           GAsyncReadyCallback    callback,
+                                           gpointer               user_data);
+void           cm_net_send_json_async     (CmNet                 *self,
+                                           int                    priority,
+                                           JsonObject            *object,
+                                           const char            *uri_path,
+                                           const char            *method, /* interned */
+                                           GHashTable            *query,
+                                           GCancellable          *cancellable,
+                                           GAsyncReadyCallback    callback,
+                                           gpointer               user_data);
+void           cm_net_get_file_async      (CmNet                 *self,
+                                           const char            *uri,
+                                           CmEncFileInfo         *file_info,
+                                           GCancellable          *cancellable,
+                                           GAsyncReadyCallback    callback,
+                                           gpointer               user_data);
+GInputStream  *cm_net_get_file_finish     (CmNet                 *self,
+                                           GAsyncResult          *result,
+                                           GError               **error);
+void          cm_net_put_file_async       (CmNet                 *self,
+                                           GFile                 *file,
+                                           gboolean               encrypt,
+                                           GFileProgressCallback  progress_callback,
+                                           gpointer               progress_user_data,
+                                           GCancellable          *cancellable,
+                                           GAsyncReadyCallback    callback,
+                                           gpointer               user_data);
+char         *cm_net_put_file_finish      (CmNet                 *self,
+                                           GAsyncResult          *result,
+                                           GError               **error);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-net.c b/subprojects/libcmatrix/src/cm-net.c
new file mode 100644
index 0000000000000000000000000000000000000000..8bedaa689d40057163c8746a3456c3ee5ca22faf
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-net.c
@@ -0,0 +1,724 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-net"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+
+#include "cm-common.h"
+#include "cm-utils-private.h"
+#include "cm-enums.h"
+#include "cm-enc-private.h"
+#include "cm-input-stream-private.h"
+#include "cm-net-private.h"
+
+/**
+ * SECTION: cm-net
+ * @title: CmNet
+ * @short_description: Matrix Network related methods
+ * @include: "cm-net.h"
+ */
+
+#define MAX_CONNECTIONS     4
+
+struct _CmNet
+{
+  GObject         parent_instance;
+
+  SoupSession    *soup_session;
+  SoupSession    *file_session;
+  GCancellable   *cancellable;
+  char           *homeserver;
+  char           *access_token;
+};
+
+
+G_DEFINE_TYPE (CmNet, cm_net, G_TYPE_OBJECT)
+
+
+static void
+net_get_file_stream_cb (GObject      *obj,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  CmNet *self;
+  g_autoptr(GTask) task = user_data;
+  GInputStream *stream;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_NET (self));
+
+  stream = soup_session_send_finish (SOUP_SESSION (obj), result, &error);
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      CmInputStream *cm_stream;
+      CmEncFileInfo *enc_file;
+
+      cm_stream = cm_input_stream_new (stream);
+
+      enc_file = g_object_get_data (user_data, "file");
+      cm_input_stream_set_file_enc (cm_stream, enc_file);
+
+      g_task_return_pointer (task, cm_stream, g_object_unref);
+    }
+}
+
+static void
+net_load_from_stream_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  CmNet *self;
+  JsonParser *parser = JSON_PARSER (object);
+  g_autoptr(GTask) task = user_data;
+  JsonNode *root = NULL;
+  GError *error = NULL;
+
+  g_assert (JSON_IS_PARSER (parser));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_NET (self));
+
+  json_parser_load_from_stream_finish (parser, result, &error);
+
+  if (!error) {
+    root = json_parser_get_root (parser);
+    error = cm_utils_json_node_get_error (root);
+  }
+
+  if (error) {
+    if (g_error_matches (error, CM_ERROR, CM_ERROR_LIMIT_EXCEEDED) &&
+        root &&
+        JSON_NODE_HOLDS_OBJECT (root)) {
+      JsonObject *obj;
+      guint retry = 0;
+
+      obj = json_node_get_object (root);
+      retry = cm_utils_json_object_get_int (obj, "retry_after_ms");
+      g_object_set_data (G_OBJECT (task), "retry-after", GINT_TO_POINTER (retry));
+    } else {
+      g_debug ("Error loading from stream: %s", error->message);
+    }
+
+    g_task_return_error (task, error);
+    return;
+  }
+
+  if (JSON_NODE_HOLDS_OBJECT (root))
+    g_task_return_pointer (task, json_node_dup_object (root),
+                           (GDestroyNotify)json_object_unref);
+  else if (JSON_NODE_HOLDS_ARRAY (root))
+    g_task_return_pointer (task, json_node_dup_array (root),
+                           (GDestroyNotify)json_array_unref);
+  else
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                             "Received invalid data");
+}
+
+static void
+session_send_cb (GObject      *object,
+                 GAsyncResult *result,
+                 gpointer      user_data)
+{
+  CmNet *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GInputStream) stream = NULL;
+  g_autoptr(JsonParser) parser = NULL;
+  GCancellable *cancellable;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_NET (self));
+
+  stream = soup_session_send_finish (SOUP_SESSION (object), result, &error);
+
+  if (error) {
+    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+      g_debug ("Error session send: %s", error->message);
+    g_task_return_error (task, error);
+    return;
+  }
+
+  cancellable = g_task_get_cancellable (task);
+  parser = json_parser_new ();
+  json_parser_load_from_stream_async (parser, stream, cancellable,
+                                      net_load_from_stream_cb,
+                                      g_steal_pointer (&task));
+}
+
+/*
+ * queue_data:
+ * @data: (transfer full)
+ * @size: non-zero if @data is not %NULL
+ * @task: (transfer full)
+ */
+static void
+queue_data (CmNet      *self,
+            char       *data,
+            gsize       size,
+            const char *uri_path,
+            const char *method, /* interned */
+            GHashTable *query,
+            GTask      *task)
+{
+  g_autoptr(SoupMessage) message = NULL;
+#if SOUP_MAJOR_VERSION == 2
+  g_autoptr(SoupURI) uri = NULL;
+#else
+  g_autoptr(GUri) uri = NULL;
+  GUri *old_uri;
+  g_autoptr(GBytes) content_data = NULL;
+#endif
+  GCancellable *cancellable;
+  SoupMessagePriority msg_priority;
+  int priority = 0;
+
+  g_assert (CM_IS_NET (self));
+  g_assert (uri_path && *uri_path);
+  g_assert (method && *method);
+  g_return_if_fail (self->homeserver && *self->homeserver);
+
+  g_assert (method == SOUP_METHOD_GET ||
+            method == SOUP_METHOD_POST ||
+            method == SOUP_METHOD_PUT);
+
+#if SOUP_MAJOR_VERSION == 2
+  uri = soup_uri_new (self->homeserver);
+  soup_uri_set_path (uri, uri_path);
+#else
+  uri = g_uri_parse (self->homeserver, SOUP_HTTP_URI_FLAGS, NULL);
+  old_uri = uri;
+  uri = soup_uri_copy (old_uri, SOUP_URI_PATH, uri_path, SOUP_URI_NONE);
+  g_clear_pointer (&old_uri, g_uri_unref);
+#endif
+
+  if (self->access_token) {
+    if (!query)
+      query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+    g_hash_table_replace (query, g_strdup ("access_token"), g_strdup (self->access_token));
+#if SOUP_MAJOR_VERSION == 2
+    soup_uri_set_query_from_form (uri, query);
+#else
+    old_uri = uri;
+    uri = soup_uri_copy (old_uri, SOUP_URI_QUERY, soup_form_encode_hash (query), SOUP_URI_NONE);
+    g_clear_pointer (&old_uri, g_uri_unref);
+#endif
+    g_hash_table_unref (query);
+  }
+
+  message = soup_message_new_from_uri (method, uri);
+#if SOUP_MAJOR_VERSION == 2
+  soup_message_headers_append (message->request_headers, "Accept-Encoding", "gzip");
+#else
+  soup_message_headers_append (soup_message_get_request_headers (message), "Accept-Encoding", "gzip");
+#endif
+
+  priority = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "priority"));
+
+  if (priority <= -2)
+    msg_priority = SOUP_MESSAGE_PRIORITY_VERY_LOW;
+  else if (priority == -1)
+    msg_priority = SOUP_MESSAGE_PRIORITY_LOW;
+  else if (priority == 1)
+    msg_priority = SOUP_MESSAGE_PRIORITY_HIGH;
+  else if (priority >= 2)
+    msg_priority = SOUP_MESSAGE_PRIORITY_VERY_HIGH;
+  else
+    msg_priority = SOUP_MESSAGE_PRIORITY_NORMAL;
+
+  soup_message_set_priority (message, msg_priority);
+
+  if (data)
+    {
+#if SOUP_MAJOR_VERSION == 2
+      soup_message_set_request (message, "application/json", SOUP_MEMORY_TAKE, data, size);
+#else
+      content_data = g_bytes_new_take (data, size);
+      soup_message_set_request_body_from_bytes (message, "application/json", content_data);
+#endif
+    }
+
+  cancellable = g_task_get_cancellable (task);
+  g_task_set_task_data (task, g_object_ref (message), g_object_unref);
+
+#if SOUP_MAJOR_VERSION == 2
+  soup_session_send_async (self->soup_session, message, cancellable,
+                           session_send_cb, task);
+#else
+  soup_session_send_async (self->soup_session, message, msg_priority, cancellable,
+                           session_send_cb, task);
+#endif
+}
+
+static void
+cm_net_finalize (GObject *object)
+{
+  CmNet *self = (CmNet *)object;
+
+  if (self->cancellable)
+    g_cancellable_cancel (self->cancellable);
+
+  soup_session_abort (self->soup_session);
+  soup_session_abort (self->file_session);
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->soup_session);
+  g_clear_object (&self->file_session);
+
+  g_free (self->homeserver);
+  g_clear_pointer (&self->access_token, gcry_free);
+
+  G_OBJECT_CLASS (cm_net_parent_class)->finalize (object);
+}
+
+static void
+cm_net_class_init (CmNetClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_net_finalize;
+}
+
+static void
+cm_net_init (CmNet *self)
+{
+  self->soup_session = g_object_new (SOUP_TYPE_SESSION,
+                                     "max-conns-per-host", MAX_CONNECTIONS,
+                                     NULL);
+  self->file_session = g_object_new (SOUP_TYPE_SESSION,
+                                     "max-conns-per-host", MAX_CONNECTIONS,
+                                     NULL);
+  self->cancellable = g_cancellable_new ();
+}
+
+CmNet *
+cm_net_new (void)
+{
+  return g_object_new (CM_TYPE_NET, NULL);
+}
+
+void
+cm_net_set_homeserver (CmNet      *self,
+                       const char *homeserver)
+{
+  g_return_if_fail (CM_IS_NET (self));
+  g_return_if_fail (homeserver && *homeserver);
+
+  g_free (self->homeserver);
+  self->homeserver = g_strdup (homeserver);
+}
+
+void
+cm_net_set_access_token (CmNet      *self,
+                         const char *access_token)
+{
+  g_return_if_fail (CM_IS_NET (self));
+
+  g_clear_pointer (&self->access_token, gcry_free);
+
+  if (access_token && *access_token)
+    {
+      self->access_token = gcry_malloc_secure (strlen (access_token) + 1);
+      strcpy (self->access_token, access_token);
+    }
+}
+
+const char *
+cm_net_get_access_token (CmNet *self)
+{
+  g_return_val_if_fail (CM_IS_NET (self), NULL);
+
+  return self->access_token;
+}
+
+/**
+ * cm_net_send_data_async:
+ * @self: A #CmNet
+ * @priority: The priority of request, 0 for default
+ * @data: (nullable) (transfer full): The data to send
+ * @size: The @data size in bytes
+ * @uri_path: A string of the matrix uri path
+ * @method: An interned string for GET, PUT, POST, etc.
+ * @query: (nullable): A query to pass to internal #GUri
+ * @cancellable: (nullable): A #GCancellable
+ * @callback: The callback to run when completed
+ * @user_data: user data for @callback
+ *
+ * Send a JSON data @object to the @uri_path endpoint.
+ * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT
+ * or %SOUP_METHOD_POST.
+ * If @cancellable is %NULL, the internal cancellable
+ * shall be used
+ */
+void
+cm_net_send_data_async (CmNet               *self,
+                        int                  priority,
+                        char                *data,
+                        gsize                size,
+                        const char          *uri_path,
+                        const char          *method, /* interned */
+                        GHashTable          *query,
+                        GCancellable        *cancellable,
+                        GAsyncReadyCallback  callback,
+                        gpointer             user_data)
+{
+  GTask *task;
+
+  g_return_if_fail (CM_IS_NET (self));
+  g_return_if_fail (uri_path && *uri_path);
+  g_return_if_fail (method && *method);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (callback);
+  g_return_if_fail (self->homeserver && *self->homeserver);
+
+  if (data && *data)
+    g_return_if_fail (size);
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority));
+
+  queue_data (self, data, size, uri_path, method, query, task);
+}
+
+/**
+ * cm_net_send_json_async:
+ * @self: A #CmNet
+ * @priority: The priority of request, 0 for default
+ * @object: (nullable) (transfer full): The data to send
+ * @uri_path: A string of the matrix uri path
+ * @method: An interned string for GET, PUT, POST, etc.
+ * @query: (nullable): A query to pass to internal #GUri
+ * @cancellable: (nullable): A #GCancellable
+ * @callback: The callback to run when completed
+ * @user_data: user data for @callback
+ *
+ * Send a JSON data @object to the @uri_path endpoint.
+ * @method should be one of %SOUP_METHOD_GET, %SOUP_METHOD_PUT
+ * or %SOUP_METHOD_POST.
+ * If @cancellable is %NULL, the internal cancellable
+ * shall be used
+ */
+void
+cm_net_send_json_async (CmNet               *self,
+                        int                  priority,
+                        JsonObject          *object,
+                        const char          *uri_path,
+                        const char          *method, /* interned */
+                        GHashTable          *query,
+                        GCancellable        *cancellable,
+                        GAsyncReadyCallback  callback,
+                        gpointer             user_data)
+{
+  GTask *task;
+  char *data = NULL;
+  gsize size = 0;
+
+  g_return_if_fail (CM_IS_NET (self));
+  g_return_if_fail (uri_path && *uri_path);
+  g_return_if_fail (method && *method);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (callback);
+  g_return_if_fail (self->homeserver && *self->homeserver);
+
+  if (object)
+    {
+      data = cm_utils_json_object_to_string (object, FALSE);
+      json_object_unref (object);
+    }
+
+  if (data && *data)
+    size = strlen (data);
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "priority", GINT_TO_POINTER (priority));
+
+  queue_data (self, data, size, uri_path, method, query, task);
+}
+
+/**
+ * cm_net_get_file_async:
+ * @self: A #CmNet
+ * @message: (nullable) (transfer full): A #ChattyMessage
+ * @file: A #ChattyFileInfo
+ * @cancellable: (nullable): A #GCancellable
+ * @progress_callback: (nullable): A #GFileProgressCallback
+ * @callback: The callback to run when completed
+ * @user_data: user data for @callback
+ *
+ * Download the file @file.  @file path shall be updated
+ * after download is completed, and if @file is encrypted
+ * and has keys to decrypt the file, the file shall be
+ * stored decrypted.
+ */
+void
+cm_net_get_file_async (CmNet                 *self,
+                       const char            *uri,
+                       CmEncFileInfo         *enc_file,
+                       GCancellable          *cancellable,
+                       GAsyncReadyCallback    callback,
+                       gpointer               user_data)
+{
+  g_autofree char *url = NULL;
+  SoupMessage *msg;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_NET (self));
+  g_return_if_fail (uri && *uri);
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  if (g_str_has_prefix (uri, "mxc://")) {
+    const char *file_url;
+
+    file_url = uri + strlen ("mxc://");
+    url = g_strconcat (self->homeserver,
+                       "/_matrix/media/r0/download/", file_url, NULL);
+  }
+
+  if (!url)
+    url = g_strdup (uri);
+
+  msg = soup_message_new (SOUP_METHOD_GET, url);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data_full (G_OBJECT (task), "url", g_strdup (url), g_free);
+  g_object_set_data (G_OBJECT (task), "file", enc_file);
+  g_object_set_data_full (G_OBJECT (task), "msg", msg, g_object_unref);
+
+#if SOUP_MAJOR_VERSION == 2
+  soup_session_send_async (self->soup_session, msg, cancellable,
+                           net_get_file_stream_cb, task);
+#else
+  soup_session_send_async (self->file_session, msg, 0, cancellable,
+                           net_get_file_stream_cb, task);
+#endif
+}
+
+GInputStream *
+cm_net_get_file_finish (CmNet         *self,
+                        GAsyncResult  *result,
+                        GError       **error)
+{
+  g_return_val_if_fail (CM_IS_NET (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+put_file_async_cb (GObject      *obj,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  CmNet *self = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  GError *error = NULL;
+  GTask *task, *local_task;
+
+  local_task = G_TASK (result);
+  g_assert (G_IS_TASK (local_task));
+
+  self = g_task_get_source_object (local_task);
+  task = g_task_get_task_data (local_task);
+
+  g_assert (G_IS_TASK (task));
+  g_assert (CM_IS_NET (self));
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+  g_assert_no_error (error);
+
+  if (root)
+    {
+      const char *file_url;
+
+      file_url = cm_utils_json_object_get_string (root, "content_uri");
+      g_task_return_pointer (task, g_strdup (file_url), g_free);
+    }
+  else
+    {
+      if (error)
+        g_task_return_error (task, error);
+      else
+        g_task_return_pointer (task, NULL, NULL);
+    }
+}
+
+#if SOUP_MAJOR_VERSION == 2
+static void
+put_file_chunk (GTask       *task,
+                SoupMessage *msg)
+{
+  CmNet *self;
+  GInputStream *stream;
+  char buffer[8 * 1024];
+  gssize n_read;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SOUP_IS_MESSAGE (msg));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_NET (self));
+
+  stream = g_object_get_data (G_OBJECT (task), "stream");
+  n_read = g_input_stream_read (stream, buffer, 8 * 1024, NULL, NULL);
+
+  if (n_read == 0)
+    {
+      soup_message_body_complete (msg->request_body);
+    }
+  else if (n_read == -1)
+    {
+      soup_session_cancel_message (self->file_session, msg, SOUP_STATUS_CANCELLED);
+    }
+  else
+    {
+      soup_message_body_append (msg->request_body, SOUP_MEMORY_COPY, buffer, n_read);
+    }
+}
+#endif
+
+static void
+wrote_body_data_cb (GTask       *task,
+                    SoupMessage *msg,
+#if SOUP_MAJOR_VERSION == 2
+                    SoupBuffer  *chunk
+#else
+                    guint       chunk_size
+#endif
+                    )
+{
+  GFileProgressCallback progress_cb;
+  gpointer progress_user_data;
+
+  progress_cb = g_object_get_data (G_OBJECT (task), "progress-cb");
+  progress_user_data = g_object_get_data (G_OBJECT (task), "progress-cb-data");
+  g_assert (progress_cb);
+
+  progress_cb (0, 0, progress_user_data);
+}
+
+void
+cm_net_put_file_async (CmNet                 *self,
+                       GFile                 *file,
+                       gboolean               encrypt,
+                       GFileProgressCallback  progress_callback,
+                       gpointer               progress_user_data,
+                       GCancellable          *cancellable,
+                       GAsyncReadyCallback    callback,
+                       gpointer               user_data)
+{
+  g_autoptr(GHashTable) query = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *url = NULL;
+  CmInputStream *cm_stream;
+  GTask *task, *local_task;
+  SoupMessage *msg;
+
+  if (!cancellable)
+    cancellable = self->cancellable;
+
+  cm_stream = cm_input_stream_new_from_file (file, encrypt, cancellable, &error);
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  if (!cm_stream)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Failed to create stream: %s", error->message ?: "");
+      return;
+    }
+
+  query = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+  g_hash_table_replace (query, g_strdup ("filename"), g_file_get_basename (file));
+  g_hash_table_replace (query, g_strdup ("access_token"), g_strdup (self->access_token));
+
+  url = g_strconcat (self->homeserver, "/_matrix/media/r0/upload", NULL);
+  msg = soup_message_new (SOUP_METHOD_POST, url);
+
+#if SOUP_MAJOR_VERSION == 2
+  soup_uri_set_query_from_form (soup_message_get_uri (msg), query);
+  soup_message_headers_set_encoding (msg->request_headers, SOUP_ENCODING_CHUNKED);
+  soup_message_body_set_accumulate (msg->request_body, FALSE);
+  soup_message_headers_set_content_length (msg->request_headers,
+                                           cm_input_stream_get_size (cm_stream));
+  soup_message_headers_set_content_type (msg->request_headers,
+                                         cm_input_stream_get_content_type (cm_stream), NULL);
+#else
+  soup_message_set_uri (msg, soup_uri_copy (soup_message_get_uri (msg), SOUP_URI_QUERY,
+                                            soup_form_encode_hash (query), SOUP_URI_NONE));
+
+  /* We're uploading files in chunk */
+  soup_message_set_request_body (msg,
+                                 cm_input_stream_get_content_type (cm_stream),
+                                 G_INPUT_STREAM (cm_stream),
+                                 cm_input_stream_get_size (cm_stream));
+#endif
+
+  g_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "msg", msg, g_object_unref);
+  g_object_set_data_full (G_OBJECT (task), "stream", cm_stream, g_object_unref);
+
+  local_task = g_task_new (self, cancellable, put_file_async_cb, self);
+  g_task_set_task_data (local_task, task, g_object_unref);
+
+#if SOUP_MAJOR_VERSION == 2
+  g_signal_connect_object (msg, "wrote-headers",
+                           G_CALLBACK (put_file_chunk), task, G_CONNECT_SWAPPED);
+  g_signal_connect_object (msg, "wrote-chunk",
+                           G_CALLBACK (put_file_chunk), task, G_CONNECT_SWAPPED);
+#endif
+
+  if (progress_callback)
+    g_signal_connect_object (msg, "wrote-body-data",
+                             G_CALLBACK (wrote_body_data_cb), task, G_CONNECT_SWAPPED);
+
+#if SOUP_MAJOR_VERSION == 2
+  soup_session_send_async (self->soup_session, msg, cancellable,
+                           session_send_cb, task);
+#else
+  soup_session_send_async (self->file_session, msg, 0, cancellable,
+                           session_send_cb, local_task);
+#endif
+}
+
+char *
+cm_net_put_file_finish (CmNet         *self,
+                        GAsyncResult  *result,
+                        GError       **error)
+{
+  g_return_val_if_fail (CM_IS_NET (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/cm-olm-private.h b/subprojects/libcmatrix/src/cm-olm-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..63c8ce20478f5583ac3b0a276ae332d9f6db46b7
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-olm-private.h
@@ -0,0 +1,95 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-olm-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "cm-db-private.h"
+
+G_BEGIN_DECLS
+
+/* The value of the items shouldn't be changed as
+ * they are used in db */
+typedef enum {
+  OLM_STATE_USABLE       = 0,
+  OLM_STATE_ROTATED      = 1,
+  OLM_STATE_INVALIDATED  = 2,
+  OLM_STATE_NOT_SET      = 8,
+} CmOlmState;
+
+#define CM_TYPE_OLM (cm_olm_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmOlm, cm_olm, CM, OLM, GObject)
+
+CmOlm      *cm_olm_new_from_pickle     (char           *pickle,
+                                        const char     *pickle_key,
+                                        const char     *sender_identity_key,
+                                        CmSessionType   session_type);
+CmOlm      *cm_olm_outbound_new        (gpointer        olm_account,
+                                        const char     *curve_key,
+                                        const char     *one_time_key,
+                                        const char     *room_id);
+CmOlm      *cm_olm_inbound_new         (gpointer        olm_account,
+                                        const char     *sender_identity_key,
+                                        const char     *one_time_key_message);
+CmOlm      *cm_olm_in_group_new        (const char     *session_key,
+                                        const char     *sender_identity_key,
+                                        const char     *session_id);
+CmOlm      *cm_olm_in_group_new_from_out (CmOlm          *out_group,
+                                          const char     *sender_identity_key);
+CmOlm      *cm_olm_out_group_new         (const char     *sender_identity_key);
+
+CmSessionType cm_olm_get_session_type    (CmOlm          *self);
+size_t      cm_olm_get_message_index   (CmOlm          *self);
+gint64      cm_olm_get_created_time    (CmOlm          *self);
+void        cm_olm_set_state           (CmOlm          *self,
+                                        CmOlmState      state);
+void        cm_olm_update_validity     (CmOlm          *self,
+                                        guint           count,
+                                        gint64          duration);
+CmOlmState  cm_olm_get_state           (CmOlm          *self);
+
+void        cm_olm_set_sender_details  (CmOlm          *self,
+                                        const char     *room_id,
+                                        GRefString     *sender_id);
+void        cm_olm_set_account_details (CmOlm          *self,
+                                        GRefString     *account_user_id,
+                                        const char     *account_device_id);
+void        cm_olm_set_db              (CmOlm          *self,
+                                        gpointer        cm_db);
+void        cm_olm_set_key             (CmOlm          *self,
+                                        const char     *key);
+gboolean    cm_olm_save                (CmOlm          *self);
+char       *cm_olm_encrypt             (CmOlm          *self,
+                                        const char     *plain_text);
+char       *cm_olm_decrypt             (CmOlm          *self,
+                                        size_t          type,
+                                        const char     *message);
+size_t      cm_olm_get_message_type    (CmOlm          *self);
+
+const char *cm_olm_get_session_id        (CmOlm        *self);
+const char *cm_olm_get_session_key       (CmOlm        *self);
+const char *cm_olm_get_room_id           (CmOlm        *self);
+const char *cm_olm_get_sender_key        (CmOlm        *self);
+GRefString *cm_olm_get_account_id        (CmOlm        *self);
+const char *cm_olm_get_account_device    (CmOlm        *self);
+
+gpointer    cm_olm_match_olm_session     (const char     *body,
+                                          gsize           body_len,
+                                          size_t          message_type,
+                                          const char     *pickle,
+                                          const char     *pickle_key,
+                                          const char     *sender_identify_key,
+                                          CmSessionType   session_type,
+                                          char          **out_decrypted);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-olm-sas-private.h b/subprojects/libcmatrix/src/cm-olm-sas-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..63d1ac5bc13cec22dd95af9c854827b14839d197
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-olm-sas-private.h
@@ -0,0 +1,43 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-olm-sas-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "cm-device.h"
+#include "events/cm-event.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_OLM_SAS (cm_olm_sas_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmOlmSas, cm_olm_sas, CM, OLM_SAS, GObject)
+
+CmOlmSas      *cm_olm_sas_new                   (void);
+void           cm_olm_sas_set_client            (CmOlmSas     *self,
+                                                 gpointer      cm_client);
+void           cm_olm_sas_set_key_verification  (CmOlmSas     *self,
+                                                 CmEvent      *event);
+gboolean       cm_olm_sas_matches_event         (CmOlmSas     *self,
+                                                 CmEvent      *event);
+const char    *cm_olm_sas_get_cancel_code       (CmOlmSas     *self);
+CmEvent       *cm_olm_sas_get_cancel_event      (CmOlmSas     *self,
+                                                 const char   *cancel_code);
+CmEvent       *cm_olm_sas_get_accept_event      (CmOlmSas     *self);
+CmEvent       *cm_olm_sas_get_key_event         (CmOlmSas     *self);
+GPtrArray     *cm_olm_sas_get_emojis            (CmOlmSas     *self);
+CmEvent       *cm_olm_sas_get_mac_event         (CmOlmSas     *self);
+CmEvent       *cm_olm_sas_get_done_event        (CmOlmSas     *self);
+gboolean       cm_olm_sas_is_verified           (CmOlmSas     *self);
+CmDevice      *cm_olm_sas_get_device            (CmOlmSas     *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-olm-sas.c b/subprojects/libcmatrix/src/cm-olm-sas.c
new file mode 100644
index 0000000000000000000000000000000000000000..e2d5f39c7aa5305007bb50ae9cd1100352d9fd91
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-olm-sas.c
@@ -0,0 +1,874 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-olm-sas.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-olm"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+#include <olm/olm.h>
+#include <olm/sas.h>
+
+#include "events/cm-event-private.h"
+#include "cm-enc-private.h"
+#include "cm-client.h"
+#include "cm-client-private.h"
+#include "cm-device-private.h"
+#include "users/cm-user-private.h"
+#include "cm-utils-private.h"
+#include "cm-olm-sas-private.h"
+
+/* The order shouldn't be changed */
+/* https://github.com/matrix-org/matrix-spec-proposals/blob/old_master/data-definitions/sas-emoji.json */
+static const char *emojis[] = {
+  "🐶",  /*  "Dog",  "U+1F436" */
+  "🐱",  /*  "Cat",  "U+1F431" */
+  "🦁",  /*  "Lion",  "U+1F981" */
+  "🐎",  /*  "Horse",  "U+1F40E" */
+  "🦄",  /*  "Unicorn",  "U+1F984" */
+  "🐷",  /*  "Pig",  "U+1F437" */
+  "🐘",  /*  "Elephant",  "U+1F418" */
+  "🐰",  /*  "Rabbit",  "U+1F430" */
+  "🐼",  /*  "Panda",  "U+1F43C" */
+  "🐓",  /*  "Rooster",  "U+1F413" */
+  "🐧",  /*  "Penguin",  "U+1F427" */
+  "🐢",  /*  "Turtle",  "U+1F422" */
+  "🐟",  /*  "Fish",  "U+1F41F" */
+  "🐙",  /*  "Octopus",  "U+1F419" */
+  "🦋",  /*  "Butterfly",  "U+1F98B" */
+  "🌷",  /*  "Flower",  "U+1F337" */
+  "🌳",  /*  "Tree",  "U+1F333" */
+  "🌵",  /*  "Cactus",  "U+1F335" */
+  "🍄",  /*  "Mushroom",  "U+1F344" */
+  "🌏",  /*  "Globe",  "U+1F30F" */
+  "🌙",  /*  "Moon",  "U+1F319" */
+  "☁️",  /*  "Cloud",  "U+2601U+FE0F" */
+  "🔥",  /*  "Fire",  "U+1F525" */
+  "🍌",  /*  "Banana",  "U+1F34C" */
+  "🍎",  /*  "Apple",  "U+1F34E" */
+  "🍓",  /*  "Strawberry",  "U+1F353" */
+  "🌽",  /*  "Corn",  "U+1F33D" */
+  "🍕",  /*  "Pizza",  "U+1F355" */
+  "🎂",  /*  "Cake",  "U+1F382" */
+  "❤️",  /*  "Heart",  "U+2764U+FE0F" */
+  "😀",  /*  "Smiley",  "U+1F600" */
+  "🤖",  /*  "Robot",  "U+1F916" */
+  "🎩",  /*  "Hat",  "U+1F3A9" */
+  "👓",  /*  "Glasses",  "U+1F453" */
+  "🔧",  /*  "Spanner",  "U+1F527" */
+  "🎅",  /*  "Santa",  "U+1F385" */
+  "👍",  /*  "Thumbs Up",  "U+1F44D" */
+  "☂️",  /*  "Umbrella",  "U+2602U+FE0F" */
+  "⌛",  /*  "Hourglass",  "U+231B" */
+  "⏰",  /*  "Clock",  "U+23F0" */
+  "🎁",  /*  "Gift",  "U+1F381" */
+  "💡",  /*  "Light Bulb",  "U+1F4A1" */
+  "📕",  /*  "Book",  "U+1F4D5" */
+  "✏️",  /*  "Pencil",  "U+270FU+FE0F" */
+  "📎",  /*  "Paperclip",  "U+1F4CE" */
+  "✂️",  /*  "Scissors",  "U+2702U+FE0F" */
+  "🔒",  /*  "Lock",  "U+1F512" */
+  "🔑",  /*  "Key",  "U+1F511" */
+  "🔨",  /*  "Hammer",  "U+1F528" */
+  "☎️",  /*  "Telephone",  "U+260EU+FE0F" */
+  "🏁",  /*  "Flag",  "U+1F3C1" */
+  "🚂",  /*  "Train",  "U+1F682" */
+  "🚲",  /*  "Bicycle",  "U+1F6B2" */
+  "✈️",  /*  "Aeroplane",  "U+2708U+FE0F" */
+  "🚀",  /*  "Rocket",  "U+1F680" */
+  "🏆",  /*  "Trophy",  "U+1F3C6" */
+  "âš½",  /*  "Ball",  "U+26BD" */
+  "🎸",  /*  "Guitar",  "U+1F3B8" */
+  "🎺",  /*  "Trumpet",  "U+1F3BA" */
+  "🔔",  /*  "Bell",  "U+1F514" */
+  "âš“",  /*  "Anchor",  "U+2693" */
+  "🎧",  /*  "Headphones",  "U+1F3A7" */
+  "📁",  /*  "Folder",  "U+1F4C1" */
+  "📌",  /*  "Pin",  "U+1F4CC" */
+};
+
+#define NUM_SAS_BYTES   (6)
+
+struct _CmOlmSas
+{
+  GObject    parent_instance;
+
+  CmClient  *cm_client;
+
+  OlmSAS    *olm_sas;
+  char      *our_pub_key;
+  char      *their_pub_key;
+
+  char      *their_user_id;
+  char      *their_device_id;
+  CmDevice  *their_device;
+
+  char      *cancel_code;
+
+  CmEvent   *key_verification_event;
+  CmEvent   *key_verification_cancel;
+  CmEvent   *key_verification_accept;
+  CmEvent   *key_verification_mac;
+  CmEvent   *key_verification_done;
+  CmEvent   *verification_key;
+  GString   *commitment_str;
+
+  guint8    *sas_bytes;
+  guint8    *sas_emoji_indices;
+  GPtrArray *sas_emojis;
+  guint16   *sas_decimals;
+
+  gboolean   verified;
+};
+
+G_DEFINE_TYPE (CmOlmSas, cm_olm_sas, G_TYPE_OBJECT)
+
+static char *
+calculate_mac (CmOlmSas   *self,
+               const char *input,
+               const char *info,
+               size_t      info_len)
+{
+  char *mac;
+  size_t len;
+
+  g_assert (CM_IS_OLM_SAS (self));
+
+  len = olm_sas_mac_length (self->olm_sas);
+  mac = g_malloc (len + 1);
+  olm_sas_calculate_mac (self->olm_sas, input, strlen (input),
+                         info, info_len, mac, len);
+  mac[len] = '\0';
+
+  return mac;
+}
+
+static void
+cm_olm_sas_generate_bytes (CmOlmSas *self)
+{
+  g_autoptr(GString) sas_info = NULL;
+  g_autofree char *their_info = NULL;
+  g_autofree char *our_info = NULL;
+  const char *user_id, *device_id, *transaction_id;
+  guint8 *bytes;
+
+  if (self->sas_bytes)
+    return;
+
+  user_id = cm_client_get_user_id (self->cm_client);
+  device_id = cm_client_get_device_id (self->cm_client);
+  our_info = g_strdup_printf ("%s|%s|%s", user_id, device_id, self->our_pub_key);
+
+  user_id = self->their_user_id;
+  device_id = self->their_device_id;
+  their_info = g_strdup_printf ("%s|%s|%s", user_id, device_id, self->their_pub_key);
+
+  sas_info = g_string_sized_new (1024);
+  transaction_id = cm_event_get_transaction_id (self->key_verification_event);
+
+  g_string_append_printf (sas_info, "MATRIX_KEY_VERIFICATION_SAS|%s|%s|%s",
+                          their_info, our_info, transaction_id);
+
+  /* Always generate 6 bytes even if we may use decimal verification */
+  /* for which we'll ignore the last byte as it requires only 5 */
+  self->sas_bytes = g_malloc (NUM_SAS_BYTES);
+  bytes = self->sas_bytes;
+  olm_sas_generate_bytes (self->olm_sas, sas_info->str, sas_info->len, self->sas_bytes, NUM_SAS_BYTES);
+
+  /* We have 7 items of 6 bit each */
+  self->sas_emoji_indices = g_malloc0 (7);
+
+  /* The indices are of 6 bits, so iterate over every byte and extract
+   * those 6 bit indices and store as bytes
+   */
+  /* Don't complicate by using loops */
+  self->sas_emoji_indices[0] = bytes[0] >> 2;
+  self->sas_emoji_indices[1] = (bytes[0] & 0b11) << 4 | bytes[1] >> 4;
+  self->sas_emoji_indices[2] = (bytes[1] & 0b1111) << 2 | bytes[2] >> 6;
+  self->sas_emoji_indices[3] = bytes[2] & 0b111111;
+  self->sas_emoji_indices[4] = bytes[3] >> 2;
+  self->sas_emoji_indices[5] = (bytes[3] & 0b11) << 4 | bytes[4] >> 4;
+  self->sas_emoji_indices[6] = (bytes[4] & 0b1111) << 2 | bytes[5] >> 6;
+
+  /* There are 3 numbers of 13 bits */
+  self->sas_decimals = g_malloc0 (4 * sizeof(guint16));
+  self->sas_decimals[0] = (bytes[0] << 5 | bytes[1] >> 3) + 1000;
+  self->sas_decimals[1] = ((bytes[1] & 0b111) << 10 | bytes[2] << 2 | bytes[3] >> 6) + 1000;
+  self->sas_decimals[2] = ((bytes[3] & 0b111111) << 7 | bytes[4] >> 1) + 1000;
+}
+
+static JsonObject *
+olm_sas_get_message_json (CmOlmSas    *self,
+                          JsonObject **content)
+{
+  JsonObject *root, *child;
+  const char *value;
+
+  root = json_object_new ();
+
+  child = json_object_new ();
+  json_object_set_object_member (root, "messages", child);
+
+  value = cm_event_get_sender_id (self->key_verification_event);
+  json_object_set_object_member (child, value, json_object_new ());
+  child = cm_utils_json_object_get_object (child, value);
+
+  value = cm_event_get_sender_device_id (self->key_verification_event);
+  json_object_set_object_member (child, value, json_object_new ());
+  child = cm_utils_json_object_get_object (child, value);
+
+  value = cm_event_get_transaction_id (self->key_verification_event);
+  json_object_set_string_member (child, "transaction_id", value);
+
+  if (content)
+    *content = child;
+
+  return root;
+}
+
+static void
+cm_olm_sas_create_commitment (CmOlmSas *self)
+{
+  g_autofree OlmUtility *olm_util = NULL;
+  g_autofree char *sha256 = NULL;
+  g_autoptr(JsonObject) json = NULL;
+  g_autoptr(GString) str = NULL;
+  JsonObject *content;
+  CmEvent *event;
+  size_t len;
+
+  g_return_if_fail (CM_IS_OLM_SAS (self));
+  g_return_if_fail (self->key_verification_event);
+
+  if (self->commitment_str->len)
+    return;
+
+  if (cm_event_get_m_type (self->key_verification_event) == CM_M_KEY_VERIFICATION_REQUEST)
+    event = g_object_get_data (G_OBJECT (self->key_verification_event), "start");
+  else
+    event = self->key_verification_event;
+
+  /* We should have an m.key.verification.start event to get commitment */
+  g_return_if_fail (event);
+
+  str = g_string_sized_new (1024);
+  if (!self->our_pub_key)
+    {
+      len = olm_sas_pubkey_length (self->olm_sas);
+      self->our_pub_key = g_malloc (len + 1);
+      olm_sas_get_pubkey (self->olm_sas, self->our_pub_key, len);
+      self->our_pub_key[len] = '\0';
+    }
+
+  g_string_append_len (str, self->our_pub_key, strlen (self->our_pub_key));
+  json = cm_event_get_json (event);
+  content = cm_utils_json_object_get_object (json, "content");
+  cm_utils_json_get_canonical (content, str);
+
+  olm_util = g_malloc (olm_utility_size ());
+  olm_utility (olm_util);
+
+  len = olm_sha256_length (olm_util);
+  sha256 = g_malloc (len);
+  olm_sha256 (olm_util, str->str, str->len, sha256, len);
+  g_string_append_len (self->commitment_str, sha256, len);
+}
+
+static void
+cm_olm_sas_finalize (GObject *object)
+{
+  CmOlmSas *self = (CmOlmSas *)object;
+
+  g_clear_object (&self->key_verification_event);
+  olm_clear_sas (self->olm_sas);
+  g_free (self->olm_sas);
+
+  if (self->commitment_str)
+    g_string_free (self->commitment_str, TRUE);
+
+  g_free (self->our_pub_key);
+  g_free (self->their_pub_key);
+  g_free (self->their_user_id);
+  g_free (self->their_device_id);
+
+  g_free (self->sas_bytes);
+
+  G_OBJECT_CLASS (cm_olm_sas_parent_class)->finalize (object);
+}
+
+static void
+cm_olm_sas_class_init (CmOlmSasClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_olm_sas_finalize;
+}
+
+static void
+cm_olm_sas_init (CmOlmSas *self)
+{
+  size_t len;
+
+  self->olm_sas = g_malloc (olm_sas_size ());
+  self->commitment_str = g_string_sized_new (256);
+  olm_sas (self->olm_sas);
+
+  len = olm_create_sas_random_length (self->olm_sas);
+  if (len)
+    {
+      cm_gcry_t buffer;
+
+      buffer = gcry_random_bytes (len, GCRY_STRONG_RANDOM);
+      olm_create_sas (self->olm_sas, buffer, len);
+      gcry_free (buffer);
+    }
+}
+
+CmOlmSas *
+cm_olm_sas_new (void)
+{
+  return g_object_new (CM_TYPE_OLM_SAS, NULL);
+}
+
+void
+cm_olm_sas_set_client (CmOlmSas *self,
+                       gpointer  cm_client)
+{
+  g_return_if_fail (CM_IS_OLM_SAS (self));
+  g_return_if_fail (CM_IS_CLIENT (cm_client));
+
+  if (!self->cm_client)
+    g_set_weak_pointer (&self->cm_client, cm_client);
+}
+
+static gboolean
+cm_olm_array_has_string (JsonObject *content,
+                         const char *array_str,
+                         const char *value)
+{
+  JsonArray *array;
+  const char *element;
+
+  if (!content)
+    return FALSE;
+
+  g_assert (array_str && *array_str);
+  g_assert (value && *value);
+
+  array = cm_utils_json_object_get_array (content, array_str);
+  for (guint i = 0; i < json_array_get_length (array); i++)
+    {
+      element = json_array_get_string_element (array, i);
+
+      if (g_strcmp0 (element, value) == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+cm_olm_sas_parse_verification_start (CmOlmSas *self,
+                                     CmEvent  *event)
+{
+  g_autoptr(JsonObject) root = NULL;
+  JsonObject *content;
+
+  g_assert (CM_IS_OLM_SAS (self));
+  g_assert (CM_IS_EVENT (event));
+
+  root = cm_event_get_json (event);
+  content = cm_utils_json_object_get_object (root, "content");
+
+  if (g_strcmp0 (cm_utils_json_object_get_string (content, "method"), "m.sas.v1") != 0 ||
+      !cm_olm_array_has_string (content, "key_agreement_protocols", "curve25519-hkdf-sha256") ||
+      !cm_olm_array_has_string (content, "hashes", "sha256") ||
+      !cm_olm_array_has_string (content, "message_authentication_codes", "hkdf-hmac-sha256") ||
+      !cm_olm_array_has_string (content, "short_authentication_string", "decimal"))
+    self->cancel_code = g_strdup ("m.unknown_method");
+}
+
+static void
+cm_olm_sas_parse_verification_mac (CmOlmSas *self,
+                                   CmEvent  *event)
+{
+  g_autoptr(GString) base_info = NULL;
+  g_autoptr(GString) key_ids = NULL;
+  g_autoptr(GList) members = NULL;
+  g_autofree char *mac = NULL;
+  JsonObject *root, *content, *mac_json;
+
+  g_assert (CM_IS_OLM_SAS (self));
+  g_assert (CM_IS_EVENT (event));
+
+  if (!g_object_get_data (G_OBJECT (self->key_verification_event), "key") &&
+      !self->cancel_code)
+    {
+      self->cancel_code = g_strdup ("m.unexpected_message");
+      return;
+    }
+
+  if (self->verified || self->cancel_code)
+    return;
+
+  root = cm_event_get_json (event);
+  content = cm_utils_json_object_get_object (root, "content");
+  mac_json = cm_utils_json_object_get_object (content, "mac");
+
+  if (!mac_json)
+    {
+      self->cancel_code = g_strdup ("m.key_mismatch");
+      return;
+    }
+
+  key_ids = g_string_sized_new (1024);
+  members = json_object_get_members (mac_json);
+
+  members = g_list_sort (members, (GCompareFunc)g_strcmp0);
+
+  for (GList *item = members; item && item->data; item = item->next)
+    {
+      g_string_append (key_ids, (char *)item->data);
+      g_string_append_c (key_ids, ',');
+    }
+
+  if (!key_ids->len)
+    {
+      self->cancel_code = g_strdup ("m.key_mismatch");
+      return;
+    }
+
+  /* Remove the trailing ',' */
+  g_string_set_size (key_ids, key_ids->len - 1);
+
+  base_info = g_string_sized_new (1024);
+  g_string_printf (base_info, "MATRIX_KEY_VERIFICATION_MAC%s%s%s%s%s",
+                   self->their_user_id, self->their_device_id,
+                   cm_client_get_user_id (self->cm_client),
+                   cm_client_get_device_id (self->cm_client),
+                   cm_event_get_transaction_id (self->key_verification_event));
+  g_string_append (base_info, "KEY_IDS");
+
+  mac = calculate_mac (self, key_ids->str, base_info->str, base_info->len);
+
+  if (g_strcmp0 (mac, cm_utils_json_object_get_string (content, "keys")) != 0)
+    {
+      g_debug ("(%p) key mismatch, mac != keys", self);
+      self->cancel_code = g_strdup ("m.key_mismatch");
+      return;
+    }
+
+  for (GList *item = members; item && item->data; item = item->next)
+    {
+      g_auto(GStrv) strv = NULL;
+      const char *key_mac;
+      CmDevice *device;
+      CmUser *user;
+
+      strv = g_strsplit (item->data, ":", -1);
+
+      if (g_strcmp0 (strv[0], "ed25519") != 0)
+        {
+          g_debug ("(%p) key mismatch, '%s' is not ed25519", self, strv[0]);
+          self->cancel_code = g_strdup ("m.key_mismatch");
+          return;
+        }
+
+      if (g_strcmp0 (strv[1], self->their_device_id) != 0)
+        continue;
+
+      key_mac = cm_utils_json_object_get_string (mac_json, item->data);
+      user = cm_event_get_sender (self->key_verification_event);
+      device = cm_user_find_device (user, self->their_device_id);
+
+      if (!device || !key_mac || !cm_device_get_ed_key (device))
+        {
+          self->cancel_code = g_strdup ("m.key_mismatch");
+          return;
+        }
+
+      g_free (mac);
+      g_string_truncate (base_info, base_info->len - strlen ("KEY_IDS"));
+      g_string_append (base_info, item->data);
+
+      mac = calculate_mac (self, cm_device_get_ed_key (device),
+                           base_info->str, base_info->len);
+
+      /* Currently we handle only one device */
+      if (g_strcmp0 (key_mac, mac) != 0)
+        {
+          self->cancel_code = g_strdup ("m.key_mismatch");
+          return;
+        }
+
+      self->verified = TRUE;
+      self->their_device = g_object_ref (device);
+      cm_device_set_verified (device, TRUE);
+    }
+}
+
+void
+cm_olm_sas_set_key_verification (CmOlmSas *self,
+                                 CmEvent  *event)
+{
+  g_autoptr(JsonObject) json = NULL;
+  CmEventType type;
+  gint64 minutes;
+
+  g_return_if_fail (CM_IS_OLM_SAS (self));
+  g_return_if_fail (CM_IS_EVENT (event));
+  g_return_if_fail (!self->key_verification_event);
+
+  type = cm_event_get_m_type (event);
+  g_return_if_fail (type == CM_M_KEY_VERIFICATION_REQUEST ||
+                    type == CM_M_KEY_VERIFICATION_START);
+  self->key_verification_event = g_object_ref (event);
+
+  /* fixme: We now only accepts verification requests */
+  self->their_user_id = g_strdup (cm_event_get_sender_id (event));
+  self->their_device_id = g_strdup (cm_event_get_sender_device_id (event));
+
+  minutes = (time (NULL) - cm_event_get_time_stamp (event) / 1000) % 60;
+
+  if (type == CM_M_KEY_VERIFICATION_START)
+    cm_olm_sas_parse_verification_start (self, event);
+
+  if (self->cancel_code)
+    return;
+
+  /* Cancel if request is 10+ minutes from the past or 5+ minutes from the future */
+  if (minutes < -5 || minutes > 10)
+    self->cancel_code = g_strdup ("m.timeout");
+}
+
+gboolean
+cm_olm_sas_matches_event (CmOlmSas *self,
+                          CmEvent  *event)
+{
+  GObject *obj = (GObject *)self->key_verification_event;
+  const char *item_txn_id, *event_txn_id;
+  CmEventType type;
+
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), FALSE);
+  g_return_val_if_fail (CM_IS_EVENT (event), FALSE);
+  g_return_val_if_fail (self->key_verification_event, FALSE);
+
+  if (event == self->key_verification_event)
+    return TRUE;
+
+  item_txn_id = cm_event_get_transaction_id (event);
+  event_txn_id = cm_event_get_transaction_id (self->key_verification_event);
+
+  if (g_strcmp0 (item_txn_id, event_txn_id) != 0)
+    return FALSE;
+
+  g_object_ref (event);
+  type = cm_event_get_m_type (event);
+
+  if (type == CM_M_KEY_VERIFICATION_KEY)
+    {
+      g_autofree char *key = NULL;
+
+      key = g_strdup (cm_event_get_verification_key (event));
+
+      if (olm_sas_is_their_key_set (self->olm_sas))
+        {
+          g_warning ("Key was already set");
+        }
+      else
+        {
+          self->their_pub_key = g_strdup (key);
+          olm_sas_set_their_key (self->olm_sas, key, strlen (key));
+        }
+    }
+
+  if (type == CM_M_KEY_VERIFICATION_CANCEL)
+    g_object_set_data_full (obj, "cancel", event, g_object_unref);
+  else if (type == CM_M_KEY_VERIFICATION_DONE)
+    g_object_set_data_full (obj, "done", event, g_object_unref);
+  else if (type == CM_M_KEY_VERIFICATION_KEY)
+    g_object_set_data_full (obj, "key", event, g_object_unref);
+  else if (type == CM_M_KEY_VERIFICATION_MAC)
+    g_object_set_data_full (obj, "mac", event, g_object_unref);
+  else if (type == CM_M_KEY_VERIFICATION_READY)
+    g_object_set_data_full (obj, "ready", event, g_object_unref);
+  else if (type == CM_M_KEY_VERIFICATION_REQUEST)
+    g_object_set_data_full (obj, "request", event, g_object_unref);
+  else if (type == CM_M_KEY_VERIFICATION_START)
+    g_object_set_data_full (obj, "start", event, g_object_unref);
+
+  if (type == CM_M_KEY_VERIFICATION_START)
+    cm_olm_sas_parse_verification_start (self, event);
+
+  if (type == CM_M_KEY_VERIFICATION_MAC)
+    cm_olm_sas_parse_verification_mac (self, event);
+
+  if (type == CM_M_KEY_VERIFICATION_CANCEL && !self->cancel_code)
+    self->cancel_code = g_strdup ("m.timeout");
+
+  /* generate emojis */
+  if (type == CM_M_KEY_VERIFICATION_KEY)
+    cm_olm_sas_get_emojis (self);
+
+  g_signal_emit_by_name (obj, "updated", 0);
+
+  return TRUE;
+}
+
+/**
+ * cm_olm_sas_get_cancel_reason:
+ * @self: A #CmOlmSas
+ *
+ * Get the error 'code' to be used for m.key.verification.cancel.
+ * This is to be checked after every update and the verification
+ * should be cancelled if a non-%NULL value is return.
+ *
+ * Returns: (nullable): A string
+ */
+const char *
+cm_olm_sas_get_cancel_code (CmOlmSas *self)
+{
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+
+  return self->cancel_code;
+}
+
+CmEvent *
+cm_olm_sas_get_cancel_event (CmOlmSas   *self,
+                             const char *cancel_code)
+{
+  JsonObject *root, *child;
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+  g_return_val_if_fail (self->key_verification_event, NULL);
+  g_return_val_if_fail (self->cm_client, NULL);
+
+  if (self->key_verification_cancel)
+    return self->key_verification_cancel;
+
+  if (!cancel_code)
+    cancel_code = "m.user";
+
+  if (g_strcmp0 (cancel_code, "m.user") != 0 &&
+      g_strcmp0 (cancel_code, "m.timeout") != 0 &&
+      g_strcmp0 (cancel_code, "m.unknown_method") != 0 &&
+      g_strcmp0 (cancel_code, "m.key_mismatch") != 0 &&
+      g_strcmp0 (cancel_code, "m.user_mismatch") != 0 &&
+      g_strcmp0 (cancel_code, "m.unexpected_message") != 0)
+    g_return_val_if_reached (NULL);
+
+  event = cm_event_new (CM_M_KEY_VERIFICATION_CANCEL);
+  cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client));
+  self->key_verification_cancel = event;
+
+  root = olm_sas_get_message_json (self, &child);
+  cm_event_set_json (event, root, NULL);
+
+  json_object_set_string_member (child, "code", cancel_code);
+
+  return self->key_verification_cancel;
+}
+
+CmEvent *
+cm_olm_sas_get_accept_event (CmOlmSas *self)
+{
+  JsonObject *root, *child;
+  JsonArray *array;
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+  g_return_val_if_fail (self->key_verification_event, NULL);
+
+  if (self->key_verification_accept)
+    return self->key_verification_accept;
+
+  if (cm_event_get_m_type (self->key_verification_event) == CM_M_KEY_VERIFICATION_REQUEST)
+    event = g_object_get_data (G_OBJECT (self->key_verification_event), "start");
+  else
+    event = self->key_verification_event;
+
+  /* We should have an m.key.verification.start event to get commitment */
+  g_return_val_if_fail (event, NULL);
+  cm_olm_sas_create_commitment (self);
+
+  event = cm_event_new (CM_M_KEY_VERIFICATION_ACCEPT);
+  cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client));
+  self->key_verification_accept = event;
+
+  root = olm_sas_get_message_json (self, &child);
+  cm_event_set_json (event, root, NULL);
+
+  json_object_set_string_member (child, "hash", "sha256");
+  json_object_set_string_member (child, "method", "m.sas.v1");
+  json_object_set_string_member (child, "key_agreement_protocol", "curve25519-hkdf-sha256");
+  json_object_set_string_member (child, "commitment", self->commitment_str->str);
+  json_object_set_string_member (child, "message_authentication_code", "hkdf-hmac-sha256");
+
+  array = json_array_new ();
+  json_array_add_string_element (array, "emoji");
+  json_array_add_string_element (array, "decimal");
+  json_object_set_array_member (child, "short_authentication_string", array);
+
+  return self->key_verification_accept;
+}
+
+CmEvent *
+cm_olm_sas_get_key_event (CmOlmSas *self)
+{
+  JsonObject *root, *child;
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+  g_return_val_if_fail (self->key_verification_event, NULL);
+
+  if (self->verification_key)
+    return self->verification_key;
+
+  if (cm_event_get_m_type (self->key_verification_event) == CM_M_KEY_VERIFICATION_REQUEST)
+    event = g_object_get_data (G_OBJECT (self->key_verification_event), "start");
+  else
+    event = self->key_verification_event;
+
+  /* We should have an m.key.verification.start event to get key event */
+  g_return_val_if_fail (event, NULL);
+  cm_olm_sas_create_commitment (self);
+
+  event = cm_event_new (CM_M_KEY_VERIFICATION_KEY);
+  cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client));
+  self->verification_key = event;
+
+  root = olm_sas_get_message_json (self, &child);
+  cm_event_set_json (event, root, NULL);
+
+  json_object_set_string_member (child, "key", self->our_pub_key);
+
+  return self->verification_key;
+}
+
+GPtrArray *
+cm_olm_sas_get_emojis (CmOlmSas *self)
+{
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+  g_return_val_if_fail (self->key_verification_event, NULL);
+
+  if (!g_object_get_data (G_OBJECT (self->key_verification_event), "key"))
+    return NULL;
+
+  /* We need to have both keys to have a drink */
+  g_return_val_if_fail (self->our_pub_key, NULL);
+  g_return_val_if_fail (self->their_pub_key, NULL);
+
+  cm_olm_sas_generate_bytes (self);
+
+  if (!self->sas_emojis)
+    {
+      self->sas_emojis = g_ptr_array_sized_new (7);
+
+      for (guint i = 0; i < 7; i++)
+        g_ptr_array_add (self->sas_emojis, g_strdup (emojis[self->sas_emoji_indices[i]]));
+
+      g_object_set_data (G_OBJECT (self->key_verification_event), "emoji", self->sas_emojis);
+      g_object_set_data (G_OBJECT (self->key_verification_event), "decimal", self->sas_decimals);
+    }
+
+  return self->sas_emojis;
+}
+
+CmEvent *
+cm_olm_sas_get_mac_event (CmOlmSas *self)
+{
+  g_autoptr(GString) base_info = NULL;
+  g_autofree char *key_id = NULL;
+  g_autofree char *keys = NULL;
+  g_autofree char *mac = NULL;
+  JsonObject *root, *content, *mac_json;
+  const char *ed25519;
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+  g_return_val_if_fail (self->key_verification_event, NULL);
+  g_return_val_if_fail (self->verification_key, NULL);
+
+  if (self->key_verification_mac)
+    return self->key_verification_mac;
+
+  base_info = g_string_sized_new (1024);
+  g_string_printf (base_info, "MATRIX_KEY_VERIFICATION_MAC%s%s%s%s%s",
+                   cm_client_get_user_id (self->cm_client),
+                   cm_client_get_device_id (self->cm_client),
+                   self->their_user_id, self->their_device_id,
+                   cm_event_get_transaction_id (self->key_verification_event));
+  key_id = g_strconcat ("ed25519:", cm_client_get_device_id (self->cm_client), NULL);
+  g_string_append (base_info, key_id);
+
+  ed25519 = cm_client_get_ed25519_key (self->cm_client);
+  mac = calculate_mac (self, ed25519, base_info->str, base_info->len);
+
+  g_string_truncate (base_info, base_info->len - strlen (key_id));
+  g_string_append (base_info, "KEY_IDS");
+  keys = calculate_mac (self, key_id, base_info->str, base_info->len);
+
+  event = cm_event_new (CM_M_KEY_VERIFICATION_MAC);
+  cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client));
+  self->key_verification_mac = event;
+
+  root = olm_sas_get_message_json (self, &content);
+  mac_json = json_object_new ();
+  json_object_set_string_member (mac_json, key_id, mac);
+  json_object_set_object_member (content, "mac", mac_json);
+  json_object_set_string_member (content, "keys", keys);
+  cm_event_set_json (event, root, NULL);
+
+  return self->key_verification_mac;
+}
+
+CmEvent *
+cm_olm_sas_get_done_event (CmOlmSas *self)
+{
+  JsonObject *root;
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+  g_return_val_if_fail (self->key_verification_event, NULL);
+  g_return_val_if_fail (self->cm_client, NULL);
+
+  if (self->key_verification_done)
+    return self->key_verification_done;
+
+  event = cm_event_new (CM_M_KEY_VERIFICATION_DONE);
+  cm_event_create_txn_id (event, cm_client_pop_event_id (self->cm_client));
+  self->key_verification_done = event;
+
+  root = olm_sas_get_message_json (self, NULL);
+  cm_event_set_json (event, root, NULL);
+
+  return self->key_verification_done;
+}
+
+gboolean
+cm_olm_sas_is_verified (CmOlmSas *self)
+{
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), FALSE);
+
+  return self->verified;
+}
+
+CmDevice *
+cm_olm_sas_get_device (CmOlmSas *self)
+{
+  g_return_val_if_fail (CM_IS_OLM_SAS (self), NULL);
+
+  return self->their_device;
+}
diff --git a/subprojects/libcmatrix/src/cm-olm.c b/subprojects/libcmatrix/src/cm-olm.c
new file mode 100644
index 0000000000000000000000000000000000000000..cf8e70493b982baf1b65f1c9e577eb2c5526bfaf
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-olm.c
@@ -0,0 +1,856 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-olm.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-olm"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define GCRYPT_NO_DEPRECATED
+#include <gcrypt.h>
+#include <olm/olm.h>
+
+#include "cm-utils-private.h"
+#include "cm-db-private.h"
+#include "cm-olm-private.h"
+
+struct _CmOlm
+{
+  GObject                  parent_instance;
+
+  CmDb                    *cm_db;
+  char                    *room_id;
+  GRefString              *sender_id;
+  char                    *device_id;
+  char                    *account_user_id;
+  char                    *account_device_id;
+  OlmAccount              *account;
+
+  char                    *curve_key;
+  char                    *pickle_key;
+  char                    *session_id;
+  char                    *session_key;
+  uint8_t                 *current_session_key;
+  OlmInboundGroupSession  *in_gp_session;
+  OlmOutboundGroupSession *out_gp_session;
+  OlmSession              *olm_session;
+
+  gint64                   created_time;
+  CmSessionType            type;
+  CmOlmState               state;
+};
+
+G_DEFINE_TYPE (CmOlm, cm_olm, G_TYPE_OBJECT)
+
+
+static char *
+cm_olm_get_olm_session_pickle (CmOlm *self)
+{
+  g_autofree char *pickle = NULL;
+  size_t len;
+
+  g_return_val_if_fail (self->pickle_key, NULL);
+
+  if (self->olm_session)
+    {
+      len = olm_pickle_session_length (self->olm_session);
+      pickle = g_malloc (len + 1);
+      olm_pickle_session (self->olm_session, self->pickle_key,
+                          strlen (self->pickle_key),
+                          pickle, len);
+    }
+  else if (self->in_gp_session)
+    {
+      len = olm_pickle_inbound_group_session_length (self->in_gp_session);
+      pickle = g_malloc (len + 1);
+      olm_pickle_inbound_group_session (self->in_gp_session, self->pickle_key,
+                                        strlen (self->pickle_key),
+                                        pickle, len);
+    }
+  else if (self->out_gp_session)
+    {
+      len = olm_pickle_outbound_group_session_length (self->out_gp_session);
+      pickle = g_malloc (len + 1);
+      olm_pickle_outbound_group_session (self->out_gp_session, self->pickle_key,
+                                         strlen (self->pickle_key),
+                                         pickle, len);
+    }
+  else
+    g_return_val_if_reached (NULL);
+
+  pickle[len] = '\0';
+
+  return g_steal_pointer (&pickle);
+}
+
+static void
+cm_olm_finalize (GObject *object)
+{
+  CmOlm *self = (CmOlm *)object;
+
+  g_free (self->pickle_key);
+  g_free (self->curve_key);
+  g_clear_pointer (&self->sender_id, g_ref_string_release);
+  g_free (self->device_id);
+  g_free (self->room_id);
+
+  g_clear_pointer (&self->account_user_id, g_ref_string_release);
+  g_free (self->account_device_id);
+
+  if (self->olm_session)
+    olm_clear_session (self->olm_session);
+  if (self->in_gp_session)
+    olm_clear_inbound_group_session (self->in_gp_session);
+  if (self->out_gp_session)
+    olm_clear_outbound_group_session (self->out_gp_session);
+
+  g_free (self->olm_session);
+  g_free (self->in_gp_session);
+  g_free (self->out_gp_session);
+
+  cm_utils_free_buffer (self->session_key);
+  cm_utils_free_buffer (self->session_id);
+
+  g_clear_object (&self->cm_db);
+
+  G_OBJECT_CLASS (cm_olm_parent_class)->finalize (object);
+}
+
+static void
+cm_olm_class_init (CmOlmClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_olm_finalize;
+}
+
+static void
+cm_olm_init (CmOlm *self)
+{
+}
+
+CmOlm *
+cm_olm_new_from_pickle (char          *pickle,
+                        const char    *pickle_key,
+                        const char    *sender_identity_key,
+                        CmSessionType  session_type)
+{
+  CmOlm *self;
+  g_autofree OlmInboundGroupSession *in_session = NULL;
+  g_autofree OlmOutboundGroupSession *out_session = NULL;
+  g_autofree OlmSession *session = NULL;
+  g_autofree uint8_t *session_key = NULL;
+  size_t err, len;
+
+  if (session_type == SESSION_MEGOLM_V1_IN)
+    {
+      in_session = g_malloc (olm_inbound_group_session_size ());
+      err = olm_unpickle_inbound_group_session (in_session, pickle_key,
+                                                strlen (pickle_key),
+                                                pickle, strlen (pickle));
+      if (err == olm_error ())
+        {
+          g_debug ("Error in group unpickle: %s",
+                   olm_inbound_group_session_last_error (in_session));
+
+          return NULL;
+        }
+    }
+  else if (session_type == SESSION_MEGOLM_V1_OUT)
+    {
+      out_session = g_malloc (olm_outbound_group_session_size ());
+      err = olm_unpickle_outbound_group_session (out_session, pickle_key,
+                                                 strlen (pickle_key),
+                                                 pickle, strlen (pickle));
+      if (err == olm_error ())
+        {
+          g_debug ("Error in group unpickle: %s",
+                   olm_outbound_group_session_last_error (out_session));
+
+          return NULL;
+        }
+
+      len = olm_outbound_group_session_key_length (out_session);
+      session_key = g_malloc (len + 1);
+      len = olm_outbound_group_session_key (out_session, session_key, len);
+      if (len == olm_error ())
+        {
+          g_warning ("Error getting session key: %s",
+                     olm_outbound_group_session_last_error (out_session));
+
+          return NULL;
+        }
+      session_key[len] = '\0';
+    }
+  else
+    {
+      session = g_malloc (olm_session_size ());
+      olm_session (session);
+      err = olm_unpickle_session (session, pickle_key, strlen (pickle_key),
+                                  pickle, strlen (pickle));
+      if (err == olm_error ())
+        return NULL;
+    }
+
+  self = g_object_new (CM_TYPE_OLM, NULL);
+  self->olm_session = g_steal_pointer (&session);
+  self->in_gp_session = g_steal_pointer (&in_session);
+  self->out_gp_session = g_steal_pointer (&out_session);
+  self->session_key = (char *)g_steal_pointer (&session_key);
+  self->curve_key = g_strdup (sender_identity_key);
+  self->pickle_key = g_strdup (pickle_key);
+  self->type = session_type;
+
+  return self;
+}
+
+CmOlm *
+cm_olm_outbound_new (gpointer    olm_account,
+                     const char *curve_key,
+                     const char *one_time_key,
+                     const char *room_id)
+{
+  CmOlm *self;
+  g_autofree OlmSession *session = NULL;
+  cm_gcry_t buffer = NULL;
+  size_t length, error;
+
+  g_return_val_if_fail (olm_account, NULL);
+
+  if (!curve_key || !one_time_key)
+    return NULL;
+
+  session = g_malloc (olm_session_size ());
+  olm_session (session);
+
+  length = olm_create_outbound_session_random_length (session);
+  if (length)
+    buffer = gcry_random_bytes (length, GCRY_STRONG_RANDOM);
+
+  error = olm_create_outbound_session (session,
+                                       olm_account,
+                                       curve_key, strlen (curve_key),
+                                       one_time_key, strlen (one_time_key),
+                                       buffer, length);
+  gcry_free (buffer);
+
+  if (error == olm_error ())
+    {
+      g_warning ("Error creating outbound olm session: %s",
+                 olm_session_last_error (session));
+      return NULL;
+    }
+
+  self = g_object_new (CM_TYPE_OLM, NULL);
+  self->olm_session = g_steal_pointer (&session);
+  self->curve_key = g_strdup (curve_key);
+  self->account = olm_account;
+  self->type = SESSION_OLM_V1_OUT;
+  /* time in milliseconds */
+  self->created_time = time (NULL) * 1000;
+
+  return self;
+}
+
+CmOlm *
+cm_olm_inbound_new (gpointer    olm_account,
+                    const char *sender_identity_key,
+                    const char *one_time_key_message)
+{
+  CmOlm *self = NULL;
+  OlmSession *session = NULL;
+  g_autofree char *message_copy = NULL;
+  size_t err;
+
+  message_copy = g_strdup (one_time_key_message);
+
+  session = g_malloc (olm_session_size ());
+  olm_session (session);
+
+  err = olm_create_inbound_session_from (session, olm_account,
+                                         sender_identity_key, strlen (sender_identity_key),
+                                         message_copy, strlen (message_copy));
+  if (err == olm_error ())
+    {
+      g_warning ("Error creating session: %s", olm_session_last_error (session));
+      olm_clear_session (session);
+      g_free (session);
+
+      return NULL;
+    }
+
+  /* Remove one time keys that are used */
+  err = olm_remove_one_time_keys (olm_account, session);
+  if (err == olm_error ())
+    g_warning ("Error removing key: %s", olm_account_last_error (olm_account));
+
+  self = g_object_new (CM_TYPE_OLM, NULL);
+  self->olm_session = g_steal_pointer (&session);
+  self->curve_key = g_strdup (sender_identity_key);
+  self->account = olm_account;
+  self->type = SESSION_OLM_V1_IN;
+
+  return self;
+}
+
+CmOlm *
+cm_olm_in_group_new (const char *session_key,
+                     const char *sender_identity_key,
+                     const char *session_id)
+{
+  CmOlm *self;
+  g_autofree OlmInboundGroupSession *session = NULL;
+  size_t err;
+
+  session = g_malloc (olm_inbound_group_session_size ());
+  olm_inbound_group_session (session);
+
+  err = olm_init_inbound_group_session (session, (gpointer)session_key,
+                                        strlen (session_key));
+  if (err == olm_error ())
+    {
+      g_warning ("Error creating group session from key: %s",
+                 olm_inbound_group_session_last_error (session));
+      return NULL;
+    }
+
+  self = g_object_new (CM_TYPE_OLM, NULL);
+  self->in_gp_session = g_steal_pointer (&session);
+  self->curve_key = g_strdup (sender_identity_key);
+  self->session_id = g_strdup (session_id);
+  self->session_key = g_strdup (session_key);
+  self->type = SESSION_MEGOLM_V1_IN;
+
+  return self;
+}
+
+CmOlm *
+cm_olm_in_group_new_from_out (CmOlm      *out_group,
+                              const char *sender_identity_key)
+{
+  CmOlm *self;
+
+  g_assert (CM_IS_OLM (out_group));
+  g_assert (out_group->out_gp_session);
+
+  self = cm_olm_in_group_new (out_group->session_key,
+                              sender_identity_key,
+                              out_group->session_id);
+  cm_olm_set_account_details (self, out_group->account_user_id,
+                              out_group->account_device_id);
+  cm_olm_set_sender_details (self, out_group->room_id, out_group->sender_id);
+  cm_olm_set_key (self, out_group->pickle_key);
+  cm_olm_set_db (self, out_group->cm_db);
+  self->created_time = out_group->created_time;
+
+  return self;
+}
+
+CmOlm *
+cm_olm_out_group_new (const char *sender_identity_key)
+{
+  CmOlm *self;
+  g_autofree OlmOutboundGroupSession *session = NULL;
+  g_autofree uint8_t *session_key = NULL;
+  g_autofree uint8_t *session_id = NULL;
+  uint8_t *random = NULL;
+  size_t length, error;
+
+  /* Initialize session */
+  session = g_malloc (olm_outbound_group_session_size ());
+  olm_outbound_group_session (session);
+
+  /* Feed in random bits */
+  length = olm_init_outbound_group_session_random_length (session);
+  if (length)
+    random = gcry_random_bytes (length, GCRY_STRONG_RANDOM);
+  error = olm_init_outbound_group_session (session, random, length);
+  gcry_free (random);
+
+  if (error == olm_error ())
+    {
+      g_warning ("Error init out group session: %s", olm_outbound_group_session_last_error (session));
+
+      return NULL;
+    }
+
+  /* Get session key */
+  length = olm_outbound_group_session_key_length (session);
+  session_key = g_malloc (length + 1);
+  length = olm_outbound_group_session_key (session, session_key, length);
+  if (length == olm_error ())
+    {
+      g_warning ("Error getting session key: %s", olm_outbound_group_session_last_error (session));
+
+      return NULL;
+    }
+  session_key[length] = '\0';
+
+  self = g_object_new (CM_TYPE_OLM, NULL);
+  self->curve_key = g_strdup (sender_identity_key);
+  self->out_gp_session = g_steal_pointer (&session);
+  self->session_id = (char *)g_steal_pointer (&session_id);
+  self->session_key = (char *)g_steal_pointer (&session_key);
+  self->created_time = time (NULL) * 1000;
+  self->type = SESSION_MEGOLM_V1_OUT;
+
+  return self;
+}
+
+CmSessionType
+cm_olm_get_session_type (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), 0);
+
+  return self->type;
+}
+
+size_t
+cm_olm_get_message_index (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), 0);
+  g_return_val_if_fail (self->out_gp_session, 0);
+
+  return olm_outbound_group_session_message_index (self->out_gp_session);
+}
+
+gint64
+cm_olm_get_created_time (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), 0);
+
+  return self->created_time;
+}
+
+/**
+ * cm_olm_set_state:
+ * @self: A #CmOlm
+ * @state: A #CmOlmState
+ *
+ * Set the usability state of @self.
+ */
+void
+cm_olm_set_state (CmOlm      *self,
+                  CmOlmState  state)
+{
+  g_return_if_fail (CM_IS_OLM (self));
+
+  if (state == self->state)
+    return;
+
+  g_return_if_fail (self->state == OLM_STATE_USABLE);
+  self->state = state;
+}
+
+CmOlmState
+cm_olm_get_state (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), OLM_STATE_NOT_SET);
+
+  return self->state;
+}
+
+void
+cm_olm_update_validity (CmOlm  *self,
+                        guint   count,
+                        gint64  duration)
+{
+  g_return_if_fail (CM_IS_OLM (self));
+  g_return_if_fail (count);
+  g_return_if_fail (duration > 0);
+
+  if (cm_olm_get_message_index (self) >= count ||
+      cm_olm_get_created_time (self) + duration <= time (NULL) * 1000)
+    cm_olm_set_state (self, OLM_STATE_ROTATED);
+}
+
+void
+cm_olm_set_sender_details (CmOlm      *self,
+                           const char *room_id,
+                           GRefString *sender_id)
+{
+  g_return_if_fail (CM_IS_OLM (self));
+  g_return_if_fail (sender_id && *sender_id == '@');
+  g_return_if_fail (!self->sender_id);
+
+  self->room_id = g_strdup (room_id);
+  self->sender_id = g_ref_string_acquire (sender_id);
+}
+
+void
+cm_olm_set_account_details (CmOlm      *self,
+                            GRefString *account_user_id,
+                            const char *account_device_id)
+{
+  g_return_if_fail (CM_IS_OLM (self));
+  g_return_if_fail (account_user_id && *account_user_id == '@');
+  g_return_if_fail (account_device_id && *account_device_id);
+  g_return_if_fail (!self->account_user_id);
+  g_return_if_fail (!self->account_device_id);
+
+  self->account_user_id = g_ref_string_acquire (account_user_id);
+  self->account_device_id = g_strdup (account_device_id);
+}
+
+void
+cm_olm_set_db (CmOlm    *self,
+               gpointer  cm_db)
+{
+  g_return_if_fail (CM_IS_OLM (self));
+  g_return_if_fail (CM_IS_DB (cm_db));
+  g_return_if_fail (!self->cm_db);
+
+  self->cm_db = g_object_ref (cm_db);
+}
+
+void
+cm_olm_set_key (CmOlm      *self,
+                const char *key)
+{
+  g_return_if_fail (CM_IS_OLM (self));
+  g_return_if_fail (key && *key);
+  g_return_if_fail (!self->pickle_key);
+
+  self->pickle_key = g_strdup (key);
+}
+
+gboolean
+cm_olm_save (CmOlm *self)
+{
+  char *pickle;
+
+  g_return_val_if_fail (CM_IS_OLM (self), FALSE);
+  g_return_val_if_fail (self->cm_db, FALSE);
+  g_return_val_if_fail (self->pickle_key, FALSE);
+  g_return_val_if_fail (self->account_user_id, FALSE);
+  g_return_val_if_fail (self->account_device_id, FALSE);
+
+  pickle = cm_olm_get_olm_session_pickle (self);
+  g_return_val_if_fail (pickle && *pickle, FALSE);
+
+  return cm_db_add_session (self->cm_db, self, pickle);
+}
+
+char *
+cm_olm_encrypt (CmOlm      *self,
+                const char *plain_text)
+{
+  g_autofree char *encrypted = NULL;
+  size_t len;
+
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+  g_assert (self->olm_session || self->out_gp_session);
+
+  if (!plain_text)
+    return NULL;
+
+  if (self->olm_session)
+    {
+      cm_gcry_t random = NULL;
+      size_t rand_len;
+
+      rand_len = olm_encrypt_random_length (self->olm_session);
+      if (rand_len)
+        random = gcry_random_bytes (rand_len, GCRY_STRONG_RANDOM);
+
+      len = olm_encrypt_message_length (self->olm_session, strlen (plain_text));
+      encrypted = g_malloc (len + 1);
+      len = olm_encrypt (self->olm_session, plain_text, strlen (plain_text),
+                         random, rand_len, encrypted, len);
+      gcry_free (random);
+    }
+  else if (self->out_gp_session)
+    {
+      len = olm_group_encrypt_message_length (self->out_gp_session, strlen (plain_text));
+      encrypted = g_malloc (len + 1);
+      len = olm_group_encrypt (self->out_gp_session,
+                               (gpointer)plain_text, strlen (plain_text),
+                               (gpointer)encrypted, len);
+    }
+  else
+    g_return_val_if_reached (NULL);
+
+  if (len == olm_error ())
+    {
+      const char *error = NULL;
+
+      if (self->olm_session)
+        error = olm_session_last_error (self->olm_session);
+      else if (self->out_gp_session)
+        error = olm_outbound_group_session_last_error (self->out_gp_session);
+
+      if (error)
+        g_warning ("Error encrypting: %s", error);
+
+      return NULL;
+    }
+  encrypted[len] = '\0';
+
+  return g_steal_pointer (&encrypted);
+}
+
+static char *
+session_decrypt (CmOlm      *self,
+                 size_t      type,
+                 const char *ciphertext)
+{
+  g_autofree char *plaintext = NULL;
+  char *copy;
+  size_t len;
+
+  g_assert (CM_IS_OLM (self));
+  g_assert (self->olm_session);
+
+  copy = g_strdup (ciphertext);
+  len = olm_decrypt_max_plaintext_length (self->olm_session,
+                                          type, copy, strlen (copy));
+  g_free (copy);
+
+  if (len == olm_error ())
+    {
+      g_warning ("Error getting max length: %s",
+                 olm_session_last_error (self->olm_session));
+
+      return NULL;
+    }
+
+  copy = g_strdup (ciphertext);
+  plaintext = g_malloc (len + 1);
+  len = olm_decrypt (self->olm_session, type, copy,
+                     strlen (copy), plaintext, len);
+  g_free (copy);
+
+  if (len == olm_error ())
+    {
+      g_warning ("Error decrypting: %s",
+                 olm_session_last_error (self->olm_session));
+
+      return NULL;
+    }
+
+  plaintext[len] = '\0';
+
+  return g_steal_pointer (&plaintext);
+}
+
+static char *
+group_session_decrypt (CmOlm      *self,
+                       const char *ciphertext)
+{
+  g_autofree char *plaintext = NULL;
+  char *copy;
+  size_t len;
+
+  g_assert (CM_IS_OLM (self));
+  g_assert (self->in_gp_session);
+
+  copy = g_strdup (ciphertext);
+  len = olm_group_decrypt_max_plaintext_length (self->in_gp_session,
+                                                (gpointer)copy, strlen (copy));
+  g_free (copy);
+
+  plaintext = g_malloc (len + 1);
+  copy = g_strdup (ciphertext);
+  len = olm_group_decrypt (self->in_gp_session, (gpointer)copy, strlen (copy),
+                           (gpointer)plaintext, len, NULL);
+  g_free (copy);
+
+  if (len == olm_error ())
+    {
+      g_warning ("Error decrypting: %s",
+                 olm_inbound_group_session_last_error (self->in_gp_session));
+      return NULL;
+    }
+
+  plaintext[len] = '\0';
+
+  return g_steal_pointer (&plaintext);
+}
+
+char *
+cm_olm_decrypt (CmOlm      *self,
+                size_t      type,
+                const char *message)
+{
+  g_assert (CM_IS_OLM (self));
+  g_return_val_if_fail (message, NULL);
+
+  if (self->olm_session)
+    return session_decrypt (self, type, message);
+
+  if (self->in_gp_session)
+    return group_session_decrypt (self, message);
+
+  return NULL;
+}
+
+size_t
+cm_olm_get_message_type (CmOlm *self)
+{
+  g_assert (CM_IS_OLM (self));
+  g_assert (self->olm_session);
+
+  return olm_encrypt_message_type (self->olm_session);
+}
+
+const char *
+cm_olm_get_session_id (CmOlm *self)
+{
+  size_t len;
+
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+
+  if (!self->session_id)
+    {
+      void *session_id = NULL;
+
+      if (self->olm_session)
+        {
+          len = olm_session_id_length (self->olm_session);
+          session_id = g_malloc (len + 1);
+          olm_session_id (self->olm_session, session_id, len);
+        }
+      else if (self->out_gp_session)
+        {
+          len = olm_outbound_group_session_id_length (self->out_gp_session);
+          session_id = g_malloc (len + 1);
+          olm_outbound_group_session_id (self->out_gp_session, session_id, len);
+        }
+      else if (self->in_gp_session)
+        {
+          len = olm_inbound_group_session_id_length (self->in_gp_session);
+          session_id = g_malloc (len + 1);
+          olm_inbound_group_session_id (self->in_gp_session, session_id, len);
+        }
+
+      if (session_id)
+        ((char *)session_id)[len] = '\0';
+      self->session_id = session_id;
+    }
+
+  return self->session_id;
+}
+
+/**
+ * cm_olm_get_session_key:
+ * @self: A #CmOlm of type %SESSION_MEGOLM_V1_OUT
+ *
+ * Get the session for the next message to be sent.
+ * The session key shall change after a message sent
+ *
+ * The session key can be used to decrypt future
+ * messages, but not the past ones.
+ *
+ * Returns: The session key string
+ */
+const char *
+cm_olm_get_session_key (CmOlm *self)
+{
+  size_t len;
+
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+
+  /* Each message is sent with a different ratchet key.  So regenerate the
+   * so that ratchet key that will be used for the next message shall be
+   * returned.  We want let the other party see only the messages sent
+   * after this session key, not the past ones.
+  */
+  cm_utils_free_buffer ((char *)self->current_session_key);
+  self->current_session_key = NULL;
+
+  if (self->out_gp_session)
+    {
+      len = olm_outbound_group_session_key_length (self->out_gp_session);
+      self->current_session_key = g_malloc (len + 1);
+      olm_outbound_group_session_key (self->out_gp_session,
+                                      self->current_session_key, len);
+      self->current_session_key[len] = '\0';
+    }
+
+  return (char *)self->current_session_key;
+}
+
+const char *
+cm_olm_get_room_id (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+
+  return self->room_id;
+}
+
+const char *
+cm_olm_get_sender_key (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+
+  return self->curve_key;
+}
+
+GRefString *
+cm_olm_get_account_id (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+
+  return self->account_user_id;
+}
+
+const char *
+cm_olm_get_account_device (CmOlm *self)
+{
+  g_return_val_if_fail (CM_IS_OLM (self), NULL);
+
+  return self->account_device_id;
+}
+
+gpointer
+cm_olm_match_olm_session (const char     *body,
+                          gsize           body_len,
+                          size_t          message_type,
+                          const char     *pickle,
+                          const char     *pickle_key,
+                          const char     *sender_identify_key,
+                          CmSessionType   session_type,
+                          char          **out_decrypted)
+{
+  g_autoptr(CmOlm) self = NULL;
+  g_autofree char *pickle_copy = NULL;
+
+  g_assert (out_decrypted);
+
+  pickle_copy = g_strdup (pickle);
+  self = cm_olm_new_from_pickle (pickle_copy, pickle_key, sender_identify_key, session_type);
+
+  if (!self)
+    return NULL;
+
+  /* If it's a pre key message, check if the session matches */
+  if (message_type == OLM_MESSAGE_TYPE_PRE_KEY)
+    {
+      g_autofree char *body_copy = NULL;
+      size_t match = 0;
+
+      body_copy = g_malloc (body_len + 1);
+      memcpy (body_copy, body, body_len + 1);
+      match = olm_matches_inbound_session (self->olm_session, body_copy, body_len);
+      /* If it doesn't match, don't consider using it for decryption */
+      if (match != 1)
+        return NULL;
+    }
+
+  /* Try decrypting with the given session */
+  *out_decrypted = cm_olm_decrypt (self, message_type, body);
+
+  if (*out_decrypted)
+    return g_steal_pointer (&self);
+
+  return NULL;
+}
diff --git a/subprojects/libcmatrix/src/cm-room-private.h b/subprojects/libcmatrix/src/cm-room-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..551f03c6a5d882e5bcc163f5ffdc65150b01c92d
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-room-private.h
@@ -0,0 +1,99 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <json-glib/json-glib.h>
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-room.h"
+
+G_BEGIN_DECLS
+
+CmRoom       *cm_room_new                          (const char          *room_id);
+CmRoom       *cm_room_new_from_json                (const char          *room_id,
+                                                    JsonObject          *root,
+                                                    CmEvent             *last_event);
+char         *cm_room_get_json                     (CmRoom              *self);
+const char   *cm_room_get_replacement_room         (CmRoom              *self);
+CmClient     *cm_room_get_client                   (CmRoom              *self);
+void          cm_room_set_client                   (CmRoom              *self,
+                                                    CmClient            *client);
+gboolean      cm_room_has_state_sync               (CmRoom              *self);
+GPtrArray    *cm_room_set_data                     (CmRoom              *self,
+                                                    JsonObject          *object);
+JsonObject   *cm_room_decrypt                      (CmRoom              *self,
+                                                    JsonObject          *root);
+void          cm_room_add_events                   (CmRoom              *self,
+                                                    GPtrArray           *events,
+                                                    gboolean             append);
+void          cm_room_user_changed                 (CmRoom              *self,
+                                                    GPtrArray           *changed_users);
+const char   *cm_room_get_prev_batch               (CmRoom              *self);
+void          cm_room_set_prev_batch               (CmRoom              *self,
+                                                    const char          *prev_batch);
+void          cm_room_set_name                     (CmRoom              *self,
+                                                    const char          *name);
+void          cm_room_set_generated_name           (CmRoom              *self,
+                                                    const char          *name);
+gint64        cm_room_get_encryption_rotation_time (CmRoom              *self);
+CmStatus      cm_room_get_status                   (CmRoom              *self);
+void          cm_room_set_status                   (CmRoom              *self,
+                                                    CmStatus             status);
+guint         cm_room_get_encryption_msg_count     (CmRoom              *self);
+gboolean      cm_room_is_direct                    (CmRoom              *self);
+void          cm_room_set_is_direct                (CmRoom              *self,
+                                                    gboolean             is_direct);
+void          cm_room_get_name_async               (CmRoom              *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+char         *cm_room_get_name_finish              (CmRoom              *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void          cm_room_load_async                   (CmRoom              *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean      cm_room_load_finish                  (CmRoom              *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void          cm_room_save                         (CmRoom              *self);
+void          cm_room_load_joined_members_async    (CmRoom              *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean      cm_room_load_joined_members_finish   (CmRoom              *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void          cm_room_is_encrypted_async           (CmRoom              *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean      cm_room_is_encrypted_finish          (CmRoom              *self,
+                                                    GAsyncResult        *result,
+                                                   GError             **error);
+void          cm_room_load_prev_batch_async        (CmRoom              *self,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+GPtrArray    *cm_room_load_prev_batch_finish       (CmRoom              *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+CmUser       *cm_room_find_user                    (CmRoom              *self,
+                                                    GRefString          *matrix_id,
+                                                    gboolean             add_if_missing);
+void          cm_room_update_user                  (CmRoom              *self,
+                                                    CmEvent             *event);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-room.c b/subprojects/libcmatrix/src/cm-room.c
new file mode 100644
index 0000000000000000000000000000000000000000..337974f98f018a7d4fdb365ff20e6c8517875236
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-room.c
@@ -0,0 +1,2588 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-room"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-input-stream-private.h"
+#include "cm-client-private.h"
+#include "cm-utils-private.h"
+#include "cm-net-private.h"
+#include "cm-enc-private.h"
+#include "cm-common.h"
+#include "events/cm-event-private.h"
+#include "events/cm-room-event-list-private.h"
+#include "events/cm-room-message-event-private.h"
+#include "users/cm-room-member-private.h"
+#include "users/cm-room-member.h"
+#include "users/cm-user.h"
+#include "users/cm-user-private.h"
+#include "users/cm-user-list-private.h"
+#include "cm-room-private.h"
+#include "cm-room.h"
+
+#define KEY_TIMEOUT         10000 /* milliseconds */
+#define TYPING_TIMEOUT      4     /* seconds */
+
+struct _CmRoom
+{
+  GObject parent_instance;
+
+  CmRoomEventList *room_event;
+  GListStore *joined_members;
+  GHashTable *joined_members_table;
+  GListStore *invited_members;
+  GHashTable *invited_members_table;
+
+  /* key: GRefString (user_id), value: #GPtrArray of #CmDevice */
+  /* Shall store only devices that are added */
+  /* Reset if any device/user is removed/left */
+  GHashTable   *changed_devices;
+  /* array of #CmUser */
+  GPtrArray    *changed_users;
+  /* array of #CmUserKey */
+  GPtrArray    *one_time_keys;
+  GCancellable *enc_cancellable;
+  JsonObject *local_json;
+  CmClient   *client;
+  char       *name;
+  char       *generated_name;
+  char       *past_name;
+  char       *room_id;
+  char       *replacement_room;
+  char       *encryption;
+  char       *prev_batch;
+
+  GQueue     *message_queue;
+  guint       retry_timeout_id;
+  gint        unread_count;
+
+  CmStatus    room_status;
+  gboolean    has_prev_batch;
+  gboolean    is_direct;
+
+  /* Use g_get_monotonic_time(), we only need the interval */
+  gint64     typing_set_time;
+  /* set doesn't mean the user has typing state set,
+   * also compare with typing_set_time and TYPING_TIMEOUT */
+  gboolean   typing;
+
+  gboolean    loading_initial_sync;
+  gboolean    loading_past_events;
+  gboolean    db_save_pending;
+  gboolean    is_sending_message;
+  gboolean    name_loaded;
+  gboolean    name_loading;
+  gboolean    joined_members_loading;
+  gboolean    joined_members_loaded;
+  gboolean    querying_keys;
+  gboolean    claiming_keys;
+  gboolean    keys_claimed;
+  gboolean    uploading_keys;
+  gboolean    initial_sync_done;
+
+  gboolean    is_accepting_invite;
+  gboolean    is_rejecting_invite;
+  gboolean    invite_accept_success;
+  gboolean    invite_reject_success;
+};
+
+G_DEFINE_TYPE (CmRoom, cm_room, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_ENCRYPTED,
+  PROP_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties[N_PROPS];
+
+/* static gboolean room_resend_message          (gpointer user_data); */
+static void     room_send_message_from_queue (CmRoom *self);
+
+static CmUser *
+room_find_user (CmRoom     *self,
+                GRefString *matrix_id,
+                gboolean    add_if_missing)
+{
+  CmUserList *user_list;
+  GListModel *model;
+  CmUser *user = NULL;
+
+  g_assert (CM_IS_ROOM (self));
+  g_assert (matrix_id && *matrix_id == '@');
+  g_return_val_if_fail (self->client, NULL);
+
+  user_list = cm_client_get_user_list (self->client);
+  user = cm_user_list_find_user (user_list, matrix_id, add_if_missing);
+  model = G_LIST_MODEL (self->joined_members);
+
+  if (user &&
+      !cm_utils_get_item_position (model, user, NULL))
+    {
+      if (cm_room_is_encrypted (self))
+        g_ptr_array_add (self->changed_users, g_object_ref (user));
+
+      g_list_store_append (self->joined_members, user);
+      g_hash_table_insert (self->joined_members_table,
+                           g_ref_string_acquire (matrix_id),
+                           g_object_ref (user));
+    }
+
+  return user;
+}
+
+static char *
+cm_room_generate_name (CmRoom *self)
+{
+  GListModel *model;
+  const char *name_a = NULL, *name_b = NULL;
+  guint n_items, count;
+
+  g_assert (CM_IS_ROOM (self));
+
+  model = G_LIST_MODEL (self->joined_members);
+  count = n_items = g_list_model_get_n_items (model);
+
+  if (n_items == 1)
+    {
+      g_autoptr(CmUser) user = NULL;
+
+      user = g_list_model_get_item (model, 0);
+
+      /* Don't add self to create room name */
+      if (cm_user_get_id (user) == cm_client_get_user_id (self->client))
+        count = n_items = 0;
+    }
+
+  if (!n_items)
+    {
+      model = G_LIST_MODEL (self->invited_members);
+      count = n_items = g_list_model_get_n_items (model);
+    }
+  for (guint i = 0; i < MIN (3, n_items); i++) {
+    g_autoptr(CmUser) user = NULL;
+
+    user = g_list_model_get_item (model, i);
+
+    /* Don't add self to create room name */
+    if (cm_user_get_id (user) == cm_client_get_user_id (self->client))
+      {
+        count--;
+        continue;
+      }
+
+    if (!name_a) {
+      name_a = cm_user_get_display_name (user);
+
+      if (!name_a || !*name_a)
+        name_a = cm_user_get_id (user);
+    } else {
+      name_b = cm_user_get_display_name (user);
+
+      if (!name_b || !*name_b)
+        name_b = cm_user_get_id (user);
+    }
+  }
+
+  /* fixme: Depend on gettext and make these strings translatable */
+  if (count == 0)
+    return g_strdup ("Empty room");
+
+  if (count == 1)
+    return g_strdup (name_a);
+
+  if (count == 2)
+    return g_strdup_printf ("%s and %s", name_a ?: "", name_b ?: "");
+
+  return g_strdup_printf ("%s and %u other(s)", name_a ?: "", count - 1);
+}
+
+static void
+room_add_event_to_db (CmRoom  *self,
+                      CmEvent *event)
+{
+  g_autoptr(GPtrArray) events = NULL;
+
+  g_assert (CM_IS_ROOM (self));
+  g_assert (CM_IS_EVENT (event));
+
+  events = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (events, g_object_ref (event));
+  cm_db_add_room_events (cm_client_get_db (self->client),
+                         self, events, FALSE);
+}
+
+static void
+room_load_device_keys_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  g_autoptr(CmRoom) self = user_data;
+  g_autoptr(GPtrArray) users = NULL;
+  g_autoptr(GError) error = NULL;
+
+  users = cm_user_list_load_devices_finish (CM_USER_LIST (object), result, &error);
+  self->querying_keys = FALSE;
+
+  g_debug ("(%p) Load user devices %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Load user devices error: %s", self, error->message);
+    }
+  else
+    {
+      room_send_message_from_queue (self);
+    }
+}
+
+static void
+room_claim_keys_cb (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  g_autoptr(CmRoom) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GPtrArray *keys;
+
+  g_clear_pointer (&self->one_time_keys, g_ptr_array_unref);
+  keys = cm_user_list_claim_keys_finish (CM_USER_LIST (object), result, &error);
+  self->one_time_keys = keys;
+  self->claiming_keys = FALSE;
+
+  g_debug ("(%p) Claim keys %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) claim keys error: %s", self, error->message);
+    }
+  else
+    {
+      self->keys_claimed = TRUE;
+      room_send_message_from_queue (self);
+    }
+}
+
+static void
+room_upload_keys_cb (GObject      *object,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  g_autoptr(CmRoom) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  cm_user_list_upload_keys_finish (CM_USER_LIST (object), result, &error);
+  self->uploading_keys = FALSE;
+  g_debug ("(%p) Upload keys %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Upload keys error: %s", self, error->message);
+    }
+  else
+    {
+      g_ptr_array_set_size (self->one_time_keys, 0);
+      room_send_message_from_queue (self);
+    }
+}
+
+static void
+ensure_encryption_keys (CmRoom *self)
+{
+  CmUserList *user_list;
+
+  g_return_if_fail (cm_room_is_encrypted (self));
+
+  if (self->enc_cancellable &&
+      g_cancellable_is_cancelled (self->enc_cancellable))
+    g_clear_object (&self->enc_cancellable);
+
+  if (!self->enc_cancellable)
+    self->enc_cancellable = g_cancellable_new ();
+
+  if (self->joined_members_loading || self->querying_keys ||
+      self->claiming_keys || self->uploading_keys)
+    return;
+
+  user_list = cm_client_get_user_list (self->client);
+
+  if (!self->joined_members_loaded)
+    cm_room_load_joined_members_async (self, self->enc_cancellable, NULL, NULL);
+  else if (self->changed_users->len)
+    {
+      self->querying_keys = TRUE;
+      self->keys_claimed = FALSE;
+      g_debug ("(%p) Load user devices", self);
+      cm_user_list_load_devices_async (user_list, self->changed_users,
+                                       room_load_device_keys_cb,
+                                       g_object_ref (self));
+    }
+  else if (!self->keys_claimed ||
+           (self->changed_devices && g_hash_table_size (self->changed_devices)))
+    {
+      g_autoptr(GHashTable) users = NULL;
+      GHashTable *table;
+
+      self->claiming_keys = TRUE;
+
+      table = g_hash_table_new_full (g_direct_hash,
+                                     g_direct_equal,
+                                     (GDestroyNotify)g_ref_string_release,
+                                     (GDestroyNotify)g_ptr_array_unref);
+
+      if (g_hash_table_size (self->changed_devices))
+        {
+          g_debug ("(%p) Has %u changed users for claiming keys",
+                   self, g_hash_table_size (self->changed_devices));
+          users = g_steal_pointer (&self->changed_devices);
+          self->changed_devices = table;
+        }
+      else
+        {
+          GListModel *members;
+
+          members = G_LIST_MODEL (self->joined_members);
+          users = table;
+
+          g_debug ("(%p) Has %u room users for claiming keys",
+                   self, g_list_model_get_n_items (members));
+          for (guint i = 0; i < g_list_model_get_n_items (members); i++)
+            {
+              g_autoptr(CmUser) user = NULL;
+              GListModel *device_list;
+              GRefString *user_id;
+              GPtrArray *devices;
+
+              user = g_list_model_get_item (members, i);
+              devices = g_ptr_array_new_full (32, g_object_unref);
+              device_list = cm_user_get_devices (user);
+
+              for (guint j = 0; j < g_list_model_get_n_items (device_list); j++)
+                g_ptr_array_add (devices, g_list_model_get_item (device_list, j));
+
+              user_id = g_ref_string_acquire (cm_user_get_id (user));
+              g_hash_table_insert (users, user_id, devices);
+            }
+        }
+
+      g_debug ("(%p) Claim keys for %u users", self, g_hash_table_size (users));
+      cm_user_list_claim_keys_async (user_list, self, users,
+                                     room_claim_keys_cb,
+                                     g_object_ref (self));
+    }
+  else
+    {
+      if (!self->one_time_keys || !self->one_time_keys->len)
+        {
+          g_warning ("(%p) no keys uploaded, and no keys left to upload", self);
+          return;
+        }
+
+      self->uploading_keys = TRUE;
+      g_debug ("(%p) Upload keys", self);
+      cm_user_list_upload_keys_async (user_list, self,
+                                      self->one_time_keys,
+                                      room_upload_keys_cb,
+                                      g_object_ref (self));
+    }
+}
+
+static void
+cm_room_get_property (GObject    *object,
+                      guint       prop_id,
+                      GValue     *value,
+                      GParamSpec *pspec)
+{
+  CmRoom *self = (CmRoom *)object;
+
+  switch (prop_id)
+    {
+    case PROP_ENCRYPTED:
+      g_value_set_boolean (value, cm_room_is_encrypted (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, cm_room_get_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+cm_room_finalize (GObject *object)
+{
+  CmRoom *self = (CmRoom *)object;
+
+  if (self->enc_cancellable)
+    g_cancellable_cancel (self->enc_cancellable);
+  g_clear_object (&self->enc_cancellable);
+
+  g_hash_table_unref (self->joined_members_table);
+  g_clear_object (&self->joined_members);
+
+  g_hash_table_unref (self->invited_members_table);
+  g_clear_object (&self->invited_members);
+
+  g_clear_pointer (&self->one_time_keys, g_ptr_array_unref);
+
+  g_clear_object (&self->client);
+  g_free (self->room_id);
+
+  g_free (self->name);
+  g_free (self->generated_name);
+
+  g_queue_free_full (self->message_queue, g_object_unref);
+
+  G_OBJECT_CLASS (cm_room_parent_class)->finalize (object);
+}
+
+static void
+cm_room_class_init (CmRoomClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = cm_room_get_property;
+  object_class->finalize = cm_room_finalize;
+
+  properties[PROP_ENCRYPTED] =
+    g_param_spec_string ("encrypted",
+                         "encrypted",
+                         "Whether room is encrypted",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  properties[PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The room name",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+cm_room_init (CmRoom *self)
+{
+  self->room_event = cm_room_event_list_new (self);
+  self->one_time_keys = g_ptr_array_new_full (32, g_free);
+  self->changed_users = g_ptr_array_new_full (32, g_object_unref);
+  self->joined_members = g_list_store_new (CM_TYPE_USER);
+  self->changed_devices = g_hash_table_new_full (g_direct_hash,
+                                                 g_direct_equal,
+                                                 (GDestroyNotify)g_ref_string_release,
+                                                 (GDestroyNotify)g_ptr_array_unref);
+  self->joined_members_table = g_hash_table_new_full (g_direct_hash,
+                                                      g_direct_equal,
+                                                      (GDestroyNotify)g_ref_string_release,
+                                                      g_object_unref);
+  self->invited_members = g_list_store_new (CM_TYPE_USER);
+  self->invited_members_table = g_hash_table_new_full (g_direct_hash,
+                                                       g_direct_equal,
+                                                       (GDestroyNotify)g_ref_string_release,
+                                                       g_object_unref);
+  self->message_queue = g_queue_new ();
+}
+
+CmRoom *
+cm_room_new (const char *room_id)
+{
+  CmRoom *self;
+
+  g_return_val_if_fail (room_id && *room_id, NULL);
+
+  self = g_object_new (CM_TYPE_ROOM, NULL);
+  self->room_id = g_strdup (room_id);
+
+  return self;
+}
+
+/*
+ * cm_room_new_from_json:
+ * @room_id: room id string
+ * @root: (nullable): A JsonObject with "local" object
+ * last_event: (nullable) (transfer full): A #CmEvent
+ *
+ * Create a new #CmRoom. @last_event is the
+ * last sync event in the room.
+ */
+CmRoom *
+cm_room_new_from_json (const char *room_id,
+                       JsonObject *root,
+                       CmEvent    *last_event)
+{
+  g_autoptr(GString) str = NULL;
+  CmRoom *self;
+
+  self = cm_room_new (room_id);
+
+  str = g_string_new (NULL);
+  cm_utils_anonymize (str, room_id);
+  CM_TRACE ("(%p) new room '%s' from json", self, str->str);
+
+  if (root)
+    {
+      JsonObject *local, *child;
+
+      self->local_json = root;
+      self->initial_sync_done = TRUE;
+      local = cm_utils_json_object_get_object (root, "local");
+      self->name = g_strdup (cm_utils_json_object_get_string (local, "alias"));
+      self->generated_name = cm_utils_json_object_dup_string (local, "generated_alias");
+      self->past_name = cm_utils_json_object_dup_string (local, "past_alias");
+      cm_room_set_is_direct (self, cm_utils_json_object_get_bool (local, "direct"));
+      self->encryption = cm_utils_json_object_dup_string (local, "encryption");
+      child = cm_utils_json_object_get_object (local, "unread_notifications");
+      self->unread_count = cm_utils_json_object_get_int (child, "highlight_count");
+
+      cm_room_event_list_set_local_json (self->room_event, root, last_event);
+
+      if (last_event)
+        g_debug ("(%p) Added 1 event from db", self);
+    }
+
+  return self;
+}
+
+static JsonObject *
+room_generate_json (CmRoom *self)
+{
+  JsonObject *json, *child;
+
+  g_assert (CM_IS_ROOM (self));
+
+  json = cm_room_event_list_get_local_json (self->room_event);
+
+  child = cm_utils_json_object_get_object (json, "local");
+
+  json_object_set_string_member (child, "generated_alias", self->generated_name);
+  if (self->past_name)
+    json_object_set_string_member (child, "past_alias", self->past_name);
+  json_object_set_string_member (child, "alias", cm_room_get_name (self));
+  /* Alias set before the current one, may be used if current one is NULL (eg: was x) */
+  json_object_set_string_member (child, "last_alias", cm_room_get_name (self));
+  json_object_set_boolean_member (child, "direct", cm_room_is_direct (self));
+  json_object_set_int_member (child, "encryption", cm_room_is_encrypted (self));
+
+  return json;
+}
+
+/*
+ * cm_room_get_json:
+ *
+ * Get the json which is to be stored
+ * in db as such
+ */
+char *
+cm_room_get_json (CmRoom *self)
+{
+  JsonObject *json;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  json = room_generate_json (self);
+
+  return cm_utils_json_object_to_string (json, FALSE);
+}
+
+/*
+ * cm_room_get_replacement_room:
+ * @self: A #CmRoom
+ *
+ * Get the id of the room this room has been
+ * replaced with, which means that this room
+ * has been obsolete and should no longer
+ * be used for conversations.
+ *
+ * Returns: (nullable): The replacement room
+ * if this room has tombstone
+ */
+const char *
+cm_room_get_replacement_room (CmRoom *self)
+{
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+
+  event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_TOMBSTONE);
+  if (!event)
+    return NULL;
+
+  return cm_room_event_get_replacement_room_id ((gpointer)event);
+}
+
+static void
+room_user_changed (CmRoom     *self,
+                   CmUser     *user,
+                   GPtrArray  *added,
+                   GPtrArray  *removed,
+                   CmUserList *user_list)
+{
+  GRefString *user_id;
+
+  g_assert (CM_IS_ROOM (self));
+  g_assert (CM_IS_USER (user));
+  g_assert (CM_IS_USER_LIST (user_list));
+
+  /* User changes has to be tracked only if the room is encrypted */
+  if (!cm_room_is_encrypted (self))
+    return;
+
+  user_id = cm_user_get_id (user);
+
+  if (!g_hash_table_contains (self->joined_members_table, user_id))
+    return;
+
+  CM_TRACE ("(%p) user changed, added: %u, removed: %u", self,
+           added ? added->len : 0, removed ? removed->len : 0);
+
+  /* If any device got removed, create a new key and invalidate old */
+  if (removed && removed->len)
+    {
+      guint n_items;
+
+      cm_enc_rm_room_group_key (cm_client_get_enc (self->client), self);
+
+      self->keys_claimed = FALSE;
+      g_ptr_array_set_size (self->changed_users, 0);
+      g_hash_table_remove_all (self->changed_devices);
+      n_items = g_list_model_get_n_items (G_LIST_MODEL (self->joined_members));
+
+      for (guint i = 0; i < n_items; i++)
+        {
+          CmUser *member;
+
+          member = g_list_model_get_item (G_LIST_MODEL (self->joined_members), i);
+          g_ptr_array_add (self->changed_users, member);
+        }
+
+      return;
+    }
+
+  if (added && added->len)
+    {
+      GPtrArray *devices;
+
+      devices = g_hash_table_lookup (self->changed_devices, user);
+
+      if (devices)
+        {
+          g_ptr_array_extend (devices, added, (gpointer)g_object_ref, NULL);
+        }
+      else
+        {
+          g_ref_string_acquire (user_id);
+          devices = g_ptr_array_copy (added, (gpointer)g_object_ref, NULL);
+          g_hash_table_insert (self->changed_devices, user_id, devices);
+        }
+    }
+}
+
+void
+cm_room_set_client (CmRoom   *self,
+                    CmClient *client)
+{
+  CmUserList *user_list;
+
+  g_return_if_fail (CM_IS_CLIENT (client));
+  g_return_if_fail (!self->client);
+
+  self->client = g_object_ref (client);
+  user_list = cm_client_get_user_list (client);
+
+  g_signal_connect_object (user_list, "user-changed",
+                           G_CALLBACK (room_user_changed),
+                           self, G_CONNECT_SWAPPED);
+  cm_room_event_list_set_client (self->room_event, client);
+}
+
+CmClient *
+cm_room_get_client (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  return self->client;
+}
+
+gboolean
+cm_room_has_state_sync (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+
+  return self->initial_sync_done;
+}
+
+/**
+ * cm_room_get_id:
+ * @self: A #CmRoom
+ *
+ * Get the matrix room id.
+ *
+ * Returns: The room id
+ */
+const char *
+cm_room_get_id (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  return self->room_id;
+}
+
+gboolean
+cm_room_self_has_power_for_event (CmRoom      *self,
+                                  CmEventType  type)
+{
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+
+  event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_POWER_LEVELS);
+  if (!event)
+    return FALSE;
+
+  return cm_room_event_user_has_power (CM_ROOM_EVENT (event),
+                                       cm_client_get_user_id (self->client),
+                                       type);
+}
+
+/**
+ * cm_room_get_name:
+ * @self: A #CmRoom
+ *
+ * Get the matrix room name.  Can be %NULL
+ * if the room name is not set, or the name
+ * is not yet loaded.
+ *
+ * Returns: (nullable): The room name
+ */
+const char *
+cm_room_get_name (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  if (!self->generated_name && !self->name)
+    {
+      g_autofree char *name = NULL;
+
+      name = cm_room_generate_name (self);
+      cm_room_set_generated_name (self, name);
+      cm_room_save (self);
+    }
+
+  if (self->name)
+    return self->name;
+
+  return self->generated_name;
+}
+
+/**
+ * cm_room_get_past_name:
+ * @self: A #CmRoom
+ *
+ * Get the matrix room name set before the current one.
+ * The past name is set only if the room is a direct
+ * room and current room name is empty (in the case
+ * where cm_room_get_name() shall return "Empty room")
+ *
+ * Returns: (nullable): The past room name
+ */
+const char *
+cm_room_get_past_name (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  if (!self->name &&
+      g_strcmp0 (self->generated_name, "Empty room") == 0)
+    return self->past_name;
+
+  return NULL;
+}
+
+/**
+ * cm_room_is_encrypted:
+ * @self: A #CmRoom
+ *
+ * Get if the matrix room @self is encrypted
+ * or not.
+ *
+ * Returns: %TRUE if @self is encrypted.
+ * %FALSE otherwise.
+ */
+gboolean
+cm_room_is_encrypted (CmRoom *self)
+{
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), TRUE);
+
+  if (self->encryption)
+    return TRUE;
+
+  event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_ENCRYPTION);
+
+  return !!event;
+}
+
+GListModel *
+cm_room_get_joined_members (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  return G_LIST_MODEL (self->joined_members);
+}
+
+GListModel *
+cm_room_get_events_list (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  return cm_room_event_list_get_events (self->room_event);
+}
+
+gint64
+cm_room_get_unread_notification_counts (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), 0);
+
+  return self->unread_count;
+}
+
+CmStatus
+cm_room_get_status (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), CM_STATUS_UNKNOWN);
+
+  return self->room_status;
+}
+
+void
+cm_room_set_status (CmRoom   *self,
+                    CmStatus  status)
+{
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (status == CM_STATUS_INVITE ||
+                    status == CM_STATUS_JOIN ||
+                    status == CM_STATUS_LEAVE);
+
+  if (self->room_status == status)
+    return;
+
+  self->room_status = status;
+
+  if (self->client)
+    {
+      self->db_save_pending = TRUE;
+      cm_room_save (self);
+    }
+}
+
+JsonObject *
+cm_room_decrypt (CmRoom     *self,
+                 JsonObject *root)
+{
+  char *plain_text = NULL;
+  JsonObject *content;
+  CmEnc *enc;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  enc = cm_client_get_enc (self->client);
+
+  if (!enc || !root)
+    return NULL;
+
+  content = cm_utils_json_object_get_object (root, "content");
+  plain_text = cm_enc_handle_join_room_encrypted (enc, self, content);
+
+  return cm_utils_string_to_json_object (plain_text);
+}
+
+void
+cm_room_add_events (CmRoom    *self,
+                    GPtrArray *events,
+                    gboolean   append)
+{
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  cm_room_event_list_add_events (self->room_event, events, append);
+}
+
+GPtrArray *
+cm_room_set_data (CmRoom     *self,
+                  JsonObject *object)
+{
+  g_autoptr(GPtrArray) events = NULL;
+  JsonObject *child, *local;
+  JsonArray *array;
+  guint length = 0;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+  g_return_val_if_fail (object, NULL);
+
+  child = cm_utils_json_object_get_object (object, "unread_notifications");
+
+  if (child)
+    {
+      local = cm_room_event_list_get_local_json (self->room_event);
+      json_object_set_object_member (local, "unread_notifications", json_object_ref (child));
+      self->unread_count = cm_utils_json_object_get_int (child, "notification_count");
+    }
+
+  events = g_ptr_array_new_full (100, g_object_unref);
+  child = cm_utils_json_object_get_object (object, "state");
+  cm_room_event_list_parse_events (self->room_event, child, NULL, FALSE);
+
+  child = cm_utils_json_object_get_object (object, "invite_state");
+  cm_room_event_list_parse_events (self->room_event, child, NULL, FALSE);
+
+  child = cm_utils_json_object_get_object (object, "timeline");
+  cm_room_event_list_parse_events (self->room_event, child, events, FALSE);
+  CM_TRACE ("(%p) New timeline events count: %u", self, events->len);
+
+  if (cm_utils_json_object_get_bool (child, "limited"))
+    {
+      const char *prev;
+
+      prev = cm_utils_json_object_get_string (child, "prev_batch");
+      cm_room_set_prev_batch (self, prev);
+    }
+  self->db_save_pending = TRUE;
+
+  length = 0;
+  array = cm_utils_json_object_get_array (object, "left");
+
+  if (array)
+    length = json_array_get_length (array);
+
+  if (length)
+    {
+      CM_TRACE ("(%p) %u users left", self, length);
+      cm_enc_rm_room_group_key (cm_client_get_enc (self->client), self);
+    }
+
+  for (guint i = 0; i < length; i++)
+    {
+      g_autoptr(GRefString) user_id = NULL;
+      CmRoomMember *member;
+      const char *member_id;
+
+      member_id = json_array_get_string_element (array, i);
+      user_id = g_ref_string_new_intern (member_id);
+      member = g_hash_table_lookup (self->joined_members_table, user_id);
+
+      if (member)
+        {
+          g_hash_table_remove (self->joined_members_table, user_id);
+          cm_utils_remove_list_item (self->joined_members, member);
+          self->keys_claimed = FALSE;
+        }
+    }
+
+  self->initial_sync_done = TRUE;
+  cm_room_save (self);
+
+  return g_steal_pointer (&events);
+}
+
+/*
+ * cm_room_user_changed:
+ * @self: A #CmRoom
+ * @changed_users: An array of #CmUser
+ *
+ * Inform that the user devices for @changed_users has
+ * has changed. The function simply returns if any of
+ * the user in @changed_users is not in the room
+ *
+ * This is useful for encrypted rooms where the user
+ * devices has to be updated before sending anything
+ * encrypted.
+ */
+void
+cm_room_user_changed (CmRoom     *self,
+                      GPtrArray  *changed_users)
+{
+  GRefString *user_id;
+  CmUser *user;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (changed_users);
+
+  /* We need to track the user changes only if room is encrypted */
+  if (!cm_room_is_encrypted (self))
+    return;
+
+  for (guint i = 0; i < changed_users->len; i++)
+    {
+      user = changed_users->pdata[i];
+      user_id = cm_user_get_id (user);
+
+      if (g_hash_table_contains (self->joined_members_table, user_id) &&
+          !g_ptr_array_find (self->changed_users, user, NULL))
+        g_ptr_array_add (self->changed_users, g_object_ref (user));
+    }
+
+  g_debug ("(%p) Room user(s) changed, count: %u", self, self->changed_users->len);
+}
+
+const char *
+cm_room_get_prev_batch (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  return self->prev_batch;
+}
+
+void
+cm_room_set_prev_batch (CmRoom     *self,
+                        const char *prev_batch)
+{
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  g_free (self->prev_batch);
+  self->prev_batch = g_strdup (prev_batch);
+}
+
+void
+cm_room_set_name (CmRoom     *self,
+                  const char *name)
+{
+  JsonObject *json, *child;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  if (g_strcmp0 (name, self->name) == 0)
+    return;
+
+  g_free (self->name);
+  self->name = g_strdup (name);
+
+  json = room_generate_json (self);
+  child = cm_utils_json_object_get_object (json, "local");
+
+  if (name)
+    json_object_set_string_member (child, "alias", name);
+  else
+    json_object_remove_member (child, "alias");
+
+  self->db_save_pending = TRUE;
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+}
+
+void
+cm_room_set_generated_name (CmRoom     *self,
+                            const char *name)
+{
+  JsonObject *local, *child;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  if (g_strcmp0 (name, self->generated_name) == 0)
+    return;
+
+  g_free (self->generated_name);
+  self->generated_name = g_strdup (name);
+
+  local = cm_room_event_list_get_local_json (self->room_event);
+  child = cm_utils_json_object_get_object (local, "local");
+  json_object_set_string_member (child, "generated_alias", name);
+
+  self->db_save_pending = TRUE;
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+}
+
+/* cm_room_get_encryption_rotation_time:
+ *
+ * Return the time in milliseconds after which the key
+ * should be rotated after the first key use.
+ *
+ * The key should be regenerated regardless of whether
+ * the maximum message count for the rotation has reached
+ * or not.
+ */
+gint64
+cm_room_get_encryption_rotation_time (CmRoom *self)
+{
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), 60 * 60 * 24 * 7);
+
+  event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_ENCRYPTION);
+  if (!event)
+    return 60 * 60 * 24 * 7;
+
+  return cm_room_event_get_rotation_time ((gpointer)event);
+}
+
+/* cm_room_get_encryption_msg_count:
+ *
+ * Return the number of messages after which the encryption
+ * key for sending messages in the room should be rotated.
+ */
+guint
+cm_room_get_encryption_msg_count (CmRoom *self)
+{
+  CmEvent *event;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), 100);
+
+  event = cm_room_event_list_get_event (self->room_event, CM_M_ROOM_ENCRYPTION);
+  if (!event)
+    return 100;
+
+  return cm_room_event_get_rotation_count ((gpointer)event);
+}
+
+gboolean
+cm_room_is_direct (CmRoom *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+
+  return self->is_direct;
+}
+
+void
+cm_room_set_is_direct (CmRoom   *self,
+                       gboolean  is_direct)
+{
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  self->is_direct = !!is_direct;
+}
+
+static void
+send_cb (GObject      *obj,
+         GAsyncResult *result,
+         gpointer      user_data)
+{
+  CmRoom *self;
+  GTask *message_task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+  const char *event_id = NULL;
+  CmEvent *event;
+
+  g_assert (G_IS_TASK (message_task));
+
+  self = g_task_get_source_object (message_task);
+  event = g_task_get_task_data (message_task);
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  event_id = cm_utils_json_object_get_string (object, "event_id");
+  g_debug ("(%p) Send message %s. txn-id: '%s'", self,
+           CM_LOG_SUCCESS (!error), cm_event_get_txn_id (event));
+
+  self->is_sending_message = FALSE;
+
+  if (error)
+    {
+      cm_event_set_state (event, CM_EVENT_STATE_SENDING_FAILED);
+      g_debug ("(%p) Send message error: %s", self, error->message);
+      g_task_return_error (message_task, error);
+    }
+  else
+    {
+      cm_event_set_state (event, CM_EVENT_STATE_SENT);
+      room_add_event_to_db (self, event);
+
+      /* Set event after saving to db so that event id is not stored in db
+       * and we replace id less events when we sync, so that the event is
+       * placed in the right order.
+       */
+      cm_event_set_id (event, event_id);
+
+      g_task_return_pointer (message_task, g_strdup (event_id), g_free);
+    }
+}
+
+static void
+room_send_file_cb (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *uri = NULL;
+  CmInputStream *stream;
+  char *mxc_uri = NULL;
+  GTask *message_task;
+  GFile *message_file;
+  CmRoomMessageEvent *message;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  message_task = g_task_get_task_data (task);
+
+  g_assert (CM_IS_ROOM (self));
+  g_assert (G_TASK (message_task));
+
+  message = g_task_get_task_data (message_task);
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (message));
+
+  mxc_uri = cm_net_put_file_finish (CM_NET (object), result, &error);
+
+  g_debug ("(%p) Upload file %s. txn-id: '%s'", self,
+           CM_LOG_SUCCESS (!error && mxc_uri),
+           cm_event_get_txn_id (CM_EVENT (message)));
+
+  if (!mxc_uri)
+    {
+      self->is_sending_message = FALSE;
+
+      g_task_return_new_error (message_task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Failed to upload file: %s", error->message ?: "");
+      room_send_message_from_queue (self);
+
+      return;
+    }
+
+  stream = g_object_get_data (G_OBJECT (result), "stream");
+  g_assert (stream);
+  g_object_ref (stream);
+
+  message_file = cm_room_message_event_get_file (message);
+  g_assert (G_IS_FILE (message_file));
+
+  g_object_set_data_full (G_OBJECT (message_file), "uri", mxc_uri, g_free);
+  g_object_set_data_full (G_OBJECT (message_file), "stream", stream, g_object_unref);
+  g_object_set_data_full (G_OBJECT (stream), "uri", g_strdup (mxc_uri), g_free);
+
+  uri = cm_event_get_api_url (CM_EVENT (message), self);
+
+  cm_net_send_json_async (cm_client_get_net (self->client), 0,
+                          cm_event_generate_json (CM_EVENT (message), self),
+                          uri, SOUP_METHOD_PUT, NULL, g_task_get_cancellable (message_task),
+                          send_cb, message_task);
+}
+
+static void
+room_send_message_from_queue (CmRoom *self)
+{
+  CmRoomMessageEvent *message;
+  GTask *message_task;
+  g_autofree char *uri = NULL;
+
+  g_assert (CM_IS_ROOM (self));
+
+  message_task = g_queue_peek_head (self->message_queue);
+
+  if (!message_task)
+    return;
+
+  if (self->is_sending_message || self->retry_timeout_id)
+    return;
+
+  if (cm_room_is_encrypted (self) &&
+      (!cm_enc_has_room_group_key (cm_client_get_enc (self->client), self) ||
+       self->changed_users->len || !self->keys_claimed ||
+       (self->one_time_keys && self->one_time_keys->len)))
+    {
+      ensure_encryption_keys (self);
+      return;
+    }
+
+  self->is_sending_message = TRUE;
+  message_task = g_queue_pop_head (self->message_queue);
+  message = g_task_get_task_data (message_task);
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (message));
+
+  if (cm_room_message_event_get_msg_type (message) == CM_CONTENT_TYPE_FILE)
+    {
+      GFileProgressCallback progress_cb;
+      gpointer progress_user_data;
+      GTask *task;
+
+      progress_cb = g_object_get_data (G_OBJECT (message_task), "progress-cb");
+      progress_user_data = g_object_get_data (G_OBJECT (message_task), "progress-cb-data");
+
+      task = g_task_new (self, NULL, NULL, NULL);
+      g_task_set_task_data (task, g_object_ref (message_task), g_object_unref);
+      g_debug ("(%p) Upload file, txn-id: '%s'",
+               self, cm_event_get_txn_id (CM_EVENT (message)));
+      cm_net_put_file_async (cm_client_get_net (self->client),
+                             cm_room_message_event_get_file (message),
+                             cm_room_is_encrypted (self),
+                             progress_cb, progress_user_data,
+                             g_task_get_cancellable (message_task),
+                             room_send_file_cb, task);
+      return;
+    }
+
+  uri = cm_event_get_api_url (CM_EVENT (message), self);
+
+  g_debug ("(%p) Send message, txn-id: '%s'",
+           self, cm_event_get_txn_id (CM_EVENT (message)));
+  cm_event_set_state (CM_EVENT (message), CM_EVENT_STATE_SENDING);
+  cm_net_send_json_async (cm_client_get_net (self->client), 0,
+                          cm_event_generate_json (CM_EVENT (message), self),
+                          uri, SOUP_METHOD_PUT, NULL, g_task_get_cancellable (message_task),
+                          send_cb, message_task);
+}
+
+/* todo */
+#if 0
+static gboolean
+room_resend_message (gpointer user_data)
+{
+  CmRoom *self = user_data;
+
+  g_assert (CM_IS_ROOM (self));
+
+  self->retry_timeout_id = 0;
+  room_send_message_from_queue (self);
+
+  return G_SOURCE_REMOVE;
+}
+#endif
+
+static void
+room_accept_invite_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean success;
+
+  self = g_task_get_source_object (task);
+  success = cm_client_join_room_by_id_finish (self->client, result, &error);
+
+  self->invite_accept_success = success;
+  self->is_accepting_invite = FALSE;
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+void
+cm_room_accept_invite_async (CmRoom              *self,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!self->is_accepting_invite);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  g_debug ("(%p) Accept room invite", self);
+
+  if (self->room_status != CM_STATUS_INVITE)
+    {
+      g_debug ("(%p) Accept room invite error, room is not invite", self);
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE,
+                               "Room is not in invite state");
+      return;
+    }
+
+  if (self->invite_accept_success)
+    {
+      g_debug ("(%p) Accept room invite already succeeded", self);
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (self->invite_accept_success)
+    {
+      g_debug ("(%p) Accept room error, user has already accepted invite", self);
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE,
+                               "User has already accepted invite");
+      return;
+    }
+
+  if (self->is_accepting_invite)
+    {
+      g_debug ("(%p) Accept room, already in progress", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
+                               "Accept room invite in progress");
+      return;
+    }
+
+  self->is_accepting_invite = TRUE;
+
+  cm_client_join_room_by_id_async (self->client, self->room_id,
+                                   cancellable,
+                                   room_accept_invite_cb,
+                                   g_steal_pointer (&task));
+}
+
+gboolean
+cm_room_accept_invite_finish (CmRoom        *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+room_reject_invite_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean success;
+
+  self = g_task_get_source_object (task);
+  success = cm_room_leave_finish (self, result, &error);
+
+  self->invite_reject_success = success;
+  self->is_rejecting_invite = FALSE;
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, success);
+}
+
+void
+cm_room_reject_invite_async (CmRoom              *self,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!self->is_accepting_invite);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+
+  g_debug ("(%p) Reject room invite", self);
+
+  if (self->room_status != CM_STATUS_INVITE)
+    {
+      g_debug ("(%p) Reject room invite error, room is not invite", self);
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE,
+                               "Room is not in invite state");
+      return;
+    }
+
+  if (self->invite_reject_success)
+    {
+      g_debug ("(%p) Reject room invite already succeeded", self);
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (self->invite_accept_success)
+    {
+      g_debug ("(%p) Reject room error, user has already accepted invite", self);
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_INVALID_ROOM_STATE,
+                               "User has already accepted invite");
+      return;
+    }
+
+  if (self->is_rejecting_invite)
+    {
+      g_debug ("(%p) Reject room, already in progress", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
+                               "Reject room invite in progress");
+      return;
+    }
+
+  self->is_rejecting_invite = TRUE;
+
+  cm_room_leave_async (self, cancellable,
+                       room_reject_invite_cb,
+                       g_steal_pointer (&task));
+}
+
+gboolean
+cm_room_reject_invite_finish (CmRoom        *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/**
+ * cm_room_send_text_async:
+ * @self: A #CmRoom
+ * @text: The text message to send
+ * @cancellable: (nullable): A #Gcancellable
+ * @callback: A #GasyncReadyCallback
+ * @user_data: The user data for @callback.
+ *
+ * Send @text as a text message to the room
+ * @self.  If the room is encrypted, the text
+ * shall be encrypted and sent.
+ *
+ * Returns: The event id string used for the event.
+ * This can be used to track the event when received
+ * via the /sync callback or so.
+ */
+const char *
+cm_room_send_text_async (CmRoom              *self,
+                         const char          *text,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  CmRoomMessageEvent *message;
+  CmUser *user;
+  GTask *task;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  message = cm_room_message_event_new (CM_CONTENT_TYPE_TEXT);
+  cm_event_set_state (CM_EVENT (message), CM_EVENT_STATE_WAITING);
+  cm_room_message_event_set_body (message, text);
+  cm_event_create_txn_id (CM_EVENT (message),
+                          cm_client_pop_event_id (self->client));
+  g_task_set_task_data (task, message, g_object_unref);
+
+  user = room_find_user (self, cm_client_get_user_id (self->client), TRUE);
+  cm_event_set_sender (CM_EVENT (message), user);
+
+  g_debug ("(%p) Queue send text message, txn-id: '%s'",
+           self, cm_event_get_txn_id (CM_EVENT (message)));
+  cm_room_event_list_append_event (self->room_event, CM_EVENT (message));
+  room_add_event_to_db (self, CM_EVENT (message));
+
+  g_queue_push_tail (self->message_queue, task);
+
+  room_send_message_from_queue (self);
+
+  return cm_event_get_txn_id (CM_EVENT (message));
+}
+
+/**
+ * cm_room_send_text_finish:
+ * @self: A #CmRoom
+ * @result: A #GAsyncResult
+ * @error: (nullable): A #GError
+ *
+ * Finish the call to cm_room_send_text_async().
+ *
+ * Returns: The event id string used for the event
+ * on success. %NULL on error.  This is the same
+ * as the one you get from cm_room_send_text_async().
+ * Free with g_free().
+ */
+char *
+cm_room_send_text_finish (CmRoom       *self,
+                          GAsyncResult *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+const char *
+cm_room_send_file_async (CmRoom                *self,
+                         GFile                 *file,
+                         const char            *body,
+                         GFileProgressCallback  progress_callback,
+                         gpointer               progress_user_data,
+                         GCancellable          *cancellable,
+                         GAsyncReadyCallback    callback,
+                         gpointer               user_data)
+{
+  CmRoomMessageEvent *message;
+  CmUser *user;
+  GTask *task;
+
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "progress-cb", progress_callback);
+  g_object_set_data (G_OBJECT (task), "progress-cb-data", progress_user_data);
+
+  message = cm_room_message_event_new (CM_CONTENT_TYPE_FILE);
+  cm_event_set_state (CM_EVENT (message), CM_EVENT_STATE_WAITING);
+  cm_room_message_event_set_file (message, body, file);
+  cm_event_create_txn_id (CM_EVENT (message),
+                          cm_client_pop_event_id (self->client));
+  g_task_set_task_data (task, message, g_object_unref);
+
+  user = room_find_user (self, cm_client_get_user_id (self->client), TRUE);
+  cm_event_set_sender (CM_EVENT (message), user);
+
+  g_debug ("(%p) Queue send file message, txn-id: '%s'",
+           self, cm_event_get_txn_id (CM_EVENT (message)));
+  cm_room_event_list_append_event (self->room_event, CM_EVENT (message));
+
+  g_queue_push_tail (self->message_queue, task);
+
+  room_send_message_from_queue (self);
+
+  return cm_event_get_txn_id (CM_EVENT (message));
+}
+
+char *
+cm_room_send_file_finish (CmRoom        *self,
+                          GAsyncResult  *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+send_typing_cb (GObject      *obj,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_TASK (task));
+  self = g_task_get_source_object (task);
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  CM_TRACE ("(%p) Set typing to '%s' %s", self,
+            CM_LOG_BOOL (self->typing), CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      self->typing = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "was-typing"));
+      self->typing_set_time = GPOINTER_TO_SIZE (g_object_get_data (G_OBJECT (task), "was-typing-time"));
+      g_debug ("(%p) Set typing error: %s", self, error->message);
+    }
+}
+
+/**
+ * cm_room_set_typing_notice_async:
+ * @self: A #CmRoom
+ * @typing: set/unset typing
+ * @cancellable: (nullable): A #Gcancellable
+ * @callback: A #GasyncReadyCallback
+ * @user_data: The user data for @callback.
+ *
+ * Set/Unset if the self user is typing or not.
+ * The typing set is timeout after 4 seconds.
+ */
+void
+cm_room_set_typing_notice_async (CmRoom              *self,
+                                 gboolean             typing,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autofree char *uri = NULL;
+  JsonObject *object;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "was-typing", GINT_TO_POINTER (self->typing));
+  g_object_set_data (G_OBJECT (task), "was-typing-time", GSIZE_TO_POINTER (self->typing_set_time));
+
+  if (typing == self->typing &&
+      g_get_monotonic_time () - self->typing_set_time < TYPING_TIMEOUT * G_USEC_PER_SEC)
+    {
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  CM_TRACE ("(%p) Set typing to '%s'", self, CM_LOG_BOOL (typing));
+  self->typing_set_time = g_get_monotonic_time ();
+  self->typing = !!typing;
+
+  /* https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-typing-userid */
+  object = json_object_new ();
+  json_object_set_boolean_member (object, "typing", !!typing);
+  if (typing)
+    json_object_set_int_member (object, "timeout", TYPING_TIMEOUT);
+
+  uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id,
+                     "/typing/", cm_client_get_user_id (self->client), NULL);
+
+  cm_net_send_json_async (cm_client_get_net (self->client), 0, object,
+                          uri, SOUP_METHOD_PUT,
+                          NULL, cancellable, send_typing_cb, task);
+}
+
+/**
+ * cm_room_set_typing_notice_finish:
+ * @self: A #CmRoom
+ * @result: A #GAsyncResult
+ * @error: (nullable): A #GError
+ *
+ * Finish the call to cm_room_set_typing_notice_async().
+ *
+ * Returns: %TRUE if typing notice was successfully
+ * set, %FALSE otherwise.
+ */
+gboolean
+cm_room_set_typing_notice_finish (CmRoom        *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+room_set_encryption_cb (GObject      *obj,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  self = g_task_get_source_object (task);
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Enable encryption %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_warning ("(%p) Enable encryption failed, error: %s", self, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      const char *event;
+
+      event = cm_utils_json_object_get_string (object, "event_id");
+      self->encryption = g_strdup ("encrypted");
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_ENCRYPTED]);
+      self->db_save_pending = TRUE;
+      cm_room_save (self);
+      g_task_return_boolean (task, !!event);
+    }
+}
+
+/**
+ * cm_room_enable_encryption_async:
+ * @self: A #CmRoom
+ * @cancellable: (nullable): A #GCancellable
+ * @callback: A #GAsyncReadyCallback
+ * @user_data: user data passed to @callback
+ *
+ * Enable encryption for @self.  You can't disable
+ * encryption once enabled.  Also, it's a noop
+ * if @self has already enabled encryption.  The
+ * @callback shall run in any case.
+ *
+ * To get the result, finish the call with
+ * cm_room_enable_encryption_finish().
+ */
+void
+cm_room_enable_encryption_async (CmRoom              *self,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+    g_autofree char *uri = NULL;
+    JsonObject *object;
+    GTask *task;
+
+    g_return_if_fail (CM_IS_ROOM (self));
+
+    task = g_task_new (self, cancellable, callback, user_data);
+    g_debug ("(%p) Enable encryption", self);
+
+    if (cm_room_is_encrypted (self))
+      {
+        g_debug ("(%p) Enable encryption. Already encrypted, ignored", self);
+        g_task_return_boolean (task, TRUE);
+        return;
+      }
+
+    object = json_object_new ();
+    json_object_set_string_member (object, "algorithm", ALGORITHM_MEGOLM);
+    uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/state/m.room.encryption", NULL);
+    cm_net_send_json_async (cm_client_get_net (self->client), 2, object, uri, SOUP_METHOD_PUT,
+                            NULL, cancellable, room_set_encryption_cb, task);
+}
+
+/**
+ * cm_room_enable_encryption_finish:
+ * @self: A #CmRoom
+ * @result: A #GAsyncResult
+ * @error: (nullable): A #GError
+ *
+ * Finish the call to cm_room_enable_encryption_async().
+ *
+ * Returns: %TRUE if encryption was enabled,
+ * %FALSE otherwise.
+ */
+gboolean
+cm_room_enable_encryption_finish (CmRoom        *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+room_leave_cb (GObject      *obj,
+               GAsyncResult *result,
+               gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  g_debug ("(%p) Leave room %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    g_debug ("(%p) Leave room error: %s", self, error->message);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_room_leave_async (CmRoom              *self,
+                     GCancellable        *cancellable,
+                     GAsyncReadyCallback  callback,
+                     gpointer             user_data)
+{
+    GTask *task;
+    g_autofree char *uri = NULL;
+
+    g_return_if_fail (CM_IS_ROOM (self));
+
+    g_debug ("(%p) leave room", self);
+    task = g_task_new (self, cancellable, callback, user_data);
+    uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/leave", self->room_id);
+    cm_net_send_json_async (cm_client_get_net (self->client), 1, NULL,
+                            uri, SOUP_METHOD_POST,
+                            NULL, cancellable, room_leave_cb, task);
+}
+
+gboolean
+cm_room_leave_finish (CmRoom        *self,
+                      GAsyncResult  *result,
+                      GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+room_set_read_marker_cb (GObject      *obj,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  self = g_task_get_source_object (task);
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  CM_TRACE ("(%p) Set read marker %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_room_set_read_marker_async (CmRoom              *self,
+                               CmEvent             *fully_read_event,
+                               CmEvent             *read_receipt_event,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  const char *fully_read_id, *read_receipt_id;
+  g_autofree char *uri = NULL;
+  JsonObject *root;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (CM_IS_EVENT (fully_read_event));
+  g_return_if_fail (CM_IS_EVENT (read_receipt_event));
+
+  fully_read_id = cm_event_get_id (fully_read_event);
+  read_receipt_id = cm_event_get_id (read_receipt_event);
+
+  root = json_object_new ();
+  json_object_set_string_member (root, "m.fully_read", fully_read_id);
+  json_object_set_string_member (root, "m.read", read_receipt_id);
+
+  task = g_task_new (self, NULL, callback, user_data);
+
+  CM_TRACE ("(%p) Set read marker", self);
+  uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/read_markers", self->room_id);
+  cm_net_send_json_async (cm_client_get_net (self->client), 0, root,
+                          uri, SOUP_METHOD_POST,
+                          NULL, NULL, room_set_read_marker_cb, task);
+}
+
+gboolean
+cm_room_set_read_marker_finish (CmRoom        *self,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+room_load_prev_batch_cb (GObject      *obj,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GPtrArray *events = NULL;
+  GError *error = NULL;
+  const char *end;
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Load prev batch %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Load prev batch error: %s", self, error->message);
+      g_task_return_error (task, error);
+      return;
+    }
+
+  end = cm_utils_json_object_get_string (object, "end");
+
+  /* If start and end are same, we have reached the start of room history */
+  if (g_strcmp0 (cm_utils_json_object_get_string (object, "end"),
+                 cm_utils_json_object_get_string (object, "start")) == 0)
+    end = NULL;
+
+  cm_room_set_prev_batch (self, end);
+  self->db_save_pending = TRUE;
+  cm_room_save (self);
+
+  events = g_ptr_array_new_full (64, g_object_unref);
+  cm_room_event_list_parse_events (self->room_event, object, events, TRUE);
+  cm_db_add_room_events (cm_client_get_db (self->client), self, events, TRUE);
+  g_debug ("(%p) Load prev batch events: %u", self, events->len);
+
+  g_task_return_pointer (task, events, (GDestroyNotify)g_ptr_array_unref);
+}
+
+void
+cm_room_load_prev_batch_async (CmRoom              *self,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autofree char *uri = NULL;
+  const char *prev_batch;
+  GHashTable *query;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  prev_batch = cm_room_get_prev_batch (self);
+  g_debug ("(%p) Load prev batch", self);
+
+  if (!prev_batch)
+    {
+      g_debug ("(%p) Load prev batch error: missing prev_batch", self);
+      g_task_return_pointer (task, NULL, NULL);
+      return;
+    }
+
+  /* Create a query to get past 30 messages */
+  query = g_hash_table_new_full (g_str_hash, g_str_equal, free,
+                                 (GDestroyNotify)cm_utils_free_buffer);
+  g_hash_table_insert (query, g_strdup ("from"), g_strdup (prev_batch));
+  g_hash_table_insert (query, g_strdup ("dir"), g_strdup ("b"));
+  g_hash_table_insert (query, g_strdup ("limit"), g_strdup ("30"));
+  /* if (upto_batch) */
+  /*   g_hash_table_insert (query, g_strdup ("to"), g_strdup (upto_batch)); */
+
+  /* https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-messages */
+  uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/messages", NULL);
+  cm_net_send_json_async (cm_client_get_net (self->client), 0, NULL,
+                          uri, SOUP_METHOD_GET,
+                          query, cancellable, room_load_prev_batch_cb, task);
+}
+
+GPtrArray *
+cm_room_load_prev_batch_finish (CmRoom        *self,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+room_prev_batch_cb (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  GPtrArray *events = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM (self));
+
+  events = cm_room_load_prev_batch_finish (self, result, &error);
+  self->loading_past_events = FALSE;
+  g_debug ("(%p) Load prev batch %s, events: %d", self,
+           CM_LOG_SUCCESS (!error), events ? events->len : 0);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, !!events);
+}
+
+static void
+room_get_past_db_events_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GPtrArray) events = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM (self));
+
+  events = cm_db_get_past_events_finish (CM_DB (object), result, &error);
+  self->loading_past_events = FALSE;
+  g_debug ("(%p) Load db events %s, count: %d", self,
+           CM_LOG_SUCCESS (!error), events ? events->len : 0);
+
+  if (events && events->len)
+    {
+      g_debug ("(%p) Loaded %u db events", self, events->len);
+
+      cm_room_add_events (self, events, FALSE);
+      g_task_return_boolean (task, TRUE);
+    }
+  else if (self->prev_batch)
+    {
+      g_debug ("(%p) Load prev batch", self);
+      self->loading_past_events = TRUE;
+      cm_room_load_prev_batch_async (self, NULL, room_prev_batch_cb,
+                                     g_steal_pointer (&task));
+    }
+  else
+    {
+      g_task_return_boolean (task, FALSE);
+    }
+}
+
+static void
+room_load_sync_cb (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  GAsyncReadyCallback callback;
+  gpointer cb_user_data;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM (self));
+
+  cm_room_load_finish (self, result, &error);
+  g_debug ("(%p) Initial sync before past events %s",
+           self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  callback = g_object_get_data (G_OBJECT (task), "callback");
+  cb_user_data = g_object_get_data (G_OBJECT (task), "cb-user-data");
+  cm_room_load_past_events_async (self, callback, cb_user_data);
+}
+
+void
+cm_room_load_past_events_async (CmRoom              *self,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(CmEvent) from = NULL;
+  g_autoptr(GTask) task = NULL;
+  GListModel *events;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (!from || CM_IS_ROOM_EVENT (from));
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "callback", callback);
+  g_object_set_data (G_OBJECT (task), "cb-user-data", user_data);
+  g_debug ("(%p) Load db events", self);
+
+  if (self->loading_initial_sync || self->loading_past_events)
+    {
+      g_debug ("(%p) Load db events, loading already in progress", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
+                               "Past events are being already loaded");
+      return;
+    }
+
+  if (!self->initial_sync_done)
+    {
+      g_debug ("(%p) Initial sync before loading past events", self);
+      cm_room_load_async (self, NULL,
+                          room_load_sync_cb,
+                          g_steal_pointer (&task));
+      return;
+    }
+
+  self->loading_past_events = TRUE;
+
+  events = cm_room_event_list_get_events (self->room_event);
+  from = g_list_model_get_item (events, 0);
+  cm_db_get_past_events_async (cm_client_get_db (self->client),
+                               self, from,
+                               room_get_past_db_events_cb,
+                               g_steal_pointer (&task));
+}
+
+gboolean
+cm_room_load_past_events_finish (CmRoom        *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+get_joined_members_cb (GObject      *obj,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  CmRoom *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Room load joined members %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      self->joined_members_loading = FALSE;
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        g_debug ("Error getting room members: %s", error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      g_autoptr(GList) members = NULL;
+      JsonObject *joined;
+
+      joined = cm_utils_json_object_get_object (object, "joined");
+      members = json_object_get_members (joined);
+
+      for (GList *member = members; member; member = member->next)
+        {
+          g_autoptr(GRefString) user_id = NULL;
+          CmUser *user;
+          JsonObject *data;
+
+          user_id = g_ref_string_new_intern (member->data);
+          user = g_hash_table_lookup (self->joined_members_table, user_id);
+
+          if (!user)
+            room_find_user (self, user_id, TRUE);
+
+          data = json_object_get_object_member (joined, member->data);
+          cm_user_set_json_data (user, data);
+        }
+
+      /* We have to keep track of user changes only if the room is encrypted */
+      if (cm_room_is_encrypted (self))
+        {
+          CmDb *db;
+
+          db = cm_client_get_db (self->client);
+          cm_db_mark_user_device_change (db, self->client, self->changed_users, TRUE, TRUE);
+        }
+
+      g_debug ("(%p) Load joined members, count; %u", self,
+               g_list_model_get_n_items (G_LIST_MODEL (self->joined_members)));
+      self->joined_members_loaded = TRUE;
+      self->joined_members_loading = FALSE;
+      g_task_return_pointer (task, self->joined_members, NULL);
+
+      /* TODO: handle failures */
+      room_send_message_from_queue (self);
+    }
+}
+
+void
+cm_room_load_joined_members_async (CmRoom              *self,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autofree char *uri = NULL;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_debug ("(%p) Load joined members", self);
+
+  if (self->joined_members_loaded)
+    {
+      g_debug ("(%p) Load joined members, members already loaded", self);
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (self->joined_members_loading)
+    {
+      g_debug ("(%p) Load joined members, members already being loaded", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Members list are already loading");
+      return;
+    }
+
+  self->joined_members_loading = TRUE;
+
+  uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/joined_members", NULL);
+  cm_net_send_json_async (cm_client_get_net (self->client), -1, NULL, uri, SOUP_METHOD_GET,
+                          NULL, cancellable, get_joined_members_cb, task);
+}
+
+gboolean
+cm_room_load_joined_members_finish (CmRoom        *self,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+get_room_state_cb (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(CmRoom) self = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  GError *error = NULL;
+  JsonArray *array;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM (self));
+
+  array = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Load room initial sync %s", self, CM_LOG_SUCCESS (!error));
+  self->loading_initial_sync = FALSE;
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  root = json_object_new ();
+  json_object_set_array_member (root, "events", array);
+  cm_room_event_list_parse_events (self->room_event, root, NULL, FALSE);
+  self->initial_sync_done = TRUE;
+
+  self->db_save_pending = TRUE;
+  cm_room_save (self);
+  g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_room_load_async (CmRoom              *self,
+                    GCancellable        *cancellable,
+                    GAsyncReadyCallback  callback,
+                    gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autofree char *uri = NULL;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_debug ("(%p) Load room initial sync", self);
+
+  if (self->initial_sync_done)
+    {
+      g_debug ("(%p) Load room initial sync already done", self);
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (self->loading_initial_sync)
+    {
+      g_debug ("(%p) Load room initial sync already being loaded", self);
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
+                               "room initial sync is already in progress");
+      return;
+    }
+
+  self->loading_initial_sync = TRUE;
+  uri = g_strconcat ("/_matrix/client/r0/rooms/", self->room_id, "/state", NULL);
+  cm_net_send_json_async (cm_client_get_net (self->client), 0, NULL,
+                          uri, SOUP_METHOD_GET,
+                          NULL, NULL, get_room_state_cb,
+                          g_steal_pointer (&task));
+}
+
+gboolean
+cm_room_load_finish (CmRoom        *self,
+                     GAsyncResult  *result,
+                     GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+save_room_cb (GObject      *object,
+              GAsyncResult *result,
+              gpointer      user_data)
+{
+  g_autoptr(CmRoom) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  if (!cm_db_save_room_finish (CM_DB (object), result, &error))
+    {
+      cm_room_event_list_set_save_pending (self->room_event, TRUE);
+      self->db_save_pending = TRUE;
+      g_warning ("(%p) Saving room details error: %s", self, error->message);
+    }
+}
+
+void
+cm_room_save (CmRoom *self)
+{
+  g_return_if_fail (CM_IS_ROOM (self));
+
+  if (!self->db_save_pending)
+    return;
+
+  self->db_save_pending = FALSE;
+  cm_room_event_list_set_save_pending (self->room_event, FALSE);
+  cm_db_save_room_async (cm_client_get_db (self->client), self->client, self,
+                         save_room_cb,
+                         g_object_ref (self));
+}
+
+CmUser *
+cm_room_find_user (CmRoom     *self,
+                   GRefString *matrix_id,
+                   gboolean    add_if_missing)
+{
+  return room_find_user (self, matrix_id, add_if_missing);
+}
+
+void
+cm_room_update_user (CmRoom  *self,
+                     CmEvent *event)
+{
+  g_autoptr(JsonObject) child = NULL;
+  CmUserList *user_list;
+  CmUser *member = NULL;
+  GRefString *user_id;
+  CmStatus member_status;
+
+  g_return_if_fail (CM_IS_ROOM (self));
+  g_return_if_fail (CM_IS_EVENT (event));
+  g_return_if_fail (cm_event_get_m_type (event) == CM_M_ROOM_MEMBER);
+
+  member_status = cm_room_event_get_status (CM_ROOM_EVENT (event));
+
+  if (member_status == CM_STATUS_JOIN &&
+      cm_event_get_sender_id (event) ==
+      cm_client_get_user_id (self->client))
+    return;
+
+  child = cm_event_get_json (event);
+  g_return_if_fail (child);
+
+  user_id = cm_room_event_get_room_member_id (CM_ROOM_EVENT (event));
+  user_list = cm_client_get_user_list (self->client);
+  member = cm_user_list_find_user (user_list, user_id, TRUE);
+  cm_user_set_json_data (member, child);
+
+  g_debug ("(%p) Updating user %p, status: %d", self, member, member_status);
+
+  if (member_status == CM_STATUS_JOIN)
+    {
+      CmRoomMember *invite;
+
+      invite = g_hash_table_lookup (self->invited_members_table, user_id);
+      if (invite)
+        {
+          g_hash_table_remove (self->invited_members_table, user_id);
+          cm_utils_remove_list_item (self->invited_members, invite);
+        }
+
+      if (g_hash_table_contains (self->joined_members_table, user_id))
+        {
+          CmRoomMember *cm_member;
+
+          cm_member = g_hash_table_lookup (self->joined_members_table, user_id);
+          cm_user_set_json_data (CM_USER (cm_member), child);
+
+          g_free (self->past_name);
+          self->past_name = g_steal_pointer (&self->generated_name);
+          g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+          return;
+        }
+
+      g_list_store_append (self->joined_members, member);
+      g_hash_table_insert (self->joined_members_table,
+                           g_ref_string_acquire (user_id),
+                           g_object_ref (member));
+
+      /* Clear the name so that it will be regenerated when name is requested */
+      g_free (self->past_name);
+      self->past_name = g_steal_pointer (&self->generated_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+    }
+  else if (member_status == CM_STATUS_INVITE)
+    {
+      if (g_hash_table_contains (self->invited_members_table, user_id))
+        {
+          CmRoomMember *cm_member;
+
+          cm_member = g_hash_table_lookup (self->invited_members_table, user_id);
+          cm_user_set_json_data (CM_USER (cm_member), child);
+
+          /* Clear the name so that it will be regenerated when name is requested */
+          g_free (self->past_name);
+          self->past_name = g_steal_pointer (&self->generated_name);
+          g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+          return;
+        }
+
+      g_list_store_append (self->invited_members, member);
+      g_hash_table_insert (self->invited_members_table,
+                           g_ref_string_acquire (user_id),
+                           g_object_ref (member));
+      self->db_save_pending = TRUE;
+
+      /* Clear the name so that it will be regenerated when name is requested */
+      g_free (self->past_name);
+      self->past_name = g_steal_pointer (&self->generated_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+    }
+  else if (member_status == CM_STATUS_LEAVE)
+    {
+      CmRoomMember *join;
+
+      /* Generate a name if it doesn't exist so that we can
+       * use it as the past name if the new name is empty
+       */
+      if (!self->name && !self->generated_name)
+        self->generated_name = cm_room_generate_name (self);
+
+      join = g_hash_table_lookup (self->joined_members_table, user_id);
+
+      if (join)
+        {
+          g_hash_table_remove (self->joined_members_table, user_id);
+          cm_utils_remove_list_item (self->joined_members, join);
+        }
+
+      /* Clear the name so that it will be regenerated when name is requested */
+      g_free (self->past_name);
+      self->past_name = g_steal_pointer (&self->generated_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+      self->db_save_pending = TRUE;
+    }
+}
diff --git a/subprojects/libcmatrix/src/cm-room.h b/subprojects/libcmatrix/src/cm-room.h
new file mode 100644
index 0000000000000000000000000000000000000000..11caf397263cfe29daba9b41809cf6971efa84c7
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-room.h
@@ -0,0 +1,106 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#include "cm-client.h"
+#include "cm-enums.h"
+#include "events/cm-event.h"
+
+#define CM_TYPE_ROOM (cm_room_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmRoom, cm_room, CM, ROOM, GObject)
+
+const char   *cm_room_get_id                      (CmRoom                *self);
+gboolean      cm_room_self_has_power_for_event    (CmRoom                *self,
+                                                   CmEventType            type);
+const char   *cm_room_get_name                    (CmRoom                *self);
+const char   *cm_room_get_past_name               (CmRoom                *self);
+gboolean      cm_room_is_encrypted                (CmRoom                *self);
+GListModel   *cm_room_get_joined_members          (CmRoom                *self);
+GListModel   *cm_room_get_events_list             (CmRoom                *self);
+gint64        cm_room_get_unread_notification_counts  (CmRoom                *self);
+void          cm_room_accept_invite_async         (CmRoom                *self,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_accept_invite_finish        (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+void          cm_room_reject_invite_async         (CmRoom                *self,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_reject_invite_finish        (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+const char   *cm_room_send_text_async             (CmRoom                *self,
+                                                   const char            *text,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+char         *cm_room_send_text_finish            (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+const char   *cm_room_send_file_async             (CmRoom                *self,
+                                                   GFile                 *file,
+                                                   const char            *body,
+                                                   GFileProgressCallback  progress_callback,
+                                                   gpointer               progress_user_data,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+char         *cm_room_send_file_finish            (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+void          cm_room_set_typing_notice_async     (CmRoom                *self,
+                                                   gboolean               typing,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_set_typing_notice_finish    (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+void          cm_room_enable_encryption_async     (CmRoom                *self,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_enable_encryption_finish    (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+void          cm_room_leave_async                 (CmRoom                *self,
+                                                   GCancellable          *cancellable,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_leave_finish                (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+void          cm_room_set_read_marker_async       (CmRoom                *self,
+                                                   CmEvent               *fully_read_event,
+                                                   CmEvent               *read_receipt_event,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_set_read_marker_finish      (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+void          cm_room_load_past_events_async      (CmRoom                *self,
+                                                   GAsyncReadyCallback    callback,
+                                                   gpointer               user_data);
+gboolean      cm_room_load_past_events_finish     (CmRoom                *self,
+                                                   GAsyncResult          *result,
+                                                   GError               **error);
+G_END_DECLS
+
diff --git a/subprojects/libcmatrix/src/cm-secret-store-private.h b/subprojects/libcmatrix/src/cm-secret-store-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..b731104665443b6195315c25660cb6d1aa252e76
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-secret-store-private.h
@@ -0,0 +1,46 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-secret-store.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib.h>
+
+#include "cm-client.h"
+
+#define CM_USERNAME_ATTRIBUTE  "username"
+#define CM_SERVER_ATTRIBUTE    "server"
+#define CM_PROTOCOL_ATTRIBUTE  "protocol"
+
+G_BEGIN_DECLS
+
+void        cm_secret_store_save_async     (CmClient            *client,
+                                            char                *access_token,
+                                            char                *pickle_key,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+gboolean    cm_secret_store_save_finish    (GAsyncResult        *result,
+                                            GError             **error);
+
+void        cm_secret_store_load_async     (GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+GPtrArray  *cm_secret_store_load_finish    (GAsyncResult        *result,
+                                            GError             **error);
+
+void        cm_secret_store_delete_async   (CmClient            *client,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data);
+gboolean    cm_secret_store_delete_finish  (GAsyncResult        *result,
+                                            GError             **error);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-secret-store.c b/subprojects/libcmatrix/src/cm-secret-store.c
new file mode 100644
index 0000000000000000000000000000000000000000..ef164bf4bdac15c0c9f5db39f8f6081c69e6b39f
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-secret-store.c
@@ -0,0 +1,240 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-secret-store.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.0-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-secret-store"
+
+#include <libsecret/secret.h>
+#include <glib/gi18n.h>
+
+#include "cm-matrix-private.h"
+#include "cm-utils-private.h"
+#include "cm-secret-store-private.h"
+
+#define PROTOCOL_MATRIX_STR  "matrix"
+
+static const SecretSchema *
+secret_store_get_schema (void)
+{
+  static SecretSchema *schema;
+  static char *secret_id;
+
+  if (schema)
+    return schema;
+
+  if (!secret_id)
+    secret_id = g_strconcat (cm_matrix_get_app_id (), ".CMatrix", NULL);
+
+  /** SECRET_SCHEMA_DONT_MATCH_NAME is used as a workaround for a bug in gnome-keyring
+   *  which prevents cold keyrings from being searched (and hence does not prompt for unlocking)
+   *  see https://gitlab.gnome.org/GNOME/gnome-keyring/-/issues/89 and
+   *  https://gitlab.gnome.org/GNOME/libsecret/-/issues/7 for more information
+   */
+  schema = secret_schema_new (secret_id, SECRET_SCHEMA_DONT_MATCH_NAME,
+                              CM_USERNAME_ATTRIBUTE, SECRET_SCHEMA_ATTRIBUTE_STRING,
+                              CM_SERVER_ATTRIBUTE,   SECRET_SCHEMA_ATTRIBUTE_STRING,
+                              CM_PROTOCOL_ATTRIBUTE, SECRET_SCHEMA_ATTRIBUTE_STRING,
+                              NULL);
+  return schema;
+}
+
+void
+cm_secret_store_save_async (CmClient            *client,
+                            char                *access_token,
+                            char                *pickle_key,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  const SecretSchema *schema;
+  g_autofree char *label = NULL;
+  const char *server, *old_pass, *username, *device_id;
+  char *password = NULL, *token = NULL, *key = NULL;
+  CmAccount *account;
+  char *credentials;
+
+  g_return_if_fail (CM_IS_CLIENT (client));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  old_pass = cm_client_get_password (client);
+
+  if (old_pass && *old_pass)
+    password = g_strescape (old_pass, NULL);
+  if (access_token && *access_token)
+    token = g_strescape (access_token, NULL);
+  if (pickle_key && *pickle_key)
+    key = g_strescape (pickle_key, NULL);
+
+  account = cm_client_get_account (client);
+  device_id = cm_client_get_device_id (client);
+  username = cm_account_get_login_id (account);
+
+  if (!device_id)
+    device_id = "";
+
+  /* We don't use json APIs here so that we can manage memory better (and securely free them)  */
+  /* TODO: Use a non-pageable memory */
+  /* XXX: We use a dumb string search, so don't change the order or spacing of the format string */
+  credentials = g_strdup_printf ("{\"username\": \"%s\",  \"password\": \"%s\","
+                                 "\"access-token\": \"%s\", "
+                                 "\"pickle-key\": \"%s\", \"device-id\": \"%s\", \"enabled\": \"%s\"}",
+                                 cm_client_get_user_id (client) ?: "",
+                                 password ? password : "", token ? token : "",
+                                 key ? key : "", device_id,
+                                 cm_client_get_enabled (client) ? "true" : "false");
+  schema = secret_store_get_schema ();
+  server = cm_client_get_homeserver (client);
+
+  if (!server)
+    {
+      g_task_report_new_error (NULL, callback, user_data, NULL,
+                               G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Homeserver required to store to db");
+      return;
+    }
+
+  /* todo: translate the string */
+  label = g_strdup_printf ("%s Matrix password for \"%s\"",
+                           cm_matrix_get_app_id (), username);
+
+  secret_password_store (schema, NULL, label, credentials,
+                         cancellable, callback, user_data,
+                         CM_USERNAME_ATTRIBUTE, username,
+                         CM_SERVER_ATTRIBUTE, server,
+                         CM_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR,
+                         NULL);
+
+  cm_utils_free_buffer (access_token);
+  cm_utils_free_buffer (credentials);
+  cm_utils_free_buffer (pickle_key);
+  cm_utils_free_buffer (password);
+  cm_utils_free_buffer (token);
+  cm_utils_free_buffer (key);
+}
+
+gboolean
+cm_secret_store_save_finish (GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return secret_password_store_finish (result, error);
+}
+
+static void
+secret_load_cb (GObject      *object,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  GPtrArray *accounts = NULL;
+  GError *error = NULL;
+  GList *secrets;
+
+  g_assert_true (G_IS_TASK (task));
+
+  secrets = secret_password_search_finish (result, &error);
+
+  if (error) {
+    g_task_return_error (task, error);
+    return;
+  }
+
+  for (GList *item = secrets; item; item = item->next) {
+    g_autofree char *label = NULL;
+    g_autofree char *expected = NULL;
+
+    label = secret_retrievable_get_label (item->data);
+    expected = g_strconcat (cm_matrix_get_app_id (), " Matrix password", NULL);
+
+    if (!label || !expected)
+      continue;
+
+    if (!g_str_has_prefix (label, expected))
+      continue;
+
+    if (!accounts)
+      accounts = g_ptr_array_new_full (5, g_object_unref);
+
+    if (item->data)
+      g_ptr_array_add (accounts, g_object_ref (item->data));
+  }
+
+  if (secrets)
+    g_list_free_full (secrets, g_object_unref);
+
+  g_task_return_pointer (task, accounts, (GDestroyNotify)g_ptr_array_unref);
+}
+
+void
+cm_secret_store_load_async (GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  const SecretSchema *schema;
+  GTask *task;
+
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  schema = secret_store_get_schema ();
+  task = g_task_new (NULL, cancellable, callback, user_data);
+
+  /** With using SECRET_SCHEMA_DONT_MATCH_NAME we need some other attribute
+   *  (apart from the schema name itself) to use for the lookup.
+   *  The protocol attribute seems like a reasonable choice.
+   */
+  secret_password_search (schema,
+                          SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS,
+                          cancellable, secret_load_cb, task,
+                          CM_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR,
+                          NULL);
+}
+
+GPtrArray *
+cm_secret_store_load_finish (GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+void
+cm_secret_store_delete_async (CmClient            *client,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  const SecretSchema *schema;
+  const char *server, *username;
+  CmAccount *account;
+
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  account = cm_client_get_account (client);
+  username = cm_account_get_login_id (account);
+
+  schema = secret_store_get_schema ();
+  server = cm_client_get_homeserver (client);
+  secret_password_clear (schema, cancellable, callback, user_data,
+                         CM_USERNAME_ATTRIBUTE, username,
+                         CM_SERVER_ATTRIBUTE, server,
+                         CM_PROTOCOL_ATTRIBUTE, PROTOCOL_MATRIX_STR,
+                         NULL);
+}
+
+gboolean
+cm_secret_store_delete_finish  (GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return secret_password_clear_finish (result, error);
+}
diff --git a/subprojects/libcmatrix/src/cm-types.h b/subprojects/libcmatrix/src/cm-types.h
new file mode 100644
index 0000000000000000000000000000000000000000..d0c1fd75366de08da2a38533aa110b23be737220
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-types.h
@@ -0,0 +1,38 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-types.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _CmUser              CmUser;
+typedef struct _CmAccount           CmAccount;
+typedef struct _CmRoomMember        CmRoomMember;
+typedef struct _CmClient            CmClient;
+typedef struct _CmRoom              CmRoom;
+typedef struct _CmEvent             CmEvent;
+typedef struct _CmRoomEvent         CmRoomEvent;
+typedef struct _CmRoomMessageEvent  CmRoomMessageEvent;
+
+/* Private types */
+#ifdef CMATRIX_COMPILATION
+typedef struct _CmDb                CmDb;
+typedef struct _CmOlm               CmOlm;
+typedef struct _CmUserList          CmUserList;
+#endif /* CMATRIX_COMPILATION */
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/cm-utils-private.h b/subprojects/libcmatrix/src/cm-utils-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..9ac43ea3a146a4aceff36801b03f52bb68989ae1
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-utils-private.h
@@ -0,0 +1,97 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <glib-object.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup.h>
+
+#include "cm-enums.h"
+
+/* Hack to check format specifier arguments match */
+static inline void check_format (const char *fmt, ...) G_GNUC_PRINTF (1, 2);
+static inline void check_format (const char *fmt, ...) {}
+
+#define CM_TRACE(fmt, ...) do {                                         \
+  check_format (fmt, ##__VA_ARGS__);                                    \
+  g_log_structured (G_LOG_DOMAIN,                                       \
+                    (1 << G_LOG_LEVEL_USER_SHIFT),                      \
+                    "CODE_FILE", __FILE__,                              \
+                    "CODE_LINE", G_STRINGIFY (__LINE__),                \
+                    "CODE_FUNC", G_STRFUNC,                             \
+                    "MESSAGE", fmt, ##__VA_ARGS__);                     \
+} while (0)
+#define CM_LOG_SUCCESS(_value) cm_utils_log_bool_str (_value, TRUE)
+#define CM_LOG_BOOL(_value) cm_utils_log_bool_str (_value, FALSE)
+
+const char   *cm_utils_log_bool_str             (gboolean             value,
+                                                 gboolean             use_success);
+const char   *cm_utils_anonymize                (GString             *str,
+                                                 const char          *value);
+GError       *cm_utils_json_node_get_error      (JsonNode            *node);
+gboolean      cm_utils_get_item_position        (GListModel          *list,
+                                                 gpointer             item,
+                                                 guint               *position);
+gboolean      cm_utils_remove_list_item         (GListStore          *store,
+                                                 gpointer             item);
+const char   *cm_utils_get_event_type_str       (CmEventType          type);
+char         *cm_utils_json_object_to_string    (JsonObject          *object,
+                                                 gboolean             prettify);
+GString      *cm_utils_json_get_canonical       (JsonObject          *object,
+                                                 GString             *out);
+JsonObject   *cm_utils_string_to_json_object    (const char          *json_str);
+gboolean      cm_utils_json_object_has_member   (JsonObject          *object,
+                                                 const char          *member);
+gint64        cm_utils_json_object_get_int      (JsonObject          *object,
+                                                 const char          *member);
+gboolean      cm_utils_json_object_get_bool     (JsonObject          *object,
+                                                 const char          *member);
+const char   *cm_utils_json_object_get_string   (JsonObject          *object,
+                                                 const char          *member);
+char         *cm_utils_json_object_dup_string   (JsonObject          *object,
+                                                 const char          *member);
+JsonObject   *cm_utils_json_object_get_object   (JsonObject          *object,
+                                                 const char          *member);
+JsonArray    *cm_utils_json_object_get_array    (JsonObject          *object,
+                                                 const char          *member);
+void          cm_utils_clear                    (char                *buffer,
+                                                 size_t               length);
+void          cm_utils_free_buffer              (char                *buffer);
+const char   *cm_utils_get_url_from_user_id     (const char          *user_id);
+gboolean      cm_utils_user_name_valid          (const char          *matrix_user_id);
+gboolean      cm_utils_user_name_is_email       (const char          *user_id);
+gboolean      cm_utils_mobile_is_valid          (const char          *mobile_num);
+
+gboolean      cm_utils_home_server_valid        (const char          *homeserver);
+void          cm_utils_read_uri_async           (const char          *uri,
+                                                 guint                timeout,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+gpointer      cm_utils_read_uri_finish          (GAsyncResult        *result,
+                                                 GError             **error);
+void          cm_utils_get_homeserver_async     (const char          *username,
+                                                 guint                timeout,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+char         *cm_utils_get_homeserver_finish    (GAsyncResult        *result,
+                                                 GError             **error);
+void          cm_utils_verify_homeserver_async  (const char          *server,
+                                                 guint                timeout,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data);
+gboolean      cm_utils_verify_homeserver_finish (GAsyncResult        *result,
+                                                 GError             **error);
+char         *cm_utils_get_path_for_m_type      (const char          *base_path,
+                                                 CmEventType          type,
+                                                 gboolean             thumbnail,
+                                                 const char          *file_name);
diff --git a/subprojects/libcmatrix/src/cm-utils.c b/subprojects/libcmatrix/src/cm-utils.c
new file mode 100644
index 0000000000000000000000000000000000000000..b27e26a5f878e1d6e612a074d355a0070487d211
--- /dev/null
+++ b/subprojects/libcmatrix/src/cm-utils.c
@@ -0,0 +1,1312 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-utils"
+#define BUFFER_SIZE 256
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#define __STDC_WANT_LIB_EXT1__ 1
+#include <stdio.h>
+#include <string.h>
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+
+#include "cm-common.h"
+#include "cm-enums.h"
+#include "cm-utils-private.h"
+
+static const char *error_codes[] = {
+  "", /* Index 0 is reserved for no error */
+  "M_FORBIDDEN",
+  "M_UNKNOWN_TOKEN",
+  "M_MISSING_TOKEN",
+  "M_BAD_JSON",
+  "M_NOT_JSON",
+  "M_NOT_FOUND",
+  "M_LIMIT_EXCEEDED",
+  "M_UNKNOWN",
+  "M_UNRECOGNIZED",
+  "M_UNAUTHORIZED",
+  "M_USER_DEACTIVATED",
+  "M_USER_IN_USE",
+  "M_INVALID_USERNAME",
+  "M_ROOM_IN_USE",
+  "M_INVALID_ROOM_STATE",
+  "M_THREEPID_IN_USE",
+  "M_THREEPID_NOT_FOUND",
+  "M_THREEPID_AUTH_FAILED",
+  "M_THREEPID_DENIED",
+  "M_SERVER_NOT_TRUSTED",
+  "M_UNSUPPORTED_ROOM_VERSION",
+  "M_INCOMPATIBLE_ROOM_VERSION",
+  "M_BAD_STATE",
+  "M_GUEST_ACCESS_FORBIDDEN",
+  "M_CAPTCHA_NEEDED",
+  "M_CAPTCHA_INVALID",
+  "M_MISSING_PARAM",
+  "M_INVALID_PARAM",
+  "M_TOO_LARGE",
+  "M_EXCLUSIVE",
+  "M_RESOURCE_LIMIT_EXCEEDED",
+  "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM",
+};
+
+const char *
+cm_utils_log_bool_str (gboolean value,
+                       gboolean use_success)
+{
+  if (!g_log_writer_supports_color (fileno (stdout)) ||
+      g_log_writer_is_journald (fileno (stderr)))
+    {
+      if (value)
+        return use_success ? "success" : "true";
+      else
+        return use_success ? "fail" : "false";
+    }
+
+  if (value)
+    {
+      if (use_success)
+        return "\033[1;32m" "success" "\033[0m";
+      else
+        return "\033[1;32m" "true" "\033[0m";
+    }
+  else
+    {
+      if (use_success)
+        return "\033[1;31m" "fail" "\033[0m";
+      else
+        return "\033[1;31m" "false" "\033[0m";
+    }
+}
+
+const char *
+cm_utils_anonymize (GString    *str,
+                    const char *value)
+{
+  gunichar c, next_c, prev_c;
+
+  g_assert (str);
+
+  if (!value || !*value)
+    return str->str;
+
+  if (str->len && str->str[str->len - 1] != ' ')
+    g_string_append_c (str, ' ');
+
+  if (!g_utf8_validate (value, -1, NULL))
+    {
+      g_string_append (str, "******");
+      return str->str;
+    }
+
+  if (*value == '!' || *value == '@' || *value == '+')
+    {
+      c = g_utf8_get_char (value);
+      value = g_utf8_next_char (value);
+      g_string_append_unichar (str, c);
+    }
+
+  if (!*value)
+    return str->str;
+
+  c = g_utf8_get_char (value);
+  value = g_utf8_next_char (value);
+  g_string_append_unichar (str, c);
+
+  if (!*value)
+    return str->str;
+
+  c = g_utf8_get_char (value);
+  value = g_utf8_next_char (value);
+  g_string_append_unichar (str, c);
+
+  while (*value)
+    {
+      prev_c = c;
+      c = g_utf8_get_char (value);
+
+      value = g_utf8_next_char (value);
+      next_c = g_utf8_get_char (value);
+
+      if (!g_unichar_isalnum (c))
+        g_string_append_unichar (str, c);
+      else if (!g_unichar_isalnum (prev_c) || !g_unichar_isalnum (next_c))
+        g_string_append_unichar (str, c);
+      else
+        g_string_append_c (str, '#');
+    }
+
+  return str->str;
+}
+
+GError *
+cm_utils_json_node_get_error (JsonNode *node)
+{
+  JsonObject *object = NULL;
+  const char *error, *err_code;
+
+  if (!node || (!JSON_NODE_HOLDS_OBJECT (node) && !JSON_NODE_HOLDS_ARRAY (node)))
+    return g_error_new (CM_ERROR, CM_ERROR_NOT_JSON,
+                        "Not JSON Object");
+
+  /* Returned by /_matrix/client/r0/rooms/{roomId}/state */
+  if (JSON_NODE_HOLDS_ARRAY (node))
+    return NULL;
+
+  object = json_node_get_object (node);
+  err_code = cm_utils_json_object_get_string (object, "errcode");
+
+  if (!err_code)
+    return NULL;
+
+  error = cm_utils_json_object_get_string (object, "error");
+
+  if (!error)
+    error = "Unknown Error";
+
+  if (!g_str_has_prefix (err_code, "M_"))
+    return g_error_new (CM_ERROR, CM_ERROR_UNKNOWN,
+                        "Invalid Error code");
+
+  for (guint i = 0; i < G_N_ELEMENTS (error_codes); i++)
+    if (g_str_equal (error_codes[i], err_code))
+      return g_error_new (CM_ERROR, i, "%s", error);
+
+  return g_error_new (CM_ERROR, CM_ERROR_UNKNOWN, "Unknown Error");
+}
+
+gboolean
+cm_utils_get_item_position (GListModel *list,
+                            gpointer    item,
+                            guint      *position)
+{
+  guint n_items;
+
+  g_return_val_if_fail (G_IS_LIST_MODEL (list), FALSE);
+  g_return_val_if_fail (item != NULL, FALSE);
+
+  n_items = g_list_model_get_n_items (list);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(GObject) object = NULL;
+
+      object = g_list_model_get_item (list, i);
+
+      if (object == item)
+        {
+          if (position)
+            *position = i;
+
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+gboolean
+cm_utils_remove_list_item (GListStore *store,
+                           gpointer    item)
+{
+  GListModel *model;
+  guint n_items;
+
+  g_return_val_if_fail (G_IS_LIST_STORE (store), FALSE);
+
+  if (!item)
+    return FALSE;
+
+  model = G_LIST_MODEL (store);
+  n_items = g_list_model_get_n_items (model);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(GObject) object = NULL;
+
+      object = g_list_model_get_item (model, i);
+
+      if (object == item)
+        {
+          g_list_store_remove (store, i);
+
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+const char *
+cm_utils_get_event_type_str (CmEventType type)
+{
+  switch (type)
+    {
+    case CM_M_CALL_ANSWER:
+      return "m.call.answer";
+
+    case CM_M_CALL_ASSERTED_IDENTITY:
+      return "m.call.asserted_identity";
+
+    case CM_M_CALL_ASSERTED_IDENTITY_PREFIX:
+      return "org.matrix.call.asserted_identity";
+
+    case CM_M_CALL_CANDIDATES:
+      return "m.call.candidates";
+
+    case CM_M_CALL_HANGUP:
+      return "m.call.hangup";
+
+    case CM_M_CALL_INVITE:
+      return "m.call.invite";
+
+    case CM_M_CALL_NEGOTIATE:
+      return "m.call.negotiate";
+
+    case CM_M_CALL_REJECT:
+      return "m.call.reject";
+
+    case CM_M_CALL_REPLACES:
+      return "m.call.replaces";
+
+    case CM_M_CALL_SELECT_ANSWER:
+      return "m.call.select_answer";
+
+    case CM_M_DIRECT:
+      return "m.direct";
+
+    case CM_M_DUMMY:
+      return "m.dummy";
+
+    case CM_M_FORWARDED_ROOM_KEY:
+      return "m.forwarded_room_key";
+
+    case CM_M_FULLY_READ:
+      return "m.fully_read";
+
+    case CM_M_IGNORED_USER_LIST:
+      return "m.ignored_user_list";
+
+    case CM_M_KEY_VERIFICATION_ACCEPT:
+      return "m.key.verification_accept";
+
+    case CM_M_KEY_VERIFICATION_CANCEL:
+      return "m.key.verification.cancel";
+
+    case CM_M_KEY_VERIFICATION_DONE:
+      return "m.key.verification.done";
+
+    case CM_M_KEY_VERIFICATION_KEY:
+      return "m.key.verification.key";
+
+    case CM_M_KEY_VERIFICATION_MAC:
+      return "m.key.verification.mac";
+
+    case CM_M_KEY_VERIFICATION_READY:
+      return "m.key.verification.ready";
+
+    case CM_M_KEY_VERIFICATION_REQUEST:
+      return "m.key.verification.request";
+
+    case CM_M_KEY_VERIFICATION_START:
+      return "m.key.verification.start";
+
+    case CM_M_PRESENCE:
+      return "m.presence";
+
+    case CM_M_PUSH_RULES:
+      return "m.push_rules";
+
+    case CM_M_REACTION:
+      return "m.reaction";
+
+    case CM_M_RECEIPT:
+      return "m.receipt";
+
+    case CM_M_ROOM_ALIASES:
+      return "m.room.aliases";
+
+    case CM_M_ROOM_AVATAR:
+      return "m.room.avatar";
+
+    case CM_M_ROOM_BOT_OPTIONS:
+      return "m.room.bot.options";
+
+    case CM_M_ROOM_CANONICAL_ALIAS:
+      return "m.room.canonical_alias";
+
+    case CM_M_ROOM_CREATE:
+      return "m.room.create";
+
+    case CM_M_ROOM_ENCRYPTED:
+      return "m.room.encrypted";
+
+    case CM_M_ROOM_ENCRYPTION:
+      return "m.room.encryption";
+
+    case CM_M_ROOM_GUEST_ACCESS:
+      return "m.room.guest_access";
+
+    case CM_M_ROOM_HISTORY_VISIBILITY:
+      return "m.room.history_visibility";
+
+    case CM_M_ROOM_JOIN_RULES:
+      return "m.room.join_rules";
+
+    case CM_M_ROOM_KEY:
+      return "m.room_key";
+
+    case CM_M_ROOM_KEY_REQUEST:
+      return "m.room_key.request";
+
+    case CM_M_ROOM_MEMBER:
+      return "m.room.member";
+
+    case CM_M_ROOM_MESSAGE:
+      return "m.room.message";
+
+    case CM_M_ROOM_MESSAGE_FEEDBACK:
+      return "m.room.message.feedback";
+
+    case CM_M_ROOM_NAME:
+      return "m.room.name";
+
+    case CM_M_ROOM_PINNED_EVENTS:
+      return "m.room.pinned_events";
+
+    case CM_M_ROOM_PLUMBING:
+      return "m.room.plumbing";
+
+    case CM_M_ROOM_POWER_LEVELS:
+      return "m.room.power_levels";
+
+    case CM_M_ROOM_REDACTION:
+      return "m.room.redaction";
+
+    case CM_M_ROOM_RELATED_GROUPS:
+      return "m.room.related_groups";
+
+    case CM_M_ROOM_SERVER_ACL:
+      return "m.room.server_acl";
+
+    case CM_M_ROOM_THIRD_PARTY_INVITE:
+      return "m.room.third_party_invite";
+
+    case CM_M_ROOM_TOMBSTONE:
+      return "m.room.tombstone";
+
+    case CM_M_ROOM_TOPIC:
+      return "m.room.topic";
+
+    case CM_M_SECRET_REQUEST:
+      return "m.secret.request";
+
+    case CM_M_SECRET_SEND:
+      return "m.secret.send";
+
+    case CM_M_SECRET_STORAGE_DEFAULT_KEY:
+      return "m.secret_storage.default_key";
+
+    case CM_M_SPACE_CHILD:
+      return "m.space.child";
+
+    case CM_M_SPACE_PARENT:
+      return "m.space.parent";
+
+    case CM_M_STICKER:
+      return "m.sticker";
+
+    case CM_M_TAG:
+      return "m.tag";
+
+    case CM_M_TYPING:
+      return "m.typing";
+
+    case CM_M_UNKNOWN:
+    case CM_M_USER_STATUS:
+    case CM_M_ROOM_INVITE:
+    case CM_M_ROOM_BAN:
+    case CM_M_ROOM_KICK:
+    default:
+      g_return_val_if_reached (NULL);
+    }
+
+  return NULL;
+}
+
+void
+cm_utils_clear (char   *buffer,
+                size_t  length)
+{
+  if (!buffer || length == 0)
+    return;
+
+  /* Note: we are not comparing with -1 */
+  if (length == (size_t)-1)
+    length = strlen (buffer);
+
+#ifdef __STDC_LIB_EXT1__
+  memset_s (buffer, length, 0xAD, length);
+#elif HAVE_EXPLICIT_BZERO
+  explicit_bzero (buffer, length);
+#else
+  {
+    volatile char *end = buffer + length;
+
+    /* Set something nonzero, so it'll likely crash on reuse */
+    while (buffer != end)
+      *(buffer++) = 0xAD;
+  }
+#endif
+}
+
+void
+cm_utils_free_buffer (char *buffer)
+{
+  cm_utils_clear (buffer, -1);
+  g_free (buffer);
+}
+
+const char *
+cm_utils_get_url_from_user_id (const char *user_id)
+{
+  if (!cm_utils_user_name_valid (user_id))
+    return NULL;
+
+  /* Return the string after ‘:’ */
+  return strchr (user_id, ':') + 1;
+}
+
+/* https://spec.matrix.org/v1.2/appendices/#user-identifiers */
+/* domain part is validated separately, so the regex is not complete */
+#define MATRIX_USER_ID_RE "^@[A-Z0-9.=_-]+:[A-Z0-9.-]+$"
+
+gboolean
+cm_utils_user_name_valid (const char *matrix_user_id)
+{
+  const char *url_start;
+
+  if (!matrix_user_id || !*matrix_user_id)
+    return FALSE;
+
+  /* GRegex is deprecated, but we can change this anytime:
+   * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/1451
+   * https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2529
+   */
+  if (!g_regex_match_simple (MATRIX_USER_ID_RE, matrix_user_id, G_REGEX_CASELESS, 0))
+    return FALSE;
+
+  url_start = strchr (matrix_user_id, ':') + 1;
+
+  if (!cm_utils_home_server_valid (url_start))
+    return FALSE;
+
+  if (G_UNLIKELY (strlen (matrix_user_id) > 255))
+    return FALSE;
+
+  return TRUE;
+}
+#undef MATRIX_USER_ID_RE
+
+#define EMAIL_RE "^[[:alnum:]._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$"
+
+gboolean
+cm_utils_user_name_is_email (const char *user_id)
+{
+  if (!user_id || !*user_id)
+    return FALSE;
+
+  if (g_regex_match_simple (EMAIL_RE, user_id, G_REGEX_CASELESS, 0))
+    return TRUE;
+
+  return FALSE;
+}
+#undef EMAIL_RE
+
+/* Rough estimate for an E.164 number */
+#define MOBILE_RE "^\\+[0-9]{10,15}$"
+
+gboolean
+cm_utils_mobile_is_valid (const char *mobile_num)
+{
+  if (!mobile_num || !*mobile_num)
+    return FALSE;
+
+  if (g_regex_match_simple (MOBILE_RE, mobile_num, 0, 0))
+    return TRUE;
+
+  return FALSE;
+}
+#undef MOBILE_RE
+
+gboolean
+cm_utils_home_server_valid (const char *homeserver)
+{
+  gboolean valid = FALSE;
+
+  if (homeserver && !*homeserver)
+    homeserver = NULL;
+
+  if (homeserver)
+    {
+      g_autofree char *server = NULL;
+      g_autoptr(GUri) uri = NULL;
+      const char *scheme = NULL;
+      const char *path = NULL;
+      const char *host = NULL;
+
+      if (!strstr (homeserver, "//"))
+        server = g_strconcat ("https://", homeserver, NULL);
+
+      uri = g_uri_parse (server ?: homeserver, G_URI_FLAGS_NONE, NULL);
+
+      if (uri)
+        {
+          scheme = g_uri_get_scheme (uri);
+          path = g_uri_get_path (uri);
+          host = g_uri_get_host (uri);
+        }
+
+      valid = scheme && *scheme;
+      valid = valid && (g_str_equal (scheme, "http") || g_str_equal (scheme, "https"));
+      valid = valid && host && *host;
+      valid = valid && !g_str_has_suffix (host, ".");
+      valid = valid && (!path || !*path || g_str_equal (path, "/"));
+    }
+
+  return valid;
+}
+
+char *
+cm_utils_json_object_to_string (JsonObject *object,
+                                gboolean    prettify)
+{
+  g_autoptr(JsonNode) node = NULL;
+
+  g_return_val_if_fail (object, NULL);
+
+  node = json_node_new (JSON_NODE_OBJECT);
+  json_node_init_object (node, object);
+
+  return json_to_string (node, !!prettify);
+}
+
+static void utils_json_canonical_array (JsonArray *array,
+                                        GString   *out);
+static void
+utils_handle_node (JsonNode *node,
+                   GString  *out)
+{
+  GType type;
+
+  g_assert (node);
+  g_assert (out);
+
+  type = json_node_get_value_type (node);
+
+  if (type == JSON_TYPE_OBJECT)
+    cm_utils_json_get_canonical (json_node_get_object (node), out);
+  else if (type == JSON_TYPE_ARRAY)
+    utils_json_canonical_array (json_node_get_array (node), out);
+  else if (type == G_TYPE_INVALID)
+    g_string_append (out, "null");
+  else if (type == G_TYPE_STRING)
+    g_string_append_printf (out, "\"%s\"", json_node_get_string (node));
+  else if (type == G_TYPE_INT64)
+    g_string_append_printf (out, "%" G_GINT64_FORMAT, json_node_get_int (node));
+  else if (type == G_TYPE_DOUBLE)
+    g_string_append_printf (out, "%f", json_node_get_double (node));
+  else if (type == G_TYPE_BOOLEAN)
+    g_string_append (out, json_node_get_boolean (node) ? "true" : "false");
+  else
+    g_return_if_reached ();
+}
+
+static void
+utils_json_canonical_array (JsonArray *array,
+                            GString   *out)
+{
+  g_autoptr(GList) elements = NULL;
+
+  g_assert (array);
+  g_assert (out);
+
+  g_string_append_c (out, '[');
+  elements = json_array_get_elements (array);
+
+  /* The order of array members shouldn’t be changed */
+  for (GList *item = elements; item; item = item->next)
+{
+    utils_handle_node (item->data, out);
+
+    if (item->next)
+      g_string_append_c (out, ',');
+  }
+
+  g_string_append_c (out, ']');
+}
+
+GString *
+cm_utils_json_get_canonical (JsonObject *object,
+                             GString    *out)
+{
+  JsonNode *signatures, *non_signed;
+
+  g_autoptr(GList) members = NULL;
+
+  g_return_val_if_fail (object, NULL);
+
+  if (!out)
+    out = g_string_sized_new (BUFFER_SIZE);
+
+  signatures = json_object_dup_member (object, "signatures");
+  non_signed = json_object_dup_member (object, "unsigned");
+
+  /* Remove the non signed members before verification */
+  json_object_remove_member (object, "signatures");
+  json_object_remove_member (object, "unsigned");
+
+  g_string_append_c (out, '{');
+
+  members = json_object_get_members (object);
+  members = g_list_sort (members, (GCompareFunc)g_strcmp0);
+
+  for (GList *item = members; item; item = item->next)
+{
+    JsonNode *node;
+
+    g_string_append_printf (out, "\"%s\":", (char *)item->data);
+
+    node = json_object_get_member (object, item->data);
+    utils_handle_node (node, out);
+
+    if (item->next)
+      g_string_append_c (out, ',');
+  }
+
+  g_string_append_c (out, '}');
+
+  /* Revert the changes we made to the JSON object */
+  if (signatures)
+    json_object_set_member (object, "signatures", signatures);
+  if (non_signed)
+    json_object_set_member (object, "unsigned", non_signed);
+
+  return out;
+}
+
+JsonObject *
+cm_utils_string_to_json_object (const char *json_str)
+{
+  g_autoptr(JsonParser) parser = NULL;
+  JsonNode *node;
+
+  if (!json_str || !*json_str)
+    return NULL;
+
+  parser = json_parser_new ();
+  if (!json_parser_load_from_data (parser, json_str, -1, NULL))
+    return NULL;
+
+  node = json_parser_get_root (parser);
+
+  if (!JSON_NODE_HOLDS_OBJECT (node))
+    return NULL;
+
+  return json_node_dup_object (node);
+}
+
+gboolean
+cm_utils_json_object_has_member (JsonObject *object,
+                                 const char *member)
+{
+  JsonNode *node;
+
+  if (!object || !member || !*member)
+    return 0;
+
+  node = json_object_get_member (object, member);
+
+  return !!node;
+}
+
+gint64
+cm_utils_json_object_get_int (JsonObject *object,
+                              const char *member)
+{
+  JsonNode *node;
+
+  if (!object || !member || !*member)
+    return 0;
+
+  node = json_object_get_member (object, member);
+
+  if (node && JSON_NODE_HOLDS_VALUE (node))
+    return json_node_get_int (node);
+
+  return 0;
+}
+
+gboolean
+cm_utils_json_object_get_bool (JsonObject *object,
+                               const char *member)
+{
+  JsonNode *node;
+
+  if (!object || !member || !*member)
+    return FALSE;
+
+  node = json_object_get_member (object, member);
+
+  if (node && JSON_NODE_HOLDS_VALUE (node))
+    return json_node_get_boolean (node);
+
+  return FALSE;
+}
+
+const char *
+cm_utils_json_object_get_string (JsonObject *object,
+                                 const char *member)
+{
+  JsonNode *node;
+
+  if (!object || !member || !*member)
+    return NULL;
+
+  node = json_object_get_member (object, member);
+
+  if (node && JSON_NODE_HOLDS_VALUE (node))
+    return json_node_get_string (node);
+
+  return NULL;
+}
+
+char *
+cm_utils_json_object_dup_string (JsonObject *object,
+                                 const char *member)
+{
+  const char *str;
+
+  str = cm_utils_json_object_get_string (object, member);
+
+  return g_strdup (str);
+}
+
+JsonObject *
+cm_utils_json_object_get_object (JsonObject *object,
+                                 const char *member)
+{
+  JsonNode *node;
+
+  if (!object || !member || !*member)
+    return NULL;
+
+  node = json_object_get_member (object, member);
+
+  if (node && JSON_NODE_HOLDS_OBJECT (node))
+    return json_node_get_object (node);
+
+  return NULL;
+}
+
+JsonArray *
+cm_utils_json_object_get_array (JsonObject *object,
+                                const char *member)
+{
+  JsonNode *node;
+
+  if (!object || !member || !*member)
+    return NULL;
+
+  node = json_object_get_member (object, member);
+
+  if (node && JSON_NODE_HOLDS_ARRAY (node))
+    return json_node_get_array (node);
+
+  return NULL;
+}
+
+static void
+load_from_stream_cb (JsonParser   *parser,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  gboolean timeout;
+
+  g_assert (JSON_IS_PARSER (parser));
+  g_assert (G_IS_TASK (task));
+
+  timeout = GPOINTER_TO_INT (g_task_get_task_data (task));
+
+  /* Task return is handled somewhere else */
+  if (timeout)
+    return;
+
+  if (json_parser_load_from_stream_finish (parser, result, &error))
+    g_task_return_pointer (task, json_parser_steal_root (parser),
+                           (GDestroyNotify)json_node_unref);
+  else
+    g_task_return_error (task, error);
+}
+
+static gboolean
+cancel_read_uri (gpointer user_data)
+{
+  g_autoptr(GTask) task = user_data;
+
+  g_assert (G_IS_TASK (task));
+
+  g_object_set_data (G_OBJECT (task), "timeout-id", 0);
+
+  /* XXX: Not thread safe? */
+  if (g_task_get_completed (task) || g_task_had_error (task))
+    return G_SOURCE_REMOVE;
+
+  g_task_set_task_data (task, GINT_TO_POINTER (TRUE), NULL);
+  g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_TIMED_OUT,
+                           "Request timeout");
+  g_cancellable_cancel (g_task_get_cancellable (task));
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+uri_file_read_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  SoupSession *session = (SoupSession *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GInputStream) stream = NULL;
+  g_autoptr(JsonParser) parser = NULL;
+  GCancellable *cancellable;
+  SoupMessage *message;
+  GError *error = NULL;
+  gboolean has_timeout;
+  GTlsCertificateFlags err_flags;
+
+  g_assert (G_IS_TASK (task));
+  g_assert (SOUP_IS_SESSION (session));
+
+  stream = soup_session_send_finish (session, result, &error);
+  message = g_object_get_data (G_OBJECT (task), "message");
+  has_timeout = GPOINTER_TO_INT (g_task_get_task_data (task));
+
+  /* Task return is handled somewhere else */
+  if (has_timeout)
+    return;
+
+  if (error)
+{
+    g_task_return_error (task, error);
+    return;
+  }
+
+#if SOUP_MAJOR_VERSION == 2
+  soup_message_get_https_status (message, NULL, &err_flags);
+#else
+  err_flags = soup_message_get_tls_peer_certificate_errors (message);
+#endif
+
+  if (message && err_flags)
+{
+    guint timeout_id, timeout;
+
+    timeout = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout"));
+    timeout_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (task), "timeout-id"));
+    g_clear_handle_id (&timeout_id, g_source_remove);
+    g_object_unref (task);
+
+    /* fixme: handle SSL errors */
+    /* if (cm_utils_handle_ssl_error (message)) */
+    /*   { */
+    /*     g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_CANCELLED, */
+    /*                              "Cancelled"); */
+    /*     return; */
+    /*   } */
+
+    timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task));
+    g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id));
+  }
+
+  cancellable = g_task_get_cancellable (task);
+  parser = json_parser_new ();
+  json_parser_load_from_stream_async (parser, stream, cancellable,
+                                      (GAsyncReadyCallback)load_from_stream_cb,
+                                      g_steal_pointer (&task));
+}
+
+static void
+message_network_event_cb (SoupMessage        *msg,
+                          GSocketClientEvent  event,
+                          GIOStream          *connection,
+                          gpointer            user_data)
+{
+  GSocketAddress *address;
+
+  /* We shall have a non %NULL @connection by %G_SOCKET_CLIENT_CONNECTING event */
+  if (event != G_SOCKET_CLIENT_CONNECTING)
+    return;
+
+  /* @connection is a #GSocketConnection */
+  address = g_socket_connection_get_remote_address (G_SOCKET_CONNECTION (connection), NULL);
+  g_object_set_data_full (user_data, "address", address, g_object_unref);
+}
+
+#if SOUP_MAJOR_VERSION == 3
+static gboolean
+accept_certificate_callback (SoupMessage          *msg,
+                             GTlsCertificate      *certificate,
+                             GTlsCertificateFlags  tls_errors,
+                             gpointer              user_data)
+{
+    /* Returning TRUE trusts it anyway. */
+    return TRUE;
+}
+#endif
+
+void
+cm_utils_read_uri_async (const char          *uri,
+                         guint                timeout,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  g_autoptr(SoupSession) session = NULL;
+  g_autoptr(SoupMessage) message = NULL;
+  g_autoptr(GCancellable) cancel = NULL;
+  g_autoptr(GTask) task = NULL;
+  guint timeout_id;
+
+  g_return_if_fail (uri && *uri);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (cancellable)
+    cancel = g_object_ref (cancellable);
+  else
+    cancel = g_cancellable_new ();
+
+  task = g_task_new (NULL, cancel, callback, user_data);
+  /* if this changes to TRUE, we consider it has been timeout */
+  g_task_set_task_data (task, GINT_TO_POINTER (FALSE), NULL);
+  g_task_set_source_tag (task, cm_utils_read_uri_async);
+
+  timeout = CLAMP (timeout, 5, 60);
+  timeout_id = g_timeout_add_seconds (timeout, cancel_read_uri, g_object_ref (task));
+  g_object_set_data (G_OBJECT (task), "timeout", GUINT_TO_POINTER (timeout));
+  g_object_set_data (G_OBJECT (task), "timeout-id", GUINT_TO_POINTER (timeout_id));
+
+  message = soup_message_new (SOUP_METHOD_GET, uri);
+  if (!message)
+{
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME,
+                             "%s is not a valid uri", uri);
+    return;
+  }
+
+  soup_message_set_flags (message, SOUP_MESSAGE_NO_REDIRECT);
+  g_object_set_data_full (G_OBJECT (task), "message", g_object_ref (message), g_object_unref);
+
+  g_signal_connect_object (message, "network-event",
+                           G_CALLBACK (message_network_event_cb), task,
+                           G_CONNECT_AFTER);
+  session = soup_session_new ();
+#if SOUP_MAJOR_VERSION == 2
+  g_object_set (G_OBJECT (session), SOUP_SESSION_SSL_STRICT, FALSE, NULL);
+
+  soup_session_send_async (session, message, cancel,
+                           uri_file_read_cb,
+                           g_steal_pointer (&task));
+#else
+  /* Accept invalid certificates */
+  g_signal_connect (message, "accept-certificate", G_CALLBACK (accept_certificate_callback), NULL);
+
+  soup_session_send_async (session, message, 0, cancel,
+                           uri_file_read_cb,
+                           g_steal_pointer (&task));
+#endif
+}
+
+gpointer
+cm_utils_read_uri_finish (GAsyncResult  *result,
+                          GError       **error)
+{
+  GTask *task = (GTask *)result;
+
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (g_task_get_source_tag (task) == cm_utils_read_uri_async, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+get_homeserver_cb (GObject      *obj,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonNode) root = NULL;
+  JsonObject *object = NULL;
+  const char *homeserver = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  root = cm_utils_read_uri_finish (result, &error);
+
+  if (!root)
+{
+    g_task_return_error (task, error);
+    return;
+  }
+
+  g_object_set_data_full (G_OBJECT (task), "address",
+                          g_object_steal_data (G_OBJECT (result), "address"),
+                          g_object_unref);
+
+  if (JSON_NODE_HOLDS_OBJECT (root))
+    object = json_node_get_object (root);
+
+  if (object)
+    object = cm_utils_json_object_get_object (object, "m.homeserver");
+
+  if (object)
+    homeserver = cm_utils_json_object_get_string (object, "base_url");
+
+  if (!homeserver)
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                             "Got invalid response from server");
+  else
+    g_task_return_pointer (task, g_strdup (homeserver), g_free);
+}
+
+/**
+ * cm_utils_get_homeserver_async:
+ * @username: A complete matrix username
+ * @timeout: timeout in seconds
+ * @cancellable: (nullable): A #GCancellable
+ * @callback: The callback to run
+ * @user_data: (nullable): The data passed to @callback
+ *
+ * Get homeserver from the given @username.  @userename
+ * should be in complete form (eg: @user:example.org)
+ *
+ * @timeout is clamped between 5 and 60 seconds.
+ *
+ * This is a network operation and shall connect to the
+ * network to fetch homeserver details.
+ *
+ * See https://matrix.org/docs/spec/client_server/r0.6.1#server-discovery
+ */
+void
+cm_utils_get_homeserver_async (const char          *username,
+                               guint                timeout,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autofree char *uri = NULL;
+  const char *url;
+
+  g_return_if_fail (username);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (callback);
+
+  task = g_task_new (NULL, cancellable, callback, user_data);
+  g_task_set_source_tag (task, cm_utils_get_homeserver_async);
+
+  if (!cm_utils_user_name_valid (username))
+{
+    g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_FILENAME,
+                             "Username '%s' is not a complete matrix id", username);
+    return;
+  }
+
+  url = cm_utils_get_url_from_user_id (username);
+  uri = g_strconcat ("https://", url, "/.well-known/matrix/client", NULL);
+
+  cm_utils_read_uri_async (uri, timeout, cancellable,
+                           get_homeserver_cb, g_steal_pointer (&task));
+}
+
+/**
+ * cm_utils_get_homeserver_finish:
+ * @result: A #GAsyncResult
+ * @error: (optional): A #GError
+ *
+ * Finish call to cm_utils_get_homeserver_async().
+ *
+ * Returns: (nullable) : The homeserver string or %NULL
+ * on error.  Free with g_free().
+ */
+char *
+cm_utils_get_homeserver_finish (GAsyncResult  *result,
+                                GError       **error)
+{
+  GTask *task = (GTask *)result;
+
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (g_task_get_source_tag (task) == cm_utils_get_homeserver_async, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+api_get_version_cb (GObject      *obj,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonNode) root = NULL;
+  JsonObject *object = NULL;
+  JsonArray *array = NULL;
+  GError *error = NULL;
+  const char *server;
+  gboolean valid;
+
+  g_assert (G_IS_TASK (task));
+
+  server = g_task_get_task_data (task);
+  root = cm_utils_read_uri_finish (result, &error);
+
+  if (!error && root)
+    error = cm_utils_json_node_get_error (root);
+
+  if (!root ||
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_TIMED_OUT))
+{
+    if (error)
+      g_task_return_error (task, error);
+    else
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED,
+                               "Failed to get version for server '%s'", server);
+    return;
+  }
+
+  g_object_set_data_full (G_OBJECT (task), "address",
+                          g_object_steal_data (G_OBJECT (result), "address"),
+                          g_object_unref);
+
+  object = json_node_get_object (root);
+  array = cm_utils_json_object_get_array (object, "versions");
+  valid = FALSE;
+
+  if (array)
+{
+    g_autoptr(GString) versions = NULL;
+    guint length;
+
+    versions = g_string_new ("");
+    length = json_array_get_length (array);
+
+    for (guint i = 0; i < length; i++)
+{
+      const char *version;
+
+      version = json_array_get_string_element (array, i);
+      g_string_append_printf (versions, " %s", version);
+
+      /* We have tested only with r0.6.x and r0.5.0 */
+      if (g_str_has_prefix (version, "r0.5.") ||
+          g_str_has_prefix (version, "r0.6.") ||
+          g_str_has_prefix (version, "v1."))
+        valid = TRUE;
+    }
+
+    g_debug ("'%s' has versions:%s, valid: %d",
+             server, versions->str, valid);
+  }
+
+  g_task_return_boolean (task, valid);
+}
+
+void
+cm_utils_verify_homeserver_async (const char          *server,
+                                  guint                timeout,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autofree char *uri = NULL;
+
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (callback);
+
+  task = g_task_new (NULL, cancellable, callback, user_data);
+  g_task_set_task_data (task, g_strdup (server), g_free);
+  g_task_set_source_tag (task, cm_utils_verify_homeserver_async);
+
+  if (!server || !*server ||
+      !g_str_has_prefix (server, "http"))
+{
+    g_task_return_new_error (task, G_IO_ERROR,
+                             G_IO_ERROR_INVALID_DATA,
+                             "URI '%s' is invalid", server);
+    return;
+  }
+
+  uri = g_strconcat (server, "/_matrix/client/versions", NULL);
+  cm_utils_read_uri_async (uri, timeout, cancellable,
+                           api_get_version_cb,
+                           g_steal_pointer (&task));
+}
+
+gboolean
+cm_utils_verify_homeserver_finish (GAsyncResult  *result,
+                                   GError       **error)
+{
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/*
+ * The @base_path should contain the base path up to 'cmatrix'
+ * directory
+ */
+char *
+cm_utils_get_path_for_m_type (const char  *base_path,
+                              CmEventType  type,
+                              gboolean     thumbnail,
+                              const char  *file_name)
+{
+  const char *thumbnail_path = NULL;
+  g_autofree char *path = NULL;
+
+  g_return_val_if_fail (base_path && *base_path, NULL);
+
+  if (thumbnail)
+    thumbnail_path = "thumbnails";
+
+  if (type == CM_M_ROOM_MESSAGE)
+    path = g_build_filename (base_path, "files", thumbnail_path, NULL);
+
+  if (type == CM_M_ROOM_AVATAR)
+    path = g_build_filename (base_path, "avatars", "rooms", thumbnail_path, NULL);
+
+  if (type == CM_M_ROOM_MEMBER)
+    path = g_build_filename (base_path, "avatars", "users", thumbnail_path, NULL);
+
+  if (path)
+    {
+      if (file_name && *file_name)
+        return g_build_filename (path, file_name, NULL);
+      else
+        return g_steal_pointer (&path);
+    }
+
+  g_return_val_if_reached (NULL);
+}
diff --git a/subprojects/libcmatrix/src/cmatrix.h b/subprojects/libcmatrix/src/cmatrix.h
new file mode 100644
index 0000000000000000000000000000000000000000..b6b5a92cb5dcd30fb8d1872bc659e56cd67a174e
--- /dev/null
+++ b/subprojects/libcmatrix/src/cmatrix.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#if !GLIB_CHECK_VERSION(2, 66, 0)
+# error "libcmatrix requires glib-2.0 >= 2.66.0"
+#endif
+
+#ifndef CMATRIX_USE_EXPERIMENTAL_API
+# error "libcmatrix API is experimental, define CMATRIX_USE_EXPERIMENTAL_API to use "
+#endif
+
+#define _CMATRIX_TAKEN
+
+#include "users/cm-user.h"
+#include "users/cm-account.h"
+#include "cm-enums.h"
+#include "cm-common.h"
+#include "cm-client.h"
+#include "cm-matrix.h"
+#include "cm-room.h"
+#include "events/cm-room-message-event.h"
+/* these are not yet public */
+/* #include "cm-room-member.h" */
+/* #include "cm-device.h" */
+
+#undef _CMATRIX_TAKEN
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-event-private.h b/subprojects/libcmatrix/src/events/cm-event-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..5a767145dfa6e26b748cc90772a545219921b56f
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-event-private.h
@@ -0,0 +1,71 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <json-glib/json-glib.h>
+
+#include "cm-types.h"
+#include "cm-event.h"
+
+G_BEGIN_DECLS
+
+/* The order if the enum SHOULD NEVER be changed,
+ * as it's used in db
+ */
+typedef enum {
+  CM_RELATION_NONE,
+  CM_RELATION_UNKNOWN,
+  CM_RELATION_ANNOTATION,
+  CM_RELATION_REPLACE,
+  CM_RELATION_REFERENCE,
+  CM_RELATION_THREAD
+} CmRelationType;
+
+CmEvent      *cm_event_new                (CmEventType   type);
+CmEvent      *cm_event_new_from_json      (JsonObject   *root,
+                                           JsonObject   *encrypted);
+void          cm_event_set_state          (CmEvent      *self,
+                                           CmEventState  state);
+const char   *cm_event_get_transaction_id   (CmEvent      *self);
+const char   *cm_event_get_verification_key (CmEvent        *self);
+const char   *cm_event_get_txn_id         (CmEvent      *self);
+void          cm_event_create_txn_id      (CmEvent      *self,
+                                           guint         id);
+const char   *cm_event_get_state_key      (CmEvent      *self);
+void          cm_event_set_id             (CmEvent      *self,
+                                           const char   *id);
+const char   *cm_event_get_replaces_id    (CmEvent      *self);
+const char   *cm_event_get_reply_to_id    (CmEvent      *self);
+void          cm_event_set_m_type         (CmEvent      *self,
+                                           CmEventType   type);
+void          cm_event_set_json           (CmEvent      *self,
+                                           JsonObject   *root,
+                                           JsonObject   *encrypted);
+GRefString   *cm_event_get_sender_id      (CmEvent      *self);
+void          cm_event_set_sender         (CmEvent      *self,
+                                           CmUser       *sender);
+const char   *cm_event_get_sender_device_id (CmEvent    *self);
+gboolean      cm_event_has_encrypted_content (CmEvent       *self);
+gboolean      cm_event_is_decrypted          (CmEvent       *self);
+
+char         *cm_event_get_json_str       (CmEvent      *self,
+                                           gboolean      prettify);
+JsonObject   *cm_event_get_json           (CmEvent      *self);
+JsonObject   *cm_event_get_encrypted_json (CmEvent      *self);
+JsonObject   *cm_event_generate_json      (CmEvent      *self,
+                                           gpointer      room);
+char         *cm_event_get_api_url         (CmEvent      *self,
+                                           gpointer      room);
+const char *cm_event_get_original_json (CmEvent    *self);
+gboolean    cm_event_is_verified       (CmEvent    *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-event.c b/subprojects/libcmatrix/src/events/cm-event.c
new file mode 100644
index 0000000000000000000000000000000000000000..5d0d3d942a8e3a2add419c4140d8af809fb59cbd
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-event.c
@@ -0,0 +1,671 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-event"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-room-private.h"
+#include "users/cm-account.h"
+#include "cm-utils-private.h"
+#include "cm-event-private.h"
+
+typedef struct
+{
+  CmUser        *sender;
+  GRefString    *sender_id;
+  char          *sender_device_id;
+  char          *event_id;
+  char          *replaces_event_id;
+  char          *reply_to_event_id;
+  /* Transaction id generated/recived for every event */
+  char          *txn_id;
+
+  /* Transaction id received in events (like key verification) */
+  char          *transaction_id;
+  char          *verification_key;
+
+  char          *state_key;
+  JsonObject    *json;
+  /* The JSON source if the event was encrypted */
+  JsonObject    *encrypted_json;
+  gint64         time_stamp;
+  CmEventType    event_type;
+  CmEventState   event_state;
+} CmEventPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (CmEvent, cm_event, G_TYPE_OBJECT)
+
+enum {
+  UPDATED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+#define event_type_string(_event_type) (cm_utils_get_event_type_str(_event_type))
+
+static char *
+create_txn_id (guint id)
+{
+  return g_strdup_printf ("cm%"G_GINT64_FORMAT".%d",
+                          g_get_real_time () / G_TIME_SPAN_MILLISECOND, id);
+}
+
+static void
+event_parse_relations (CmEvent    *self,
+                       JsonObject *root)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+  JsonObject *child;
+  const char *type, *value;
+
+  g_assert (CM_IS_EVENT (self));
+
+  child = cm_utils_json_object_get_object (root, "content");
+  child = cm_utils_json_object_get_object (child, "m.relates_to");
+
+  type = cm_utils_json_object_get_string (child, "rel_type");
+  value = cm_utils_json_object_get_string (child, "event_id");
+
+  if (g_strcmp0 (type, "m.replace") == 0)
+    priv->replaces_event_id = g_strdup (value);
+
+  if (!priv->replaces_event_id)
+    {
+      child = cm_utils_json_object_get_object (root, "unsigned");
+      priv->replaces_event_id = g_strdup (cm_utils_json_object_get_string (child, "replaces_state"));
+    }
+
+  if (!priv->replaces_event_id)
+    {
+      child = cm_utils_json_object_get_object (root, "unsigned");
+      child = cm_utils_json_object_get_object (child, "m.relations");
+      child = cm_utils_json_object_get_object (child, "m.replace");
+      priv->replaces_event_id = g_strdup (cm_utils_json_object_get_string (child, "event_id"));
+    }
+
+  if (!priv->replaces_event_id)
+    {
+      type = cm_utils_json_object_get_string (root, "type");
+
+      if (g_strcmp0 (event_type_string (CM_M_ROOM_REDACTION), type) == 0)
+        priv->replaces_event_id = cm_utils_json_object_dup_string (root, "redacts");
+    }
+}
+
+static gpointer
+cm_event_real_generate_json (CmEvent  *self,
+                             gpointer  room)
+{
+  /* todo */
+  g_assert_not_reached ();
+
+  return NULL;
+}
+
+static char *
+cm_event_real_get_api_url (CmEvent  *self,
+                           gpointer  room)
+{
+  /* todo */
+  g_assert_not_reached ();
+
+  return NULL;
+}
+
+static void
+cm_event_finalize (GObject *object)
+{
+  CmEvent *self = (CmEvent *)object;
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_clear_object (&priv->sender);
+  g_clear_pointer (&priv->sender_id, g_ref_string_release);
+  g_free (priv->sender_device_id);
+  g_free (priv->event_id);
+  g_free (priv->replaces_event_id);
+  g_free (priv->reply_to_event_id);
+  g_free (priv->txn_id);
+  g_free (priv->transaction_id);
+  g_free (priv->verification_key);
+  g_free (priv->state_key);
+  g_clear_pointer (&priv->encrypted_json, json_object_unref);
+  g_clear_pointer (&priv->json, json_object_unref);
+
+  G_OBJECT_CLASS (cm_event_parent_class)->finalize (object);
+}
+
+static void
+cm_event_class_init (CmEventClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  CmEventClass *event_class = CM_EVENT_CLASS (klass);
+
+  object_class->finalize = cm_event_finalize;
+
+  event_class->generate_json = cm_event_real_generate_json;
+  event_class->get_api_url = cm_event_real_get_api_url;
+
+  signals [UPDATED] =
+    g_signal_new ("updated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+cm_event_init (CmEvent *self)
+{
+}
+
+CmEvent *
+cm_event_new (CmEventType type)
+{
+  CmEventPrivate *priv;
+  CmEvent *self;
+
+  g_return_val_if_fail (type == CM_M_UNKNOWN ||
+                        (type >= CM_M_KEY_VERIFICATION_ACCEPT &&
+                         type <= CM_M_KEY_VERIFICATION_START), NULL);
+
+  self = g_object_new (CM_TYPE_EVENT, NULL);
+  priv = cm_event_get_instance_private (self);
+
+  priv->event_type = type;
+
+  return self;
+}
+
+CmEvent *
+cm_event_new_from_json (JsonObject *root,
+                        JsonObject *encrypted)
+{
+  CmEventPrivate *priv;
+  JsonObject *child;
+  CmEvent *self;
+  CmEventType type;
+
+  g_return_val_if_fail (root || encrypted, NULL);
+
+  self = g_object_new (CM_TYPE_EVENT, NULL);
+  priv = cm_event_get_instance_private (self);
+  cm_event_set_json (self, root, encrypted);
+
+  type = cm_event_get_m_type (self);
+
+  if (type >= CM_M_KEY_VERIFICATION_ACCEPT &&
+      type <= CM_M_KEY_VERIFICATION_START)
+    {
+      child = cm_utils_json_object_get_object (root, "content");
+      priv->transaction_id = cm_utils_json_object_dup_string (child, "transaction_id");
+
+      if (type == CM_M_KEY_VERIFICATION_KEY)
+        priv->verification_key = cm_utils_json_object_dup_string (child, "key");
+
+      if (type == CM_M_KEY_VERIFICATION_REQUEST ||
+          type == CM_M_KEY_VERIFICATION_START)
+        {
+          priv->time_stamp = cm_utils_json_object_get_int (child, "timestamp");
+          priv->sender_device_id = cm_utils_json_object_dup_string (child, "from_device");
+        }
+    }
+
+  return self;
+}
+
+const char *
+cm_event_get_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->event_id;
+}
+
+void
+cm_event_set_id (CmEvent    *self,
+                 const char *id)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_if_fail (CM_IS_EVENT (self));
+  g_return_if_fail (!priv->event_id);
+
+  priv->event_id = g_strdup (id);
+}
+
+const char *
+cm_event_get_replaces_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->replaces_event_id;
+}
+
+const char *
+cm_event_get_reply_to_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->reply_to_event_id;
+}
+
+const char *
+cm_event_get_transaction_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->transaction_id;
+}
+
+const char *
+cm_event_get_verification_key (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->verification_key;
+}
+
+const char *
+cm_event_get_txn_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->txn_id;
+}
+
+void
+cm_event_create_txn_id (CmEvent *self,
+                        guint    id)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_if_fail (CM_IS_EVENT (self));
+  g_return_if_fail (!priv->event_id);
+
+  priv->txn_id = create_txn_id (id);
+}
+
+const char *
+cm_event_get_state_key (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  if (priv->state_key && *priv->state_key)
+    return priv->state_key;
+
+  return NULL;
+}
+
+CmEventType
+cm_event_get_m_type (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), CM_M_UNKNOWN);
+
+  return priv->event_type;
+}
+
+void
+cm_event_set_m_type (CmEvent     *self,
+                     CmEventType  type)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_if_fail (CM_IS_EVENT (self));
+  g_return_if_fail (!priv->event_type);
+  g_return_if_fail (type != CM_M_UNKNOWN);
+
+  priv->event_type = type;
+}
+
+CmEventState
+cm_event_get_state (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), CM_EVENT_STATE_UNKNOWN);
+
+  if (priv->event_state == CM_EVENT_STATE_UNKNOWN &&
+      CM_IS_ACCOUNT (priv->sender))
+    return CM_EVENT_STATE_SENT;
+
+  return priv->event_state;
+}
+
+void
+cm_event_set_state (CmEvent      *self,
+                    CmEventState  state)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  if (priv->event_state == state)
+    return;
+
+  priv->event_state = state;
+  g_signal_emit (self, signals[UPDATED], 0);
+}
+
+void
+cm_event_set_json (CmEvent    *self,
+                   JsonObject *root,
+                   JsonObject *encrypted)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+  JsonObject *child;
+  const char *type;
+
+  g_return_if_fail (CM_IS_EVENT (self));
+
+  if (!root && !encrypted)
+    return;
+
+  type = cm_utils_json_object_get_string (root, "type");
+
+  if (!type)
+    type = cm_utils_json_object_get_string (encrypted, "type");
+
+  /* todo: Handle content less encrypted events (eg: redactions) */
+  if (g_strcmp0 (type, event_type_string (CM_M_ROOM_ENCRYPTED)) == 0)
+    {
+      /* We got something encrypted */
+      if (!encrypted)
+        encrypted = g_steal_pointer (&root);
+      priv->event_type = CM_M_ROOM_ENCRYPTED;
+    }
+
+  if (encrypted)
+    priv->encrypted_json = json_object_ref (encrypted);
+
+  priv->event_id = g_strdup (cm_utils_json_object_get_string (encrypted ?: root, "event_id"));
+  priv->time_stamp = cm_utils_json_object_get_int (encrypted ?: root, "origin_server_ts");
+  if (cm_utils_json_object_get_string (encrypted ?: root, "sender"))
+    priv->sender_id = g_ref_string_new_intern (cm_utils_json_object_get_string (encrypted ?: root, "sender"));
+
+  child = cm_utils_json_object_get_object (encrypted ?: root, "unsigned");
+  if (cm_utils_json_object_has_member (child, "transaction_id"))
+    {
+      g_free (priv->txn_id);
+      priv->txn_id = cm_utils_json_object_dup_string (child, "transaction_id");
+    }
+
+  if (encrypted)
+    event_parse_relations (self, encrypted);
+
+  if (!root)
+    return;
+
+  event_parse_relations (self, root);
+  priv->state_key = g_strdup (cm_utils_json_object_get_string (root, "state_key"));
+  priv->json = json_object_ref (root);
+
+  type = cm_utils_json_object_get_string (root, "type");
+
+  if (g_strcmp0 (type, event_type_string (CM_M_ROOM_MESSAGE)) == 0)
+    priv->event_type = CM_M_ROOM_MESSAGE;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_MEMBER)) == 0)
+    priv->event_type = CM_M_ROOM_MEMBER;
+  else if (g_strcmp0 (type, event_type_string (CM_M_REACTION)) == 0)
+    priv->event_type = CM_M_REACTION;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_REDACTION)) == 0)
+    priv->event_type = CM_M_ROOM_REDACTION;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_TOPIC)) == 0)
+    priv->event_type = CM_M_ROOM_TOPIC;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_AVATAR)) == 0)
+    priv->event_type = CM_M_ROOM_AVATAR;
+  else if (g_strcmp0 (type, event_type_string (CM_M_CALL_INVITE)) == 0)
+    priv->event_type = CM_M_CALL_INVITE;
+  else if (g_strcmp0 (type, event_type_string (CM_M_CALL_CANDIDATES)) == 0)
+    priv->event_type = CM_M_CALL_CANDIDATES;
+  else if (g_strcmp0 (type, event_type_string (CM_M_CALL_ANSWER)) == 0)
+    priv->event_type = CM_M_CALL_ANSWER;
+  else if (g_strcmp0 (type, event_type_string (CM_M_CALL_SELECT_ANSWER)) == 0)
+    priv->event_type = CM_M_CALL_SELECT_ANSWER;
+  else if (g_strcmp0 (type, event_type_string (CM_M_CALL_HANGUP)) == 0)
+    priv->event_type = CM_M_CALL_HANGUP;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_CANONICAL_ALIAS)) == 0)
+    priv->event_type = CM_M_ROOM_CANONICAL_ALIAS;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_NAME)) == 0)
+    priv->event_type = CM_M_ROOM_NAME;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_CREATE)) == 0)
+    priv->event_type = CM_M_ROOM_CREATE;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_POWER_LEVELS)) == 0)
+    priv->event_type = CM_M_ROOM_POWER_LEVELS;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_GUEST_ACCESS)) == 0)
+    priv->event_type = CM_M_ROOM_GUEST_ACCESS;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_HISTORY_VISIBILITY)) == 0)
+    priv->event_type = CM_M_ROOM_HISTORY_VISIBILITY;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_JOIN_RULES)) == 0)
+    priv->event_type = CM_M_ROOM_JOIN_RULES;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_SERVER_ACL)) == 0)
+    priv->event_type = CM_M_ROOM_SERVER_ACL;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_ENCRYPTION)) == 0)
+    priv->event_type = CM_M_ROOM_ENCRYPTION;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_THIRD_PARTY_INVITE)) == 0)
+    priv->event_type = CM_M_ROOM_THIRD_PARTY_INVITE;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_RELATED_GROUPS)) == 0)
+    priv->event_type = CM_M_ROOM_RELATED_GROUPS;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_TOMBSTONE)) == 0)
+    priv->event_type = CM_M_ROOM_TOMBSTONE;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_PINNED_EVENTS)) == 0)
+    priv->event_type = CM_M_ROOM_PINNED_EVENTS;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_PLUMBING)) == 0)
+    priv->event_type = CM_M_ROOM_PLUMBING;
+  else if (g_strcmp0 (type, event_type_string (CM_M_ROOM_BOT_OPTIONS)) == 0)
+    priv->event_type = CM_M_ROOM_BOT_OPTIONS;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_ACCEPT)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_ACCEPT;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_CANCEL)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_CANCEL;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_DONE)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_DONE;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_KEY)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_KEY;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_MAC)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_MAC;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_READY)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_READY;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_REQUEST)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_REQUEST;
+  else if (g_strcmp0 (type, event_type_string (CM_M_KEY_VERIFICATION_START)) == 0)
+    priv->event_type = CM_M_KEY_VERIFICATION_START;
+  else
+    CM_TRACE ("unhandled event type: %s", type);
+}
+
+GRefString *
+cm_event_get_sender_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  if (priv->sender)
+    return cm_user_get_id (priv->sender);
+
+  return priv->sender_id;
+}
+
+CmUser *
+cm_event_get_sender (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->sender;
+}
+
+void
+cm_event_set_sender (CmEvent *self,
+                     CmUser  *sender)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_if_fail (CM_IS_EVENT (self));
+  g_return_if_fail (!priv->sender);
+
+  if (priv->sender_id &&
+      priv->sender_id != cm_user_get_id (sender))
+    g_critical ("user name '%s' and '%s' doesn't match",
+                priv->sender_id, cm_user_get_id (sender));
+
+  priv->sender = g_object_ref (sender);
+}
+
+const char *
+cm_event_get_sender_device_id (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  return priv->sender_device_id;
+}
+
+gboolean
+cm_event_is_encrypted (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), FALSE);
+
+  return !!priv->encrypted_json;
+}
+
+gboolean
+cm_event_has_encrypted_content (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+  JsonObject *child;
+
+  g_return_val_if_fail (CM_IS_EVENT (self), FALSE);
+
+  if (!priv->encrypted_json)
+    return FALSE;
+
+  child = cm_utils_json_object_get_object (priv->encrypted_json, "content");
+
+  return cm_utils_json_object_has_member (child, "ciphertext");
+}
+
+gboolean
+cm_event_is_decrypted (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), FALSE);
+
+  return !!priv->json;
+}
+
+/**
+ * cm_event_get_time_stamp:
+ * @self: A #CmEvent
+ *
+ * Get the event time stamp in milliseconds
+ *
+ * Returns: The milliseconds since Unix epoc
+ */
+gint64
+cm_event_get_time_stamp (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), FALSE);
+
+  if (!priv->time_stamp)
+    return time (NULL) * 1000;
+
+  return priv->time_stamp;
+}
+
+char *
+cm_event_get_json_str (CmEvent  *self,
+                       gboolean  prettify)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  if (priv->json)
+    return cm_utils_json_object_to_string (priv->json, prettify);
+
+  return NULL;
+}
+
+/*
+ * cm_event_get_json:
+ *
+ * Can return %NULL, eg: when the event is encrypted,
+ * and was not able to decrypt.
+ *
+ * Returns: (transfer full) (nullable)
+ */
+JsonObject *
+cm_event_get_json (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  if (priv->json)
+    return json_object_ref (priv->json);
+
+  return NULL;
+}
+
+JsonObject *
+cm_event_get_encrypted_json (CmEvent *self)
+{
+  CmEventPrivate *priv = cm_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+
+  if (priv->encrypted_json)
+    return json_object_ref (priv->encrypted_json);
+
+  return NULL;
+}
+
+JsonObject *
+cm_event_generate_json (CmEvent  *self,
+                        gpointer  room)
+{
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+  g_return_val_if_fail (!room || CM_IS_ROOM (room), NULL);
+
+  return CM_EVENT_GET_CLASS (self)->generate_json (self, room);
+}
+
+char *
+cm_event_get_api_url (CmEvent  *self,
+                      gpointer  room)
+{
+  g_return_val_if_fail (CM_IS_EVENT (self), NULL);
+  g_return_val_if_fail (!room || CM_IS_ROOM (room), NULL);
+
+  return CM_EVENT_GET_CLASS (self)->get_api_url (self, room);
+}
diff --git a/subprojects/libcmatrix/src/events/cm-event.h b/subprojects/libcmatrix/src/events/cm-event.h
new file mode 100644
index 0000000000000000000000000000000000000000..296c44e11b1b933e1c8f40fa94390d02801f8d24
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-event.h
@@ -0,0 +1,49 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-event.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-types.h"
+#include "cm-enums.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_EVENT (cm_event_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (CmEvent, cm_event, CM, EVENT, GObject)
+
+struct _CmEventClass
+{
+  GObjectClass   parent_class;
+
+  /*< private >*/
+  gpointer     (*generate_json) (CmEvent     *self,
+                                 gpointer     room);
+  char        *(*get_api_url)   (CmEvent     *self,
+                                 gpointer     room);
+  gpointer       reserved[8];
+};
+
+const char   *cm_event_get_id            (CmEvent      *self);
+CmUser       *cm_event_get_sender        (CmEvent      *self);
+CmEventType   cm_event_get_m_type        (CmEvent      *self);
+CmEventState  cm_event_get_state         (CmEvent      *self);
+gint64        cm_event_get_time_stamp    (CmEvent      *self);
+gboolean      cm_event_is_encrypted      (CmEvent      *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-room-event-list-private.h b/subprojects/libcmatrix/src/events/cm-room-event-list-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..215fde7fc4cbb01f48ad0091e4aa596521686b98
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-event-list-private.h
@@ -0,0 +1,48 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-client.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <json-glib/json-glib.h>
+#include <gio/gio.h>
+
+#include "cm-types.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_ROOM_EVENT_LIST (cm_room_event_list_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmRoomEventList, cm_room_event_list, CM, ROOM_EVENT_LIST, GObject)
+
+CmRoomEventList *cm_room_event_list_new              (CmRoom          *room);
+void             cm_room_event_list_set_client       (CmRoomEventList *self,
+                                                      CmClient        *client);
+CmEvent         *cm_room_event_list_get_event        (CmRoomEventList *self,
+                                                      CmEventType      type);
+GListModel      *cm_room_event_list_get_events       (CmRoomEventList *self);
+void             cm_room_event_list_set_save_pending (CmRoomEventList *self,
+                                                      gboolean         save_pending);
+gboolean         cm_room_event_list_save_pending     (CmRoomEventList *self);
+JsonObject      *cm_room_event_list_get_local_json   (CmRoomEventList *self);
+void             cm_room_event_list_append_event     (CmRoomEventList *self,
+                                                      CmEvent         *event);
+void             cm_room_event_list_add_events       (CmRoomEventList *self,
+                                                      GPtrArray       *events,
+                                                      gboolean         append);
+void             cm_room_event_list_set_local_json   (CmRoomEventList *self,
+                                                      JsonObject      *root,
+                                                      CmEvent         *last_event);
+void             cm_room_event_list_parse_events     (CmRoomEventList *self,
+                                                      JsonObject      *root,
+                                                      GPtrArray       *events,
+                                                      gboolean         past);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-room-event-list.c b/subprojects/libcmatrix/src/events/cm-room-event-list.c
new file mode 100644
index 0000000000000000000000000000000000000000..74c09497702343b7f179671477385b6456ec8a06
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-event-list.c
@@ -0,0 +1,565 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-client.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-room-event-list"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-utils-private.h"
+#include "cm-event-private.h"
+#include "cm-client-private.h"
+#include "users/cm-user-private.h"
+#include "users/cm-user-list-private.h"
+#include "users/cm-room-member-private.h"
+#include "cm-enc-private.h"
+#include "cm-room-event-private.h"
+#include "cm-room-message-event-private.h"
+#include "cm-room-private.h"
+#include "cm-room-event-list-private.h"
+
+struct _CmRoomEventList
+{
+  GObject       parent_instance;
+
+  CmRoom       *room;
+  CmClient     *client;
+
+  GListStore   *events_list;
+  CmEvent      *room_create_event;
+  CmEvent      *room_name_event;
+  CmEvent      *canonical_alias_event;
+  CmEvent      *room_topic_event;
+  CmEvent      *power_level_event;
+  CmEvent      *encryption_event;
+  CmEvent      *guest_access_event;
+  CmEvent      *join_rules_event;
+  CmEvent      *history_visibility_event;
+  CmEvent      *tombstone_event;
+
+  JsonObject   *local_json;
+
+  gboolean      save_pending: 1;
+};
+
+G_DEFINE_TYPE (CmRoomEventList, cm_room_event_list, G_TYPE_OBJECT)
+
+#define event_m_type_str(_type) (cm_utils_get_event_type_str (_type))
+#define set_json_from_event(_event, _json) do {                 \
+  CmEventType _type;                                            \
+                                                                \
+  if (!_event)                                                  \
+    break;                                                      \
+                                                                \
+  _type = cm_event_get_m_type(_event);                          \
+  json_object_set_object_member (_json,                         \
+                                 event_m_type_str (_type),      \
+                                 cm_event_get_json (_event));   \
+} while (0)
+
+#define set_event_from_json(_room, _event, _json, _m_type) do { \
+  JsonObject *_child;                                           \
+  const char *_type;                                            \
+                                                                \
+  _type = cm_utils_get_event_type_str (_m_type);                \
+  _child = cm_utils_json_object_get_object (_json, _type);      \
+  if (!_child)                                                  \
+    break;                                                      \
+                                                                \
+  _event = (gpointer)cm_room_event_new_from_json (_room,        \
+                                                  _child,       \
+                                                  NULL);        \
+} while (0)
+
+#define set_local_json_event(_json, _event) do {                \
+  JsonObject *_local;                                           \
+  CmEventType _type;                                            \
+                                                                \
+  if (!cm_utils_json_object_get_object (_json, "local"))        \
+    break;                                                      \
+                                                                \
+  _local = cm_utils_json_object_get_object (_json, "local");    \
+  _type = cm_event_get_m_type(_event);                          \
+  json_object_set_object_member (_local,                        \
+                                 event_m_type_str (_type),      \
+                                 cm_event_get_json (event));    \
+} while (0)
+
+static void
+remove_event_with_txn_id (CmRoomEventList *self,
+                          CmEvent         *event)
+{
+  GListModel *events;
+  guint n_items;
+
+  g_assert (CM_IS_ROOM_EVENT_LIST (self));
+  g_assert (CM_IS_EVENT (event));
+
+  if (!cm_event_get_txn_id (event))
+    return;
+
+  events = G_LIST_MODEL (self->events_list);
+  n_items = g_list_model_get_n_items (events);
+
+  /* i and n_items are unsigned */
+  for (guint i = n_items - 1; i + 1 > 0; i--)
+    {
+      g_autoptr(CmEvent) item = NULL;
+
+      item = g_list_model_get_item (events, i);
+
+      if (!cm_event_get_txn_id (item))
+        continue;
+
+      if (g_strcmp0 (cm_event_get_txn_id (event),
+                     cm_event_get_txn_id (item)) == 0)
+        {
+          cm_utils_remove_list_item (self->events_list, item);
+          break;
+        }
+    }
+}
+
+static void
+room_event_list_generate_json (CmRoomEventList *self)
+{
+  JsonObject *json, *child;
+
+  g_assert (CM_IS_ROOM_EVENT_LIST (self));
+  g_assert (!self->local_json);
+
+  json = json_object_new ();
+  child = json_object_new ();
+  self->local_json = json;
+
+  json_object_set_object_member (json, "local", child);
+
+  json_object_set_string_member (child, "alias", cm_room_get_name (self->room));
+  /* Alias set before the current one, may be used if current one is NULL (eg: was x) */
+  json_object_set_string_member (child, "last_alias", cm_room_get_name (self->room));
+  json_object_set_boolean_member (child, "direct", cm_room_is_direct (self->room));
+  json_object_set_int_member (child, "encryption", cm_room_is_encrypted (self->room));
+
+  set_json_from_event (self->room_create_event, child);
+  set_json_from_event (self->room_name_event, child);
+  set_json_from_event (self->canonical_alias_event, child);
+  set_json_from_event (self->room_topic_event, child);
+  set_json_from_event (self->encryption_event, child);
+  set_json_from_event (self->power_level_event, child);
+  set_json_from_event (self->guest_access_event, child);
+  set_json_from_event (self->join_rules_event, child);
+  set_json_from_event (self->history_visibility_event, child);
+  set_json_from_event (self->tombstone_event, child);
+
+  /* todo */
+  /* Set only if there is only one member in the room */
+  /* json_object_set_string_member (child, "m.room.member", "bad"); */
+  /* json_object_set_object_member (child, "summary", "bad"); */
+  /* json_object_set_object_member (child, "unread_notifications", "bad"); */
+}
+
+static void
+cm_room_event_list_finalize (GObject *object)
+{
+  CmRoomEventList *self = (CmRoomEventList *)object;
+
+  g_clear_object (&self->events_list);
+  g_clear_object (&self->room_create_event);
+  g_clear_object (&self->room_name_event);
+  g_clear_object (&self->canonical_alias_event);
+  g_clear_object (&self->room_topic_event);
+  g_clear_object (&self->power_level_event);
+  g_clear_object (&self->encryption_event);
+  g_clear_object (&self->guest_access_event);
+  g_clear_object (&self->join_rules_event);
+  g_clear_object (&self->history_visibility_event);
+  g_clear_object (&self->tombstone_event);
+  g_clear_pointer (&self->local_json, json_object_unref);
+
+  G_OBJECT_CLASS (cm_room_event_list_parent_class)->finalize (object);
+}
+
+static void
+cm_room_event_list_class_init (CmRoomEventListClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_room_event_list_finalize;
+}
+
+static void
+cm_room_event_list_init (CmRoomEventList *self)
+{
+  self->events_list = g_list_store_new (CM_TYPE_EVENT);
+}
+
+CmRoomEventList *
+cm_room_event_list_new (CmRoom *room)
+{
+  CmRoomEventList *self;
+
+  self = g_object_new (CM_TYPE_ROOM_EVENT_LIST, NULL);
+  g_set_weak_pointer (&self->room, room);
+
+  g_debug ("(%p) New event list for room %p", self, room);
+
+  return self;
+}
+
+void
+cm_room_event_list_set_client (CmRoomEventList *self,
+                               CmClient        *client)
+{
+  guint n_items;
+
+  g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+  g_return_if_fail (!self->client);
+
+  g_set_weak_pointer (&self->client, client);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->events_list));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmEvent) event = NULL;
+      CmUser *user;
+
+      event = g_list_model_get_item (G_LIST_MODEL (self->events_list), i);
+      user = cm_event_get_sender (event);
+
+      if (!user)
+        {
+          user = cm_room_find_user (self->room, cm_event_get_sender_id (event), TRUE);
+          cm_event_set_sender (event, user);
+        }
+
+      cm_user_set_client (user, client);
+    }
+}
+
+CmEvent *
+cm_room_event_list_get_event (CmRoomEventList *self,
+                              CmEventType      type)
+{
+  g_return_val_if_fail (CM_IS_ROOM_EVENT_LIST (self), NULL);
+
+  if (type == CM_M_ROOM_CREATE)
+    return self->room_create_event;
+
+  if (type == CM_M_ROOM_NAME)
+    return self->tombstone_event;
+
+  if (type == CM_M_ROOM_CANONICAL_ALIAS)
+    return self->canonical_alias_event;
+
+  if (type == CM_M_ROOM_POWER_LEVELS)
+    return self->power_level_event;
+
+  if (type == CM_M_ROOM_ENCRYPTION)
+    return self->encryption_event;
+
+  if (type == CM_M_ROOM_GUEST_ACCESS)
+    return self->guest_access_event;
+
+  if (type == CM_M_ROOM_JOIN_RULES)
+    return self->join_rules_event;
+
+  if (type == CM_M_ROOM_HISTORY_VISIBILITY)
+    return self->history_visibility_event;
+
+  if (type == CM_M_ROOM_TOMBSTONE)
+    return self->tombstone_event;
+
+  return NULL;
+}
+
+GListModel *
+cm_room_event_list_get_events (CmRoomEventList *self)
+{
+  g_return_val_if_fail (self, NULL);
+  g_return_val_if_fail (self->room, NULL);
+
+  return G_LIST_MODEL (self->events_list);
+}
+
+void
+cm_room_event_list_set_save_pending (CmRoomEventList *self,
+                                     gboolean         save_pending)
+{
+  g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self));
+
+  self->save_pending = !!save_pending;
+}
+
+gboolean
+cm_room_event_list_save_pending (CmRoomEventList *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM_EVENT_LIST (self), FALSE);
+
+  return self->save_pending;
+}
+
+JsonObject *
+cm_room_event_list_get_local_json (CmRoomEventList *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM_EVENT_LIST (self), NULL);
+  g_return_val_if_fail (self->room, NULL);
+
+  if (!self->local_json)
+    room_event_list_generate_json (self);
+
+  return self->local_json;
+}
+
+void
+cm_room_event_list_append_event (CmRoomEventList *self,
+                                 CmEvent         *event)
+{
+  g_assert (CM_IS_ROOM_EVENT_LIST (self));
+  g_assert (CM_IS_ROOM (self->room));
+
+  g_list_store_append (self->events_list, event);
+}
+
+void
+cm_room_event_list_add_events (CmRoomEventList *self,
+                               GPtrArray       *events,
+                               gboolean         append)
+{
+  CmClient *client;
+  guint position = 0;
+
+  g_assert (CM_IS_ROOM_EVENT_LIST (self));
+  g_assert (CM_IS_ROOM (self->room));
+
+  if (!events || !events->len)
+    return;
+
+  client = cm_room_get_client (self->room);
+
+  for (guint i = 0; i < events->len; i++)
+    {
+      CmEvent *event = events->pdata[i];
+      CmUser *user;
+
+      if (cm_event_get_sender (event) || !client)
+        continue;
+
+      user = cm_room_find_user (self->room, cm_event_get_sender_id (event), TRUE);
+      cm_event_set_sender (event, user);
+    }
+
+  if (append)
+    {
+      position = g_list_model_get_n_items (G_LIST_MODEL (self->events_list));
+      g_list_store_splice (self->events_list,
+                           position, 0, events->pdata, events->len);
+    }
+  else
+    {
+      g_autoptr(GPtrArray) reversed = NULL;
+
+      reversed = g_ptr_array_sized_new (events->len);
+
+      for (guint i = 0; i < events->len; i++)
+        g_ptr_array_insert (reversed, 0, events->pdata[i]);
+      g_list_store_splice (self->events_list,
+                           0, 0, reversed->pdata, reversed->len);
+    }
+}
+
+void
+cm_room_event_list_set_local_json (CmRoomEventList *self,
+                                   JsonObject      *root,
+                                   CmEvent         *last_event)
+{
+  GListModel *model;
+  JsonObject *local;
+  CmRoom *room;
+
+  g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self));
+  g_return_if_fail (!self->local_json);
+
+  model = G_LIST_MODEL (self->events_list);
+  /* Local json should be set only before the events are loaded */
+  g_return_if_fail (g_list_model_get_n_items (model) == 0);
+
+  if (last_event)
+    g_list_store_append (self->events_list, last_event);
+
+  if (!root)
+    return;
+
+  room = self->room;
+  self->local_json = json_object_ref (root);
+  local = cm_utils_json_object_get_object (root, "local");
+
+  set_event_from_json (room, self->room_name_event, local, CM_M_ROOM_NAME);
+  set_event_from_json (room, self->room_topic_event, local, CM_M_ROOM_TOPIC);
+  set_event_from_json (room, self->room_create_event, local, CM_M_ROOM_CREATE);
+  set_event_from_json (room, self->tombstone_event, local, CM_M_ROOM_TOMBSTONE);
+  set_event_from_json (room, self->encryption_event, local, CM_M_ROOM_ENCRYPTION);
+  set_event_from_json (room, self->join_rules_event, local, CM_M_ROOM_JOIN_RULES);
+  set_event_from_json (room, self->power_level_event, local, CM_M_ROOM_POWER_LEVELS);
+  set_event_from_json (room, self->guest_access_event, local, CM_M_ROOM_GUEST_ACCESS);
+  set_event_from_json (room, self->canonical_alias_event, local, CM_M_ROOM_CANONICAL_ALIAS);
+  set_event_from_json (room, self->history_visibility_event, local, CM_M_ROOM_HISTORY_VISIBILITY);
+}
+
+static JsonObject *
+event_list_decrypt (CmRoomEventList *self,
+                    JsonObject      *root)
+{
+  char *plain_text = NULL;
+  JsonObject *content;
+  CmClient *client;
+  CmEnc *enc;
+
+  g_assert (CM_IS_ROOM_EVENT_LIST (self));
+
+  client = cm_room_get_client (self->room);
+  enc = cm_client_get_enc (client);
+
+  if (!enc || !root)
+    return NULL;
+
+  content = cm_utils_json_object_get_object (root, "content");
+  plain_text = cm_enc_handle_join_room_encrypted (enc, self->room, content);
+
+  return cm_utils_string_to_json_object (plain_text);
+}
+
+void
+cm_room_event_list_parse_events (CmRoomEventList *self,
+                                 JsonObject      *root,
+                                 GPtrArray       *events,
+                                 gboolean         past)
+{
+  JsonObject *child;
+  JsonArray *array;
+  guint length = 0;
+
+  g_return_if_fail (CM_IS_ROOM_EVENT_LIST (self));
+  g_return_if_fail (self->room);
+
+  if (!root)
+    return;
+
+  /* If @events is NULL, they are considered to be state
+   * events and thus it shouldn't be past events.
+   */
+  if (!events)
+    g_return_if_fail (!past);
+
+  g_debug ("(%p) Parsing events %p, state event: %s, past events: %s",
+           self->room, root, CM_LOG_BOOL (!events), CM_LOG_BOOL (past));
+
+  array = cm_utils_json_object_get_array (root, "events");
+
+  if (!array)
+    array = cm_utils_json_object_get_array (root, "chunk");
+
+  if (array)
+    length = json_array_get_length (array);
+
+  for (guint i = 0; i < length; i++)
+    {
+      g_autoptr(CmEvent) event = NULL;
+      JsonObject *decrypted = NULL;
+      CmUser *user;
+      const char *value;
+      CmEventType type;
+      gboolean encrypted = FALSE;
+
+      child = json_array_get_object_element (array, i);
+
+      if (g_strcmp0 (cm_utils_json_object_get_string (child, "type"),
+                     "m.room.encrypted") == 0)
+        {
+          decrypted = event_list_decrypt (self, child);
+          encrypted = TRUE;
+        }
+
+      event = (gpointer)cm_room_event_new_from_json (self->room, encrypted ? decrypted : child,
+                                                     encrypted ? child : NULL);
+      if (!event)
+        {
+          g_debug ("no event created from json");
+          continue;
+        }
+
+      value = cm_event_get_sender_id (event);
+      user = cm_room_find_user (self->room, cm_event_get_sender_id (event), TRUE);
+      cm_event_set_sender (event, user);
+
+      if (events)
+        {
+          if (CM_IS_ROOM_MESSAGE_EVENT (event) &&
+              cm_event_get_txn_id (event))
+            remove_event_with_txn_id (self, event);
+
+          g_ptr_array_add (events, g_object_ref (event));
+        }
+
+      /* past events shouldn't alter room state, as they may be obsolete */
+      if (past)
+        continue;
+
+      type = cm_event_get_m_type (event);
+
+      if (type == CM_M_ROOM_NAME)
+        {
+          g_set_object (&self->room_name_event, event);
+          value = cm_room_event_get_room_name (CM_ROOM_EVENT (event));
+          cm_room_set_name (self->room, value);
+        }
+      else if (type == CM_M_ROOM_MEMBER)
+        cm_room_update_user (self->room, event);
+      else if (type == CM_M_ROOM_POWER_LEVELS)
+        g_set_object (&self->power_level_event, event);
+      else if (type == CM_M_ROOM_ENCRYPTION)
+        g_set_object (&self->encryption_event, event);
+      else if (type == CM_M_ROOM_CANONICAL_ALIAS)
+        g_set_object (&self->canonical_alias_event, event);
+      else if (type == CM_M_ROOM_TOPIC)
+        g_set_object (&self->room_topic_event, event);
+      else if (type == CM_M_ROOM_GUEST_ACCESS)
+        g_set_object (&self->guest_access_event, event);
+      else if (type == CM_M_ROOM_JOIN_RULES)
+        g_set_object (&self->join_rules_event, event);
+      else if (type == CM_M_ROOM_HISTORY_VISIBILITY)
+        g_set_object (&self->history_visibility_event, event);
+      else if (type == CM_M_ROOM_TOMBSTONE)
+        g_set_object (&self->tombstone_event, event);
+
+      if (type == CM_M_ROOM_NAME ||
+          type == CM_M_ROOM_POWER_LEVELS ||
+          type == CM_M_ROOM_ENCRYPTION ||
+          type == CM_M_ROOM_CANONICAL_ALIAS ||
+          type == CM_M_ROOM_TOPIC ||
+          type == CM_M_ROOM_GUEST_ACCESS ||
+          type == CM_M_ROOM_JOIN_RULES ||
+          type == CM_M_ROOM_HISTORY_VISIBILITY ||
+          type == CM_M_ROOM_TOMBSTONE)
+        {
+          set_local_json_event (self->local_json, event);
+          self->save_pending = TRUE;
+        }
+
+      if (type == CM_M_ROOM_ENCRYPTION)
+        g_object_notify (G_OBJECT (self->room), "encrypted");
+    }
+
+  if (events && events->len)
+    cm_room_event_list_add_events (self, events, !past);
+}
diff --git a/subprojects/libcmatrix/src/events/cm-room-event-private.h b/subprojects/libcmatrix/src/events/cm-room-event-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..e7aa1c990827f398f829a7e18ab240e5d3d0b2b3
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-event-private.h
@@ -0,0 +1,43 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <json-glib/json-glib.h>
+
+#include "cm-types.h"
+#include "cm-room-event.h"
+
+G_BEGIN_DECLS
+
+CmRoomEvent  *cm_room_event_new_from_json           (gpointer             room,
+                                                     JsonObject          *root,
+                                                     JsonObject          *encrypted);
+const char   *cm_room_event_get_room_name           (CmRoomEvent         *self);
+const char   *cm_room_event_get_encryption          (CmRoomEvent         *self);
+JsonObject   *cm_room_event_get_room_member_json    (CmRoomEvent         *self,
+                                                     const char         **user_id);
+void          cm_room_event_set_room_member         (CmRoomEvent         *self,
+                                                     CmUser              *user);
+GRefString   *cm_room_event_get_room_member_id      (CmRoomEvent         *self);
+gboolean      cm_room_event_user_has_power          (CmRoomEvent         *self,
+                                                     const char          *user_id,
+                                                     CmEventType          event);
+
+GPtrArray    *cm_room_event_get_admin_ids           (CmRoomEvent         *self);
+void          cm_room_event_set_admin_users         (CmRoomEvent         *self,
+                                                     GPtrArray           *users);
+CmStatus      cm_room_event_get_status              (CmRoomEvent         *self);
+const char   *cm_room_event_get_replacement_room_id (CmRoomEvent         *self);
+guint         cm_room_event_get_rotation_count      (CmRoomEvent         *self);
+gint64        cm_room_event_get_rotation_time       (CmRoomEvent         *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-room-event.c b/subprojects/libcmatrix/src/events/cm-room-event.c
new file mode 100644
index 0000000000000000000000000000000000000000..a9b0f96495ff46492f791a89c1d090408d17f1af
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-event.c
@@ -0,0 +1,476 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-room-event"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-room.h"
+#include "cm-room-private.h"
+#include "cm-utils-private.h"
+#include "cm-event-private.h"
+#include "cm-room-message-event-private.h"
+#include "cm-room-event-private.h"
+#include "cm-room-event.h"
+
+typedef struct
+{
+  CmRoom        *room;
+  char          *room_name;
+  char          *encryption;
+  GRefString    *member_id;
+  char          *replacement_room_id;
+  GPtrArray     *users;
+  JsonObject    *json;
+  CmStatus       member_status;
+
+  guint          enc_rotation_count;
+  guint          enc_rotation_time;
+} CmRoomEventPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (CmRoomEvent, cm_room_event, CM_TYPE_EVENT)
+
+#define ret_val_if_fail(_event, _val1, _val2, _ret) do {                \
+    CmEventType type, val;                                              \
+                                                                        \
+    val = _val1 ?: _val2;                                               \
+    type = cm_event_get_m_type (CM_EVENT (_event));                     \
+    g_return_val_if_fail (type == _val1 || type == val, _ret);          \
+  } while (0)
+
+#define ret_if_fail(_event, _expected) do {             \
+    CmEventType type;                                   \
+                                                        \
+    type = cm_event_get_m_type (CM_EVENT (_event));     \
+    g_return_if_fail (type == _expected);               \
+  } while (0)
+
+static void
+cm_room_event_finalize (GObject *object)
+{
+  CmRoomEvent *self = (CmRoomEvent *)object;
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_clear_object (&priv->room);
+  g_free (priv->room_name);
+  g_free (priv->encryption);
+  g_clear_pointer (&priv->member_id, g_ref_string_release);
+  g_free (priv->replacement_room_id);
+  g_clear_pointer (&priv->users, g_ptr_array_unref);
+  g_clear_pointer (&priv->json, json_object_unref);
+
+  G_OBJECT_CLASS (cm_room_event_parent_class)->finalize (object);
+}
+
+static void
+cm_room_event_class_init (CmRoomEventClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_room_event_finalize;
+}
+
+static void
+cm_room_event_init (CmRoomEvent *self)
+{
+}
+
+CmRoomEvent *
+cm_room_event_new_from_json (gpointer    room,
+                             JsonObject *root,
+                             JsonObject *encrypted)
+{
+  CmRoomEventPrivate *priv;
+  CmRoomEvent *self = NULL;
+  JsonObject *child;
+  const char *value, *event_type;
+  CmEventType type;
+
+  g_return_val_if_fail (CM_IS_ROOM (room), NULL);
+  g_return_val_if_fail (root || encrypted, NULL);
+
+  event_type = cm_utils_json_object_get_string (root, "type");
+
+  /* currently, only room messages are encrypted */
+  if (encrypted && root)
+    self = cm_room_message_event_new_from_json (root);
+
+  if (!self)
+    {
+      if (g_strcmp0 (event_type, "m.room.message") == 0)
+        self = cm_room_message_event_new_from_json (root);
+      else
+        self = g_object_new (CM_TYPE_ROOM_EVENT, NULL);
+    }
+
+  priv = cm_room_event_get_instance_private (self);
+  priv->room = g_object_ref (room);
+  cm_event_set_json (CM_EVENT (self), root, encrypted);
+
+  if (!root)
+    return self;
+
+  priv->json = json_object_ref (root);
+
+  if (CM_IS_ROOM_MESSAGE_EVENT (self))
+    return self;
+
+  type = cm_event_get_m_type (CM_EVENT (self));
+  child = cm_utils_json_object_get_object (root, "content");
+
+  if (type == CM_M_ROOM_NAME)
+    {
+      value = cm_utils_json_object_get_string (child, "name");
+      g_free (priv->room_name);
+      priv->room_name = g_strdup (value);
+    }
+  else if (type == CM_M_ROOM_ENCRYPTION)
+    {
+      value = cm_utils_json_object_get_string (child, "algorithm");
+      priv->encryption = g_strdup (value);
+      priv->enc_rotation_count = cm_utils_json_object_get_int (child, "rotation_period_msgs");
+      priv->enc_rotation_time = cm_utils_json_object_get_int (child, "rotation_period_ms");
+
+      /* Set recommended defaults if not set */
+      if (!priv->enc_rotation_count)
+        priv->enc_rotation_count = 100;
+
+      if (!priv->enc_rotation_time)
+        priv->enc_rotation_time = 60 * 60 * 24 * 7; /* One week */
+    }
+  else if (type == CM_M_ROOM_MEMBER)
+    {
+      const char *membership;
+
+      membership = cm_utils_json_object_get_string (child, "membership");
+      priv->member_status = CM_STATUS_UNKNOWN;
+
+      if (g_strcmp0 (membership, "join") == 0)
+        priv->member_status = CM_STATUS_JOIN;
+      else if (g_strcmp0 (membership, "invite") == 0)
+        priv->member_status = CM_STATUS_INVITE;
+      else if (g_strcmp0 (membership, "leave") == 0)
+        priv->member_status = CM_STATUS_LEAVE;
+      else if (g_strcmp0 (membership, "ban") == 0)
+        priv->member_status = CM_STATUS_BAN;
+      else if (g_strcmp0 (membership, "knock") == 0)
+        priv->member_status = CM_STATUS_KNOCK;
+
+      if (priv->member_status == CM_STATUS_INVITE)
+        priv->member_id = g_ref_string_new_intern (cm_event_get_state_key (CM_EVENT (self)));
+      else
+        priv->member_id = g_ref_string_new_intern (cm_event_get_sender_id (CM_EVENT (self)));
+    }
+  else if (type == CM_M_ROOM_TOMBSTONE)
+    {
+      value = cm_utils_json_object_get_string (child, "replacement_room");
+      priv->replacement_room_id = g_strdup (value);
+    }
+
+  return self;
+}
+
+CmRoom *
+cm_room_event_get_room (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+
+  return priv->room;
+}
+
+const char *
+cm_room_event_get_room_name (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  ret_val_if_fail (self, CM_M_ROOM_NAME, 0, NULL);
+
+  return priv->room_name;
+}
+
+const char *
+cm_room_event_get_encryption (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  ret_val_if_fail (self, CM_M_ROOM_ENCRYPTION, 0, NULL);
+
+  return priv->encryption;
+}
+
+JsonObject *
+cm_room_event_get_room_member_json (CmRoomEvent  *self,
+                                    const char  **user_id)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+  JsonObject *child;
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, NULL);
+
+  child = cm_utils_json_object_get_object (priv->json, "content");
+
+  if (user_id)
+    {
+      if (g_strcmp0 (cm_utils_json_object_get_string (child, "membership"), "join") == 0)
+        *user_id = cm_utils_json_object_get_string (priv->json, "sender");
+      else
+        *user_id = cm_utils_json_object_get_string (priv->json, "state_key");
+
+      if (G_UNLIKELY (!*user_id || !**user_id))
+        *user_id = cm_utils_json_object_get_string (priv->json, "sender");
+    }
+
+  return child;
+}
+
+void
+cm_room_event_set_room_member (CmRoomEvent *self,
+                               CmUser      *user)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+  const char *user_id;
+
+  g_return_if_fail (CM_IS_ROOM_EVENT (self));
+  g_return_if_fail (CM_IS_USER (user));
+  g_return_if_fail (!priv->users);
+  ret_if_fail (self, CM_M_ROOM_MEMBER);
+
+  cm_room_event_get_room_member_json (self, &user_id);
+  g_return_if_fail (g_strcmp0 (cm_user_get_id (user), user_id) == 0);
+
+  priv->users = g_ptr_array_new_full (1, g_object_unref);
+  g_ptr_array_add (priv->users, g_object_ref (user));
+}
+
+CmUser *
+cm_room_event_get_room_member (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, NULL);
+  g_return_val_if_fail (priv->users, NULL);
+
+  return priv->users->pdata[0];
+}
+
+GRefString *
+cm_room_event_get_room_member_id (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, NULL);
+
+  return priv->member_id;
+}
+
+gboolean
+cm_room_event_user_has_power (CmRoomEvent *self,
+                              const char  *user_id,
+                              CmEventType  event)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+  JsonObject *child, *content;
+  int user_power = 0;
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), FALSE);
+  g_return_val_if_fail (user_id && *user_id == '@', FALSE);
+  g_return_val_if_fail (priv->json, FALSE);
+  ret_val_if_fail (self, CM_M_ROOM_POWER_LEVELS, 0, FALSE);
+
+  if (!priv->json)
+    return FALSE;
+
+  content = cm_utils_json_object_get_object (priv->json, "content");
+  child = cm_utils_json_object_get_object (content, "users");
+  user_power = cm_utils_json_object_get_int (child, user_id);
+
+  if (!user_power)
+    user_power = cm_utils_json_object_get_int (content, "users_default");
+
+  child = cm_utils_json_object_get_object (content, "events");
+
+  if (event == CM_M_ROOM_NAME)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.name");
+
+  if (event == CM_M_ROOM_POWER_LEVELS)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.power_levels");
+
+  if (event == CM_M_ROOM_HISTORY_VISIBILITY)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.history_visibility");
+
+  if (event == CM_M_ROOM_CANONICAL_ALIAS)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.canonical_alias");
+
+  if (event == CM_M_ROOM_AVATAR)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.avatar");
+
+  if (event == CM_M_ROOM_TOMBSTONE)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.tombstone");
+
+  if (event == CM_M_ROOM_SERVER_ACL)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.server_acl");
+
+  if (event == CM_M_ROOM_ENCRYPTION)
+    return user_power >= cm_utils_json_object_get_int (child, "m.room.encryption");
+
+  if (event == CM_M_ROOM_INVITE)
+    {
+      if (!cm_utils_json_object_has_member (content, "invite"))
+        return user_power >= 50;
+
+      return user_power >= cm_utils_json_object_get_int (content, "invite");
+    }
+
+  if (event == CM_M_ROOM_BAN)
+    {
+      if (!cm_utils_json_object_has_member (content, "ban"))
+        return user_power >= 50;
+
+      return user_power >= cm_utils_json_object_get_int (content, "ban");
+    }
+
+  if (event == CM_M_ROOM_KICK)
+    {
+      if (!cm_utils_json_object_has_member (content, "kick"))
+        return user_power >= 50;
+
+      return user_power >= cm_utils_json_object_get_int (content, "kick");
+    }
+
+  return user_power >= cm_utils_json_object_get_int (content, "events_default");
+}
+
+/*
+ * cm_room_event_get_power_level_admins:
+ * @self: A #CmRoomEvent
+ *
+ * Get the list of users that have power greater than
+ * the default room power level, as reported by the
+ * server.
+ *
+ * Returns: (transfer full): An GPtrArray of strings
+ */
+GPtrArray *
+cm_room_event_get_admin_ids (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+  g_autoptr(GList) users = NULL;
+  GPtrArray *admin_ids;
+  JsonObject *child;
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  g_return_val_if_fail (priv->json, NULL);
+  ret_val_if_fail (self, CM_M_ROOM_POWER_LEVELS, 0, NULL);
+
+  child = cm_utils_json_object_get_object (priv->json, "content");
+  child = cm_utils_json_object_get_object (child, "users");
+  if (child)
+    users = json_object_get_members (child);
+
+  admin_ids = g_ptr_array_new_full (16, g_free);
+
+  for (GList *node = users; node && node->data; node = node->next)
+    g_ptr_array_add (admin_ids, g_strdup (node->data));
+
+  return admin_ids;
+}
+
+/*
+ * cm_room_event_set_admin_users:
+ * @self: A #CmRoomEvent
+ * @users: (transfer full): An array of #CmUser
+ *
+ * Set the list of admin users, the list of user id
+ * should match cm_room_event_get_admin_ids()
+ *
+ * each member in @users should have a ref added.
+ */
+void
+cm_room_event_set_admin_users (CmRoomEvent *self,
+                               GPtrArray   *users)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+  JsonObject *child;
+
+  g_return_if_fail (CM_IS_ROOM_EVENT (self));
+  g_return_if_fail (users);
+  g_return_if_fail (priv->json);
+  g_return_if_fail (!priv->users);
+  ret_if_fail (self, CM_M_ROOM_POWER_LEVELS);
+
+  child = cm_utils_json_object_get_object (priv->json, "content");
+  child = cm_utils_json_object_get_object (child, "users");
+  g_return_if_fail (child);
+  g_return_if_fail (json_object_get_size (child) == users->len);
+
+  for (guint i = 0; i < users->len; i++)
+    {
+      CmUser *user = users->pdata[i];
+
+      /* Never supposed to happen */
+      if (!json_object_has_member (child, cm_user_get_id (user)))
+        {
+          g_critical ("User '%s' not in list", cm_user_get_id (user));
+          return;
+        }
+    }
+
+  priv->users = users;
+}
+
+CmStatus
+cm_room_event_get_status (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), CM_STATUS_UNKNOWN);
+  ret_val_if_fail (self, CM_M_ROOM_MEMBER, 0, CM_STATUS_UNKNOWN);
+
+  return priv->member_status;
+}
+
+const char *
+cm_room_event_get_replacement_room_id (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), NULL);
+  ret_val_if_fail (self, CM_M_ROOM_TOMBSTONE, 0, NULL);
+
+  return priv->replacement_room_id;
+}
+
+guint
+cm_room_event_get_rotation_count (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), 100);
+  ret_val_if_fail (self, CM_M_ROOM_ENCRYPTION, 0, 100);
+
+  return priv->enc_rotation_count;
+}
+
+gint64
+cm_room_event_get_rotation_time (CmRoomEvent *self)
+{
+  CmRoomEventPrivate *priv = cm_room_event_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_ROOM_EVENT (self), 100);
+  ret_val_if_fail (self, CM_M_ROOM_ENCRYPTION, 0, 100);
+
+  return priv->enc_rotation_time;
+}
diff --git a/subprojects/libcmatrix/src/events/cm-room-event.h b/subprojects/libcmatrix/src/events/cm-room-event.h
new file mode 100644
index 0000000000000000000000000000000000000000..2e77ea35a44092e0c7aca4810436944965c2ccdd
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-event.h
@@ -0,0 +1,38 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-room-event.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include "cm-event.h"
+#include "cm-types.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_ROOM_EVENT (cm_room_event_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (CmRoomEvent, cm_room_event, CM, ROOM_EVENT, CmEvent)
+
+struct _CmRoomEventClass
+{
+  CmEventClass parent_class;
+
+  /*< private >*/
+  gpointer reserved[8];
+};
+
+CmRoom *cm_room_event_get_room        (CmRoomEvent *self);
+CmUser *cm_room_event_get_room_member (CmRoomEvent *self);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-room-message-event-private.h b/subprojects/libcmatrix/src/events/cm-room-message-event-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..0f36b31e0a7cba5acd93988a80ecb4c2bda8227a
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-message-event-private.h
@@ -0,0 +1,33 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <json-glib/json-glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-room-event-private.h"
+#include "cm-room-message-event.h"
+
+G_BEGIN_DECLS
+
+CmRoomMessageEvent *cm_room_message_event_new           (CmContentType       type);
+CmRoomEvent        *cm_room_message_event_new_from_json (JsonObject         *root);
+void                cm_room_message_event_set_body      (CmRoomMessageEvent *self,
+                                                         const char         *text);
+void                cm_room_message_event_set_file      (CmRoomMessageEvent *self,
+                                                         const char         *body,
+                                                         GFile              *file);
+GFile              *cm_room_message_event_get_file      (CmRoomMessageEvent *self);
+
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/events/cm-room-message-event.c b/subprojects/libcmatrix/src/events/cm-room-message-event.c
new file mode 100644
index 0000000000000000000000000000000000000000..485affea2df755cfa3fb7c79a0a75e8fdfc23cbc
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-message-event.c
@@ -0,0 +1,450 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-room-message-event"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-utils-private.h"
+#include "cm-common.h"
+#include "cm-matrix-private.h"
+#include "cm-enums.h"
+#include "cm-client-private.h"
+#include "cm-room-private.h"
+#include "cm-input-stream-private.h"
+#include "cm-event-private.h"
+#include "cm-room-message-event-private.h"
+
+struct _CmRoomMessageEvent
+{
+  CmRoomEvent     parent_instance;
+
+  JsonObject     *json;
+  CmContentType   type;
+
+  char           *body;
+  GFile          *file;
+  GInputStream   *file_istream;
+  char           *mxc_uri;
+
+  gboolean       downloading_file;
+};
+
+G_DEFINE_TYPE (CmRoomMessageEvent, cm_room_message_event, CM_TYPE_ROOM_EVENT)
+
+static JsonObject *
+room_message_generate_json (CmRoomMessageEvent *self,
+                            CmRoom             *room)
+{
+  g_autofree char *uri = NULL;
+  const char *body, *room_id;
+  CmClient *client;
+  JsonObject *root;
+  GFile *file;
+
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (self));
+  g_assert (CM_IS_ROOM (room));
+
+  body = cm_room_message_event_get_body (self);
+  file = cm_room_message_event_get_file (self);
+  client = cm_room_get_client (room);
+  room_id = cm_room_get_id (room);
+
+  root = json_object_new ();
+  if (file)
+    {
+      g_autofree char *name = NULL;
+
+      name = g_file_get_basename (file);
+      json_object_set_string_member (root, "msgtype", "m.file");
+      json_object_set_string_member (root, "body", name);
+      json_object_set_string_member (root, "filename", name);
+      if (!cm_room_is_encrypted (room))
+        {
+          const char *mxc_uri;
+
+          mxc_uri = g_object_get_data (G_OBJECT (file), "uri");
+          if (mxc_uri)
+            json_object_set_string_member (root, "url", mxc_uri);
+          else
+            g_warn_if_reached ();
+        }
+    }
+  else
+    {
+      json_object_set_string_member (root, "msgtype", "m.text");
+      json_object_set_string_member (root, "body", body);
+    }
+
+  if (cm_room_is_encrypted (room))
+    {
+      g_autofree char *text = NULL;
+      JsonObject *object;
+
+      object = json_object_new ();
+      json_object_set_string_member (object, "type", "m.room.message");
+      json_object_set_string_member (object, "room_id", room_id);
+      json_object_set_object_member (object, "content", root);
+
+      if (file)
+        {
+          JsonObject *file_json;
+          CmInputStream *stream;
+
+          stream = g_object_get_data (G_OBJECT (file), "stream");
+          file_json = cm_input_stream_get_file_json (stream);
+          json_object_set_object_member (root, "file", file_json);
+        }
+
+      text = cm_utils_json_object_to_string (object, FALSE);
+      json_object_unref (object);
+      object = cm_enc_encrypt_for_chat (cm_client_get_enc (client),
+                                        room, text);
+      return object;
+    }
+  else
+    {
+      return root;
+    }
+}
+
+static gpointer
+cm_room_message_event_generate_json (CmEvent  *event,
+                                     gpointer  room)
+{
+  CmRoomMessageEvent *self = (CmRoomMessageEvent *)event;
+
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (self));
+  g_return_val_if_fail (CM_IS_ROOM (room), NULL);
+
+  return room_message_generate_json (self, room);
+}
+
+static char *
+cm_room_message_event_get_api_url (CmEvent  *event,
+                                   gpointer  room)
+{
+  CmRoomMessageEvent *self = (CmRoomMessageEvent *)event;
+  char *uri;
+
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (self));
+  g_return_val_if_fail (CM_IS_ROOM (room), NULL);
+  g_return_val_if_fail (cm_event_get_txn_id (event), NULL);
+
+  if (cm_room_is_encrypted (room))
+    uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.encrypted/%s",
+                           cm_room_get_id (room),
+                           cm_event_get_txn_id (event));
+  else
+    uri = g_strdup_printf ("/_matrix/client/r0/rooms/%s/send/m.room.message/%s",
+                           cm_room_get_id (room),
+                           cm_event_get_txn_id (event));
+
+  return uri;
+}
+
+static void
+cm_room_message_event_finalize (GObject *object)
+{
+  CmRoomMessageEvent *self = (CmRoomMessageEvent *)object;
+
+  g_clear_pointer (&self->json, json_object_unref);
+  g_free (self->body);
+  g_free (self->mxc_uri);
+
+  G_OBJECT_CLASS (cm_room_message_event_parent_class)->finalize (object);
+}
+
+static void
+cm_room_message_event_class_init (CmRoomMessageEventClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  CmEventClass *event_class = CM_EVENT_CLASS (klass);
+
+  object_class->finalize = cm_room_message_event_finalize;
+
+  event_class->generate_json = cm_room_message_event_generate_json;
+  event_class->get_api_url = cm_room_message_event_get_api_url;
+}
+
+static void
+cm_room_message_event_init (CmRoomMessageEvent *self)
+{
+}
+
+CmRoomMessageEvent *
+cm_room_message_event_new (CmContentType type)
+{
+  CmRoomMessageEvent *self;
+
+  self = g_object_new (CM_TYPE_ROOM_MESSAGE_EVENT, NULL);
+  self->type = type;
+
+  return self;
+}
+
+CmRoomEvent *
+cm_room_message_event_new_from_json (JsonObject *root)
+{
+  CmRoomMessageEvent *self;
+  const char *type, *body;
+  JsonObject *child;
+
+  g_return_val_if_fail (root, NULL);
+
+  type = cm_utils_json_object_get_string (root, "type");
+
+  if (g_strcmp0 (type, "m.room.message") != 0)
+    g_return_val_if_reached (NULL);
+
+  self = g_object_new (CM_TYPE_ROOM_MESSAGE_EVENT, NULL);
+  self->json = json_object_ref (root);
+
+  child = cm_utils_json_object_get_object (root, "content");
+  type = cm_utils_json_object_get_string (child, "msgtype");
+  body = cm_utils_json_object_get_string (child, "body");
+  self->body = g_strdup (body);
+  self->mxc_uri = g_strdup (cm_utils_json_object_get_string (child, "url"));
+
+  if (g_strcmp0 (type, "m.text") == 0)
+    self->type = CM_CONTENT_TYPE_TEXT;
+  else if (g_strcmp0 (type, "m.file") == 0)
+    self->type = CM_CONTENT_TYPE_FILE;
+  else if (g_strcmp0 (type, "m.image") == 0)
+    self->type = CM_CONTENT_TYPE_IMAGE;
+  else if (g_strcmp0 (type, "m.audio") == 0)
+    self->type = CM_CONTENT_TYPE_AUDIO;
+  else if (g_strcmp0 (type, "m.location") == 0)
+    self->type = CM_CONTENT_TYPE_LOCATION;
+  else if (g_strcmp0 (type, "m.emote") == 0)
+    self->type = CM_CONTENT_TYPE_EMOTE;
+  else if (g_strcmp0 (type, "m.notice") == 0)
+    self->type = CM_CONTENT_TYPE_NOTICE;
+
+  return CM_ROOM_EVENT (self);
+}
+
+CmContentType
+cm_room_message_event_get_msg_type (CmRoomMessageEvent *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), CM_CONTENT_TYPE_UNKNOWN);
+
+  return self->type;
+}
+
+void
+cm_room_message_event_set_body (CmRoomMessageEvent *self,
+                                const char         *text)
+{
+  g_return_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self));
+  g_return_if_fail (self->type == CM_CONTENT_TYPE_TEXT);
+
+  g_free (self->body);
+  self->body = g_strdup (text);
+}
+
+const char *
+cm_room_message_event_get_body (CmRoomMessageEvent *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), NULL);
+
+  return self->body;
+}
+
+void
+cm_room_message_event_set_file (CmRoomMessageEvent *self,
+                                const char         *body,
+                                GFile              *file)
+{
+  g_return_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self));
+  g_return_if_fail (self->type == CM_CONTENT_TYPE_FILE);
+  g_return_if_fail (!self->file);
+
+  g_set_object (&self->file, file);
+  g_free (self->body);
+
+  if (body && *body)
+    self->body = g_strdup (body);
+  else if (file)
+    self->body = g_file_get_basename (file);
+}
+
+GFile *
+cm_room_message_event_get_file (CmRoomMessageEvent *self)
+{
+  g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), NULL);
+
+  return self->file;
+}
+
+static void
+message_file_stream_cb (GObject      *obj,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  CmRoomMessageEvent *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (self));
+
+  g_output_stream_splice_finish (G_OUTPUT_STREAM (obj), result, &error);
+  self->downloading_file = FALSE;
+
+  if (error) {
+    g_task_return_error (task, error);
+  } else {
+    g_autofree char *file_name = NULL;
+    GFile *out_file;
+
+    out_file = g_object_steal_data (user_data, "out-file");
+    self->file_istream = (GInputStream *)g_file_read (out_file, NULL, NULL);
+    g_object_set_data_full (G_OBJECT (self->file_istream), "out-file",
+                            out_file, g_object_unref);
+    g_task_return_pointer (task, g_object_ref (self->file_istream), g_object_unref);
+  }
+}
+
+static void
+message_event_get_file_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  CmRoomMessageEvent *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GInputStream) istream = NULL;
+  GOutputStream *out_stream = NULL;
+  GFile *out_file = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ROOM_MESSAGE_EVENT (self));
+
+  istream = cm_client_get_file_finish (CM_CLIENT (object), result, &error);
+  self->downloading_file = FALSE;
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+
+      return;
+    }
+  else
+    {
+      g_autofree char *file_path = NULL;
+      g_autofree char *file_name = NULL;
+      const char *path;
+
+      path = cm_matrix_get_data_dir ();
+      file_name = g_path_get_basename (self->mxc_uri);
+      file_path = cm_utils_get_path_for_m_type (path, CM_M_ROOM_MESSAGE, FALSE, file_name);
+      out_file = g_file_new_build_filename (file_path, NULL);
+      out_stream = (GOutputStream *)g_file_create (out_file, G_FILE_CREATE_NONE, NULL, &error);
+      g_object_set_data_full (G_OBJECT (task), "out-file", out_file, g_object_unref);
+    }
+
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+    {
+      GFileIOStream *io_stream;
+
+      g_clear_error (&error);
+      io_stream = g_file_open_readwrite (out_file, NULL, &error);
+      if (io_stream)
+        {
+          g_object_set_data_full (G_OBJECT (task), "io-stream", io_stream, g_object_unref);
+          out_stream = g_io_stream_get_output_stream (G_IO_STREAM (io_stream));
+        }
+    }
+
+  if (out_stream)
+    {
+      self->downloading_file = TRUE;
+      g_output_stream_splice_async (G_OUTPUT_STREAM (out_stream), istream,
+                                    G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE |
+                                    G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+                                    0, NULL,
+                                    message_file_stream_cb,
+                                    g_steal_pointer (&task));
+    }
+  else
+    {
+      if (error)
+        g_task_return_error (task, error);
+      else
+        g_task_return_boolean (task, FALSE);
+    }
+}
+
+void
+cm_room_message_event_get_file_async (CmRoomMessageEvent    *self,
+                                      GCancellable          *cancellable,
+                                      GFileProgressCallback  progress_callback,
+                                      gpointer               progress_user_data,
+                                      GAsyncReadyCallback    callback,
+                                      gpointer               user_data)
+{
+  CmRoom *room;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (self->type == CM_CONTENT_TYPE_FILE ||
+                    self->type == CM_CONTENT_TYPE_AUDIO ||
+                    self->type == CM_CONTENT_TYPE_IMAGE);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "progress-cb", progress_callback);
+  g_object_set_data (G_OBJECT (task), "progress-cb-data", progress_user_data);
+
+  if (self->downloading_file)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_PENDING,
+                               "File already being downloaded");
+      return;
+    }
+
+  if (self->file && !self->mxc_uri && !self->file_istream)
+    self->file_istream = (gpointer)g_file_read (self->file, NULL, NULL);
+
+  if (self->file_istream)
+    {
+      g_task_return_pointer (task, g_object_ref (self->file_istream), g_object_unref);
+      return;
+    }
+
+  g_return_if_fail (self->mxc_uri);
+
+  self->downloading_file = TRUE;
+  room = cm_room_event_get_room (CM_ROOM_EVENT (self));
+  cm_client_get_file_async (cm_room_get_client (room),
+                            self->mxc_uri,
+                            cancellable,
+                            NULL, NULL,
+                            message_event_get_file_cb, task);
+}
+
+GInputStream *
+cm_room_message_event_get_file_finish (CmRoomMessageEvent  *self,
+                                       GAsyncResult        *result,
+                                       GError             **error)
+{
+  g_return_val_if_fail (CM_IS_ROOM_MESSAGE_EVENT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/events/cm-room-message-event.h b/subprojects/libcmatrix/src/events/cm-room-message-event.h
new file mode 100644
index 0000000000000000000000000000000000000000..50f640d700c5ba4824bcb0632c221c3886137d6f
--- /dev/null
+++ b/subprojects/libcmatrix/src/events/cm-room-message-event.h
@@ -0,0 +1,43 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-room-message-event.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-room-event.h"
+#include "cm-enums.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_ROOM_MESSAGE_EVENT (cm_room_message_event_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmRoomMessageEvent, cm_room_message_event, CM, ROOM_MESSAGE_EVENT, CmRoomEvent)
+
+CmContentType       cm_room_message_event_get_msg_type    (CmRoomMessageEvent   *self);
+const char         *cm_room_message_event_get_body        (CmRoomMessageEvent    *self);
+void                cm_room_message_event_get_file_async  (CmRoomMessageEvent    *self,
+                                                           GCancellable          *cancellable,
+                                                           GFileProgressCallback  progress_callback,
+                                                           gpointer               progress_user_data,
+                                                           GAsyncReadyCallback    callback,
+                                                           gpointer               user_data);
+GInputStream       *cm_room_message_event_get_file_finish (CmRoomMessageEvent    *self,
+                                                           GAsyncResult          *result,
+                                                           GError               **error);
+
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/meson.build b/subprojects/libcmatrix/src/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..eadc6f18b7cc81ea22bfc68f76bdd5b6dd81cf0c
--- /dev/null
+++ b/subprojects/libcmatrix/src/meson.build
@@ -0,0 +1,71 @@
+libcmatrix_sources = [
+  'cm-client.c',
+  'cm-common.c',
+  'cm-db.c',
+  'cm-device.c',
+  'cm-enc.c',
+  'cm-olm.c',
+  'cm-olm-sas.c',
+  'cm-matrix.c',
+  'cm-net.c',
+  'cm-room.c',
+  'cm-secret-store.c',
+  'cm-input-stream.c',
+  'cm-utils.c',
+  'events/cm-event.c',
+  'events/cm-room-event.c',
+  'events/cm-room-message-event.c',
+  'events/cm-room-event-list.c',
+  'users/cm-user.c',
+  'users/cm-account.c',
+  'users/cm-room-member.c',
+  'users/cm-user-list.c',
+]
+
+libcmatrix_headers = [
+  'cmatrix.h',
+  'cm-client.h',
+  'cm-common.h',
+  'cm-device.h',
+  'cm-enums.h',
+  'cm-matrix.h',
+  'cm-room.h',
+  'users/cm-user.h',
+  'users/cm-account.h',
+  'users/cm-room-member.h',
+]
+
+# Currently we do only have static library and should be
+# used only as a subproject
+cmatrix_lib = static_library('libcmatrix',
+                             [libcmatrix_headers, libcmatrix_sources],
+                             include_directories: [ root_inc, src_inc ],
+                             dependencies: cmatrix_deps
+)
+
+libcmatrix_dep = declare_dependency(
+  include_directories: include_directories('.'),
+  dependencies: cmatrix_deps,
+  link_with: cmatrix_lib
+)
+
+if get_option('gtk_doc')
+  libcmatrix_gir_extra_args = [
+    '-L@0@'.format(meson.current_build_dir()),
+    '--quiet',
+   ]
+
+  libcmatrix_gir = gnome.generate_gir(cmatrix_lib,
+                 sources: libcmatrix_sources + libcmatrix_headers,
+               nsversion: '0',
+               namespace: 'CM',
+         export_packages: 'cmatrix-0',
+           symbol_prefix: 'cm',
+       identifier_prefix: 'Cm',
+               link_with: cmatrix_lib,
+                includes: ['Gio-2.0', 'Soup-2.4'],
+                 install: false,
+              extra_args: libcmatrix_gir_extra_args,
+   )
+
+endif
diff --git a/subprojects/libcmatrix/src/users/cm-account.c b/subprojects/libcmatrix/src/users/cm-account.c
new file mode 100644
index 0000000000000000000000000000000000000000..364fec6008334d3fe0cd6e6676ddedcb1d3476d7
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-account.c
@@ -0,0 +1,480 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-account.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-account"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <libsoup/soup.h>
+
+#include "cm-client-private.h"
+#include "cm-net-private.h"
+#include "cm-utils-private.h"
+#include "cm-user-private.h"
+#include "cm-user.h"
+#include "cm-account.h"
+
+/**
+ * SECTION: cm-account
+ * @title: CmAccount
+ * @short_description: A high-level API to manage the client owner’s account.
+ * @include: "cm-account.h"
+ */
+
+struct _CmAccount
+{
+  CmUser        parent_instance;
+
+  /* @login_username can be email/[incomplete] matrix-id/phone-number etc */
+  char         *login_id;
+};
+
+G_DEFINE_TYPE (CmAccount, cm_account, CM_TYPE_USER)
+
+static void
+cm_account_finalize (GObject *object)
+{
+  CmAccount *self = (CmAccount *)object;
+
+  g_free (self->login_id);
+
+  G_OBJECT_CLASS (cm_account_parent_class)->finalize (object);
+}
+
+static void
+cm_account_class_init (CmAccountClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_account_finalize;
+}
+
+static void
+cm_account_init (CmAccount *self)
+{
+}
+
+/**
+ * cm_account_set_login_id:
+ * @self: A #CmAccount
+ * @login_id: A login ID for the client.
+ *
+ * Set login ID for the account.  The login can be a
+ * fully qualified matrix ID, or an email address
+ * which shall be used to log in to the server
+ * in to the server.
+ *
+ * Returns: %TRUE if @login_id was successfully set,
+ * %FALSE otherwise.
+ */
+gboolean
+cm_account_set_login_id (CmAccount  *self,
+                         const char *login_id)
+{
+  g_autoptr(GString) str = NULL;
+  CmClient *client;
+
+  g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE);
+
+  client = cm_user_get_client (CM_USER (self));
+  g_return_val_if_fail (CM_IS_CLIENT (client), FALSE);
+  g_return_val_if_fail (!cm_client_get_logged_in (client), FALSE);
+  g_return_val_if_fail (!cm_client_get_logged_in (client), FALSE);
+  g_return_val_if_fail (!cm_client_is_sync (client), FALSE);
+
+  if (login_id && g_strcmp0 (login_id, self->login_id) == 0)
+    return TRUE;
+
+  str = g_string_new (NULL);
+
+  if (cm_utils_user_name_valid (login_id) ||
+      cm_utils_user_name_is_email (login_id))
+    {
+      g_free (self->login_id);
+      self->login_id = g_strdup (login_id);
+      g_debug ("(%p) New login id set: '%s'", client,
+               cm_utils_anonymize (str, login_id));
+
+      return TRUE;
+    }
+
+  g_debug ("(%p) New login id failed to set: '%s'", client,
+           cm_utils_anonymize (str, login_id));
+
+  return FALSE;
+}
+
+/**
+ * cm_account_get_login_id:
+ * @self: A #CmAccount
+ *
+ * Get the login ID set for the account
+ *
+ * Returns: (nullable): The login ID of the account
+ */
+const char *
+cm_account_get_login_id (CmAccount *self)
+{
+  g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE);
+
+  return self->login_id;
+}
+
+static void
+account_set_display_name_cb (GObject      *obj,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  CmAccount *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ACCOUNT (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+/**
+ * cm_account_set_name_async:
+ * @self: A #CmAccount
+ * @name: (nullable): The display name for the account user
+ * @cancellable: A #GCancellable
+ * @callback: A callback to run when finished
+ * @user_data: The user data for @callback
+ *
+ * Set the user's name of @self to @name.  If
+ * @name is %NULL, the currently set name is
+ * unset
+ *
+ * Finish the call with cm_account_set_name_finish().
+ */
+void
+cm_account_set_display_name_async (CmAccount           *self,
+                                   const char          *name,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autofree char *uri = NULL;
+  JsonObject *root = NULL;
+  CmClient *client;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ACCOUNT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (name && *name)
+    {
+      root = json_object_new ();
+      json_object_set_string_member (root, "displayname", name);
+    }
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_task_data (task, g_strdup (name), g_free);
+
+  client = cm_user_get_client (CM_USER (self));
+  uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/displayname",
+                         cm_client_get_user_id (client));
+  cm_net_send_json_async (cm_client_get_net (client), 1,
+                          root, uri, SOUP_METHOD_PUT,
+                          NULL, cancellable, account_set_display_name_cb, task);
+}
+
+/**
+ * cm_account_set_name_finish:
+ * @self: A #CmAccount
+ * @result: A #GAsyncResult
+ * @error: A #GError
+ *
+ * Finish the call started with cm_account_set_name_async().
+ *
+ * Returns: %TRUE if setting name was a success.
+ * %FALSE otherwise with @error set.
+ */
+gboolean
+cm_account_set_display_name_finish (CmAccount     *self,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+account_set_user_avatar_cb (GObject      *obj,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  CmAccount *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ACCOUNT (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_account_set_user_avatar_async (CmAccount           *self,
+                                  GFile               *image_file,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  CmClient *client;
+  CmNet *cm_net;
+
+  g_return_if_fail (CM_IS_ACCOUNT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  client = cm_user_get_client (CM_USER (self));
+  cm_net = cm_client_get_net (client);
+
+  if (!image_file)
+    {
+      g_autofree char *uri = NULL;
+      const char *data;
+
+      data = "{\"avatar_url\":\"\"}";
+      uri = g_strdup_printf ("/_matrix/client/r0/profile/%s/avatar_url",
+                             cm_client_get_user_id (client));
+      cm_net_send_data_async (cm_net, 2, g_strdup (data), strlen (data),
+                              uri, SOUP_METHOD_PUT, NULL, cancellable,
+                              account_set_user_avatar_cb, g_steal_pointer (&task));
+    }
+  else
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+                               "Setting new user avatar not implemented");
+    }
+}
+
+gboolean
+cm_account_set_user_avatar_finish (CmAccount     *self,
+                                   GAsyncResult  *result,
+                                   GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+account_get_3pid_cb (GObject      *obj,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  CmAccount *self;
+  g_autoptr(GTask) task = user_data;
+  GPtrArray *emails, *phones;
+  GError *error = NULL;
+  g_autoptr(JsonObject) object = NULL;
+  JsonArray *array;
+  guint length;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ACCOUNT (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  array = cm_utils_json_object_get_array (object, "threepids");
+
+  g_debug ("Getting 3pid, success: %d, user: %s", !error,
+           cm_client_get_user_id (cm_user_get_client (CM_USER (self))));
+
+  if (!array)
+    {
+      if (error)
+        g_task_return_error (task, error);
+      else
+        g_task_return_boolean (task, FALSE);
+
+      return;
+    }
+
+  emails = g_ptr_array_new_full (1, g_free);
+  phones = g_ptr_array_new_full (1, g_free);
+
+  length = json_array_get_length (array);
+
+  for (guint i = 0; i < length; i++)
+    {
+      const char *type, *value;
+
+      object = json_array_get_object_element (array, i);
+      value = cm_utils_json_object_get_string (object, "address");
+      type = cm_utils_json_object_get_string (object, "medium");
+
+      if (g_strcmp0 (type, "email") == 0)
+        g_ptr_array_add (emails, g_strdup (value));
+      else if (g_strcmp0 (type, "msisdn") == 0)
+        g_ptr_array_add (phones, g_strdup (value));
+    }
+
+  g_object_set_data_full (G_OBJECT (task), "email", emails,
+                          (GDestroyNotify)g_ptr_array_unref);
+  g_object_set_data_full (G_OBJECT (task), "phone", phones,
+                          (GDestroyNotify)g_ptr_array_unref);
+
+  g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_account_get_3pids_async (CmAccount           *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  const char *user_id;
+  CmClient *client;
+
+  g_return_if_fail (CM_IS_ACCOUNT (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  client = cm_user_get_client (CM_USER (self));
+  user_id = cm_client_get_user_id (client);
+
+  if (!user_id)
+    {
+      g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+                               "user hasn't logged in yet");
+      return;
+    }
+
+  g_debug ("Getting 3pid of user '%s'", user_id);
+
+  cm_net_send_json_async (cm_client_get_net (client), 1, NULL,
+                          "/_matrix/client/r0/account/3pid", SOUP_METHOD_GET,
+                          NULL, cancellable, account_get_3pid_cb,
+                          g_steal_pointer (&task));
+}
+
+gboolean
+cm_account_get_3pids_finish (CmAccount     *self,
+                             GPtrArray    **emails,
+                             GPtrArray    **phones,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  if (emails)
+    *emails = g_object_steal_data (G_OBJECT (result), "email");
+  if (phones)
+    *phones = g_object_steal_data (G_OBJECT (result), "phone");
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+account_delete_3pid_cb (GObject      *obj,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  CmAccount *self;
+  g_autoptr(GTask) task = user_data;
+  GError *error = NULL;
+  g_autoptr(JsonObject) object = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  g_assert (CM_IS_ACCOUNT (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+void
+cm_account_delete_3pid_async (CmAccount           *self,
+                              const char          *value,
+                              const char          *type,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  CmClient *client;
+  JsonObject *root;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_ACCOUNT (self));
+  g_return_if_fail (value && *value);
+  g_return_if_fail (g_strcmp0 (type, "email") == 0 || g_strcmp0 (type, "msisdn") == 0);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  root = json_object_new ();
+  json_object_set_string_member (root, "address", value);
+  if (g_strcmp0 (type, "msisdn") == 0)
+    json_object_set_string_member (root, "medium", "msisdn");
+  else
+    json_object_set_string_member (root, "medium", "email");
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_object_set_data (G_OBJECT (task), "type", GINT_TO_POINTER (type));
+  g_object_set_data_full (G_OBJECT (task), "value",
+                          g_strdup (value), g_free);
+
+  client = cm_user_get_client (CM_USER (self));
+  cm_net_send_json_async (cm_client_get_net (client), 2, root,
+                          "/_matrix/client/r0/account/3pid/delete", SOUP_METHOD_POST,
+                          NULL, cancellable, account_delete_3pid_cb, task);
+}
+
+gboolean
+cm_account_delete_3pid_finish (CmAccount      *self,
+                               GAsyncResult  *result,
+                               GError       **error)
+{
+  g_return_val_if_fail (CM_IS_ACCOUNT (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/users/cm-account.h b/subprojects/libcmatrix/src/users/cm-account.h
new file mode 100644
index 0000000000000000000000000000000000000000..45719d4718414b10b5f661a56c9518c99fb7c019
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-account.h
@@ -0,0 +1,67 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-account.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-user.h"
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_ACCOUNT (cm_account_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmAccount, cm_account, CM, ACCOUNT, CmUser)
+
+gboolean      cm_account_set_login_id                 (CmAccount           *self,
+                                                       const char          *login_id);
+const char   *cm_account_get_login_id                 (CmAccount           *self);
+void          cm_account_set_display_name_async       (CmAccount           *self,
+                                                       const char          *name,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+gboolean      cm_account_set_display_name_finish      (CmAccount           *self,
+                                                       GAsyncResult        *result,
+                                                       GError             **error);
+void          cm_account_set_user_avatar_async        (CmAccount           *self,
+                                                       GFile               *image_file,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+gboolean      cm_account_set_user_avatar_finish       (CmAccount           *self,
+                                                       GAsyncResult        *result,
+                                                       GError             **error);
+void          cm_account_get_3pids_async              (CmAccount           *self,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+gboolean      cm_account_get_3pids_finish             (CmAccount           *self,
+                                                       GPtrArray          **emails,
+                                                       GPtrArray          **phones,
+                                                       GAsyncResult        *result,
+                                                       GError             **error);
+void          cm_account_delete_3pid_async            (CmAccount           *self,
+                                                       const char          *value,
+                                                       const char          *type,
+                                                       GCancellable        *cancellable,
+                                                       GAsyncReadyCallback  callback,
+                                                       gpointer             user_data);
+gboolean      cm_account_delete_3pid_finish           (CmAccount           *self,
+                                                       GAsyncResult        *result,
+                                                       GError             **error);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/users/cm-room-member-private.h b/subprojects/libcmatrix/src/users/cm-room-member-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..fa6b0820ff22eec4d76c3965ebcd1d1f1fae4214
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-room-member-private.h
@@ -0,0 +1,28 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-room-member-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+#include "cm-room-member.h"
+
+G_BEGIN_DECLS
+
+CmRoomMember *cm_room_member_new                  (GRefString            *user_id);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/users/cm-room-member.c b/subprojects/libcmatrix/src/users/cm-room-member.c
new file mode 100644
index 0000000000000000000000000000000000000000..d313f7eb6d54f5a336ed18786a78ee5c90d3c559
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-room-member.c
@@ -0,0 +1,58 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-room-member.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-room-member"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-client.h"
+#include "cm-client-private.h"
+#include "cm-utils-private.h"
+#include "cm-device.h"
+#include "cm-device-private.h"
+#include "cm-room.h"
+#include "cm-room-private.h"
+#include "cm-enc-private.h"
+#include "cm-user-private.h"
+#include "cm-room-member-private.h"
+#include "cm-room-member.h"
+
+struct _CmRoomMember
+{
+  CmUser      parent_instance;
+};
+
+G_DEFINE_TYPE (CmRoomMember, cm_room_member, CM_TYPE_USER)
+
+static void
+cm_room_member_class_init (CmRoomMemberClass *klass)
+{
+}
+
+static void
+cm_room_member_init (CmRoomMember *self)
+{
+}
+
+CmRoomMember *
+cm_room_member_new (GRefString *user_id)
+{
+  CmRoomMember *self;
+
+  g_return_val_if_fail (user_id && *user_id == '@', NULL);
+
+  self = g_object_new (CM_TYPE_ROOM_MEMBER, NULL);
+  cm_user_set_user_id (CM_USER (self), user_id);
+
+  return self;
+}
diff --git a/subprojects/libcmatrix/src/users/cm-room-member.h b/subprojects/libcmatrix/src/users/cm-room-member.h
new file mode 100644
index 0000000000000000000000000000000000000000..53d9a9a8c9b89aeec3e5fa7db65127eec51a4fe2
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-room-member.h
@@ -0,0 +1,34 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-room-member.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#include "cm-user.h"
+#include "cm-client.h"
+#include "cm-enums.h"
+
+#define CM_TYPE_ROOM_MEMBER (cm_room_member_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmRoomMember, cm_room_member, CM, ROOM_MEMBER, CmUser)
+
+const char    *cm_room_member_get_name             (CmRoomMember          *self);
+
+G_END_DECLS
+
diff --git a/subprojects/libcmatrix/src/users/cm-user-list-private.h b/subprojects/libcmatrix/src/users/cm-user-list-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..d968355fb278b925b3ffb5e0ff1d1efa6eb24a73
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-user-list-private.h
@@ -0,0 +1,69 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-user-list-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#include <json-glib/json-glib.h>
+#include <glib-object.h>
+
+#include "cm-types.h"
+
+G_BEGIN_DECLS
+
+typedef struct _CmUserKey CmUserKey;
+
+struct _CmUserKey {
+  CmUser *user;
+  GPtrArray *devices;
+  GPtrArray *keys;
+};
+
+#define CM_TYPE_USER_LIST (cm_user_list_get_type ())
+
+G_DECLARE_FINAL_TYPE (CmUserList, cm_user_list, CM, USER_LIST, GObject)
+
+void          cm_user_key_free                     (gpointer             data);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (CmUserKey, cm_user_key_free)
+
+CmUserList   *cm_user_list_new                     (CmClient            *self);
+void          cm_user_list_device_changed          (CmUserList          *self,
+                                                    JsonObject          *root,
+                                                    GPtrArray           *changed);
+void          cm_user_list_set_account             (CmUserList          *self,
+                                                    CmAccount           *account);
+CmUser       *cm_user_list_find_user               (CmUserList          *self,
+                                                    GRefString          *user_id,
+                                                    gboolean             create_if_missing);
+void          cm_user_list_load_devices_async      (CmUserList          *self,
+                                                    GPtrArray           *users,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+GPtrArray    *cm_user_list_load_devices_finish     (CmUserList          *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void          cm_user_list_claim_keys_async        (CmUserList          *self,
+                                                    CmRoom              *room,
+                                                    GHashTable          *users,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+GPtrArray    *cm_user_list_claim_keys_finish       (CmUserList          *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+void          cm_user_list_upload_keys_async       (CmUserList          *self,
+                                                    CmRoom              *room,
+                                                    GPtrArray           *one_time_keys,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+gboolean      cm_user_list_upload_keys_finish      (CmUserList          *self,
+                                                    GAsyncResult        *result,
+                                                    GError             **error);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/users/cm-user-list.c b/subprojects/libcmatrix/src/users/cm-user-list.c
new file mode 100644
index 0000000000000000000000000000000000000000..4172d9d4e851f549d035e401f563cae6adff0ac6
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-user-list.c
@@ -0,0 +1,821 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-user-list.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-user-list"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <libsoup/soup.h>
+#include <json-glib/json-glib.h>
+
+#include "events/cm-event-private.h"
+#include "cm-user-private.h"
+#include "cm-net-private.h"
+#include "cm-room-member-private.h"
+#include "cm-utils-private.h"
+#include "cm-common.h"
+#include "cm-device-private.h"
+#include "cm-client-private.h"
+#include "cm-user-list-private.h"
+
+/**
+ * SECTION: cm-user-list
+ * @title: CmUserList
+ * @short_description: Track all users that belongs to the account
+ * @include: "cm-user-list.h"
+ *
+ * #CmUserList tracks all users associated with the account, instead
+ * of tracking them per room individually. Please note that only
+ * relevant users (eg: Users that shares an encrypted room, or users
+ * that have sent an event recently) are stored to avoid eating too
+ * much memory
+ */
+
+/*
+ * Device key request (ie, loading all devices of user(s) from server):
+ *   https://matrix.org/docs/spec/client_server/r0.6.1#tracking-the-device-list-for-a-user
+ *   - We keep track of all changed users in `changed_users` hash table
+ *     - changed_users may not contain users that don't share any
+ *       encrypted room.
+ *   - Only one request shall run at a time so as to avoid races
+ *   - On a request to load user devices, if no requested user is in the
+ *     changed_users table, return early.
+ *   - On a request, Remove the users from `changed_users`, and keep the
+ *     items in `current_request`.
+ *     - We remove the items early so that if the user devices change
+ *       again midst the request, `changed_users` shall have them again.
+ *     - Add back to changed_users if the request fails.
+ *     - On success, check if any of the requested user is in `changed_users`
+ *       - If so return CM_ERROR_USER_DEVICE_CHANGED,
+ *       - else return %TRUE
+ */
+
+#define KEY_TIMEOUT         10000 /* milliseconds */
+
+struct _CmUserList
+{
+  GObject       parent_instance;
+
+  CmClient     *client;
+  GHashTable   *users_table;
+  GHashTable   *changed_users;
+
+  /* We make only one request a time */
+  GQueue       *device_request_queue;
+  GTask        *current_request;
+
+  gboolean      is_requesting_device;
+};
+
+G_DEFINE_TYPE (CmUserList, cm_user_list, G_TYPE_OBJECT)
+
+enum {
+  USER_CHANGED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static void request_device_keys_from_queue (CmUserList *self);
+
+static void
+device_keys_query_cb (GObject      *obj,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+  CmUserList *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GPtrArray *users = NULL;
+  GError *error = NULL;
+  CmUser *user = NULL;
+
+  self = g_task_get_source_object (task);
+  users = g_task_get_task_data (task);
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+
+  g_assert (CM_IS_USER_LIST (self));
+  g_assert (users);
+
+  g_debug ("(%p) Load user devices %s", users, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      /* Re-add the users to changed_users */
+      for (guint i = 0; i < users->len; i++)
+        {
+          GRefString *user_id;
+
+          user = users->pdata[i];
+          user_id = cm_user_get_id (user);
+          g_hash_table_insert (self->changed_users, user_id, g_object_ref (user));
+        }
+
+      g_debug ("(%p) Load user devices error: %s", users, error->message);
+    }
+  else
+    {
+      g_autoptr(GList) members = NULL;
+      JsonObject *keys;
+
+      keys = cm_utils_json_object_get_object (object, "device_keys");
+      if (object)
+        members = json_object_get_members (keys);
+
+      g_debug ("(%p) Load user devices, to load: %u, loaded: %u", users, users->len,
+               keys ? json_object_get_size (keys) : 0);
+      for (GList *member = members; member; member = member->next)
+        {
+          g_autoptr(GRefString) user_id = NULL;
+          g_autoptr(GPtrArray) removed = NULL;
+          g_autoptr(GPtrArray) added = NULL;
+          JsonObject *key;
+          gboolean check_again;
+
+          user_id = g_ref_string_new_intern (member->data);
+          user = g_hash_table_lookup (self->users_table, user_id);
+
+          if (!user)
+            {
+              g_debug ("(%p) Load user devices, '%s' not in users list",
+                       users, user_id);
+              if (user)
+                g_ptr_array_remove (users, user);
+              continue;
+            }
+
+          added = g_ptr_array_new_full (32, g_object_unref);
+          removed = g_ptr_array_new_full (32, g_object_unref);
+          check_again = g_hash_table_contains (self->changed_users, user_id);
+          key = cm_utils_json_object_get_object (keys, member->data);
+          cm_user_set_devices (user, key, !check_again, added, removed);
+          cm_db_update_user_devices (cm_client_get_db (self->client), self->client,
+                                     user, added, removed, FALSE);
+          g_signal_emit (self, signals[USER_CHANGED], 0, user, added, removed);
+          g_ptr_array_remove (users, user);
+
+          g_debug ("(%p) Load user devices, user: %s, devices, added: %u, removed: %u",
+                   users, user_id, added->len, removed->len);
+        }
+    }
+
+  if (!error && users->len)
+    g_debug ("(%p) Load user devices, %u users changed again",
+             users, users->len);
+
+  g_clear_object (&self->current_request);
+  self->is_requesting_device = FALSE;
+  request_device_keys_from_queue (self);
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_pointer (task,
+                           g_ptr_array_ref (users),
+                           (GDestroyNotify)g_ptr_array_unref);
+}
+
+static void
+remove_unlisted_users (CmUserList *self,
+                       GPtrArray  *users)
+{
+  GPtrArray *old_users = NULL;
+  guint len;
+
+  g_assert (CM_IS_USER_LIST (self));
+  g_assert (users);
+
+  len = users->len;
+
+  if (self->current_request)
+    old_users = g_task_get_task_data (self->current_request);
+
+  /* If the users list contain users not in the changed users table, or in
+   * current request, remove them as we shall have already loaded them */
+  for (guint i = 0; i < users->len;)
+    {
+      GListModel *devices;
+      CmUser *user = users->pdata[i];
+      GRefString *user_id;
+
+      user_id = cm_user_get_id (user);
+      devices = cm_user_get_devices (user);
+
+      /* Don't remove if the user is in changed users or in the current request */
+      if (g_hash_table_contains (self->changed_users, user_id) ||
+          g_list_model_get_n_items (devices) == 0 ||
+          users == old_users ||
+          (old_users && g_ptr_array_find (old_users, user_id, NULL)))
+        i++;
+      else
+        g_ptr_array_remove_index (users, i);
+    }
+
+  if (len != users->len)
+    g_debug ("(%p) Request to load device keys removed %u users from %u",
+             users, len - users->len, len);
+}
+
+static void
+request_device_keys_from_queue (CmUserList *self)
+{
+  GCancellable *cancellable;
+  GPtrArray *users;
+  JsonObject *object, *child;
+  GTask *task;
+
+  g_assert (CM_IS_USER_LIST (self));
+
+  if (!g_queue_peek_head (self->device_request_queue) ||
+      self->is_requesting_device)
+    return;
+
+  self->is_requesting_device = TRUE;
+  task = g_queue_pop_head (self->device_request_queue);
+  users = g_task_get_task_data (task);
+  cancellable = g_task_get_cancellable (task);
+
+  g_assert (users);
+  g_assert (!self->current_request);
+  g_set_object (&self->current_request, task);
+
+  for (guint i = 0; i < users->len; i++)
+    g_hash_table_remove (self->changed_users, cm_user_get_id (users->pdata[i]));
+
+  remove_unlisted_users (self, users);
+
+  /* If no users left to request, return */
+  if (users->len == 0)
+    {
+      g_debug ("(%p) Load user devices %s", users, CM_LOG_SUCCESS (TRUE));
+      g_task_return_boolean (task, TRUE);
+      self->is_requesting_device = FALSE;
+      /* Repeat */
+      request_device_keys_from_queue (self);
+      return;
+    }
+
+  object = json_object_new ();
+  child = json_object_new ();
+  json_object_set_int_member (object, "timeout", KEY_TIMEOUT);
+  json_object_set_object_member (object, "device_keys", child);
+
+  for (guint i = 0; i < users->len; i++)
+    json_object_set_array_member (child,
+                                  cm_user_get_id (users->pdata[i]),
+                                  json_array_new ());
+
+  g_debug ("(%p) Load user devices, users count: %u", users, users->len);
+  cm_net_send_json_async (cm_client_get_net (self->client), 0, object,
+                          "/_matrix/client/r0/keys/query", SOUP_METHOD_POST,
+                          NULL, cancellable, device_keys_query_cb, task);
+}
+
+static void
+cm_user_list_finalize (GObject *object)
+{
+  CmUserList *self = (CmUserList *)object;
+
+  g_clear_object (&self->client);
+  g_hash_table_unref (self->users_table);
+  g_clear_object (&self->current_request);
+  g_hash_table_unref (self->changed_users);
+
+  G_OBJECT_CLASS (cm_user_list_parent_class)->finalize (object);
+}
+
+static void
+cm_user_list_class_init (CmUserListClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_user_list_finalize;
+
+  /**
+   * CmUserList::user-changed:
+   * @self: a #CmUserList
+   * @added_devices: The number of devices newly added
+   * @removed_devices: The number of existing devices removed
+   *
+   * user-changed signal is emitted when the user's
+   * device list changes.
+   */
+  signals [USER_CHANGED] =
+    g_signal_new ("user-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 3,
+                  CM_TYPE_USER,
+                  G_TYPE_PTR_ARRAY,
+                  G_TYPE_PTR_ARRAY);
+}
+
+static void
+cm_user_list_init (CmUserList *self)
+{
+  self->device_request_queue = g_queue_new ();
+  self->users_table = g_hash_table_new_full (g_direct_hash,
+                                             g_direct_equal,
+                                             (GDestroyNotify)g_ref_string_release,
+                                             g_object_unref);
+  self->changed_users = g_hash_table_new_full (g_direct_hash,
+                                               g_direct_equal,
+                                               (GDestroyNotify)g_ref_string_release,
+                                               g_object_unref);
+}
+
+void
+cm_user_key_free (gpointer data)
+{
+  CmUserKey *key = data;
+
+  if (!data)
+    return;
+
+  g_clear_object (&key->user);
+  g_clear_pointer (&key->devices, g_ptr_array_unref);
+  g_clear_pointer (&key->keys, g_ptr_array_unref);
+  g_free (key);
+}
+
+CmUserList *
+cm_user_list_new (CmClient *client)
+{
+  CmUserList *self;
+
+  g_return_val_if_fail (CM_IS_CLIENT (client), NULL);
+
+  self = g_object_new (CM_TYPE_USER_LIST, NULL);
+  self->client = g_object_ref (client);
+
+  g_debug ("(%p) New user list with client %p created", self, client);
+
+  return self;
+}
+
+/**
+ * cm_user_list_device_changed:
+ * @self: A #CmUserList
+ * @root: A #JsonObject
+ * @changed: (out): A #GPtrArray
+ *
+ * @changed should be created with g_object_unref()
+ * as free function.  @changed shall be filled with
+ * the #CmUser that got changed.
+ */
+void
+cm_user_list_device_changed (CmUserList *self,
+                             JsonObject *root,
+                             GPtrArray  *changed)
+{
+  JsonArray *users;
+  CmUser *user;
+  guint length = 0;
+
+  g_return_if_fail (CM_IS_USER_LIST (self));
+  g_return_if_fail (root);
+  /* The user list should be empty, we shall fill it in */
+  g_return_if_fail (changed && changed->len == 0);
+
+  users = cm_utils_json_object_get_array (root, "changed");
+  if (users)
+    length = json_array_get_length (users);
+
+  for (guint i = 0; i < length; i++)
+    {
+      GRefString *matrix_id;
+      const char *user_id;
+
+      user_id = json_array_get_string_element (users, i);
+      CM_TRACE ("(%p) User '%s' device changed", self->client, user_id);
+      matrix_id = g_ref_string_new_intern (user_id);
+      user = cm_user_list_find_user (self, matrix_id, TRUE);
+      g_ptr_array_add (changed, g_object_ref (user));
+      g_hash_table_insert (self->changed_users, matrix_id,
+                           g_object_ref (user));
+    }
+}
+
+/**
+ * cm_user_list_find_user:
+ * @self: A #CmUserList
+ * @user_id: A fully qualified matrix id
+ * @create_if_missing: Create if missing
+ *
+ * Find the #CmUser for the given @user_id.  If
+ * @create_if_missing is %FALSE and the user is not
+ * already in the cache, %NULL shall be returned.
+ *
+ * Returns: (nullable) (transfer none): A #CmUser
+ */
+CmUser *
+cm_user_list_find_user (CmUserList *self,
+                        GRefString *user_id,
+                        gboolean    create_if_missing)
+{
+  CmUser *user;
+
+  g_return_val_if_fail (CM_IS_USER_LIST (self), NULL);
+  g_return_val_if_fail (user_id && *user_id == '@', NULL);
+
+  user = g_hash_table_lookup (self->users_table, user_id);
+
+  if (user || !create_if_missing)
+    return user;
+
+  user = (CmUser *)cm_room_member_new (user_id);
+  cm_user_set_client (user, self->client);
+  g_hash_table_insert (self->users_table,
+                       g_ref_string_acquire (user_id), user);
+
+  return user;
+}
+
+/*
+ * cm_user_list_set_account:
+ * @self: A #CmUserList
+ * @account: A #CmAccount
+ *
+ * Set the @account of @self after the matrix user id
+ * of @account is set.  This should be set before adding
+ * any user to the list.
+ */
+void
+cm_user_list_set_account (CmUserList *self,
+                          CmAccount  *account)
+{
+  GRefString *user_id;
+
+  g_return_if_fail (CM_IS_USER_LIST (self));
+  g_return_if_fail (CM_IS_ACCOUNT (account));
+
+  user_id = cm_user_get_id (CM_USER (account));
+  g_return_if_fail (user_id);
+
+  if (g_hash_table_contains (self->users_table, user_id))
+    return;
+
+  /* @account should be the first user added to the table */
+  g_return_if_fail (g_hash_table_size (self->users_table) == 0);
+
+  g_hash_table_insert (self->users_table,
+                       g_ref_string_acquire (user_id), account);
+}
+
+/**
+ * cm_user_list_load_devices_async:
+ * @self: A #CmUserList
+ * @users: A #GPtrArray of #CmUsers
+ * @callback: A #GAsyncReadyCallback
+ * @user_data: user data for @callback
+ *
+ * Load all devices for the given users.  Each #CmUser
+ * in @users should have a ref added.
+ */
+void
+cm_user_list_load_devices_async (CmUserList          *self,
+                                 GPtrArray           *users,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (CM_IS_USER_LIST (self));
+  g_return_if_fail (users && users->len > 0);
+
+  g_debug ("(%p) Queue Load %p user devices, users count: %u",
+           self->client, users, users->len);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_task_data (task, g_ptr_array_ref (users),
+                        (GDestroyNotify)g_ptr_array_unref);
+  remove_unlisted_users (self, users);
+
+  /* If no users left to request, return */
+  if (users->len == 0)
+    {
+      g_debug ("(%p) Load %p user devices %s", self->client, users, CM_LOG_SUCCESS (TRUE));
+      g_task_return_boolean (task, TRUE);
+    }
+  else
+    {
+      g_queue_push_tail (self->device_request_queue, g_steal_pointer (&task));
+    }
+
+  request_device_keys_from_queue (self);
+}
+
+/**
+ * cm_user_list_load_devices_finish:
+ * @self: A #CmUserList
+ * @result: A #GAsyncResult
+ * @error: A #GError
+ *
+ * Returns: (transfer full): An array of #CmUser whose
+ * device keys was not updated.
+ */
+GPtrArray *
+cm_user_list_load_devices_finish (CmUserList    *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_return_val_if_fail (CM_IS_USER_LIST (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+claim_keys_cb (GObject      *obj,
+               GAsyncResult *result,
+               gpointer      user_data)
+{
+  CmUserList *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) root = NULL;
+  JsonObject *object = NULL;
+  GPtrArray *users = NULL;
+  GError *error = NULL;
+  CmRoom *room;
+
+  self = g_task_get_source_object (task);
+  room = g_object_get_data (user_data, "cm-room");
+  users = g_task_get_task_data (task);
+  g_assert (CM_IS_USER_LIST (self));
+  g_assert (users);
+
+  root = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Claim %p user keys %s", room, users, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Claim %p user keys, error: %s", room, users, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      g_autoptr(GPtrArray) one_time_keys = NULL;
+      g_autoptr(GList) members = NULL;
+      const char *room_id;
+
+      object = cm_utils_json_object_get_object (root, "one_time_keys");
+      one_time_keys = g_ptr_array_new_full (32, cm_user_key_free);
+      room_id = cm_room_get_id (room);
+
+      if (object)
+        members = json_object_get_members (object);
+
+      for (GList *member = members; member; member = member->next)
+        {
+          g_autoptr(GRefString) user_id = NULL;
+          CmUser *user;
+          JsonObject *keys;
+
+          user_id = g_ref_string_new_intern (member->data);
+          user = g_hash_table_lookup (self->users_table, user_id);
+
+          keys = cm_utils_json_object_get_object (object, member->data);
+          cm_user_add_one_time_keys (user, room_id, keys, one_time_keys);
+        }
+
+      g_debug ("(%p) Claim %p user keys success, keys: %u",
+               room, users, one_time_keys->len);
+
+      g_task_return_pointer (task,
+                             g_steal_pointer (&one_time_keys),
+                             (GDestroyNotify)g_ptr_array_unref);
+    }
+}
+
+/**
+ * cm_user_list_claim_keys_async:
+ * @self: A #CmUserList
+ * @room: A #CmRoom
+ * @users: A #GHashTable of users
+ * @callback: A callback to run when finished
+ * @user_data: The user data for @callback
+ *
+ * Claim one time keys for the devices of given
+ * users in @users.
+ *
+ * The key in @users should be a user_id #GRefString.
+ * The value of @users should a #GPtrArray of #CmDevices
+ * for which the one time keys should be claimed.
+ *
+ * The method shall return %CM_ERROR_USER_DEVICE_CHANGED
+ * error if any of the user in @users is in the list of
+ * changed users in @self.
+ */
+void
+cm_user_list_claim_keys_async (CmUserList          *self,
+                               CmRoom              *room,
+                               GHashTable          *users,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GList) keys = NULL;
+  JsonObject *root, *child;
+  guint changed_count = 0;
+
+  g_return_if_fail (CM_IS_USER_LIST (self));
+  g_return_if_fail (CM_IS_ROOM (room));
+  g_return_if_fail (users);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_task_data (task, g_hash_table_ref (users),
+                        (GDestroyNotify)g_hash_table_unref);
+  g_object_set_data_full (G_OBJECT (task),
+                          "cm-room", g_object_ref (room),
+                          g_object_unref);
+
+  g_debug ("(%p) Claim %p user keys, users: %u",
+           room, users, g_hash_table_size (users));
+
+  keys = g_hash_table_get_keys (users);
+
+  /* Check if any user's device got changed ... */
+  for (GList *node = keys; node; node = node->next)
+    {
+      GRefString *user_id = node->data;
+
+      if (g_hash_table_contains (self->changed_users, user_id))
+        changed_count++;
+    }
+
+  /* ... if so, return an error as the caller should update user devices. */
+  if (changed_count)
+    {
+      g_debug ("(%p) Claim %p user keys error, %u users pending update",
+               room, users, changed_count);
+      g_task_return_new_error (task, CM_ERROR, CM_ERROR_USER_DEVICE_CHANGED,
+                               "%u users have their devices changed", changed_count);
+      return;
+    }
+
+  /* https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-keys-claim */
+  root = json_object_new ();
+  json_object_set_int_member (root, "timeout", KEY_TIMEOUT);
+  child = json_object_new ();
+
+  for (GList *node = keys; node; node = node->next)
+    {
+      GRefString *user_id = node->data;
+      GPtrArray *devices;
+      JsonObject *object;
+
+      devices = g_hash_table_lookup (users, user_id);
+      object = json_object_new ();
+
+      for (guint i = 0; i < devices->len; i++)
+        {
+          const char *device_id;
+
+          device_id = cm_device_get_id (devices->pdata[i]);
+          json_object_set_string_member (object, device_id, "signed_curve25519");
+        }
+
+      if (object)
+        json_object_set_object_member (child, user_id, object);
+    }
+
+  json_object_set_object_member (root, "one_time_keys", child);
+
+  cm_net_send_json_async (cm_client_get_net (self->client), 0, root,
+                          "/_matrix/client/r0/keys/claim", SOUP_METHOD_POST,
+                          NULL, NULL, claim_keys_cb,
+                          g_steal_pointer (&task));
+}
+
+/**
+ * cm_user_list_claim_keys_finish:
+ * @self: A #CmUserList
+ * @result: A #GAsyncResult
+ * @error: A #GError
+ *
+ * Get the claimed one time keys
+ *
+ * Returns: (transfer full): A #GPtrArray of #CmUserKey
+ */
+GPtrArray *
+cm_user_list_claim_keys_finish (CmUserList    *self,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (CM_IS_USER_LIST (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+upload_group_keys_cb (GObject      *obj,
+                      GAsyncResult *result,
+                      gpointer      user_data)
+{
+  CmUserList *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(JsonObject) object = NULL;
+  GError *error = NULL;
+  CmRoom *room = NULL;
+
+  self = g_task_get_source_object (task);
+  room = g_task_get_task_data (task);
+  g_assert (CM_IS_USER_LIST (self));
+  g_assert (CM_IS_ROOM (room));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Upload group keys %s", room, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_debug ("(%p) Upload group keys error: %s", room, error->message);
+      g_task_return_error (task, error);
+    }
+  else
+    {
+      gpointer session;
+
+      session = g_object_get_data (G_OBJECT (task), "session");
+      cm_enc_set_room_group_key (cm_client_get_enc (self->client),
+                                 room, session);
+      g_task_return_boolean (task, TRUE);
+    }
+}
+
+/**
+ * cm_user_list_upload_keys_async:
+ * @self: A #CmUserList
+ * @room: A #CmRoom
+ * @one_time_keys: A #GptrArray for #CmUserKey
+ * @callback: A callback to run when finished
+ * @user_data: The user data for @callback
+ *
+ * Upload the given @one_time_keys to server.
+ */
+void
+cm_user_list_upload_keys_async (CmUserList          *self,
+                                CmRoom              *room,
+                                GPtrArray           *one_time_keys,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(CmEvent) event = NULL;
+  g_autoptr(GTask) task = NULL;
+  g_autofree char *uri = NULL;
+  JsonObject *root, *object;
+  gpointer olm_session = NULL;
+
+  g_return_if_fail (CM_IS_USER_LIST (self));
+  g_return_if_fail (CM_IS_ROOM (room));
+  g_return_if_fail (one_time_keys && one_time_keys->len);
+
+  g_debug ("(%p) Upload group keys, keys count: %u",
+           room, one_time_keys->len);
+
+  task = g_task_new (self, NULL, callback, user_data);
+  g_task_set_task_data (task, g_object_ref (room), g_object_unref);
+
+  root = json_object_new ();
+  object = cm_enc_create_out_group_keys (cm_client_get_enc (self->client),
+                                         room, one_time_keys, &olm_session);
+  g_object_set_data_full (G_OBJECT (task), "session", olm_session, g_object_unref);
+  json_object_set_object_member (root, "messages", object);
+
+  /* Create an event only to create event id */
+  event = cm_event_new (CM_M_UNKNOWN);
+  cm_event_create_txn_id (event, cm_client_pop_event_id (self->client));
+
+  uri = g_strdup_printf ("/_matrix/client/r0/sendToDevice/m.room.encrypted/%s",
+                         cm_event_get_txn_id (event));
+  cm_net_send_json_async (cm_client_get_net (self->client),
+                          0, root, uri, SOUP_METHOD_PUT,
+                          NULL, NULL,
+                          upload_group_keys_cb,
+                          g_steal_pointer (&task));
+}
+
+gboolean
+cm_user_list_upload_keys_finish (CmUserList    *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_return_val_if_fail (CM_IS_USER_LIST (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/subprojects/libcmatrix/src/users/cm-user-private.h b/subprojects/libcmatrix/src/users/cm-user-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..9d688a86524bf8b72ed69d0ffd079ef712e27da7
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-user-private.h
@@ -0,0 +1,52 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-user-private.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <json-glib/json-glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "cm-client.h"
+#include "cm-device.h"
+#include "cm-user.h"
+
+G_BEGIN_DECLS
+
+JsonObject    *cm_user_generate_json         (CmUser        *self);
+void           cm_user_set_json_data         (CmUser        *self,
+                                              JsonObject    *root);
+void           cm_user_set_client            (CmUser        *self,
+                                              CmClient      *client);
+CmClient      *cm_user_get_client            (CmUser        *self);
+void           cm_user_set_user_id           (CmUser        *self,
+                                              GRefString    *user_id);
+void           cm_user_set_details          (CmUser         *self,
+                                             const char     *display_name,
+                                             const char     *avatar_url);
+GListModel    *cm_user_get_devices          (CmUser         *self);
+CmDevice      *cm_user_find_device          (CmUser         *self,
+                                             const char     *device_id);
+void           cm_user_set_devices          (CmUser         *self,
+                                             JsonObject     *root,
+                                             gboolean        update_state,
+                                             GPtrArray      *added,
+                                             GPtrArray      *removed);
+void           cm_user_add_one_time_keys    (CmUser         *self,
+                                             const char     *room_id,
+                                             JsonObject     *root,
+                                             GPtrArray      *out_keys);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/src/users/cm-user.c b/subprojects/libcmatrix/src/users/cm-user.c
new file mode 100644
index 0000000000000000000000000000000000000000..63a7548f65ca0794c199e68214e060743dcdc941
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-user.c
@@ -0,0 +1,622 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-user.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#define G_LOG_DOMAIN "cm-user"
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include "cm-utils-private.h"
+#include "cm-client-private.h"
+#include "cm-device.h"
+#include "cm-device-private.h"
+#include "cm-matrix-private.h"
+#include "cm-user-list-private.h"
+#include "cm-user-private.h"
+#include "cm-user.h"
+
+typedef struct
+{
+  CmClient *cm_client;
+
+  GRefString   *user_id;
+  char *display_name;
+  char *avatar_url;
+  char *avatar_file_path;
+
+  GFile        *avatar_file;
+  JsonObject   *generated_json;
+
+  GListStore   *devices;
+  GHashTable   *devices_table;
+
+  /* Set when we know about some change, but not sure what it is */
+  gboolean      device_changed;
+
+  gboolean info_loaded;
+} CmUserPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (CmUser, cm_user, G_TYPE_OBJECT)
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS];
+
+static void
+cm_user_finalize (GObject *object)
+{
+  CmUser *self = (CmUser *)object;
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_clear_pointer (&priv->user_id, g_ref_string_release);
+  g_free (priv->display_name);
+  g_free (priv->avatar_url);
+  g_free (priv->avatar_file_path);
+
+  g_list_store_remove_all (priv->devices);
+  g_clear_object (&priv->devices);
+  g_clear_pointer (&priv->devices_table, g_hash_table_unref);
+
+  g_clear_object (&priv->avatar_file);
+  g_clear_pointer (&priv->generated_json, json_object_unref);
+
+  G_OBJECT_CLASS (cm_user_parent_class)->finalize (object);
+}
+
+static void
+cm_user_class_init (CmUserClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = cm_user_finalize;
+
+  /**
+   * CmUser::changed:
+   * @self: a #CmUser
+   *
+   * changed signal is emitted when name or avatar
+   * of the user changes.
+   */
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+cm_user_init (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  priv->devices = g_list_store_new (CM_TYPE_DEVICE);
+  priv->devices_table = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                               g_free, g_object_unref);
+}
+
+JsonObject *
+cm_user_generate_json (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+
+  if (!priv->generated_json &&
+      (priv->display_name || priv->avatar_url || priv->avatar_file))
+    {
+      g_autofree char *avatar_path = NULL;
+      JsonObject *local, *child;
+      GFile *parent;
+
+      parent = g_file_new_for_path (cm_matrix_get_data_dir ());
+      local = json_object_new ();
+      json_object_set_object_member (local, "local", json_object_new ());
+      priv->generated_json = local;
+
+      if (priv->avatar_file)
+        avatar_path = g_file_get_relative_path (parent, priv->avatar_file);
+
+      child = json_object_get_object_member (local, "local");
+      if (priv->display_name)
+        json_object_set_string_member (child, "display_name", priv->display_name);
+      if (priv->avatar_url)
+        json_object_set_string_member (child, "avatar_url", priv->avatar_url);
+      if (avatar_path)
+        json_object_set_string_member (child, "avatar_path", avatar_path);
+    }
+
+  return priv->generated_json;
+}
+
+void
+cm_user_set_json_data (CmUser     *self,
+                       JsonObject *root)
+{
+  const char *name, *avatar_url;
+  JsonObject *child;
+
+  g_return_if_fail (CM_IS_USER (self));
+
+  if (!root)
+    return;
+
+  child = cm_utils_json_object_get_object (root, "content");
+
+  if (!child)
+    child = root;
+
+  name = cm_utils_json_object_get_string (child, "display_name");
+  if (!name)
+    name = cm_utils_json_object_get_string (child, "displayname");
+
+  avatar_url = cm_utils_json_object_get_string (child, "avatar_url");
+  cm_user_set_details (self, name, avatar_url);
+}
+
+void
+cm_user_set_client (CmUser   *self,
+                    CmClient *client)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_if_fail (CM_IS_USER (self));
+  g_return_if_fail (CM_IS_CLIENT (client));
+
+  if (!priv->cm_client)
+    priv->cm_client = g_object_ref (client);
+}
+
+CmClient *
+cm_user_get_client (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+
+  return priv->cm_client;
+}
+
+void
+cm_user_set_user_id (CmUser     *self,
+                     GRefString *user_id)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_if_fail (CM_IS_USER (self));
+
+  if (priv->user_id == user_id)
+    return;
+
+  /* Allow setting user id only once, and never allow changing */
+  g_return_if_fail (!priv->user_id);
+  g_return_if_fail (user_id && *user_id == '@');
+
+  priv->user_id = g_ref_string_acquire (user_id);
+}
+
+void
+cm_user_set_details (CmUser     *self,
+                     const char *display_name,
+                     const char *avatar_url)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+  gboolean changed = FALSE;
+
+  g_return_if_fail (CM_IS_USER (self));
+
+  if (g_strcmp0 (display_name, priv->display_name) != 0)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      changed = TRUE;
+    }
+
+  if (g_strcmp0 (avatar_url, priv->avatar_url) != 0)
+    {
+      g_free (priv->avatar_url);
+      priv->avatar_url = g_strdup (avatar_url);
+      changed = TRUE;
+    }
+
+  /* If we are not already loading info, mark as info has loaded */
+  if (changed)
+    g_signal_emit (self, signals[CHANGED], 0);
+}
+
+GRefString *
+cm_user_get_id (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+
+  return priv->user_id;
+}
+
+const char *
+cm_user_get_display_name (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+
+  return priv->display_name;
+}
+
+const char *
+cm_user_get_avatar_url (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+
+  return priv->avatar_url;
+}
+
+static void
+user_get_user_info_cb (GObject      *obj,
+                       GAsyncResult *result,
+                       gpointer      user_data);
+
+static void
+user_get_avatar_cb (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  CmUser *self;
+  g_autoptr(GTask) task = user_data;
+  GInputStream *stream;
+  GError *error = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  stream = cm_client_get_file_finish (CM_CLIENT (object), result, &error);
+  g_debug ("(%p) Get avatar %s", self, CM_LOG_SUCCESS (!error));
+
+  if (error)
+    g_task_return_error (task, error);
+  else
+    g_task_return_pointer (task, stream, g_object_unref);
+}
+
+void
+cm_user_get_avatar_async (CmUser              *self,
+                          GCancellable        *cancellable,
+                          GAsyncReadyCallback  callback,
+                          gpointer             user_data)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+  GTask *task;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, cm_user_get_avatar_async);
+
+  g_debug ("(%p) Get avatar", self);
+
+  if ((!priv->display_name && !priv->avatar_url) || !priv->info_loaded)
+    cm_user_load_info_async (self, cancellable,
+                             user_get_avatar_cb, task);
+  else if (priv->avatar_url)
+    cm_client_get_file_async (priv->cm_client, priv->avatar_url, cancellable,
+                              NULL, NULL,
+                              user_get_avatar_cb, g_steal_pointer (&task));
+  else
+    g_task_return_pointer (task, NULL, NULL);
+}
+
+GInputStream *
+cm_user_get_avatar_finish (CmUser        *self,
+                           GAsyncResult  *result,
+                           GError       **error)
+{
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+  g_return_val_if_fail (G_IS_TASK (result), NULL);
+  g_return_val_if_fail (!error || !*error, NULL);
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+user_get_user_info_cb (GObject      *obj,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  CmUser *self;
+  CmUserPrivate *priv;
+  g_autoptr(GTask) task = user_data;
+  const char *name, *avatar_url;
+  GError *error = NULL;
+  g_autoptr(JsonObject) object = NULL;
+
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+  priv = cm_user_get_instance_private (self);
+  g_assert (CM_IS_USER (self));
+
+  object = g_task_propagate_pointer (G_TASK (result), &error);
+  g_debug ("(%p) Load info %s", self,  CM_LOG_SUCCESS (!error));
+
+  if (error)
+    {
+      g_task_return_error (task, error);
+      return;
+    }
+
+  name = cm_utils_json_object_get_string (object, "displayname");
+  avatar_url = cm_utils_json_object_get_string (object, "avatar_url");
+
+  g_free (priv->display_name);
+  g_free (priv->avatar_url);
+
+  priv->display_name = g_strdup (name);
+  priv->avatar_url = g_strdup (avatar_url);
+  priv->info_loaded = TRUE;
+
+  if (g_task_get_source_tag (task) == cm_user_get_avatar_async)
+    {
+      GCancellable *cancellable;
+
+      cancellable = g_task_get_cancellable (task);
+
+      if (priv->avatar_url)
+        cm_client_get_file_async (priv->cm_client, priv->avatar_url, cancellable,
+                                  NULL, NULL,
+                                  user_get_avatar_cb, g_steal_pointer (&task));
+      else
+        g_task_return_pointer (task, NULL, NULL);
+    }
+  else
+    {
+      g_task_return_boolean (task, TRUE);
+    }
+}
+
+void
+cm_user_load_info_async (CmUser              *self,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+  g_autofree char *uri = NULL;
+  GTask *task;
+
+  g_return_if_fail (CM_IS_USER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_debug ("(%p) Load info", self);
+
+  uri = g_strdup_printf ("/_matrix/client/r0/profile/%s", priv->user_id);
+  cm_net_send_json_async (cm_client_get_net (priv->cm_client),
+                          1, NULL, uri, SOUP_METHOD_GET,
+                          NULL, cancellable, user_get_user_info_cb, task);
+}
+
+gboolean
+cm_user_load_info_finish (CmUser        *self,
+                          GAsyncResult  *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (CM_IS_USER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (!error || !*error, FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+GListModel *
+cm_user_get_devices (CmUser *self)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+
+  return G_LIST_MODEL (priv->devices);
+}
+
+CmDevice *
+cm_user_find_device (CmUser     *self,
+                     const char *device_id)
+{
+  GListModel *devices;
+  guint n_items;
+
+  g_return_val_if_fail (CM_IS_USER (self), NULL);
+  g_return_val_if_fail (device_id && *device_id, NULL);
+
+  devices = cm_user_get_devices (self);
+  n_items = g_list_model_get_n_items (devices);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmDevice) device = NULL;
+      const char *id;
+
+      device = g_list_model_get_item (devices, i);
+      id = cm_device_get_id (device);
+
+      if (g_strcmp0 (id, device_id) == 0)
+        return device;
+    }
+
+  return NULL;
+}
+
+/*
+ * cm_user_set_devices:
+ * @self: A #CmUser
+ * @root: A #JsonObject
+ * @update_state: Whether to update state
+ * @added: (out): The number of new devices added
+ * @removed: (out): The number of existing devices removed
+ *
+ * Set devices for @self removing all
+ * non existing devices in @root
+ *
+ * If @update_state is %FALSE, the device changed info
+ * shall not be updated, and so cm_user_get_device_changed()
+ * shall return the old values.
+ *
+ */
+void
+cm_user_set_devices (CmUser     *self,
+                     JsonObject *root,
+                     gboolean    update_state,
+                     GPtrArray  *added,
+                     GPtrArray  *removed)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+  g_autoptr(GHashTable) devices_table = NULL;
+  g_autoptr(GList) members = NULL;
+  GHashTable *old_devices;
+  JsonObject *child;
+
+  g_return_if_fail (CM_IS_USER (self));
+  g_return_if_fail (root);
+
+  /* Create a table of devices and add the items here */
+  devices_table = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                         g_free, g_object_unref);
+  members = json_object_get_members (root);
+
+  for (GList *member = members; member; member = member->next)
+    {
+      g_autoptr(CmDevice) device = NULL;
+      g_autofree char *device_name = NULL;
+      const char *device_id, *user;
+
+      child = cm_utils_json_object_get_object (root, member->data);
+      device_id = cm_utils_json_object_get_string (child, "device_id");
+      user = cm_utils_json_object_get_string (child, "user_id");
+
+      if (!device_id || !*device_id)
+        continue;
+
+      if (priv->devices_table &&
+          (device = g_hash_table_lookup (priv->devices_table, device_id)))
+        {
+          /* If the device is already in the old table, remove it there
+           * so that it's present only in the new devices_table.
+           */
+          /* device variable is autofree, so the ref is unref later automatically */
+          g_object_ref (device);
+          g_hash_table_remove (priv->devices_table, device_id);
+          g_hash_table_insert (devices_table, g_strdup (device_id), g_object_ref (device));
+          continue;
+        }
+
+      if (g_strcmp0 (user, cm_user_get_id (self)) != 0)
+        {
+          g_warning ("‘%s’ and ‘%s’ are not the same users",
+                     user, cm_user_get_id (self));
+          continue;
+        }
+
+      if (g_strcmp0 (member->data, device_id) != 0)
+        {
+          g_warning ("‘%s’ and ‘%s’ are not the same device", (char *)member->data, device_id);
+          continue;
+        }
+
+      device = cm_device_new (self, priv->cm_client, child);
+      g_hash_table_insert (devices_table, g_strdup (device_id), g_object_ref (device));
+      g_list_store_append (priv->devices, device);
+      if (added)
+        g_ptr_array_add (added, g_object_ref (device));
+    }
+
+  old_devices = priv->devices_table;
+  priv->devices_table = g_steal_pointer (&devices_table);
+  /* Assign so as to autofree */
+  devices_table = old_devices;
+
+  if (old_devices)
+    {
+      g_autoptr(GList) devices = NULL;
+
+      /* The old table now contains the devices that are not used by the user anymore */
+      devices = g_hash_table_get_values (old_devices);
+
+      for (GList *device = devices; device && device->data; device = device->next)
+        {
+          if (removed)
+            g_ptr_array_add (removed, g_object_ref (device->data));
+          cm_utils_remove_list_item (priv->devices, device->data);
+        }
+    }
+}
+
+void
+cm_user_add_one_time_keys (CmUser     *self,
+                           const char *room_id,
+                           JsonObject *root,
+                           GPtrArray  *out_keys)
+{
+  CmUserPrivate *priv = cm_user_get_instance_private (self);
+  g_autoptr(CmUserKey) key = NULL;
+  JsonObject *object, *child;
+  guint n_items;
+
+  g_return_if_fail (CM_IS_USER (self));
+  g_return_if_fail (root);
+  g_return_if_fail (out_keys);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (priv->devices));
+
+  key = g_new (CmUserKey, 1);
+  key->user = g_object_ref (self);
+  key->devices = g_ptr_array_new_full (n_items, g_object_unref);
+  key->keys = g_ptr_array_new_full (n_items, g_free);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(CmDevice) device = NULL;
+      g_autoptr(GList) members = NULL;
+      const char *device_id;
+
+      device = g_list_model_get_item (G_LIST_MODEL (priv->devices), i);
+      device_id = cm_device_get_id (device);
+      child = cm_utils_json_object_get_object (root, device_id);
+
+      if (!child)
+        {
+          g_debug ("device '%s' doesn't have any keys", device_id);
+          continue;
+        }
+
+      members = json_object_get_members (child);
+
+      for (GList *node = members; node; node = node->next)
+        {
+          object = cm_utils_json_object_get_object (child, node->data);
+
+          if (cm_enc_verify (cm_client_get_enc (priv->cm_client), object,
+                             cm_user_get_id (self), device_id,
+                             cm_device_get_ed_key (device)))
+            {
+              g_ptr_array_add (key->devices, g_object_ref (device));
+              g_ptr_array_add (key->keys, cm_utils_json_object_dup_string (object, "key"));
+            }
+        }
+    }
+
+  if (key->devices->len)
+    g_ptr_array_add (out_keys, g_steal_pointer (&key));
+}
diff --git a/subprojects/libcmatrix/src/users/cm-user.h b/subprojects/libcmatrix/src/users/cm-user.h
new file mode 100644
index 0000000000000000000000000000000000000000..b7f161780184288bb5332c041048a8d7995b7c8e
--- /dev/null
+++ b/subprojects/libcmatrix/src/users/cm-user.h
@@ -0,0 +1,53 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-user.h
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_CMATRIX_TAKEN) && !defined(CMATRIX_COMPILATION)
+# error "Only <cmatrix.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define CM_TYPE_USER (cm_user_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (CmUser, cm_user, CM, USER, GObject)
+
+struct _CmUserClass
+{
+  GObjectClass parent_class;
+
+  /*< private >*/
+  gpointer reserved[8];
+};
+
+GRefString   *cm_user_get_id                  (CmUser              *self);
+const char   *cm_user_get_display_name        (CmUser              *self);
+const char   *cm_user_get_avatar_url          (CmUser              *self);
+void          cm_user_get_avatar_async        (CmUser              *self,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data);
+GInputStream *cm_user_get_avatar_finish       (CmUser              *self,
+                                               GAsyncResult        *result,
+                                               GError             **error);
+void          cm_user_load_info_async         (CmUser              *self,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data);
+gboolean      cm_user_load_info_finish        (CmUser              *self,
+                                               GAsyncResult        *result,
+                                               GError             **error);
+
+G_END_DECLS
diff --git a/subprojects/libcmatrix/tests/client.c b/subprojects/libcmatrix/tests/client.c
new file mode 100644
index 0000000000000000000000000000000000000000..8c9e313c6afd52b000f0f1e70073d7e62b4f8aa1
--- /dev/null
+++ b/subprojects/libcmatrix/tests/client.c
@@ -0,0 +1,91 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* utils.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#undef NDEBUG
+#undef G_DISABLE_ASSERT
+#undef G_DISABLE_CHECKS
+#undef G_DISABLE_CAST_CHECKS
+#undef G_LOG_DOMAIN
+
+#include "cm-client.h"
+
+static void
+test_cm_client_new (void)
+{
+  CmClient *client;
+  CmAccount *account;
+
+  client = cm_client_new ();
+  /* Mark client to not save changes to db */
+  g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (TRUE));
+  g_assert (CM_IS_CLIENT (client));
+
+  g_assert_null (cm_client_get_user_id (client));
+  cm_client_set_user_id (client, "@invalid:bad:");
+  g_assert_null (cm_client_get_user_id (client));
+  cm_client_set_user_id (client, "@user:example.com");
+  g_assert_cmpstr (cm_client_get_user_id (client), ==, "@user:example.com");
+
+  g_assert_false (cm_client_get_enabled (client));
+
+  account = cm_client_get_account (client);
+  g_assert_null (cm_account_get_login_id (account));
+  cm_account_set_login_id (account, "user@@invalid");
+  g_assert_null (cm_account_get_login_id (account));
+  cm_account_set_login_id (account, "user@example.com");
+  g_assert_cmpstr (cm_account_get_login_id (account), ==, "user@example.com");
+
+  g_assert_null (cm_client_get_homeserver (client));
+  cm_client_set_homeserver (client, "http://localhost:8008/");
+  g_assert_cmpstr (cm_client_get_homeserver (client), ==, "http://localhost:8008");
+  cm_client_set_homeserver (client, "http://sub.domain.example.com/");
+  g_assert_cmpstr (cm_client_get_homeserver (client), ==, "http://sub.domain.example.com");
+
+  g_assert_null (cm_client_get_password (client));
+  cm_client_set_password (client, "hunter2");
+  g_assert_cmpstr (cm_client_get_password (client), ==, "hunter2");
+
+  g_assert_null (cm_client_get_access_token (client));
+  cm_client_set_access_token (client, "ec-8b67-37f0683");
+  g_assert_cmpstr (cm_client_get_access_token (client), ==, "ec-8b67-37f0683");
+
+  g_assert_null (cm_client_get_device_id (client));
+  cm_client_set_device_id (client, "DEADBEAF");
+  g_assert_cmpstr (cm_client_get_device_id (client), ==, "DEADBEAF");
+
+  g_assert_null (cm_client_get_device_name (client));
+  cm_client_set_device_name (client, "Chatty");
+  g_assert_cmpstr (cm_client_get_device_name (client), ==, "Chatty");
+
+  g_assert_null (cm_client_get_pickle_key (client));
+  cm_client_set_pickle_key (client, "passw@rd");
+  /* We don't have set encryption, so password shall be NULL */
+  g_assert_null (cm_client_get_pickle_key (client));
+
+  g_assert_false (cm_client_is_sync (client));
+  g_assert_false (cm_client_get_logging_in (client));
+  g_assert_false (cm_client_get_logged_in (client));
+
+  /* todo */
+  /* g_assert_finalize_object (client); */
+  g_object_unref (client);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/cm-client/new", test_cm_client_new);
+
+  return g_test_run ();
+}
diff --git a/subprojects/libcmatrix/tests/cm-db.c b/subprojects/libcmatrix/tests/cm-db.c
new file mode 100644
index 0000000000000000000000000000000000000000..5fb363a423c9bf8cd5d2d6293853f1cf305a10c9
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db.c
@@ -0,0 +1,500 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-db.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#undef NDEBUG
+#undef G_DISABLE_ASSERT
+#undef G_DISABLE_CHECKS
+#undef G_DISABLE_CAST_CHECKS
+#undef G_LOG_DOMAIN
+
+#include <glib/gstdio.h>
+#include <sqlite3.h>
+
+#include "cm-matrix.h"
+#include "cm-db-private.h"
+#include "cm-client.h"
+
+typedef struct _Data
+{
+  const char *username;
+  const char *device_id;
+} Data;
+
+static void
+finish_bool_cb (GObject      *object,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+  GTask *task = user_data;
+  GObject *obj;
+  gboolean status;
+
+  g_assert_true (G_IS_TASK (task));
+
+  status = g_task_propagate_boolean (G_TASK (result), &error);
+  g_assert_no_error (error);
+
+  obj = G_OBJECT (result);
+  g_object_set_data (user_data, "enabled", g_object_get_data (obj, "enabled"));
+  g_object_set_data_full (user_data, "pickle", g_object_steal_data (obj, "pickle"), g_free);
+  g_object_set_data_full (user_data, "device", g_object_steal_data (obj, "device"), g_free);
+  g_object_set_data_full (user_data, "username", g_object_steal_data (obj, "username"), g_free);
+  g_task_return_boolean (task, status);
+}
+
+static gboolean
+client_matches_user_details (gconstpointer client,
+                             gconstpointer data)
+{
+  Data *details = (gpointer)data;
+
+  g_assert_true (CM_IS_CLIENT ((gpointer)client));
+  g_assert_true (details);
+  g_assert_nonnull (details->username);
+  g_assert_nonnull (details->device_id);
+
+  return g_strcmp0 (details->username, cm_client_get_user_id ((gpointer)client)) == 0 &&
+    g_strcmp0 (details->device_id, cm_client_get_device_id ((gpointer)client)) == 0;
+}
+
+static void
+add_matrix_account (CmDb       *db,
+                    GPtrArray  *client_array,
+                    const char *username,
+                    const char *pickle,
+                    const char *device_id,
+                    gboolean    enabled)
+{
+  CmClient *client;
+  GObject *object;
+  GTask *task;
+  GError *error = NULL;
+  gboolean success;
+  g_autofree Data *data = NULL;
+  guint i;
+
+  g_assert_true (CM_IS_DB (db));
+  g_assert_nonnull (client_array);
+  g_assert_nonnull (username);
+
+  data = g_new0 (Data, 1);
+  data->device_id = device_id;
+  data->username = username;
+
+  if (g_ptr_array_find_with_equal_func (client_array, data,
+                                        client_matches_user_details, &i)) {
+    client = client_array->pdata[i];
+  } else {
+    client = cm_client_new ();
+    /* Mark client to not save changes to db */
+    g_object_set_data (G_OBJECT (client), "no-save", GINT_TO_POINTER (TRUE));
+    cm_client_set_user_id (client, username);
+    cm_client_set_device_id (client, device_id);
+    g_ptr_array_add (client_array, client);
+  }
+
+  g_assert_true (CM_IS_CLIENT (client));
+  object = G_OBJECT (client);
+
+  g_object_set_data (object, "enabled", GINT_TO_POINTER (enabled));
+  g_object_set_data_full (object, "pickle", g_strdup (pickle), g_free);
+  g_object_set_data_full (object, "device", g_strdup (device_id), g_free);
+
+  task = g_task_new (NULL, NULL, NULL, NULL);
+  cm_db_save_client_async (db, client, g_strdup (pickle),
+                           finish_bool_cb, task);
+
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  success = g_task_propagate_boolean (task, &error);
+  g_assert_no_error (error);
+  g_assert_true (success);
+  g_clear_object (&task);
+
+  g_assert_true (g_ptr_array_find (client_array, client, &i));
+  client = client_array->pdata[i];
+  task = g_task_new (NULL, NULL, NULL, NULL);
+  cm_db_load_client_async (db, client, device_id, finish_bool_cb, task);
+
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  success = g_task_propagate_boolean (task, &error);
+  g_assert_no_error (error);
+  g_assert_true (success);
+  g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "username"), ==,
+                   cm_client_get_user_id (client));
+  g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "pickle"), ==,
+                   g_object_get_data (object, "pickle"));
+  g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "device"), ==,
+                   g_object_get_data (object, "device"));
+  g_clear_object (&task);
+}
+
+static void
+test_cm_db_account (void)
+{
+  GTask *task;
+  CmDb *db;
+  gboolean status;
+  GPtrArray *account_array;
+
+  g_remove (g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL));
+
+  db = cm_db_new ();
+  task = g_task_new (NULL, NULL, NULL, NULL);
+  cm_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)),
+                        "test-matrix.db", finish_bool_cb, task);
+
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  status = g_task_propagate_boolean (task, NULL);
+  g_assert_finalize_object (task);
+  g_assert_true (status);
+
+  account_array = g_ptr_array_new ();
+  g_ptr_array_set_free_func (account_array, (GDestroyNotify)g_object_unref);
+
+  add_matrix_account (db, account_array, "@alice:example.org",
+                      NULL, "AABBCCDD", TRUE);
+  add_matrix_account (db, account_array, "@alice:example.org",
+                      NULL, "CCDDEE", FALSE);
+  add_matrix_account (db, account_array, "@alice:example.com",
+                      NULL, "XXAABBDD", FALSE);
+  add_matrix_account (db, account_array, "@alice:example.com",
+                      "Some Pickle", "XXAABBDD", TRUE);
+
+  add_matrix_account (db, account_array, "@bob:example.org",
+                      NULL, "XXAABBDD", FALSE);
+
+  g_ptr_array_unref (account_array);
+}
+
+static void
+test_cm_db_new (void)
+{
+  const char *file_name;
+  CmDb *db;
+  GTask *task;
+  gboolean status;
+
+  file_name = g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL);
+  g_remove (file_name);
+  g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS));
+
+  db = cm_db_new ();
+  task = g_task_new (NULL, NULL, NULL, NULL);
+  cm_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)),
+                    "test-matrix.db", finish_bool_cb, task);
+
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  status = g_task_propagate_boolean (task, NULL);
+  g_assert_true (g_file_test (file_name, G_FILE_TEST_IS_REGULAR));
+  g_assert_true (cm_db_is_open (db));
+  g_assert_true (status);
+  g_clear_object (&task);
+
+  task = g_task_new (NULL, NULL, NULL, NULL);
+  cm_db_close_async (db, finish_bool_cb, task);
+
+  while (!g_task_get_completed (task))
+    g_main_context_iteration (NULL, TRUE);
+
+  status = g_task_propagate_boolean (task, NULL);
+  g_assert_true (status);
+  g_assert_false (cm_db_is_open (db));
+  g_clear_object (&db);
+  g_clear_object (&task);
+
+  g_remove (file_name);
+  g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS));
+}
+
+static void
+matrix_export_sql_file (const char  *sql_path,
+                        const char  *file_name,
+                        sqlite3    **db)
+{
+  g_autofree char *export_file = NULL;
+  g_autofree char *input_file = NULL;
+  g_autofree char *content = NULL;
+  g_autoptr(GError) err = NULL;
+  char *error = NULL;
+  int status;
+
+  g_assert (sql_path && *sql_path);
+  g_assert (file_name && *file_name);
+  g_assert (db);
+
+  input_file = g_build_filename (sql_path, file_name, NULL);
+  export_file = g_test_build_filename (G_TEST_BUILT, file_name, NULL);
+  strcpy (export_file + strlen (export_file) - strlen ("sql"), "db");
+  g_remove (export_file);
+  status = sqlite3_open (export_file, db);
+  g_assert_cmpint (status, ==, SQLITE_OK);
+  g_file_get_contents (input_file, &content, NULL, &err);
+  g_assert_no_error (err);
+
+  status = sqlite3_exec (*db, content, NULL, NULL, &error);
+  if (error)
+    g_warning ("%s error: %s", G_STRLOC, error);
+  g_assert_cmpint (status, ==, SQLITE_OK);
+}
+
+static int
+db_get_int (sqlite3    *db,
+            const char *statement)
+{
+  sqlite3_stmt *stmt;
+  int value, status;
+
+  g_assert (db);
+
+  status = sqlite3_prepare_v2 (db, statement, -1, &stmt, NULL);
+  g_assert_cmpint (status, ==, SQLITE_OK);
+
+  status = sqlite3_step (stmt);
+  g_assert_cmpint (status, ==, SQLITE_ROW);
+
+  value = sqlite3_column_int (stmt, 0);
+
+  sqlite3_finalize (stmt);
+
+  return value;
+}
+
+static void
+compare_table (sqlite3    *db,
+               const char *sql,
+               int         expected_count,
+               int         count)
+{
+  sqlite3_stmt *stmt;
+  int status;
+
+  status = sqlite3_prepare_v2 (db, sql, -1, &stmt, NULL);
+  if (status != SQLITE_OK)
+    g_warning ("sql error: %s", sqlite3_errmsg (db));
+
+  g_assert_cmpint (status, ==, SQLITE_OK);
+
+  if (expected_count != count)
+    g_warning ("%d %d sql: %s", expected_count, count, sql);
+  status = sqlite3_step (stmt);
+  g_assert_cmpint (status, ==, SQLITE_ROW);
+  g_assert_cmpint (expected_count, ==, count);
+
+  count = sqlite3_column_int (stmt, 0);
+  if (expected_count != count)
+    g_warning ("%d %d sql: %s", expected_count, count, sql);
+
+  g_assert_cmpint (expected_count, ==, count);
+
+  sqlite3_finalize (stmt);
+}
+
+static void
+compare_matrix_db (sqlite3 *db)
+{
+  /* Pragma version should match */
+  g_assert_cmpint (db_get_int (db, "PRAGMA main.user_version;"), ==,
+                   db_get_int (db, "PRAGMA test.user_version;"));
+
+  /* Each db should have the same count of table rows */
+  g_assert_cmpint (db_get_int (db, "SELECT COUNT(*) FROM main.sqlite_master;"),
+                   ==,
+                   db_get_int (db, "SELECT COUNT(*) FROM test.sqlite_master;"));
+
+  /* As duplicate rows are removed, SELECT count should match the size of one table. */
+  compare_table (db,
+                 "SELECT COUNT (*) FROM ("
+                 "SELECT username,json_data FROM main.users "
+                 "UNION "
+                 "SELECT username,json_data FROM test.users "
+                 ");",
+                 db_get_int (db, "SELECT COUNT(*) FROM main.users;"),
+                 db_get_int (db, "SELECT COUNT(*) FROM test.users;"));
+
+  /* sqlite doesn't guarantee `id` to be always incremented by one.  It
+   * may depend on the order they are updated.  And so
+   * compare by values.
+   */
+  compare_table (db,
+                 "SELECT COUNT (*) FROM ("
+                 "SELECT username,device,users.json_data FROM main.user_devices "
+                 "INNER JOIN main.users ON user_devices.user_id=users.id "
+                 "UNION "
+                 "SELECT username,device,users.json_data FROM test.user_devices "
+                 "INNER JOIN main.users ON user_devices.user_id=users.id "
+                 ");",
+                 db_get_int (db, "SELECT COUNT(*) FROM main.user_devices;"),
+                 db_get_int (db, "SELECT COUNT(*) FROM test.user_devices;"));
+
+  compare_table (db,
+                 "SELECT COUNT (*) FROM ("
+                 "SELECT username,device,next_batch,pickle,enabled,accounts.json_data FROM main.accounts "
+                 "INNER JOIN main.user_devices ON user_devices.id=accounts.user_device_id "
+                 "INNER JOIN main.users ON users.id=user_devices.user_id "
+                 "UNION "
+                 "SELECT username,device,next_batch,pickle,enabled,accounts.json_data FROM test.accounts "
+                 "INNER JOIN test.user_devices ON user_devices.id=accounts.user_device_id "
+                 "INNER JOIN test.users ON users.id=user_devices.user_id "
+                 ");",
+                 db_get_int (db, "SELECT COUNT(*) FROM main.accounts;"),
+                 db_get_int (db, "SELECT COUNT(*) FROM test.accounts;"));
+
+  compare_table (db,
+                 "SELECT COUNT (*) FROM ("
+                 "SELECT username,device,room_name,prev_batch,rooms.json_data FROM main.rooms "
+                 "INNER JOIN main.accounts ON accounts.id=rooms.account_id "
+                 "INNER JOIN main.user_devices ON user_devices.id=accounts.user_device_id "
+                 "INNER JOIN main.users ON user_devices.user_id=users.id "
+                 "UNION "
+                 "SELECT username,device,room_name,prev_batch,rooms.json_data FROM test.rooms "
+                 "INNER JOIN test.accounts ON accounts.id=rooms.account_id "
+                 "INNER JOIN test.user_devices ON user_devices.id=accounts.user_device_id "
+                 "INNER JOIN test.users ON user_devices.user_id=users.id "
+                 ");",
+                 db_get_int (db, "SELECT COUNT(*) FROM main.rooms;"),
+                 db_get_int (db, "SELECT COUNT(*) FROM test.rooms;"));
+
+  compare_table (db,
+                 "SELECT COUNT (*) FROM ("
+                 "SELECT file_url,file_sha256,iv,version,algorithm,key,type,"
+                 "extractable,json_data FROM main.encryption_keys "
+                 "UNION "
+                 "SELECT file_url,file_sha256,iv,version,algorithm,key,type,"
+                 "extractable,json_data FROM test.encryption_keys "
+                 ");",
+                 db_get_int (db, "SELECT COUNT(*) FROM main.encryption_keys;"),
+                 db_get_int (db, "SELECT COUNT(*) FROM test.encryption_keys;"));
+
+  compare_table (db,
+                 "SELECT COUNT (*) FROM ("
+                 "SELECT username,device,sender_key,session_id,type,sessions.pickle,time,sessions.json_data FROM main.sessions "
+                 "INNER JOIN main.accounts ON accounts.id=sessions.account_id "
+                 "INNER JOIN main.user_devices ON user_devices.id=accounts.user_device_id "
+                 "INNER JOIN main.users ON user_devices.user_id=users.id "
+                 "UNION "
+                 "SELECT username,device,sender_key,session_id,type,sessions.pickle,time,sessions.json_data FROM test.sessions "
+                 "INNER JOIN test.accounts ON accounts.id=sessions.account_id "
+                 "INNER JOIN test.user_devices ON user_devices.id=accounts.user_device_id "
+                 "INNER JOIN test.users ON user_devices.user_id=users.id "
+                 ");",
+                 db_get_int (db, "SELECT COUNT(*) FROM main.sessions;"),
+                 db_get_int (db, "SELECT COUNT(*) FROM test.sessions;"));
+}
+
+static void
+test_cm_db_migration (void)
+{
+  g_autoptr(GDir) dir = NULL;
+  g_autofree char *path = NULL;
+  g_autoptr(GError) error = NULL;
+  const char *name;
+
+  path = g_test_build_filename (G_TEST_DIST, "cm-db", NULL);
+  dir = g_dir_open (path, 0, &error);
+  g_assert_no_error (error);
+
+  while ((name = g_dir_read_name (dir)) != NULL) {
+    g_autofree char *expected_file = NULL;
+    g_autofree char *input_file = NULL;
+    g_autofree char *input = NULL;
+    CmDb *cm_db;
+    sqlite3 *db = NULL;
+    GTask *task;
+    int status;
+
+    if (g_str_has_suffix (name, "v2.sql"))
+      continue;
+
+    g_assert_true (g_str_has_suffix (name, "sql"));
+    g_debug ("Migrating %s", name);
+
+    /* Export old version sql file */
+    matrix_export_sql_file (path, name, &db);
+    sqlite3_close (db);
+
+    /* Export migrated version sql file */
+    expected_file = g_strdelimit (g_strdup (name), "01", '2');
+    matrix_export_sql_file (path, expected_file, &db);
+
+    /* Open history with old db, which will result in db migration */
+    input_file = g_strdup (name);
+    strcpy (input_file + strlen (input_file) - strlen ("sql"), "db");
+
+    cm_db = cm_db_new ();
+    task = g_task_new (NULL, NULL, NULL, NULL);
+    cm_db_open_async (cm_db, g_strdup (g_test_get_dir (G_TEST_BUILT)),
+                      input_file, finish_bool_cb, task);
+
+    while (!g_task_get_completed (task))
+      g_main_context_iteration (NULL, TRUE);
+
+    status = g_task_propagate_boolean (task, NULL);
+    g_assert_true (cm_db_is_open (cm_db));
+    g_assert_true (status);
+    g_assert_finalize_object (task);
+
+    task = g_task_new (NULL, NULL, NULL, NULL);
+    cm_db_close_async (cm_db, finish_bool_cb, task);
+
+    while (!g_task_get_completed (task))
+      g_main_context_iteration (NULL, TRUE);
+
+    status = g_task_propagate_boolean (task, NULL);
+    g_assert_true (status);
+    g_assert_false (cm_db_is_open (cm_db));
+    g_assert_finalize_object (task);
+    /* xxx: g_assert_finalize_object (cm_db) fails sometimes when used as subproject */
+    g_object_unref (cm_db);
+    g_free (input_file);
+
+    /* Attach old (now migrated) db with expected migrated db */
+    input_file = g_test_build_filename (G_TEST_BUILT, name, NULL);
+    strcpy (input_file + strlen (input_file) - strlen ("sql"), "db");
+    /* The db that's verified is 'main' (shipped as testcase), and the
+     * generated one by matrix-db is named 'test', which is to be tested */
+    input = g_strconcat ("ATTACH '", input_file, "' as test;", NULL);
+    status = sqlite3_exec (db, input, NULL, NULL, NULL);
+    g_assert_cmpint (status, ==, SQLITE_OK);
+
+    compare_matrix_db (db);
+    sqlite3_close (db);
+  }
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_autoptr(CmMatrix) matrix = NULL;
+
+  g_test_init (&argc, &argv, NULL);
+
+  cm_init (TRUE);
+  matrix = cm_matrix_new (g_test_get_dir (G_TEST_BUILT),
+                          g_test_get_dir (G_TEST_BUILT),
+                          "org.example.CMatrix",
+                          FALSE);
+
+  g_test_add_func ("/cm-db/new", test_cm_db_new);
+  g_test_add_func ("/cm-db/account", test_cm_db_account);
+  g_test_add_func ("/cm-db/migration", test_cm_db_migration);
+
+  return g_test_run ();
+}
diff --git a/subprojects/libcmatrix/tests/cm-db/content-v0.sql b/subprojects/libcmatrix/tests/cm-db/content-v0.sql
new file mode 100644
index 0000000000000000000000000000000000000000..a4e24c74c59600b48ff9baf85ef96f4cc8d2a51d
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db/content-v0.sql
@@ -0,0 +1,87 @@
+BEGIN TRANSACTION;
+
+CREATE TABLE devices(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  device TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE users(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  username TEXT NOT NULL,
+  device_id INTEGER REFERENCES devices(id),
+  UNIQUE (username, device_id)
+);
+
+CREATE TABLE accounts(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  next_batch TEXT,
+  pickle TEXT,
+  enabled INTEGER DEFAULT 0,
+  UNIQUE (user_id)
+);
+
+CREATE TABLE rooms(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  room_name TEXT NOT NULL,
+  prev_batch TEXT,
+  UNIQUE (account_id, room_name)
+);
+
+CREATE TABLE encryption_keys(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  file_url TEXT NOT NULL,
+  file_sha256 TEXT,
+  iv TEXT NOT NULL,
+  version INT DEFAULT 2 NOT NULL,
+  algorithm INT NOT NULL,
+  key TEXT NOT NULL,
+  type INT NOT NULL,
+  extractable INT DEFAULT 1 NOT NULL,
+  UNIQUE (file_url)
+);
+
+CREATE TABLE session(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  sender_key TEXT NOT NULL,
+  session_id TEXT NOT NULL,
+  type INTEGER NOT NULL,
+  pickle TEXT NOT NULL,
+  time INT,
+  UNIQUE (account_id, sender_key, session_id)
+);
+
+
+INSERT INTO devices VALUES(3,'ALICE EXAMPLE COM');
+INSERT INTO devices VALUES(2,'BOB EXAMPLE COM');
+INSERT INTO devices VALUES(4,'ALICE EXAMPLE NET');
+INSERT INTO devices VALUES(5,'ALICE EXAMPLE NET 2');
+INSERT INTO devices VALUES(1,'ALICE EXAMPLE NET 3');
+
+INSERT INTO users VALUES(1,'@alice:example.com', 3);
+INSERT INTO users VALUES(3,'@bob:example.com', 2);
+INSERT INTO users VALUES(2,'@alice:example.net', 4);
+INSERT INTO users VALUES(5,'@alice:example.net', 5);
+INSERT INTO users VALUES(4,'@alice:example.net', 1);
+
+INSERT INTO accounts VALUES(3, 4, 'alice example net batch', 'alice example net pickle', 1);
+INSERT INTO accounts VALUES(2, 1, 'alice example com batch', 'alice example com pickle', 1);
+INSERT INTO accounts VALUES(4, 3, 'bob example com batch', 'bob example com pickle', 0);
+
+INSERT INTO rooms VALUES(7, 3, 'alice example net room A', 'prev batch 1');
+INSERT INTO rooms VALUES(2, 3, 'alice example net room B', 'prev batch 2');
+INSERT INTO rooms VALUES(4, 4, 'bob example com room C', 'bob com batch 3');
+INSERT INTO rooms VALUES(5, 3, 'alice example net room C', 'prev batch 3');
+INSERT INTO rooms VALUES(1, 3, 'alice example net room D', 'prev batch 4');
+INSERT INTO rooms VALUES(3, 4, 'bob example com room A', 'bob com batch 1');
+INSERT INTO rooms VALUES(9, 4, 'bob example com room B', 'bob com batch 2');
+
+INSERT INTO session VALUES(1, 2, 'alice com key 1', 'alice com id 1', 1, 'alice com id 1', 11111111);
+INSERT INTO session VALUES(2, 4, 'bob key 1', 'bob id 1', 1, 'bob id 1', 22222222);
+INSERT INTO session VALUES(3, 4, 'bob key 2', 'bob id 2', 1, 'bob id 2', 33333333);
+INSERT INTO session VALUES(4, 4, 'bob key 3', 'bob id 3', 2, 'bob id 3', 44444444);
+INSERT INTO session VALUES(5, 3, 'net key 1', 'net id 1', 1, 'netid 1', 555555);
+
+COMMIT;
diff --git a/subprojects/libcmatrix/tests/cm-db/content-v1.sql b/subprojects/libcmatrix/tests/cm-db/content-v1.sql
new file mode 100644
index 0000000000000000000000000000000000000000..5e39d994545bbbcb12c2723f40be6e8a00ad990d
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db/content-v1.sql
@@ -0,0 +1,98 @@
+BEGIN TRANSACTION;
+
+PRAGMA user_version = 1;
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE users(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  username TEXT NOT NULL UNIQUE,
+  outdated INTEGER DEFAULT 1,
+  json_data TEXT
+);
+
+CREATE TABLE user_devices(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  device TEXT NOT NULL,
+  curve25519_key TEXT,
+  ed25519_key TEXT,
+  verification INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_id, device)
+);
+
+CREATE TABLE accounts(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_device_id INTEGER NOT NULL REFERENCES user_devices(id),
+  next_batch TEXT,
+  pickle TEXT,
+  enabled INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_device_id)
+);
+
+CREATE TABLE rooms(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  room_name TEXT NOT NULL,
+  prev_batch TEXT,
+  replacement_room_id INTEGER REFERENCES rooms(id),
+  json_data TEXT,
+  UNIQUE (account_id, room_name)
+);
+
+CREATE TABLE encryption_keys(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  file_url TEXT NOT NULL,
+  file_sha256 TEXT,
+  iv TEXT NOT NULL,
+  version INT DEFAULT 2 NOT NULL,
+  algorithm INT NOT NULL,
+  key TEXT NOT NULL,
+  type INT NOT NULL,
+  extractable INT DEFAULT 1 NOT NULL,
+  json_data TEXT,
+  UNIQUE (file_url)
+);
+
+CREATE TABLE session(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  sender_key TEXT NOT NULL,
+  session_id TEXT NOT NULL,
+  type INTEGER NOT NULL,
+  pickle TEXT NOT NULL,
+  time INT,
+  json_data TEXT,
+  UNIQUE (account_id, sender_key, session_id)
+);
+
+INSERT INTO users VALUES(1,'@alice:example.com', 1, NULL);
+INSERT INTO users VALUES(2,'@alice:example.net', 1, NULL);
+INSERT INTO users VALUES(3,'@bob:example.com', 1, NULL);
+
+INSERT INTO user_devices VALUES(3, 1, 'ALICE EXAMPLE COM', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(2, 2, 'ALICE EXAMPLE NET 3', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(4, 3, 'BOB EXAMPLE COM', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(6, 2, 'ALICE EXAMPLE NET', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(5, 2, 'ALICE EXAMPLE NET 2', NULL, NULL, 0, NULL);
+
+INSERT INTO accounts VALUES(3, 2, 'alice example net batch', 'alice example net pickle', 1, NULL);
+INSERT INTO accounts VALUES(1, 3, 'alice example com batch', 'alice example com pickle', 1, NULL);
+INSERT INTO accounts VALUES(4, 4, 'bob example com batch', 'bob example com pickle', 0, NULL);
+
+INSERT INTO rooms VALUES(8, 3, 'alice example net room A', 'prev batch 1', NULL, NULL);
+INSERT INTO rooms VALUES(6, 3, 'alice example net room B', 'prev batch 2', NULL, NULL);
+INSERT INTO rooms VALUES(4, 4, 'bob example com room C', 'bob com batch 3', NULL, NULL);
+INSERT INTO rooms VALUES(3, 4, 'bob example com room A', 'bob com batch 1', NULL, NULL);
+INSERT INTO rooms VALUES(5, 3, 'alice example net room C', 'prev batch 3', NULL, NULL);
+INSERT INTO rooms VALUES(9, 4, 'bob example com room B', 'bob com batch 2', NULL, NULL);
+INSERT INTO rooms VALUES(2, 3, 'alice example net room D', 'prev batch 4', NULL, NULL);
+
+INSERT INTO session VALUES(1, 1, 'alice com key 1', 'alice com id 1', 1, 'alice com id 1', 11111111, NULL);
+INSERT INTO session VALUES(2, 4, 'bob key 1', 'bob id 1', 1, 'bob id 1', 22222222, NULL);
+INSERT INTO session VALUES(3, 4, 'bob key 2', 'bob id 2', 1, 'bob id 2', 33333333, NULL);
+INSERT INTO session VALUES(4, 4, 'bob key 3', 'bob id 3', 2, 'bob id 3', 44444444, NULL);
+INSERT INTO session VALUES(5, 3, 'net key 1', 'net id 1', 1, 'netid 1', 555555, NULL);
+
+COMMIT;
diff --git a/subprojects/libcmatrix/tests/cm-db/content-v2.sql b/subprojects/libcmatrix/tests/cm-db/content-v2.sql
new file mode 100644
index 0000000000000000000000000000000000000000..6c83311ae68eafb13a7b35d182d4aa47e983ebbd
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db/content-v2.sql
@@ -0,0 +1,168 @@
+BEGIN TRANSACTION;
+
+PRAGMA user_version = 2;
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE users(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE,
+  username TEXT NOT NULL,
+  tracking INTEGER NOT NULL DEFAULT 0,
+  outdated INTEGER DEFAULT 1,
+  json_data TEXT,
+  UNIQUE (account_id, username)
+);
+
+CREATE TABLE user_devices(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  device TEXT NOT NULL,
+  curve25519_key TEXT,
+  ed25519_key TEXT,
+  verification INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_id, device)
+);
+
+CREATE TABLE accounts(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_device_id INTEGER NOT NULL REFERENCES user_devices(id),
+  next_batch TEXT,
+  pickle TEXT,
+  enabled INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_device_id)
+);
+
+CREATE TABLE rooms(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
+  room_name TEXT NOT NULL,
+  prev_batch TEXT,
+  replacement_room_id INTEGER REFERENCES rooms(id),
+  room_state INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (account_id, room_name)
+);
+
+CREATE TABLE IF NOT EXISTS room_members (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  user_state INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (room_id, user_id)
+);
+
+CREATE TABLE IF NOT EXISTS room_events_cache (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+  sender_id INTEGER REFERENCES room_members(id),
+  event_uid TEXT NOT NULL,
+  origin_server_ts INTEGER,
+  json_data TEXT,
+  UNIQUE (room_id, event_uid)
+);
+
+CREATE TABLE IF NOT EXISTS room_events (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  sorted_id INTEGER NOT NULL,
+  room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+  sender_id INTEGER NOT NULL REFERENCES room_members(id),
+  event_type INTEGER NOT NULL,
+  event_uid TEXT,
+  txnid TEXT,
+  replaces_event_id INTEGER REFERENCES room_events(id),
+  replaces_event_cache_id INTEGER REFERENCES room_events_cache(id),
+  replaced_with_id INTEGER REFERENCES room_events(id),
+  reply_to_id INTEGER REFERENCES room_events(id),
+  event_state INTEGER,
+  state_key TEXT,
+  origin_server_ts INTEGER NOT NULL,
+  decryption INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (room_id, event_uid)
+);
+
+CREATE TABLE encryption_keys(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE,
+  file_url TEXT NOT NULL,
+  file_sha256 TEXT,
+  iv TEXT NOT NULL,
+  version INT DEFAULT 2 NOT NULL,
+  algorithm INT NOT NULL,
+  key TEXT NOT NULL,
+  type INT NOT NULL,
+  extractable INT DEFAULT 1 NOT NULL,
+  json_data TEXT,
+  UNIQUE (account_id, file_url)
+);
+
+CREATE TABLE sessions (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
+  sender_key TEXT NOT NULL,
+  session_id TEXT NOT NULL,
+  type INTEGER NOT NULL,
+  pickle TEXT NOT NULL,
+  time INT,
+  origin_server_ts INTEGER,
+  chain_index INTEGER,
+  session_state INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (account_id, sender_key, session_id)
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid);
+CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid);
+CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device);
+CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key);
+CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid);
+CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url);
+CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key);
+CREATE INDEX IF NOT EXISTS user_idx ON users (username);
+
+CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT
+ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL
+BEGIN
+  UPDATE room_events SET replaced_with_id=NEW.id
+  WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id);
+END;
+
+CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id
+ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL
+BEGIN
+  UPDATE room_events SET replaced_with_id=NEW.id
+  WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id);
+END;
+
+INSERT INTO users VALUES(1,NULL,'@alice:example.com', 0, 1, NULL);
+INSERT INTO users VALUES(2,NULL,'@alice:example.net', 0, 1, NULL);
+INSERT INTO users VALUES(3,NULL,'@bob:example.com', 0, 1, NULL);
+
+INSERT INTO user_devices VALUES(3, 1, 'ALICE EXAMPLE COM', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(2, 2, 'ALICE EXAMPLE NET 3', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(4, 3, 'BOB EXAMPLE COM', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(6, 2, 'ALICE EXAMPLE NET', NULL, NULL, 0, NULL);
+INSERT INTO user_devices VALUES(5, 2, 'ALICE EXAMPLE NET 2', NULL, NULL, 0, NULL);
+
+INSERT INTO accounts VALUES(3, 2, 'alice example net batch', 'alice example net pickle', 1, NULL);
+INSERT INTO accounts VALUES(1, 3, 'alice example com batch', 'alice example com pickle', 1, NULL);
+INSERT INTO accounts VALUES(4, 4, 'bob example com batch', 'bob example com pickle', 0, NULL);
+
+INSERT INTO rooms VALUES(8, 3, 'alice example net room A', 'prev batch 1', NULL, 0, NULL);
+INSERT INTO rooms VALUES(6, 3, 'alice example net room B', 'prev batch 2', NULL, 0, NULL);
+INSERT INTO rooms VALUES(4, 4, 'bob example com room C', 'bob com batch 3', NULL, 0, NULL);
+INSERT INTO rooms VALUES(3, 4, 'bob example com room A', 'bob com batch 1', NULL, 0, NULL);
+INSERT INTO rooms VALUES(5, 3, 'alice example net room C', 'prev batch 3', NULL, 0, NULL);
+INSERT INTO rooms VALUES(9, 4, 'bob example com room B', 'bob com batch 2', NULL, 0, NULL);
+INSERT INTO rooms VALUES(2, 3, 'alice example net room D', 'prev batch 4', NULL, 0, NULL);
+
+INSERT INTO sessions VALUES(1, 1, 'alice com key 1', 'alice com id 1', 1, 'alice com id 1', 11111111, NULL, NULL, 0, NULL);
+INSERT INTO sessions VALUES(2, 4, 'bob key 1', 'bob id 1', 1, 'bob id 1', 22222222, NULL, NULL, 0, NULL);
+INSERT INTO sessions VALUES(3, 4, 'bob key 2', 'bob id 2', 1, 'bob id 2', 33333333, NULL, NULL, 0, NULL);
+INSERT INTO sessions VALUES(4, 4, 'bob key 3', 'bob id 3', 2, 'bob id 3', 44444444, NULL, NULL, 0, NULL);
+INSERT INTO sessions VALUES(5, 3, 'net key 1', 'net id 1', 1, 'netid 1', 555555, NULL, NULL, 0, NULL);
+
+COMMIT;
diff --git a/subprojects/libcmatrix/tests/cm-db/empty-v0.sql b/subprojects/libcmatrix/tests/cm-db/empty-v0.sql
new file mode 100644
index 0000000000000000000000000000000000000000..093911633265e00267192e93baebb512787ab4c9
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db/empty-v0.sql
@@ -0,0 +1,56 @@
+BEGIN TRANSACTION;
+
+CREATE TABLE devices(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  device TEXT NOT NULL UNIQUE
+);
+
+CREATE TABLE users(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  username TEXT NOT NULL,
+  device_id INTEGER REFERENCES devices(id),
+  UNIQUE (username, device_id)
+);
+
+CREATE TABLE accounts(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  next_batch TEXT,
+  pickle TEXT,
+  enabled INTEGER DEFAULT 0,
+  UNIQUE (user_id)
+);
+
+CREATE TABLE rooms(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  room_name TEXT NOT NULL,
+  prev_batch TEXT,
+  UNIQUE (account_id, room_name)
+);
+
+CREATE TABLE encryption_keys(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  file_url TEXT NOT NULL,
+  file_sha256 TEXT,
+  iv TEXT NOT NULL,
+  version INT DEFAULT 2 NOT NULL,
+  algorithm INT NOT NULL,
+  key TEXT NOT NULL,
+  type INT NOT NULL,
+  extractable INT DEFAULT 1 NOT NULL,
+  UNIQUE (file_url)
+);
+
+CREATE TABLE session(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  sender_key TEXT NOT NULL,
+  session_id TEXT NOT NULL,
+  type INTEGER NOT NULL,
+  pickle TEXT NOT NULL,
+  time INT,
+  UNIQUE (account_id, sender_key, session_id)
+);
+
+COMMIT;
diff --git a/subprojects/libcmatrix/tests/cm-db/empty-v1.sql b/subprojects/libcmatrix/tests/cm-db/empty-v1.sql
new file mode 100644
index 0000000000000000000000000000000000000000..a3a30515bacb202d4f38a51a431c48f7a03c62cb
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db/empty-v1.sql
@@ -0,0 +1,70 @@
+BEGIN TRANSACTION;
+
+PRAGMA user_version = 1;
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE users(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  username TEXT NOT NULL UNIQUE,
+  outdated INTEGER DEFAULT 1,
+  json_data TEXT
+);
+
+CREATE TABLE user_devices(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  device TEXT NOT NULL,
+  curve25519_key TEXT,
+  ed25519_key TEXT,
+  verification INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_id, device)
+);
+
+CREATE TABLE accounts(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_device_id INTEGER NOT NULL REFERENCES user_devices(id),
+  next_batch TEXT,
+  pickle TEXT,
+  enabled INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_device_id)
+);
+
+CREATE TABLE rooms(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  room_name TEXT NOT NULL,
+  prev_batch TEXT,
+  replacement_room_id INTEGER REFERENCES rooms(id),
+  json_data TEXT,
+  UNIQUE (account_id, room_name)
+);
+
+CREATE TABLE encryption_keys(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  file_url TEXT NOT NULL,
+  file_sha256 TEXT,
+  iv TEXT NOT NULL,
+  version INT DEFAULT 2 NOT NULL,
+  algorithm INT NOT NULL,
+  key TEXT NOT NULL,
+  type INT NOT NULL,
+  extractable INT DEFAULT 1 NOT NULL,
+  json_data TEXT,
+  UNIQUE (file_url)
+);
+
+CREATE TABLE session(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id),
+  sender_key TEXT NOT NULL,
+  session_id TEXT NOT NULL,
+  type INTEGER NOT NULL,
+  pickle TEXT NOT NULL,
+  time INT,
+  json_data TEXT,
+  UNIQUE (account_id, sender_key, session_id)
+);
+
+COMMIT;
diff --git a/subprojects/libcmatrix/tests/cm-db/empty-v2.sql b/subprojects/libcmatrix/tests/cm-db/empty-v2.sql
new file mode 100644
index 0000000000000000000000000000000000000000..698ddd5b8a773d440e325a1f0601bd10b6f6ff33
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-db/empty-v2.sql
@@ -0,0 +1,140 @@
+BEGIN TRANSACTION;
+
+PRAGMA user_version = 2;
+PRAGMA foreign_keys = ON;
+
+CREATE TABLE users(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE,
+  username TEXT NOT NULL,
+  tracking INTEGER NOT NULL DEFAULT 0,
+  outdated INTEGER DEFAULT 1,
+  json_data TEXT,
+  UNIQUE (account_id, username)
+);
+
+CREATE TABLE user_devices(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  device TEXT NOT NULL,
+  curve25519_key TEXT,
+  ed25519_key TEXT,
+  verification INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_id, device)
+);
+
+CREATE TABLE accounts(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  user_device_id INTEGER NOT NULL REFERENCES user_devices(id),
+  next_batch TEXT,
+  pickle TEXT,
+  enabled INTEGER DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (user_device_id)
+);
+
+CREATE TABLE rooms(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
+  room_name TEXT NOT NULL,
+  prev_batch TEXT,
+  replacement_room_id INTEGER REFERENCES rooms(id),
+  room_state INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (account_id, room_name)
+);
+
+CREATE TABLE IF NOT EXISTS room_members (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+  user_id INTEGER NOT NULL REFERENCES users(id),
+  user_state INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (room_id, user_id)
+);
+
+CREATE TABLE IF NOT EXISTS room_events_cache (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+  sender_id INTEGER REFERENCES room_members(id),
+  event_uid TEXT NOT NULL,
+  origin_server_ts INTEGER,
+  json_data TEXT,
+  UNIQUE (room_id, event_uid)
+);
+
+CREATE TABLE IF NOT EXISTS room_events (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  sorted_id INTEGER NOT NULL,
+  room_id INTEGER NOT NULL REFERENCES rooms(id) ON DELETE CASCADE,
+  sender_id INTEGER NOT NULL REFERENCES room_members(id),
+  event_type INTEGER NOT NULL,
+  event_uid TEXT,
+  txnid TEXT,
+  replaces_event_id INTEGER REFERENCES room_events(id),
+  replaces_event_cache_id INTEGER REFERENCES room_events_cache(id),
+  replaced_with_id INTEGER REFERENCES room_events(id),
+  reply_to_id INTEGER REFERENCES room_events(id),
+  event_state INTEGER,
+  state_key TEXT,
+  origin_server_ts INTEGER NOT NULL,
+  decryption INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (room_id, event_uid)
+);
+
+CREATE TABLE encryption_keys(
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER REFERENCES accounts(id) ON DELETE CASCADE,
+  file_url TEXT NOT NULL,
+  file_sha256 TEXT,
+  iv TEXT NOT NULL,
+  version INT DEFAULT 2 NOT NULL,
+  algorithm INT NOT NULL,
+  key TEXT NOT NULL,
+  type INT NOT NULL,
+  extractable INT DEFAULT 1 NOT NULL,
+  json_data TEXT,
+  UNIQUE (account_id, file_url)
+);
+
+CREATE TABLE sessions (
+  id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+  account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
+  sender_key TEXT NOT NULL,
+  session_id TEXT NOT NULL,
+  type INTEGER NOT NULL,
+  pickle TEXT NOT NULL,
+  time INT,
+  origin_server_ts INTEGER,
+  chain_index INTEGER,
+  session_state INTEGER NOT NULL DEFAULT 0,
+  json_data TEXT,
+  UNIQUE (account_id, sender_key, session_id)
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS room_event_idx ON room_events (room_id, event_uid);
+CREATE UNIQUE INDEX IF NOT EXISTS room_event_txn_idx ON room_events (room_id, txnid);
+CREATE UNIQUE INDEX IF NOT EXISTS user_device_idx ON user_devices (user_id, device);
+CREATE INDEX IF NOT EXISTS room_event_state_idx ON room_events (state_key);
+CREATE UNIQUE INDEX IF NOT EXISTS room_event_cache_idx ON room_events_cache (room_id, event_uid);
+CREATE UNIQUE INDEX IF NOT EXISTS encryption_key_idx ON encryption_keys (account_id, file_url);
+CREATE INDEX IF NOT EXISTS session_sender_idx ON sessions (account_id, sender_key);
+CREATE INDEX IF NOT EXISTS user_idx ON users (username);
+
+CREATE TRIGGER IF NOT EXISTS insert_replaced_with_id AFTER INSERT
+ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL
+BEGIN
+  UPDATE room_events SET replaced_with_id=NEW.id
+  WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id);
+END;
+
+CREATE TRIGGER IF NOT EXISTS update_replaced_with_id AFTER UPDATE OF replaces_event_id
+ON room_events FOR EACH ROW WHEN NEW.replaces_event_id IS NOT NULL
+BEGIN
+  UPDATE room_events SET replaced_with_id=NEW.id
+  WHERE id=NEW.replaces_event_id AND (replaced_with_id IS NULL or replaced_with_id < NEW.id);
+END;
+
+COMMIT;
diff --git a/tests/matrix-enc.c b/subprojects/libcmatrix/tests/cm-enc.c
similarity index 78%
rename from tests/matrix-enc.c
rename to subprojects/libcmatrix/tests/cm-enc.c
index c1ad7c550f61b0494c7b7239e45b7fc4fc3fe428..38d887c340c5a5ccd9f9041af6cc809f7adcf07c 100644
--- a/tests/matrix-enc.c
+++ b/subprojects/libcmatrix/tests/cm-enc.c
@@ -1,7 +1,7 @@
 /* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* matrix-utils.c
  *
- * Copyright 2020 Purism SPC
+ * Copyright 2022 Purism SPC
  *
  * Author(s):
  *   Mohammed Sadiq <sadiq@sadiqpk.org>
@@ -23,8 +23,8 @@
 #include <olm/olm.h>
 #include <sys/random.h>
 
-#include "matrix-utils.h"
-#include "matrix-enc.h"
+#include "cm-utils-private.h"
+#include "cm-enc-private.h"
 
 typedef struct EncData {
   char *user_id;
@@ -72,27 +72,27 @@ EncData enc[] = {
 
 
 static void
-test_matrix_enc_verify (void)
+test_cm_enc_verify (void)
 {
-  g_autoptr(MatrixEnc) enc1 = NULL;
-  g_autoptr(MatrixEnc) enc2 = NULL;
+  g_autoptr(CmEnc) enc1 = NULL;
+  g_autoptr(CmEnc) enc2 = NULL;
   JsonObject *object, *root;
   char *sign, *json, *key_label;
   const char *message;
   EncData data;
 
   data = enc[0];
-  enc1 = matrix_enc_new (NULL, enc[0].olm2_pickle, enc[0].pickle_key);
-  enc2 = matrix_enc_new (NULL, enc[1].olm2_pickle, enc[1].pickle_key);
-  g_assert (MATRIX_IS_ENC (enc1));
-  g_assert (MATRIX_IS_ENC (enc2));
+  enc1 = cm_enc_new (NULL, enc[0].olm2_pickle, enc[0].pickle_key);
+  enc2 = cm_enc_new (NULL, enc[1].olm2_pickle, enc[1].pickle_key);
+  g_assert (CM_IS_ENC (enc1));
+  g_assert (CM_IS_ENC (enc2));
 
   /* @message is in canonical form */
   message = "{\"timeout\":20000,\"type\":\"m.message\"}";
-  sign = matrix_enc_sign_string (enc1, message, -1);
+  sign = cm_enc_sign_string (enc1, message, -1);
   g_assert_nonnull (sign);
 
-  root = matrix_utils_string_to_json_object (message);
+  root = cm_utils_string_to_json_object (message);
   g_assert_nonnull (root);
 
   json_object_set_object_member (root, "signatures", json_object_new ());
@@ -103,63 +103,63 @@ test_matrix_enc_verify (void)
 
   key_label = g_strconcat ("ed25519:", data.device_id, NULL);
   json_object_set_string_member (object, key_label, sign);
-  json = matrix_utils_json_object_to_string (root, FALSE);
+  json = cm_utils_json_object_to_string (root, FALSE);
   g_assert_nonnull (json);
   g_free (sign);
   g_free (key_label);
   json_object_unref (root);
 
-  root = matrix_utils_string_to_json_object (json);
+  root = cm_utils_string_to_json_object (json);
   g_assert_nonnull (root);
 
-  g_assert_true (matrix_enc_verify (enc1, root, data.user_id,
-                                    data.device_id, data.ed_key));
-  g_assert_true (matrix_enc_verify (enc2, root, data.user_id,
-                                    data.device_id, data.ed_key));
-  g_assert_false (matrix_enc_verify (enc1, root, data.user_id,
-                                    data.device_id, data.curve_key));
-  g_assert_false (matrix_enc_verify (enc1, root, enc[1].user_id,
-                                    data.device_id, data.ed_key));
+  g_assert_true (cm_enc_verify (enc1, root, data.user_id,
+                                data.device_id, data.ed_key));
+  g_assert_true (cm_enc_verify (enc2, root, data.user_id,
+                                data.device_id, data.ed_key));
+  g_assert_false (cm_enc_verify (enc1, root, data.user_id,
+                                 data.device_id, data.curve_key));
+  g_assert_false (cm_enc_verify (enc1, root, enc[1].user_id,
+                                 data.device_id, data.ed_key));
   json_object_unref (root);
   g_free (json);
 }
 
 static void
-test_matrix_enc_new (void)
+test_cm_enc_new (void)
 {
   const char *value;
   char *pickle;
   EncData data;
 
   for (guint i = 0; i < G_N_ELEMENTS (enc); i++) {
-    g_autoptr(MatrixEnc) matrix_enc = NULL;
+    g_autoptr(CmEnc) cm_enc = NULL;
 
     data = enc[i];
 
 #ifdef HAVE_OLM3
-    matrix_enc = matrix_enc_new (NULL, data.olm3_pickle, data.pickle_key);
-    g_assert (MATRIX_IS_ENC (matrix_enc));
+    cm_enc = cm_enc_new (NULL, data.olm3_pickle, data.pickle_key);
+    g_assert (CM_IS_ENC (cm_enc));
 
-    pickle = matrix_enc_get_account_pickle (matrix_enc);
+    pickle = cm_enc_get_pickle (cm_enc);
 # ifdef OLM_ACCOUNT_PICKLE_V4
     g_assert_cmpstr (pickle, ==, data.olm3_v4_pickle);
 # else
     g_assert_cmpstr (pickle, ==, data.olm3_pickle);
 # endif
     g_clear_pointer (&pickle, g_free);
-    g_object_unref (matrix_enc);
+    g_object_unref (cm_enc);
 #endif
 
-    matrix_enc = matrix_enc_new (NULL, data.olm2_pickle, data.pickle_key);
-    g_assert (MATRIX_IS_ENC (matrix_enc));
+    cm_enc = cm_enc_new (NULL, data.olm2_pickle, data.pickle_key);
+    g_assert (CM_IS_ENC (cm_enc));
 
-    value = matrix_enc_get_curve25519_key (matrix_enc);
+    value = cm_enc_get_curve25519_key (cm_enc);
     g_assert_cmpstr (value, ==, data.curve_key);
 
-    value = matrix_enc_get_ed25519_key (matrix_enc);
+    value = cm_enc_get_ed25519_key (cm_enc);
     g_assert_cmpstr (value, ==, data.ed_key);
 
-    pickle = matrix_enc_get_account_pickle (matrix_enc);
+    pickle = cm_enc_get_pickle (cm_enc);
 #ifdef HAVE_OLM3
 # ifdef OLM_ACCOUNT_PICKLE_V4
     g_assert_cmpstr (pickle, ==, data.olm3_v4_pickle);
@@ -179,8 +179,8 @@ main (int   argc,
 {
   g_test_init (&argc, &argv, NULL);
 
-  g_test_add_func ("/matrix/enc/new", test_matrix_enc_new);
-  g_test_add_func ("/matrix/enc/verify", test_matrix_enc_verify);
+  g_test_add_func ("/matrix/enc/new", test_cm_enc_new);
+  g_test_add_func ("/matrix/enc/verify", test_cm_enc_verify);
 
   return g_test_run ();
 }
diff --git a/subprojects/libcmatrix/tests/cm-utils.c b/subprojects/libcmatrix/tests/cm-utils.c
new file mode 100644
index 0000000000000000000000000000000000000000..900802650b45c803e15dfde62ab2d3c9d0ac85f8
--- /dev/null
+++ b/subprojects/libcmatrix/tests/cm-utils.c
@@ -0,0 +1,209 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* cm-utils.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#undef NDEBUG
+#undef G_DISABLE_ASSERT
+#undef G_DISABLE_CHECKS
+#undef G_DISABLE_CAST_CHECKS
+#undef G_LOG_DOMAIN
+
+#include "cm-utils-private.h"
+
+static JsonObject *
+get_json_object_for_file (const char *dir,
+                          const char *file_name)
+{
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree char *path = NULL;
+
+  path = g_build_filename (dir, file_name, NULL);
+  parser = json_parser_new ();
+  json_parser_load_from_file (parser, path, &error);
+  g_assert_no_error (error);
+
+  return json_node_dup_object (json_parser_get_root (parser));
+}
+
+static void
+test_utils_canonical (void)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GDir) dir = NULL;
+  g_autofree char *path = NULL;
+  const char *name;
+
+  path = g_test_build_filename (G_TEST_DIST, "cm-utils", NULL);
+  dir = g_dir_open (path, 0, &error);
+  g_assert_no_error (error);
+
+  while ((name = g_dir_read_name (dir)) != NULL) {
+    g_autofree char *expected_file = NULL;
+    g_autofree char *expected_name = NULL;
+    g_autofree char *expected_json = NULL;
+    g_autoptr(JsonObject) object = NULL;
+    g_autoptr(GString) json_str = NULL;
+
+    if (!g_str_has_suffix (name, ".json"))
+      continue;
+
+    expected_name = g_strconcat (name, ".expected", NULL);
+    expected_file = g_build_filename (path, expected_name, NULL);
+    g_file_get_contents (expected_file, &expected_json, NULL, &error);
+    g_assert_no_error (error);
+
+    object = get_json_object_for_file (path, name);
+    json_str = cm_utils_json_get_canonical (object, NULL);
+    g_assert_cmpstr (json_str->str, ==, expected_json);
+  }
+}
+
+static void
+test_utils_valid_user_name (void)
+{
+  struct Data
+  {
+    const char *user_name;
+    gboolean valid;
+  } data[] = {
+     {NULL, FALSE},
+     {"", FALSE},
+     {"@:.", FALSE},
+     {"@bob:", FALSE},
+     {"@:example.org", FALSE},
+     {"abc", FALSE},
+     {"good@bad:com", FALSE},
+     {"@a:example.org", TRUE},
+     {"@alice:example.org", TRUE},
+     {"@alice:example.org@alice:example.org", FALSE},
+     {"@alice:sub.example.org", TRUE},
+     {"@bob:localhost", TRUE},
+  };
+
+  for (guint i = 0; i < G_N_ELEMENTS (data); i++)
+    {
+      const char *user_name = data[i].user_name;
+      gboolean valid = data[i].valid;
+
+      if (valid)
+        g_assert_true (cm_utils_user_name_valid (user_name));
+      else
+        g_assert_false (cm_utils_user_name_valid (user_name));
+    }
+}
+
+static void
+test_utils_valid_email (void)
+{
+  struct Data
+  {
+    const char *email;
+    gboolean valid;
+  } data[] = {
+    {"", FALSE},
+    {"@:.", FALSE},
+    {"@bob:", FALSE},
+    {"@:example.org", FALSE},
+    {"abc", FALSE},
+    {"good@bad:com", FALSE},
+    {"@a:example.org", FALSE},
+    {"@alice:example.org", FALSE},
+    {"test@user.com", TRUE},
+    {"test@user.comtest@user.com", FALSE},
+    {"തറ@home.com", TRUE},
+  };
+
+  for (guint i = 0; i < G_N_ELEMENTS (data); i++)
+    {
+      const char *email = data[i].email;
+      gboolean valid = data[i].valid;
+
+      if (valid)
+        g_assert_true (cm_utils_user_name_is_email (email));
+      else
+        g_assert_false (cm_utils_user_name_is_email (email));
+    }
+}
+
+static void
+test_utils_valid_phone (void)
+{
+  struct Data
+  {
+    const char *phone;
+    gboolean valid;
+  } data[] = {
+     {"", FALSE},
+     {"123", FALSE},
+     {"+9123", FALSE},
+     {"+91223344", FALSE},
+     {"+91123456789", TRUE},
+     {"+13123456789", TRUE},
+     {"+13123456789002211443", FALSE},
+  };
+
+  for (guint i = 0; i < G_N_ELEMENTS (data); i++)
+    {
+      const char *phone = data[i].phone;
+      gboolean valid = data[i].valid;
+
+      if (valid)
+        g_assert_true (cm_utils_mobile_is_valid (phone));
+      else
+        g_assert_false (cm_utils_mobile_is_valid (phone));
+    }
+}
+
+static void
+test_utils_valid_home_server (void)
+{
+  struct Data
+  {
+    const char *uri;
+    gboolean valid;
+  } data[] = {
+    {"", FALSE},
+    {"http://", FALSE},
+    {"ftp://example.com", FALSE},
+    {"http://example.com", TRUE},
+    {"https://example.com", TRUE},
+    {"http://example.com/", TRUE},
+    {"http://example.com.", FALSE},
+    {"http://localhost:8008", TRUE},
+    {"http://localhost:8008/path", FALSE},
+  };
+
+  for (guint i = 0; i < G_N_ELEMENTS (data); i++)
+    {
+      const char *uri = data[i].uri;
+      gboolean valid = data[i].valid;
+
+      if (valid)
+        g_assert_true (cm_utils_home_server_valid (uri));
+      else
+        g_assert_false (cm_utils_home_server_valid (uri));
+    }
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/cm-utils/canonical", test_utils_canonical);
+  g_test_add_func ("/cm-utils/valid-user-name", test_utils_valid_user_name);
+  g_test_add_func ("/cm-utils/valid-email", test_utils_valid_email);
+  g_test_add_func ("/cm-utils/valid-phone", test_utils_valid_phone);
+  g_test_add_func ("/cm-utils/valid-home-server", test_utils_valid_home_server);
+
+  return g_test_run ();
+}
diff --git a/tests/matrix-utils/canonical-0.json b/subprojects/libcmatrix/tests/cm-utils/canonical-0.json
similarity index 100%
rename from tests/matrix-utils/canonical-0.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-0.json
diff --git a/tests/matrix-utils/canonical-0.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-0.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-0.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-0.json.expected
diff --git a/tests/matrix-utils/canonical-1.json b/subprojects/libcmatrix/tests/cm-utils/canonical-1.json
similarity index 100%
rename from tests/matrix-utils/canonical-1.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-1.json
diff --git a/tests/matrix-utils/canonical-1.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-1.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-1.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-1.json.expected
diff --git a/tests/matrix-utils/canonical-2.json b/subprojects/libcmatrix/tests/cm-utils/canonical-2.json
similarity index 100%
rename from tests/matrix-utils/canonical-2.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-2.json
diff --git a/tests/matrix-utils/canonical-2.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-2.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-2.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-2.json.expected
diff --git a/tests/matrix-utils/canonical-3.json b/subprojects/libcmatrix/tests/cm-utils/canonical-3.json
similarity index 100%
rename from tests/matrix-utils/canonical-3.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-3.json
diff --git a/tests/matrix-utils/canonical-3.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-3.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-3.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-3.json.expected
diff --git a/tests/matrix-utils/canonical-4.json b/subprojects/libcmatrix/tests/cm-utils/canonical-4.json
similarity index 100%
rename from tests/matrix-utils/canonical-4.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-4.json
diff --git a/tests/matrix-utils/canonical-4.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-4.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-4.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-4.json.expected
diff --git a/tests/matrix-utils/canonical-5.json b/subprojects/libcmatrix/tests/cm-utils/canonical-5.json
similarity index 100%
rename from tests/matrix-utils/canonical-5.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-5.json
diff --git a/tests/matrix-utils/canonical-5.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-5.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-5.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-5.json.expected
diff --git a/tests/matrix-utils/canonical-6.json b/subprojects/libcmatrix/tests/cm-utils/canonical-6.json
similarity index 100%
rename from tests/matrix-utils/canonical-6.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-6.json
diff --git a/tests/matrix-utils/canonical-6.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-6.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-6.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-6.json.expected
diff --git a/tests/matrix-utils/canonical-7.json b/subprojects/libcmatrix/tests/cm-utils/canonical-7.json
similarity index 100%
rename from tests/matrix-utils/canonical-7.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-7.json
diff --git a/tests/matrix-utils/canonical-7.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-7.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-7.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-7.json.expected
diff --git a/tests/matrix-utils/canonical-8.json b/subprojects/libcmatrix/tests/cm-utils/canonical-8.json
similarity index 100%
rename from tests/matrix-utils/canonical-8.json
rename to subprojects/libcmatrix/tests/cm-utils/canonical-8.json
diff --git a/tests/matrix-utils/canonical-8.json.expected b/subprojects/libcmatrix/tests/cm-utils/canonical-8.json.expected
similarity index 100%
rename from tests/matrix-utils/canonical-8.json.expected
rename to subprojects/libcmatrix/tests/cm-utils/canonical-8.json.expected
diff --git a/subprojects/libcmatrix/tests/enc-chat.c b/subprojects/libcmatrix/tests/enc-chat.c
new file mode 100644
index 0000000000000000000000000000000000000000..0f0a96b879f2456284ea3b66fbf07986c7212ff1
--- /dev/null
+++ b/subprojects/libcmatrix/tests/enc-chat.c
@@ -0,0 +1,171 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* session.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#undef NDEBUG
+#undef G_DISABLE_ASSERT
+#undef G_DISABLE_CHECKS
+#undef G_DISABLE_CAST_CHECKS
+#undef G_LOG_DOMAIN
+
+#include "src/cm-enc.c"
+#include "cm-matrix.h"
+#include "cm-matrix-private.h"
+
+static void
+finish_bool_cb (GObject      *object,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+  GTask *task = user_data;
+  gboolean status;
+
+  g_assert_true (G_IS_TASK (task));
+
+  status = g_task_propagate_boolean (G_TASK (result), &error);
+  g_assert_no_error (error);
+  g_task_return_boolean (task, status);
+}
+
+/* 'n' starts from 1 */
+static JsonObject *
+get_nth_member (JsonObject *root,
+                guint       n)
+{
+  g_autoptr(GList) members = NULL;
+
+  members = json_object_get_members (root);
+
+  if (g_list_length (members) < n)
+    return NULL;
+
+  return json_object_get_object_member (root, g_list_nth_data (members, n));
+}
+
+static void
+test_enc_chat_new (void)
+{
+  char *alice_one_time, *bob_one_time;
+  CmEnc *alice_enc, *bob_enc;
+  JsonObject *obj, *root;
+  CmMatrix *matrix;
+  size_t len;
+
+  matrix = g_object_new (CM_TYPE_MATRIX, NULL);
+
+  {
+    GTask *task;
+    GError *error = NULL;
+
+    task = g_task_new (NULL, NULL, NULL, NULL);
+    cm_matrix_open_async (matrix,
+                          g_test_get_dir (G_TEST_BUILT),
+                          "test-chat.c",
+                          NULL,
+                          finish_bool_cb, task);
+
+    while (!g_task_get_completed (task))
+      g_main_context_iteration (NULL, TRUE);
+
+    g_assert_true (g_task_propagate_boolean (task, &error));
+    g_assert_no_error (error);
+    g_assert_finalize_object (task);
+  }
+
+  /* Generate new keys for each */
+  {
+    GRefString *matrix_id;
+
+    matrix_id = g_ref_string_new_intern ("@alice:example.org");
+    alice_enc = cm_enc_new (cm_matrix_get_db (matrix), NULL, NULL);
+    cm_enc_set_details (alice_enc, matrix_id, "SYNAPSE");
+    g_ref_string_release (matrix_id);
+
+    matrix_id = g_ref_string_new_intern ("@bob:example.org");
+    bob_enc = cm_enc_new (cm_matrix_get_db (matrix), NULL, NULL);
+    cm_enc_set_details (bob_enc, matrix_id, "DENDRITE");
+    g_ref_string_release (matrix_id);
+  }
+
+  /* Generate one time keys */
+  len = cm_enc_create_one_time_keys (alice_enc, 3);
+  g_assert_cmpint (len, ==, 3);
+  len = cm_enc_create_one_time_keys (bob_enc, 3);
+  g_assert_cmpint (len, ==, 3);
+
+  alice_one_time = cm_enc_get_one_time_keys_json (alice_enc);
+  bob_one_time = cm_enc_get_one_time_keys_json (bob_enc);
+  g_assert_nonnull (alice_one_time);
+  g_assert_nonnull (bob_one_time);
+
+  cm_enc_publish_one_time_keys (alice_enc);
+  cm_enc_publish_one_time_keys (bob_enc);
+  g_assert_null (cm_enc_get_one_time_keys_json (alice_enc));
+  g_assert_null (cm_enc_get_one_time_keys_json (bob_enc));
+
+  root = cm_utils_string_to_json_object (alice_one_time);
+  g_assert_nonnull (root);
+  obj = cm_utils_json_object_get_object (root, "one_time_keys");
+  obj = get_nth_member (obj, 1);
+  g_assert_nonnull (obj);
+  /* Verify Alice's one time key with Bob's device */
+  g_assert_true (cm_enc_verify (bob_enc, obj,
+                                cm_enc_get_user_id (alice_enc),
+                                cm_enc_get_device_id (alice_enc),
+                                cm_enc_get_ed25519_key (alice_enc)));
+  json_object_unref (root);
+
+  root = cm_utils_string_to_json_object (bob_one_time);
+  g_assert_nonnull (root);
+  obj = cm_utils_json_object_get_object (root, "one_time_keys");
+  obj = get_nth_member (obj, 1);
+  g_assert_nonnull (obj);
+  /* Verify Bob's one time key with Alice's device */
+  g_assert_true (cm_enc_verify (alice_enc, obj,
+                                cm_enc_get_user_id (bob_enc),
+                                cm_enc_get_device_id (bob_enc),
+                                cm_enc_get_ed25519_key (bob_enc)));
+  /* Verify Bob's one time key with Bob's device */
+  g_assert_true (cm_enc_verify (bob_enc, obj,
+                                cm_enc_get_user_id (bob_enc),
+                                cm_enc_get_device_id (bob_enc),
+                                cm_enc_get_ed25519_key (bob_enc)));
+  g_assert_false (cm_enc_verify (bob_enc, obj,
+                                 cm_enc_get_user_id (alice_enc),
+                                 cm_enc_get_device_id (alice_enc),
+                                 cm_enc_get_ed25519_key (alice_enc)));
+  json_object_unref (root);
+
+  g_free (alice_one_time);
+  g_free (bob_one_time);
+
+  g_assert_finalize_object (alice_enc);
+  g_assert_finalize_object (bob_enc);
+  g_assert_finalize_object (matrix);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_autoptr(CmMatrix) matrix = NULL;
+
+  g_test_init (&argc, &argv, NULL);
+
+  cm_init (TRUE);
+  matrix = cm_matrix_new (g_test_get_dir (G_TEST_BUILT),
+                          g_test_get_dir (G_TEST_BUILT),
+                          "org.example.CMatrix",
+                          FALSE);
+  g_test_add_func ("/enc-chat/new", test_enc_chat_new);
+
+  return g_test_run ();
+}
diff --git a/subprojects/libcmatrix/tests/meson.build b/subprojects/libcmatrix/tests/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..dafe69c32766a08237b81891d1c6625f2bb2c384
--- /dev/null
+++ b/subprojects/libcmatrix/tests/meson.build
@@ -0,0 +1,32 @@
+tests_inc = [
+  root_inc,
+  src_inc,
+]
+
+env = environment()
+env.set('G_TEST_SRCDIR', meson.current_source_dir())
+env.set('G_TEST_BUILDDIR', meson.current_build_dir())
+env.set('MALLOC_CHECK_', '2')
+
+test_items = []
+
+test_items = [
+  'client',
+  'enc-chat',
+  'cm-db',
+  'cm-enc',
+  'room',
+  'room-member',
+  'cm-utils',
+]
+
+foreach item: test_items
+  t = executable(
+    item,
+    item + '.c',
+    include_directories: tests_inc,
+    link_with: cmatrix_lib,
+    dependencies: cmatrix_deps,
+  )
+  test(item, t, env: env, timeout: 120)
+endforeach
diff --git a/subprojects/libcmatrix/tests/room-member.c b/subprojects/libcmatrix/tests/room-member.c
new file mode 100644
index 0000000000000000000000000000000000000000..d0c86986a4ba68ed485e3251d4242a3d8d7f9b83
--- /dev/null
+++ b/subprojects/libcmatrix/tests/room-member.c
@@ -0,0 +1,57 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* room-member.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#undef NDEBUG
+#undef G_DISABLE_ASSERT
+#undef G_DISABLE_CHECKS
+#undef G_DISABLE_CAST_CHECKS
+#undef G_LOG_DOMAIN
+
+#include "cm-room-private.h"
+
+#undef G_LOG_DOMAIN
+#include "users/cm-room-member.c"
+
+static void
+test_room_member_new (void)
+{
+  CmRoomMember *member;
+  GRefString *user_id;
+  CmClient *client;
+  CmRoom *room;
+  CmUser *user;
+
+  room = cm_room_new ("random room");
+  client = cm_client_new ();
+  cm_room_set_client (room, client);
+  user_id = g_ref_string_new_intern ("@alice:example.co");
+  member = cm_room_member_new (user_id);
+  g_ref_string_release (user_id);
+  user = CM_USER (member);
+  cm_user_set_client (user, client);
+  g_object_unref (client);
+  g_assert (CM_IS_ROOM_MEMBER (member));
+
+  g_assert_cmpstr (cm_user_get_id (user), ==, "@alice:example.co");
+
+  g_assert_finalize_object (room);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/room-member/new", test_room_member_new);
+
+  return g_test_run ();
+}
diff --git a/subprojects/libcmatrix/tests/room.c b/subprojects/libcmatrix/tests/room.c
new file mode 100644
index 0000000000000000000000000000000000000000..3a01e8eb36fe754b3dab80c80a900afd6a8ed193
--- /dev/null
+++ b/subprojects/libcmatrix/tests/room.c
@@ -0,0 +1,44 @@
+/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
+/* room.c
+ *
+ * Copyright 2022 Purism SPC
+ *
+ * Author(s):
+ *   Mohammed Sadiq <sadiq@sadiqpk.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#undef NDEBUG
+#undef G_DISABLE_ASSERT
+#undef G_DISABLE_CHECKS
+#undef G_DISABLE_CAST_CHECKS
+#undef G_LOG_DOMAIN
+
+#include "cm-room.c"
+
+static void
+test_room_new (void)
+{
+  CmRoom *room;
+
+  room = cm_room_new ("some-room-id");
+  g_assert (CM_IS_ROOM (room));
+
+  g_assert_cmpstr (cm_room_get_id (room), ==, "some-room-id");
+
+  g_assert_false (cm_room_is_encrypted (room));
+
+  g_assert_finalize_object (room);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/room/new", test_room_new);
+
+  return g_test_run ();
+}
diff --git a/tests/history.c b/tests/history.c
index 954d3589cce53f5df477570a097df8e130015315..3f5707968e92f22dc8e39000a80126c9b2c8b1c4 100644
--- a/tests/history.c
+++ b/tests/history.c
@@ -738,69 +738,6 @@ add_chatty_message (ChattyHistory      *history,
   }
 }
 
-static void
-add_chat_and_test (ChattyHistory   *history,
-                   ChattyMaAccount *account,
-                   const char      *room_id,
-                   const char      *room_name,
-                   gboolean         hidden,
-                   guint            total_count)
-{
-  g_autoptr(GPtrArray) chat_list = NULL;
-  g_autoptr(GTask) task = NULL;
-
-  if (room_id) {
-    g_autoptr(ChattyMaChat) chat = NULL;
-
-    chat = chatty_ma_chat_new (room_id, room_name, NULL, FALSE);
-    g_assert (CHATTY_IS_MA_CHAT (chat));
-    if (hidden)
-      chatty_item_set_state (CHATTY_ITEM (chat), CHATTY_ITEM_HIDDEN);
-    chatty_ma_account_add_chat (account, CHATTY_CHAT (chat));
-    g_assert_true (chatty_history_update_chat (history, CHATTY_CHAT (chat)));
-  }
-
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  chatty_history_get_chats_async (history, CHATTY_ACCOUNT (account),
-                                  finish_pointer_cb, task);
-
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-  chat_list = g_task_propagate_pointer (task, NULL);
-  if (total_count) {
-    g_assert_nonnull (chat_list);
-    g_assert_cmpint (chat_list->len, ==, total_count);
-  } else {
-    g_assert_null (chat_list);
-  }
-}
-
-static void
-test_history_chat (void)
-{
-  g_autoptr(ChattyHistory) history = NULL;
-  g_autoptr(ChattyMaAccount) ma_account = NULL;
-  const char *account;
-
-  g_remove (g_test_get_filename (G_TEST_BUILT, "test-history.db", NULL));
-
-  history = chatty_history_new ();
-  chatty_history_open (history, g_test_get_dir (G_TEST_BUILT), "test-history.db");
-  g_assert_true (chatty_history_is_open (history));
-
-  account = "@alice:example.com";
-  ma_account = chatty_ma_account_new (account, NULL);
-  g_assert (CHATTY_IS_MA_ACCOUNT (ma_account));
-
-  add_chat_and_test (history, ma_account, NULL, NULL, FALSE, 0);
-  add_chat_and_test (history, ma_account, "!xdesSDcdsSXXs", "Test room", FALSE, 1);
-  add_chat_and_test (history, ma_account, "!aabbdesSDcdsS", "Another test room", FALSE, 2);
-  add_chat_and_test (history, ma_account, "!aabbdesSDcdsS", "Name changed", FALSE, 2);
-  add_chat_and_test (history, ma_account, "!aabbdesSDcdsS", "Name changed", TRUE, 1);
-
-  chatty_history_close (history);
-}
-
 static void
 test_history_message (void)
 {
@@ -1236,7 +1173,6 @@ main (int   argc,
   g_setenv ("GSETTINGS_BACKEND", "memory", TRUE);
 
   g_test_add_func ("/history/new", test_history_new);
-  g_test_add_func ("/history/chat", test_history_chat);
   g_test_add_func ("/history/message", test_history_message);
   g_test_add_func ("/history/raw_message", test_history_raw_message);
   g_test_add_func ("/history/db", test_history_db);
diff --git a/tests/matrix-api.c b/tests/matrix-api.c
deleted file mode 100644
index 8be72fc24dfca6c773fd79e4f355eacc996a3159..0000000000000000000000000000000000000000
--- a/tests/matrix-api.c
+++ /dev/null
@@ -1,71 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-api.c
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#include "matrix-api.h"
-
-static void
-test_matrix_api_new (void)
-{
-  MatrixApi *api;
-  const char *name;
-
-  name = "@alice:example.com";
-  api = matrix_api_new (NULL);
-  g_assert (MATRIX_IS_API (api));
-  g_assert_cmpstr (matrix_api_get_username (api), ==, NULL);
-  matrix_api_set_login_username (api, name);
-  g_assert_cmpstr (matrix_api_get_login_username (api), ==, name);
-  g_object_unref (api);
-
-  name = "@alice:example.org";
-  api = matrix_api_new (name);
-  g_assert (MATRIX_IS_API (api));
-  g_assert_cmpstr (matrix_api_get_login_username (api), ==, name);
-  g_assert_cmpstr (matrix_api_get_password (api), ==, NULL);
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, NULL);
-
-  matrix_api_set_password (api, "hunter2");
-  g_assert_cmpstr (matrix_api_get_password (api), ==, "hunter2");
-  matrix_api_set_password (api, NULL);
-  g_assert_cmpstr (matrix_api_get_password (api), ==, "hunter2");
-
-  matrix_api_set_homeserver (api, "example.net");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, NULL);
-  matrix_api_set_homeserver (api, "https://example.com");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://example.com");
-  matrix_api_set_homeserver (api, NULL);
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://example.com");
-  matrix_api_set_homeserver (api, "https://example.org/");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://example.org");
-  matrix_api_set_homeserver (api, "https://chat.example.net/page");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://chat.example.net");
-  matrix_api_set_homeserver (api, "http://talk.example.com");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "http://talk.example.com");
-  matrix_api_set_homeserver (api, "http://example.com:80");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "http://example.com");
-  matrix_api_set_homeserver (api, "http://example.com:80");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "http://example.com");
-  matrix_api_set_homeserver (api, "https://talk.example.net:80");
-  g_assert_cmpstr (matrix_api_get_homeserver (api), ==, "https://talk.example.net:80");
-
-  g_object_unref (api);
-}
-
-int
-main (int   argc,
-      char *argv[])
-{
-  g_test_init (&argc, &argv, NULL);
-
-  g_test_add_func ("/matrix/api/new", test_matrix_api_new);
-
-  return g_test_run ();
-}
diff --git a/tests/matrix-db.c b/tests/matrix-db.c
deleted file mode 100644
index 2ddd66850e8347dbccf055def34adbb72dc7a301..0000000000000000000000000000000000000000
--- a/tests/matrix-db.c
+++ /dev/null
@@ -1,230 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-db.c
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#undef NDEBUG
-#undef G_DISABLE_ASSERT
-#undef G_DISABLE_CHECKS
-#undef G_DISABLE_CAST_CHECKS
-#undef G_LOG_DOMAIN
-
-#include <glib/gstdio.h>
-#include <sqlite3.h>
-
-#include "chatty-ma-account.h"
-#include "matrix-db.h"
-
-static void
-finish_bool_cb (GObject      *object,
-                GAsyncResult *result,
-                gpointer      user_data)
-{
-  g_autoptr(GError) error = NULL;
-  GTask *task = user_data;
-  GObject *obj;
-  gboolean status;
-
-  g_assert_true (G_IS_TASK (task));
-
-  status = g_task_propagate_boolean (G_TASK (result), &error);
-  g_assert_no_error (error);
-
-  obj = G_OBJECT (result);
-  g_object_set_data (user_data, "enabled", g_object_get_data (obj, "enabled"));
-  g_object_set_data_full (user_data, "pickle", g_object_steal_data (obj, "pickle"), g_free);
-  g_object_set_data_full (user_data, "device", g_object_steal_data (obj, "device"), g_free);
-  g_object_set_data_full (user_data, "username", g_object_steal_data (obj, "username"), g_free);
-  g_task_return_boolean (task, status);
-}
-
-static gboolean
-account_matches_username (gconstpointer account,
-                          gconstpointer username)
-{
-  const char *id = username;
-
-  g_assert_true (CHATTY_IS_MA_ACCOUNT ((gpointer)account));
-  g_assert_true (id && *id);
-
-  return g_strcmp0 (id, chatty_item_get_username ((gpointer)account)) == 0;
-}
-
-static void
-add_matrix_account (MatrixDb   *db,
-                    GPtrArray  *account_array,
-                    const char *username,
-                    const char *pickle,
-                    const char *device_id,
-                    gboolean    enabled)
-{
-  ChattyMaAccount *account;
-  GObject *object;
-  GTask *task;
-  GError *error = NULL;
-  gboolean success;
-  guint i;
-
-  g_assert_true (MATRIX_IS_DB (db));
-  g_assert_nonnull (account_array);
-  g_assert_nonnull (username);
-
-  if (g_ptr_array_find_with_equal_func (account_array, username,
-                                        account_matches_username, &i)) {
-    account = account_array->pdata[i];
-  } else {
-    account = chatty_ma_account_new (username, NULL);
-    g_ptr_array_add (account_array, account);
-  }
-
-  g_assert_true (CHATTY_IS_MA_ACCOUNT (account));
-  object = G_OBJECT (account);
-
-  g_object_set_data (object, "enabled", GINT_TO_POINTER (enabled));
-  g_object_set_data_full (object, "pickle", g_strdup (pickle), g_free);
-  g_object_set_data_full (object, "device", g_strdup (device_id), g_free);
-
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  matrix_db_save_account_async (db, CHATTY_ACCOUNT (account), enabled, g_strdup (pickle), device_id,
-                                NULL, finish_bool_cb, task);
-
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-
-  success = g_task_propagate_boolean (task, &error);
-  g_assert_no_error (error);
-  g_assert_true (success);
-  g_clear_object (&task);
-
-  g_assert_true (g_ptr_array_find (account_array, account, &i));
-  account = account_array->pdata[i];
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  matrix_db_load_account_async (db, CHATTY_ACCOUNT (account), device_id, finish_bool_cb, task);
-
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-
-  success = g_task_propagate_boolean (task, &error);
-  g_assert_no_error (error);
-  g_assert_true (success);
-  g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "username"), ==,
-                   chatty_item_get_username (CHATTY_ITEM (account)));
-  g_assert_cmpint (GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "enabled")),
-                   ==, GPOINTER_TO_INT (g_object_get_data (object, "enabled")));
-  g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "pickle"), ==,
-                   g_object_get_data (object, "pickle"));
-  g_assert_cmpstr (g_object_get_data (G_OBJECT (task), "device"), ==,
-                   g_object_get_data (object, "device"));
-  g_clear_object (&task);
-}
-
-static void
-test_matrix_db_account (void)
-{
-  GTask *task;
-  MatrixDb *db;
-  gboolean status;
-  GPtrArray *account_array;
-
-  g_remove (g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL));
-
-  db = matrix_db_new ();
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  matrix_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)),
-                        "test-matrix.db", finish_bool_cb, task);
-
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-
-  status = g_task_propagate_boolean (task, NULL);
-  g_assert_finalize_object (task);
-  g_assert_true (status);
-
-  account_array = g_ptr_array_new ();
-  g_ptr_array_set_free_func (account_array, (GDestroyNotify)g_object_unref);
-
-  add_matrix_account (db, account_array, "@alice:example.org",
-                       NULL, NULL, TRUE);
-  add_matrix_account (db, account_array, "@alice:example.org",
-                      NULL, NULL, FALSE);
-  add_matrix_account (db, account_array, "@alice:example.com",
-                      NULL, "XXAABBDD", FALSE);
-  add_matrix_account (db, account_array, "@alice:example.com",
-                      "Some Pickle", "XXAABBDD", TRUE);
-  add_matrix_account (db, account_array, "@alice:example.org",
-                      NULL, NULL, TRUE);
-
-  add_matrix_account (db, account_array, "@bob:example.org",
-                      NULL, NULL, FALSE);
-  add_matrix_account (db, account_array, "@alice:example.org",
-                      NULL, NULL, FALSE);
-  add_matrix_account (db, account_array, "@bob:example.org",
-                      NULL, NULL, TRUE);
-
-  add_matrix_account (db, account_array, "@alice:example.net",
-                      NULL, NULL, TRUE);
-  add_matrix_account (db, account_array, "@alice:example.com",
-                      NULL, NULL, FALSE);
-
-  g_ptr_array_unref (account_array);
-}
-
-static void
-test_matrix_db_new (void)
-{
-  const char *file_name;
-  MatrixDb *db;
-  GTask *task;
-  gboolean status;
-
-  file_name = g_test_get_filename (G_TEST_BUILT, "test-matrix.db", NULL);
-  g_remove (file_name);
-  g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS));
-
-  db = matrix_db_new ();
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  matrix_db_open_async (db, g_strdup (g_test_get_dir (G_TEST_BUILT)),
-                        "test-matrix.db", finish_bool_cb, task);
-
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-
-  status = g_task_propagate_boolean (task, NULL);
-  g_assert_true (g_file_test (file_name, G_FILE_TEST_IS_REGULAR));
-  g_assert_true (matrix_db_is_open (db));
-  g_assert_true (status);
-  g_clear_object (&task);
-
-  task = g_task_new (NULL, NULL, NULL, NULL);
-  matrix_db_close_async (db, finish_bool_cb, task);
-
-  while (!g_task_get_completed (task))
-    g_main_context_iteration (NULL, TRUE);
-
-  status = g_task_propagate_boolean (task, NULL);
-  g_assert_true (status);
-  g_assert_false (matrix_db_is_open (db));
-  g_clear_object (&db);
-  g_clear_object (&task);
-
-  g_remove (file_name);
-  g_assert_false (g_file_test (file_name, G_FILE_TEST_EXISTS));
-}
-
-int
-main (int   argc,
-      char *argv[])
-{
-  g_test_init (&argc, &argv, NULL);
-
-  g_test_add_func ("/matrix-db/new", test_matrix_db_new);
-  g_test_add_func ("/matrix-db/account", test_matrix_db_account);
-
-  return g_test_run ();
-}
diff --git a/tests/matrix-utils.c b/tests/matrix-utils.c
deleted file mode 100644
index f64bfd6fec8c8c8618e022aab3c1c90bba88e680..0000000000000000000000000000000000000000
--- a/tests/matrix-utils.c
+++ /dev/null
@@ -1,83 +0,0 @@
-/* -*- mode: c; c-basic-offset: 2; indent-tabs-mode: nil; -*- */
-/* matrix-utils.c
- *
- * Copyright 2020 Purism SPC
- *
- * Author(s):
- *   Mohammed Sadiq <sadiq@sadiqpk.org>
- *
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#undef NDEBUG
-#undef G_DISABLE_ASSERT
-#undef G_DISABLE_CHECKS
-#undef G_DISABLE_CAST_CHECKS
-#undef G_LOG_DOMAIN
-
-#include <glib.h>
-
-#include "matrix-utils.h"
-
-static JsonObject *
-get_json_object_for_file (const char *dir,
-                          const char *file_name)
-{
-  g_autoptr(JsonParser) parser = NULL;
-  g_autoptr(GError) error = NULL;
-  g_autofree char *path = NULL;
-
-  path = g_build_filename (dir, file_name, NULL);
-  parser = json_parser_new ();
-  json_parser_load_from_file (parser, path, &error);
-  g_assert_no_error (error);
-
-  return json_node_dup_object (json_parser_get_root (parser));
-}
-
-static void
-test_matrix_utils_canonical (void)
-{
-  g_autoptr(GError) error = NULL;
-  g_autoptr(GDir) dir = NULL;
-  g_autofree char *path = NULL;
-  const char *name;
-
-  path = g_test_build_filename (G_TEST_DIST, "matrix-utils", NULL);
-  dir = g_dir_open (path, 0, &error);
-  g_assert_no_error (error);
-
-  while ((name = g_dir_read_name (dir)) != NULL) {
-    g_autofree char *expected_file = NULL;
-    g_autofree char *expected_name = NULL;
-    g_autofree char *expected_json = NULL;
-    g_autoptr(JsonObject) object = NULL;
-    g_autoptr(GString) json_str = NULL;
-
-    if (!g_str_has_suffix (name, ".json"))
-      continue;
-
-    expected_name = g_strconcat (name, ".expected", NULL);
-    expected_file = g_build_filename (path, expected_name, NULL);
-    g_file_get_contents (expected_file, &expected_json, NULL, &error);
-    g_assert_no_error (error);
-
-    object = get_json_object_for_file (path, name);
-    json_str = matrix_utils_json_get_canonical (object, NULL);
-    g_assert_cmpstr (json_str->str, ==, expected_json);
-  }
-}
-
-int
-main (int   argc,
-      char *argv[])
-{
-  /* DEBUG */
-  g_setenv ("G_TEST_SRCDIR", "/media/sadiq/temp/jhbuild/checkout/chatty/tests/", FALSE);
-
-  g_test_init (&argc, &argv, NULL);
-
-  g_test_add_func ("/matrix/utils/canonical", test_matrix_utils_canonical);
-
-  return g_test_run ();
-}
diff --git a/tests/meson.build b/tests/meson.build
index 78069afb0f4adda8050759ec414e620904a7b1aa..86147b0ef467eaf66bb8ce43df85469cad282113 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -6,7 +6,8 @@ tests_inc = [
 env = environment()
 env.set('G_TEST_SRCDIR', meson.current_source_dir())
 env.set('G_TEST_BUILDDIR', meson.current_build_dir())
-env.set('GSETTINGS_SCHEMA_DIR', join_paths(meson.build_root(), 'data'))
+env.set('GSETTINGS_BACKEND', 'memory')
+env.set('GSETTINGS_SCHEMA_DIR', join_paths(meson.project_build_root(), 'data'))
 env.set('MALLOC_CHECK_', '2')
 
 test_items = []
@@ -20,10 +21,6 @@ test_items = [
   'history',
   'settings',
   'utils',
-  'matrix-api',
-  'matrix-db',
-  'matrix-enc',
-  'matrix-utils',
   'message-text-item',
   'mm-account',
   'sms-uri',
@@ -34,7 +31,7 @@ foreach item: test_items
     item,
     item + '.c',
     include_directories: tests_inc,
-    link_with: libchatty.get_static_lib(),
+    link_with: libchatty_static,
     dependencies: chatty_deps,
   )
   test(item, t, env: env, timeout: 300)
diff --git a/tests/sms-uri.c b/tests/sms-uri.c
index da02b34acdb5a46e49bcbaf3586b71ff3cacfc95..3830d6868e4e1f4fd2788ff7965f223ffcd5be34 100644
--- a/tests/sms-uri.c
+++ b/tests/sms-uri.c
@@ -42,6 +42,7 @@ data array[] = {
   { "sms://+919995112233?body=I'm busy", "I'm busy", "+919995112233", 1, TRUE, TRUE},
   { "sms://123,453?body=a ചെറിയ test", "a ചെറിയ test", "123,453", 2, TRUE, TRUE},
   { "sms://453,123,145,123,453?body=HELP", "HELP", "123,145,453", 3, TRUE, TRUE},
+  { "sms://123,123,123,123,123?body=HELP", "HELP", "123", 1, TRUE, TRUE},
   { "sms://453,123,145,123,453?body=HELP%20me", "HELP me", "123,145,453", 3, TRUE, TRUE},
   { "sms:9995 123 123?body=Call me later", "Call me later", "+919995123123", 1, TRUE, TRUE, "IN"},
   { "sms:+919995 123 123?body= before and after ", " before and after ", "+919995123123", 1, TRUE, TRUE, "US"},