diff --git a/CMakeLists.txt b/CMakeLists.txt
index fb104807ce7f64cded02f7e39ec67e3ca014e402..ae4ce2fa375a829ac3ffd6727354c2bd0a53164b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -356,7 +356,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        5d2f055ea9403770039ddf66b1900f890cd5cde7
+		GIT_TAG        fdb2016eff4f2e91f17c343e9fcb0bfab5e78b63
 		)
 	set(BUILD_LIB_EXAMPLES OFF CACHE INTERNAL "")
 	set(BUILD_LIB_TESTS OFF CACHE INTERNAL "")
@@ -458,7 +458,7 @@ endif()
 
 # single instance functionality
 set(QAPPLICATION_CLASS QApplication CACHE STRING "Inheritance class for SingleApplication")
-add_subdirectory(third_party/SingleApplication-3.2.0-dc8042b/)
+add_subdirectory(third_party/SingleApplication-3.3.0/)
 
 feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
 
diff --git a/io.github.NhekoReborn.Nheko.yaml b/io.github.NhekoReborn.Nheko.yaml
index 2e2ec44655228fdbbea0f0e16e91cb8cb9df8cb9..564b08a991c986cdcb669cd44d9384286bda167d 100644
--- a/io.github.NhekoReborn.Nheko.yaml
+++ b/io.github.NhekoReborn.Nheko.yaml
@@ -150,7 +150,7 @@ modules:
     buildsystem: cmake-ninja
     name: mtxclient
     sources:
-      - commit: 5d2f055ea9403770039ddf66b1900f890cd5cde7
+      - commit: fdb2016eff4f2e91f17c343e9fcb0bfab5e78b63
         type: git
         url: https://github.com/Nheko-Reborn/mtxclient.git
   - config-opts:
diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml
index df2bf41fc4505b83fcc848c4d33ff603bf768081..cd323a97b56961554ca54308cbfc2562b19bae13 100644
--- a/resources/qml/ChatPage.qml
+++ b/resources/qml/ChatPage.qml
@@ -71,7 +71,7 @@ Rectangle {
         AdaptiveLayoutElement {
             id: timlineViewC
 
-            minimumWidth: fontMetrics.averageCharacterWidth * 40 + Nheko.avatarSize + 2* Nheko.paddingMedium
+            minimumWidth: fontMetrics.averageCharacterWidth * 40 + Nheko.avatarSize + 2 * Nheko.paddingMedium
 
             TimelineView {
                 id: timeline
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index 167899a549db77cb8c5e2851536a54d534346a3c..65d66315bb469f0d82c05d4c07f46748f6070e4b 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -12,7 +12,8 @@ TextEdit {
     focus: false
     wrapMode: Text.Wrap
     selectByMouse: !Settings.mobileMode
-    enabled: selectByMouse
+    // this always has to be enabled, otherwise you can't click links anymore!
+    //enabled: selectByMouse
     color: Nheko.colors.text
     onLinkActivated: Nheko.openLink(link)
     ToolTip.visible: hoveredLink
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index 5845eb8143d5b6924e944f11a24ef5acbae1d105..c4a8bcfb1c1347f441853d0e363bf53cf76a205a 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -255,6 +255,8 @@ Page {
                         Label {
                             id: timestamp
 
+                            visible: !model.isInvite && !model.isSpace
+                            width: visible ? 0 : undefined
                             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             color: roomItem.unimportantText
@@ -266,12 +268,11 @@ Page {
                     RowLayout {
                         Layout.fillWidth: true
                         spacing: 0
-                        visible: !model.isInvite
+                        visible: !model.isInvite && !model.isSpace
                         height: visible ? 0 : undefined
 
                         ElidedLabel {
                             color: roomItem.unimportantText
-                            font.weight: Font.Thin
                             font.pixelSize: fontMetrics.font.pixelSize * 0.9
                             elideWidth: textContent.width - (notificationBubble.visible ? notificationBubble.width : 0) - Nheko.paddingSmall
                             fullText: model.lastMessage
@@ -485,7 +486,6 @@ Page {
                     ElidedLabel {
                         Layout.alignment: Qt.AlignTop
                         color: Nheko.colors.buttonText
-                        font.weight: Font.Thin
                         font.pointSize: fontMetrics.font.pointSize * 0.9
                         elideWidth: col.width
                         fullText: userInfoGrid.profile ? userInfoGrid.profile.userid : ""
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 90e2816666f8b4148bd9fda7d3112c8da0092d19..2c7c943abc5cdad9e063a64569e9ea8886e0ee3a 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -41,7 +41,8 @@ Item {
     ColumnLayout {
         id: timelineLayout
 
-        visible: room != null
+        visible: room != null && !room.isSpace
+        enabled: visible
         anchors.fill: parent
         spacing: 0
 
@@ -127,6 +128,79 @@ Item {
 
     }
 
+    ColumnLayout {
+        visible: room != null && room.isSpace
+        enabled: visible
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingLarge
+        spacing: Nheko.paddingLarge
+
+        Avatar {
+            url: room ? room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") : ""
+            displayName: room ? room.roomName : ""
+            height: 130
+            width: 130
+            Layout.alignment: Qt.AlignHCenter
+            enabled: false
+        }
+
+        MatrixText {
+            text: room ? room.roomName : ""
+            font.pixelSize: 24
+            Layout.alignment: Qt.AlignHCenter
+        }
+
+        MatrixText {
+            text: qsTr("%1 member(s)").arg(room ? room.roomMemberCount : 0)
+            Layout.alignment: Qt.AlignHCenter
+        }
+
+        ScrollView {
+            Layout.alignment: Qt.AlignHCenter
+            width: timelineView.width - Nheko.paddingLarge * 2
+
+            TextArea {
+                text: TimelineManager.escapeEmoji(room ? room.roomTopic : "")
+                wrapMode: TextEdit.WordWrap
+                textFormat: TextEdit.RichText
+                readOnly: true
+                background: null
+                selectByMouse: true
+                color: Nheko.colors.text
+                horizontalAlignment: TextEdit.AlignHCenter
+                onLinkActivated: Nheko.openLink(link)
+
+                CursorShape {
+                    anchors.fill: parent
+                    cursorShape: parent.hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
+                }
+
+            }
+
+        }
+
+        Item {
+            Layout.fillHeight: true
+        }
+
+    }
+
+    ImageButton {
+        id: backToRoomsButton
+
+        anchors.top: parent.top
+        anchors.left: parent.left
+        anchors.margins: Nheko.paddingMedium
+        width: Nheko.avatarSize
+        height: Nheko.avatarSize
+        visible: room != null && room.isSpace && showBackButton
+        enabled: visible
+        image: ":/icons/icons/ui/angle-pointing-to-left.png"
+        ToolTip.visible: hovered
+        ToolTip.text: qsTr("Back to room list")
+        onClicked: Rooms.resetCurrentRoom()
+    }
+
     NhekoDropArea {
         anchors.fill: parent
         roomid: room ? room.roomId() : ""
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 2178bbfbb991a6cce57062244589e5a2a92f5ac8..144a2d9ab43a548801a751edeb758d9a61706515 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -55,6 +55,10 @@ constexpr auto BATCH_SIZE = 100;
 //! Format: room_id -> RoomInfo
 constexpr auto ROOMS_DB("rooms");
 constexpr auto INVITES_DB("invites");
+//! maps each room to its parent space (id->id)
+constexpr auto SPACES_PARENTS_DB("space_parents");
+//! maps each space to its current children (id->id)
+constexpr auto SPACES_CHILDREN_DB("space_children");
 //! Information that  must be kept between sync requests.
 constexpr auto SYNC_STATE_DB("sync_state");
 //! Read receipts per room/event.
@@ -237,12 +241,14 @@ Cache::setup()
                 env_.open(cacheDirectory_.toStdString().c_str());
         }
 
-        auto txn         = lmdb::txn::begin(env_);
-        syncStateDb_     = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
-        roomsDb_         = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
-        invitesDb_       = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
-        readReceiptsDb_  = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
-        notificationsDb_ = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
+        auto txn          = lmdb::txn::begin(env_);
+        syncStateDb_      = lmdb::dbi::open(txn, SYNC_STATE_DB, MDB_CREATE);
+        roomsDb_          = lmdb::dbi::open(txn, ROOMS_DB, MDB_CREATE);
+        spacesChildrenDb_ = lmdb::dbi::open(txn, SPACES_CHILDREN_DB, MDB_CREATE | MDB_DUPSORT);
+        spacesParentsDb_  = lmdb::dbi::open(txn, SPACES_PARENTS_DB, MDB_CREATE | MDB_DUPSORT);
+        invitesDb_        = lmdb::dbi::open(txn, INVITES_DB, MDB_CREATE);
+        readReceiptsDb_   = lmdb::dbi::open(txn, READ_RECEIPTS_DB, MDB_CREATE);
+        notificationsDb_  = lmdb::dbi::open(txn, NOTIFICATIONS_DB, MDB_CREATE);
 
         // Device management
         devicesDb_    = lmdb::dbi::open(txn, DEVICES_DB, MDB_CREATE);
@@ -899,7 +905,9 @@ Cache::runMigrations()
                                                    std::reverse(oldMessages.events.begin(),
                                                                 oldMessages.events.end());
                                                    // save messages using the new method
-                                                   saveTimelineMessages(txn, room_id, oldMessages);
+                                                   auto eventsDb = getEventsDb(txn, room_id);
+                                                   saveTimelineMessages(
+                                                     txn, eventsDb, room_id, oldMessages);
                                            }
 
                                            // delete old messages db
@@ -1194,24 +1202,73 @@ Cache::saveState(const mtx::responses::Sync &res)
 
         auto userKeyCacheDb = getUserKeysDb(txn);
 
+        std::set<std::string> spaces_with_updates;
+        std::set<std::string> rooms_with_space_updates;
+
         // Save joined rooms
         for (const auto &room : res.rooms.join) {
                 auto statesdb    = getStatesDb(txn, room.first);
                 auto stateskeydb = getStatesKeyDb(txn, room.first);
                 auto membersdb   = getMembersDb(txn, room.first);
-
-                saveStateEvents(
-                  txn, statesdb, stateskeydb, membersdb, room.first, room.second.state.events);
-                saveStateEvents(
-                  txn, statesdb, stateskeydb, membersdb, room.first, room.second.timeline.events);
-
-                saveTimelineMessages(txn, room.first, room.second.timeline);
+                auto eventsDb    = getEventsDb(txn, room.first);
+
+                saveStateEvents(txn,
+                                statesdb,
+                                stateskeydb,
+                                membersdb,
+                                eventsDb,
+                                room.first,
+                                room.second.state.events);
+                saveStateEvents(txn,
+                                statesdb,
+                                stateskeydb,
+                                membersdb,
+                                eventsDb,
+                                room.first,
+                                room.second.timeline.events);
+
+                saveTimelineMessages(txn, eventsDb, room.first, room.second.timeline);
 
                 RoomInfo updatedInfo;
                 updatedInfo.name       = getRoomName(txn, statesdb, membersdb).toStdString();
                 updatedInfo.topic      = getRoomTopic(txn, statesdb).toStdString();
                 updatedInfo.avatar_url = getRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
                 updatedInfo.version    = getRoomVersion(txn, statesdb).toStdString();
+                updatedInfo.is_space   = getRoomIsSpace(txn, statesdb);
+
+                if (updatedInfo.is_space) {
+                        bool space_updates = false;
+                        for (const auto &e : room.second.state.events)
+                                if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
+                                    std::holds_alternative<StateEvent<state::PowerLevels>>(e))
+                                        space_updates = true;
+                        for (const auto &e : room.second.timeline.events)
+                                if (std::holds_alternative<StateEvent<state::space::Child>>(e) ||
+                                    std::holds_alternative<StateEvent<state::PowerLevels>>(e))
+                                        space_updates = true;
+
+                        if (space_updates)
+                                spaces_with_updates.insert(room.first);
+                }
+
+                {
+                        bool room_has_space_update = false;
+                        for (const auto &e : room.second.state.events) {
+                                if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
+                                        spaces_with_updates.insert(se->state_key);
+                                        room_has_space_update = true;
+                                }
+                        }
+                        for (const auto &e : room.second.timeline.events) {
+                                if (auto se = std::get_if<StateEvent<state::space::Parent>>(&e)) {
+                                        spaces_with_updates.insert(se->state_key);
+                                        room_has_space_update = true;
+                                }
+                        }
+
+                        if (room_has_space_update)
+                                rooms_with_space_updates.insert(room.first);
+                }
 
                 bool has_new_tags = false;
                 // Process the account_data associated with this room
@@ -1291,6 +1348,8 @@ Cache::saveState(const mtx::responses::Sync &res)
 
         removeLeftRooms(txn, res.rooms.leave);
 
+        updateSpaces(txn, spaces_with_updates, std::move(rooms_with_space_updates));
+
         txn.commit();
 
         std::map<QString, bool> readStatus;
@@ -1339,6 +1398,7 @@ Cache::saveInvites(lmdb::txn &txn, const std::map<std::string, mtx::responses::I
                 updatedInfo.topic = getInviteRoomTopic(txn, statesdb).toStdString();
                 updatedInfo.avatar_url =
                   getInviteRoomAvatarUrl(txn, statesdb, membersdb).toStdString();
+                updatedInfo.is_space  = getInviteRoomIsSpace(txn, statesdb);
                 updatedInfo.is_invite = true;
 
                 invitesDb_.put(txn, room.first, json(updatedInfo).dump());
@@ -1427,27 +1487,6 @@ Cache::roomsWithStateUpdates(const mtx::responses::Sync &res)
         return rooms;
 }
 
-std::vector<std::string>
-Cache::roomsWithTagUpdates(const mtx::responses::Sync &res)
-{
-        using namespace mtx::events;
-
-        std::vector<std::string> rooms;
-        for (const auto &room : res.rooms.join) {
-                bool hasUpdates = false;
-                for (const auto &evt : room.second.account_data.events) {
-                        if (std::holds_alternative<AccountDataEvent<account_data::Tags>>(evt)) {
-                                hasUpdates = true;
-                        }
-                }
-
-                if (hasUpdates)
-                        rooms.emplace_back(room.first);
-        }
-
-        return rooms;
-}
-
 RoomInfo
 Cache::singleRoomInfo(const std::string &room_id)
 {
@@ -1733,6 +1772,13 @@ Cache::relatedEvents(const std::string &room_id, const std::string &event_id)
         return related_ids;
 }
 
+size_t
+Cache::memberCount(const std::string &room_id)
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+        return getMembersDb(txn, room_id).size(txn);
+}
+
 QMap<QString, RoomInfo>
 Cache::roomInfo(bool withInvites)
 {
@@ -2337,6 +2383,29 @@ Cache::getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
         return QString("1");
 }
 
+bool
+Cache::getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb)
+{
+        using namespace mtx::events;
+        using namespace mtx::events::state;
+
+        std::string_view event;
+        bool res = statesdb.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+
+        if (res) {
+                try {
+                        StateEvent<Create> msg = json::parse(event);
+
+                        return msg.content.type == mtx::events::state::room_type::space;
+                } catch (const json::exception &e) {
+                        nhlog::db()->warn("failed to parse m.room.create event: {}", e.what());
+                }
+        }
+
+        nhlog::db()->warn("m.room.create event is missing room version, assuming version \"1\"");
+        return false;
+}
+
 std::optional<mtx::events::state::CanonicalAlias>
 Cache::getRoomAliases(const std::string &roomid)
 {
@@ -2464,6 +2533,27 @@ Cache::getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &db)
         return QString();
 }
 
+bool
+Cache::getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db)
+{
+        using namespace mtx::events;
+        using namespace mtx::events::state;
+
+        std::string_view event;
+        bool res = db.get(txn, to_string(mtx::events::EventType::RoomCreate), event);
+
+        if (res) {
+                try {
+                        StrippedEvent<Create> msg = json::parse(event);
+                        return msg.content.type == mtx::events::state::room_type::space;
+                } catch (const json::exception &e) {
+                        nhlog::db()->warn("failed to parse m.room.topic event: {}", e.what());
+                }
+        }
+
+        return false;
+}
+
 std::vector<std::string>
 Cache::joinedRooms()
 {
@@ -2506,42 +2596,6 @@ Cache::getMember(const std::string &room_id, const std::string &user_id)
         return std::nullopt;
 }
 
-std::vector<RoomSearchResult>
-Cache::searchRooms(const std::string &query, std::uint8_t max_items)
-{
-        std::multimap<int, std::pair<std::string, RoomInfo>> items;
-
-        auto txn    = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
-        auto cursor = lmdb::cursor::open(txn, roomsDb_);
-
-        std::string_view room_id, room_data;
-        while (cursor.get(room_id, room_data, MDB_NEXT)) {
-                RoomInfo tmp = json::parse(room_data);
-
-                const int score = utils::levenshtein_distance(
-                  query, QString::fromStdString(tmp.name).toLower().toStdString());
-                items.emplace(score, std::make_pair(room_id, tmp));
-        }
-
-        cursor.close();
-
-        auto end = items.begin();
-
-        if (items.size() >= max_items)
-                std::advance(end, max_items);
-        else if (items.size() > 0)
-                std::advance(end, items.size());
-
-        std::vector<RoomSearchResult> results;
-        for (auto it = items.begin(); it != end; it++) {
-                results.push_back(RoomSearchResult{it->second.first, it->second.second});
-        }
-
-        txn.commit();
-
-        return results;
-}
-
 std::vector<RoomMember>
 Cache::getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
 {
@@ -2600,11 +2654,12 @@ void
 Cache::savePendingMessage(const std::string &room_id,
                           const mtx::events::collections::TimelineEvent &message)
 {
-        auto txn = lmdb::txn::begin(env_);
+        auto txn      = lmdb::txn::begin(env_);
+        auto eventsDb = getEventsDb(txn, room_id);
 
         mtx::responses::Timeline timeline;
         timeline.events.push_back(message.data);
-        saveTimelineMessages(txn, room_id, timeline);
+        saveTimelineMessages(txn, eventsDb, room_id, timeline);
 
         auto pending = getPendingMessagesDb(txn, room_id);
 
@@ -2672,13 +2727,13 @@ Cache::removePendingStatus(const std::string &room_id, const std::string &txn_id
 
 void
 Cache::saveTimelineMessages(lmdb::txn &txn,
+                            lmdb::dbi &eventsDb,
                             const std::string &room_id,
                             const mtx::responses::Timeline &res)
 {
         if (res.events.empty())
                 return;
 
-        auto eventsDb    = getEventsDb(txn, room_id);
         auto relationsDb = getRelationsDb(txn, room_id);
 
         auto orderDb     = getEventOrderDb(txn, room_id);
@@ -3203,6 +3258,147 @@ Cache::deleteOldData() noexcept
         }
 }
 
+void
+Cache::updateSpaces(lmdb::txn &txn,
+                    const std::set<std::string> &spaces_with_updates,
+                    std::set<std::string> rooms_with_updates)
+{
+        if (spaces_with_updates.empty() && rooms_with_updates.empty())
+                return;
+
+        for (const auto &space : spaces_with_updates) {
+                // delete old entries
+                {
+                        auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
+                        bool first          = true;
+                        std::string_view sp = space, space_child = "";
+
+                        if (cursor.get(sp, space_child, MDB_SET)) {
+                                while (cursor.get(
+                                  sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                                        first = false;
+                                        spacesParentsDb_.del(txn, space_child, space);
+                                }
+                        }
+                        cursor.close();
+                        spacesChildrenDb_.del(txn, space);
+                }
+
+                for (const auto &event :
+                     getStateEventsWithType<mtx::events::state::space::Child>(txn, space)) {
+                        if (event.content.via.has_value() && event.state_key.size() > 3 &&
+                            event.state_key.at(0) == '!') {
+                                spacesChildrenDb_.put(txn, space, event.state_key);
+                                spacesParentsDb_.put(txn, event.state_key, space);
+                        }
+                }
+        }
+
+        const auto space_event_type = to_string(mtx::events::EventType::RoomPowerLevels);
+
+        for (const auto &room : rooms_with_updates) {
+                for (const auto &event :
+                     getStateEventsWithType<mtx::events::state::space::Parent>(txn, room)) {
+                        if (event.content.via.has_value() && event.state_key.size() > 3 &&
+                            event.state_key.at(0) == '!') {
+                                const std::string &space = event.state_key;
+
+                                auto pls =
+                                  getStateEvent<mtx::events::state::PowerLevels>(txn, space);
+
+                                if (!pls)
+                                        continue;
+
+                                if (pls->content.user_level(event.sender) >=
+                                    pls->content.state_level(space_event_type)) {
+                                        spacesChildrenDb_.put(txn, space, room);
+                                        spacesParentsDb_.put(txn, room, space);
+                                }
+                        }
+                }
+        }
+}
+
+QMap<QString, std::optional<RoomInfo>>
+Cache::spaces()
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+        QMap<QString, std::optional<RoomInfo>> ret;
+        {
+                auto cursor = lmdb::cursor::open(txn, spacesChildrenDb_);
+                bool first  = true;
+                std::string_view space_id, space_child;
+                while (cursor.get(space_id, space_child, first ? MDB_FIRST : MDB_NEXT)) {
+                        first = false;
+
+                        if (!space_child.empty()) {
+                                std::string_view room_data;
+                                if (roomsDb_.get(txn, space_id, room_data)) {
+                                        RoomInfo tmp = json::parse(std::move(room_data));
+                                        ret.insert(
+                                          QString::fromUtf8(space_id.data(), space_id.size()), tmp);
+                                } else {
+                                        ret.insert(
+                                          QString::fromUtf8(space_id.data(), space_id.size()),
+                                          std::nullopt);
+                                }
+                        }
+                }
+                cursor.close();
+        }
+
+        return ret;
+}
+
+std::vector<std::string>
+Cache::getParentRoomIds(const std::string &room_id)
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+        std::vector<std::string> roomids;
+        {
+                auto cursor         = lmdb::cursor::open(txn, spacesParentsDb_);
+                bool first          = true;
+                std::string_view sp = room_id, space_parent;
+                if (cursor.get(sp, space_parent, MDB_SET)) {
+                        while (cursor.get(sp, space_parent, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                                first = false;
+
+                                if (!space_parent.empty())
+                                        roomids.emplace_back(space_parent);
+                        }
+                }
+                cursor.close();
+        }
+
+        return roomids;
+}
+
+std::vector<std::string>
+Cache::getChildRoomIds(const std::string &room_id)
+{
+        auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+
+        std::vector<std::string> roomids;
+        {
+                auto cursor         = lmdb::cursor::open(txn, spacesChildrenDb_);
+                bool first          = true;
+                std::string_view sp = room_id, space_child;
+                if (cursor.get(sp, space_child, MDB_SET)) {
+                        while (cursor.get(sp, space_child, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                                first = false;
+
+                                if (!space_child.empty())
+                                        roomids.emplace_back(space_child);
+                        }
+                }
+                cursor.close();
+        }
+
+        return roomids;
+}
+
 std::optional<mtx::events::collections::RoomAccountDataEvents>
 Cache::getAccountData(lmdb::txn &txn, mtx::events::EventType type, const std::string &room_id)
 {
@@ -3884,6 +4080,7 @@ to_json(json &j, const RoomInfo &info)
         j["avatar_url"]   = info.avatar_url;
         j["version"]      = info.version;
         j["is_invite"]    = info.is_invite;
+        j["is_space"]     = info.is_space;
         j["join_rule"]    = info.join_rule;
         j["guest_access"] = info.guest_access;
 
@@ -3903,6 +4100,7 @@ from_json(const json &j, RoomInfo &info)
         info.version    = j.value(
           "version", QCoreApplication::translate("RoomInfo", "no version stored").toStdString());
         info.is_invite    = j.at("is_invite");
+        info.is_space     = j.value("is_space", false);
         info.join_rule    = j.at("join_rule");
         info.guest_access = j.at("guest_access");
 
@@ -4158,12 +4356,6 @@ getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb)
         return instance_->getRoomAvatarUrl(txn, statesdb, membersdb);
 }
 
-QString
-getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb)
-{
-        return instance_->getRoomVersion(txn, statesdb);
-}
-
 std::vector<RoomMember>
 getMembers(const std::string &room_id, std::size_t startIndex, std::size_t len)
 {
@@ -4305,11 +4497,7 @@ roomsWithStateUpdates(const mtx::responses::Sync &res)
 {
         return instance_->roomsWithStateUpdates(res);
 }
-std::vector<std::string>
-roomsWithTagUpdates(const mtx::responses::Sync &res)
-{
-        return instance_->roomsWithTagUpdates(res);
-}
+
 std::map<QString, RoomInfo>
 getRoomInfo(const std::vector<std::string> &rooms)
 {
@@ -4329,12 +4517,6 @@ calculateRoomReadStatus()
         instance_->calculateRoomReadStatus();
 }
 
-std::vector<RoomSearchResult>
-searchRooms(const std::string &query, std::uint8_t max_items)
-{
-        return instance_->searchRooms(query, max_items);
-}
-
 void
 markSentNotification(const std::string &event_id)
 {
diff --git a/src/Cache.h b/src/Cache.h
index 74ec9695c9bbe7b8f4c7c8a17e9b533d5c19b6fd..b0520f6b112489170576b3583fc4134e2d07c4f4 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -79,9 +79,6 @@ getRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
 //! Retrieve the room avatar's url if any.
 QString
 getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
-//! Retrieve the version of the room if any.
-QString
-getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
 
 //! Retrieve member info from a room.
 std::vector<RoomMember>
@@ -166,9 +163,6 @@ calculateRoomReadStatus(const std::string &room_id);
 void
 calculateRoomReadStatus();
 
-std::vector<RoomSearchResult>
-searchRooms(const std::string &query, std::uint8_t max_items = 5);
-
 void
 markSentNotification(const std::string &event_id);
 //! Removes an event from the sent notifications.
diff --git a/src/CacheStructs.h b/src/CacheStructs.h
index f7d6f0e24d8f534e04a04f79a0c846070728c04f..1d0f0d705aaeaeaa57745ff8515f19c479d0bf77 100644
--- a/src/CacheStructs.h
+++ b/src/CacheStructs.h
@@ -76,6 +76,8 @@ struct RoomInfo
         std::string version;
         //! Whether or not the room is an invite.
         bool is_invite = false;
+        //! Wheter or not the room is a space
+        bool is_space = false;
         //! Total number of members in the room.
         size_t member_count = 0;
         //! Who can access to the room.
diff --git a/src/Cache_p.h b/src/Cache_p.h
index 669f1895212150e537cc70c486d51446aa617d5b..cfcf9c9eb6358ca99a2edab4ddf2a7fbc4c589ac 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -72,6 +72,7 @@ public:
         std::optional<mtx::events::state::CanonicalAlias> getRoomAliases(const std::string &roomid);
         QHash<QString, RoomInfo> invites();
         std::optional<RoomInfo> invite(std::string_view roomid);
+        QMap<QString, std::optional<RoomInfo>> spaces();
 
         //! Calculate & return the name of the room.
         QString getRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
@@ -84,6 +85,8 @@ public:
         QString getRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
         //! Retrieve the version of the room if any.
         QString getRoomVersion(lmdb::txn &txn, lmdb::dbi &statesdb);
+        //! Retrieve if the room is a space
+        bool getRoomIsSpace(lmdb::txn &txn, lmdb::dbi &statesdb);
 
         //! Get a specific state event
         template<typename T>
@@ -98,6 +101,7 @@ public:
         std::vector<RoomMember> getMembers(const std::string &room_id,
                                            std::size_t startIndex = 0,
                                            std::size_t len        = 30);
+        size_t memberCount(const std::string &room_id);
 
         void saveState(const mtx::responses::Sync &res);
         bool isInitialized();
@@ -146,7 +150,6 @@ public:
 
         RoomInfo singleRoomInfo(const std::string &room_id);
         std::vector<std::string> roomsWithStateUpdates(const mtx::responses::Sync &res);
-        std::vector<std::string> roomsWithTagUpdates(const mtx::responses::Sync &res);
         std::map<QString, RoomInfo> getRoomInfo(const std::vector<std::string> &rooms);
 
         //! Calculates which the read status of a room.
@@ -154,9 +157,6 @@ public:
         bool calculateRoomReadStatus(const std::string &room_id);
         void calculateRoomReadStatus();
 
-        std::vector<RoomSearchResult> searchRooms(const std::string &query,
-                                                  std::uint8_t max_items = 5);
-
         void markSentNotification(const std::string &event_id);
         //! Removes an event from the sent notifications.
         void removeReadNotification(const std::string &event_id);
@@ -222,6 +222,8 @@ public:
         void deleteOldData() noexcept;
         //! Retrieve all saved room ids.
         std::vector<std::string> getRoomIds(lmdb::txn &txn);
+        std::vector<std::string> getParentRoomIds(const std::string &room_id);
+        std::vector<std::string> getChildRoomIds(const std::string &room_id);
 
         //! Mark a room that uses e2e encryption.
         void setEncryptedRoom(lmdb::txn &txn, const std::string &room_id);
@@ -327,12 +329,14 @@ private:
         QString getInviteRoomName(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
         QString getInviteRoomTopic(lmdb::txn &txn, lmdb::dbi &statesdb);
         QString getInviteRoomAvatarUrl(lmdb::txn &txn, lmdb::dbi &statesdb, lmdb::dbi &membersdb);
+        bool getInviteRoomIsSpace(lmdb::txn &txn, lmdb::dbi &db);
 
         std::optional<MemberInfo> getMember(const std::string &room_id, const std::string &user_id);
 
         std::string getLastEventId(lmdb::txn &txn, const std::string &room_id);
         DescInfo getLastMessageInfo(lmdb::txn &txn, const std::string &room_id);
         void saveTimelineMessages(lmdb::txn &txn,
+                                  lmdb::dbi &eventsDb,
                                   const std::string &room_id,
                                   const mtx::responses::Timeline &res);
 
@@ -351,11 +355,12 @@ private:
                              lmdb::dbi &statesdb,
                              lmdb::dbi &stateskeydb,
                              lmdb::dbi &membersdb,
+                             lmdb::dbi &eventsDb,
                              const std::string &room_id,
                              const std::vector<T> &events)
         {
                 for (const auto &e : events)
-                        saveStateEvent(txn, statesdb, stateskeydb, membersdb, room_id, e);
+                        saveStateEvent(txn, statesdb, stateskeydb, membersdb, eventsDb, room_id, e);
         }
 
         template<class T>
@@ -363,6 +368,7 @@ private:
                             lmdb::dbi &statesdb,
                             lmdb::dbi &stateskeydb,
                             lmdb::dbi &membersdb,
+                            lmdb::dbi &eventsDb,
                             const std::string &room_id,
                             const T &event)
         {
@@ -399,8 +405,10 @@ private:
                 }
 
                 std::visit(
-                  [&txn, &statesdb, &stateskeydb](auto e) {
-                          if constexpr (isStateEvent(e))
+                  [&txn, &statesdb, &stateskeydb, &eventsDb](auto e) {
+                          if constexpr (isStateEvent(e)) {
+                                  eventsDb.put(txn, e.event_id, json(e).dump());
+
                                   if (e.type != EventType::Unsupported) {
                                           if (e.state_key.empty())
                                                   statesdb.put(
@@ -415,6 +423,7 @@ private:
                                                                  })
                                                       .dump());
                                   }
+                          }
                   },
                   event);
         }
@@ -430,20 +439,22 @@ private:
 
                 if (room_id.empty())
                         return std::nullopt;
+                const auto typeStr = to_string(type);
 
                 std::string_view value;
                 if (state_key.empty()) {
                         auto db = getStatesDb(txn, room_id);
-                        if (!db.get(txn, to_string(type), value)) {
+                        if (!db.get(txn, typeStr, value)) {
                                 return std::nullopt;
                         }
                 } else {
-                        auto db               = getStatesKeyDb(txn, room_id);
-                        std::string d         = json::object({{"key", state_key}}).dump();
-                        std::string_view data = d;
+                        auto db                   = getStatesKeyDb(txn, room_id);
+                        std::string d             = json::object({{"key", state_key}}).dump();
+                        std::string_view data     = d;
+                        std::string_view typeStrV = typeStr;
 
                         auto cursor = lmdb::cursor::open(txn, db);
-                        if (!cursor.get(state_key, data, MDB_GET_BOTH))
+                        if (!cursor.get(typeStrV, data, MDB_GET_BOTH))
                                 return std::nullopt;
 
                         try {
@@ -463,6 +474,47 @@ private:
                 }
         }
 
+        template<typename T>
+        std::vector<mtx::events::StateEvent<T>> getStateEventsWithType(lmdb::txn &txn,
+                                                                       const std::string &room_id)
+
+        {
+                constexpr auto type = mtx::events::state_content_to_type<T>;
+                static_assert(type != mtx::events::EventType::Unsupported,
+                              "Not a supported type in state events.");
+
+                if (room_id.empty())
+                        return {};
+
+                std::vector<mtx::events::StateEvent<T>> events;
+
+                {
+                        auto db                   = getStatesKeyDb(txn, room_id);
+                        auto eventsDb             = getEventsDb(txn, room_id);
+                        const auto typeStr        = to_string(type);
+                        std::string_view typeStrV = typeStr;
+                        std::string_view data;
+                        std::string_view value;
+
+                        auto cursor = lmdb::cursor::open(txn, db);
+                        bool first  = true;
+                        if (cursor.get(typeStrV, data, MDB_SET)) {
+                                while (cursor.get(
+                                  typeStrV, data, first ? MDB_FIRST_DUP : MDB_NEXT_DUP)) {
+                                        first = false;
+
+                                        if (eventsDb.get(txn,
+                                                         json::parse(data)["id"].get<std::string>(),
+                                                         value))
+                                                events.push_back(
+                                                  json::parse(value)
+                                                    .get<mtx::events::StateEvent<T>>());
+                                }
+                        }
+                }
+
+                return events;
+        }
         void saveInvites(lmdb::txn &txn,
                          const std::map<std::string, mtx::responses::InvitedRoom> &rooms);
 
@@ -482,6 +534,10 @@ private:
                 }
         }
 
+        void updateSpaces(lmdb::txn &txn,
+                          const std::set<std::string> &spaces_with_updates,
+                          std::set<std::string> rooms_with_updates);
+
         lmdb::dbi getPendingReceiptsDb(lmdb::txn &txn)
         {
                 return lmdb::dbi::open(txn, "pending_receipts", MDB_CREATE);
@@ -548,8 +604,8 @@ private:
 
         lmdb::dbi getStatesKeyDb(lmdb::txn &txn, const std::string &room_id)
         {
-                auto db =
-                  lmdb::dbi::open(txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE);
+                auto db = lmdb::dbi::open(
+                  txn, std::string(room_id + "/state_by_key").c_str(), MDB_CREATE | MDB_DUPSORT);
                 lmdb::dbi_set_dupsort(txn, db, compare_state_key);
                 return db;
         }
@@ -611,6 +667,7 @@ private:
         lmdb::env env_;
         lmdb::dbi syncStateDb_;
         lmdb::dbi roomsDb_;
+        lmdb::dbi spacesChildrenDb_, spacesParentsDb_;
         lmdb::dbi invitesDb_;
         lmdb::dbi readReceiptsDb_;
         lmdb::dbi notificationsDb_;
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 9b9065552687e6547364b9096c1dd750bfc844ed..6c0d8728f726d8d0ec421409a9c2613aa51918f1 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -613,6 +613,7 @@ UserSettings::save()
         settings.setValue("mobile_mode", mobileMode_);
         settings.setValue("font_size", baseFontSize_);
         settings.setValue("typing_notifications", typingNotifications_);
+        settings.setValue("sort_by_unread", sortByImportance_);
         settings.setValue("minor_events", sortByImportance_);
         settings.setValue("read_receipts", readReceipts_);
         settings.setValue("group_view", groupView_);
diff --git a/src/timeline/CommunitiesModel.cpp b/src/timeline/CommunitiesModel.cpp
index 6c2367842dbffc01dd08e3d378fa5cb25d537a24..97bfa76d58dfdfd53af7c8b45b22d1163b2c5834 100644
--- a/src/timeline/CommunitiesModel.cpp
+++ b/src/timeline/CommunitiesModel.cpp
@@ -44,8 +44,23 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                 case CommunitiesModel::Roles::Id:
                         return "";
                 }
-        } else if (index.row() - 1 < tags_.size()) {
-                auto tag = tags_.at(index.row() - 1);
+        } else if (index.row() - 1 < spaceOrder_.size()) {
+                auto id = spaceOrder_.at(index.row() - 1);
+                switch (role) {
+                case CommunitiesModel::Roles::AvatarUrl:
+                        return QString::fromStdString(spaces_.at(id).avatar_url);
+                case CommunitiesModel::Roles::DisplayName:
+                case CommunitiesModel::Roles::Tooltip:
+                        return QString::fromStdString(spaces_.at(id).name);
+                case CommunitiesModel::Roles::ChildrenHidden:
+                        return true;
+                case CommunitiesModel::Roles::Hidden:
+                        return hiddentTagIds_.contains("space:" + id);
+                case CommunitiesModel::Roles::Id:
+                        return "space:" + id;
+                }
+        } else if (index.row() - 1 < tags_.size() + spaceOrder_.size()) {
+                auto tag = tags_.at(index.row() - 1 - spaceOrder_.size());
                 if (tag == "m.favourite") {
                         switch (role) {
                         case CommunitiesModel::Roles::AvatarUrl:
@@ -78,7 +93,6 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
                         case CommunitiesModel::Roles::AvatarUrl:
                                 return QString(":/icons/icons/ui/tag.png");
                         case CommunitiesModel::Roles::DisplayName:
-                                return tag.mid(2);
                         case CommunitiesModel::Roles::Tooltip:
                                 return tag.mid(2);
                         }
@@ -99,17 +113,27 @@ CommunitiesModel::data(const QModelIndex &index, int role) const
 void
 CommunitiesModel::initializeSidebar()
 {
+        beginResetModel();
+        tags_.clear();
+        spaceOrder_.clear();
+        spaces_.clear();
+
         std::set<std::string> ts;
-        for (const auto &e : cache::roomInfo()) {
-                for (const auto &t : e.tags) {
-                        if (t.find("u.") == 0 || t.find("m." == 0)) {
-                                ts.insert(t);
+        std::vector<RoomInfo> tempSpaces;
+        auto infos = cache::roomInfo();
+        for (auto it = infos.begin(); it != infos.end(); it++) {
+                if (it.value().is_space) {
+                        spaceOrder_.push_back(it.key());
+                        spaces_[it.key()] = it.value();
+                } else {
+                        for (const auto &t : it.value().tags) {
+                                if (t.find("u.") == 0 || t.find("m." == 0)) {
+                                        ts.insert(t);
+                                }
                         }
                 }
         }
 
-        beginResetModel();
-        tags_.clear();
         for (const auto &t : ts)
                 tags_.push_back(QString::fromStdString(t));
 
@@ -143,6 +167,25 @@ CommunitiesModel::sync(const mtx::responses::Rooms &rooms)
                               mtx::events::AccountDataEvent<mtx::events::account_data::Tags>>(e)) {
                                 tagsUpdated = true;
                         }
+                for (const auto &e : room.state.events)
+                        if (std::holds_alternative<
+                              mtx::events::StateEvent<mtx::events::state::space::Child>>(e) ||
+                            std::holds_alternative<
+                              mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) {
+                                tagsUpdated = true;
+                        }
+                for (const auto &e : room.timeline.events)
+                        if (std::holds_alternative<
+                              mtx::events::StateEvent<mtx::events::state::space::Child>>(e) ||
+                            std::holds_alternative<
+                              mtx::events::StateEvent<mtx::events::state::space::Parent>>(e)) {
+                                tagsUpdated = true;
+                        }
+        }
+        for (const auto &[roomid, room] : rooms.leave) {
+                (void)room;
+                if (spaceOrder_.contains(QString::fromStdString(roomid)))
+                        tagsUpdated = true;
         }
 
         if (tagsUpdated)
@@ -161,6 +204,15 @@ CommunitiesModel::setCurrentTagId(QString tagId)
                                 return;
                         }
                 }
+        } else if (tagId.startsWith("space:")) {
+                auto tag = tagId.mid(6);
+                for (const auto &t : spaceOrder_) {
+                        if (t == tag) {
+                                this->currentTagId_ = tagId;
+                                emit currentTagIdChanged(currentTagId_);
+                                return;
+                        }
+                }
         }
 
         this->currentTagId_ = "";
@@ -181,7 +233,13 @@ CommunitiesModel::toggleTagId(QString tagId)
         if (tagId.startsWith("tag:")) {
                 auto idx = tags_.indexOf(tagId.mid(4));
                 if (idx != -1)
-                        emit dataChanged(index(idx), index(idx), {Hidden});
+                        emit dataChanged(index(idx + 1 + spaceOrder_.size()),
+                                         index(idx + 1 + spaceOrder_.size()),
+                                         {Hidden});
+        } else if (tagId.startsWith("space:")) {
+                auto idx = spaceOrder_.indexOf(tagId.mid(6));
+                if (idx != -1)
+                        emit dataChanged(index(idx + 1), index(idx + 1), {Hidden});
         }
 
         emit hiddenTagsChanged();
diff --git a/src/timeline/CommunitiesModel.h b/src/timeline/CommunitiesModel.h
index 66d6b21b6148cf568157cf5b3c7ad378076f4ab5..8c40ec5b05ba1d1d4cde235822f49ed7c797e8ac 100644
--- a/src/timeline/CommunitiesModel.h
+++ b/src/timeline/CommunitiesModel.h
@@ -11,6 +11,8 @@
 
 #include <mtx/responses/sync.hpp>
 
+#include "CacheStructs.h"
+
 class CommunitiesModel : public QAbstractListModel
 {
         Q_OBJECT
@@ -71,4 +73,6 @@ private:
         QStringList tags_;
         QString currentTagId_;
         QStringList hiddentTagIds_;
+        QStringList spaceOrder_;
+        std::map<QString, RoomInfo> spaces_;
 };
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 04f7ef7685b2f3daedc60c229d3320a37488c626..9a91ff7940d7c0833f25fbc7a7b73252c80776c5 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -675,6 +675,9 @@ EventStore::decryptEvent(const IdIndex &idx,
                                               index.room_id,
                                               index.session_id,
                                               e.sender);
+                        // we may not want to request keys during initial sync and such
+                        if (suppressKeyRequests)
+                                break;
                         // TODO: Check if this actually works and look in key backup
                         auto copy    = e;
                         copy.room_id = room_id_;
@@ -816,6 +819,18 @@ EventStore::decryptEvent(const IdIndex &idx,
         return asCacheEntry(std::move(decryptionResult.event.value()));
 }
 
+void
+EventStore::enableKeyRequests(bool suppressKeyRequests_)
+{
+        if (!suppressKeyRequests_) {
+                for (const auto &key : decryptedEvents_.keys())
+                        if (key.room == this->room_id_)
+                                decryptedEvents_.remove(key);
+                suppressKeyRequests = false;
+        } else
+                suppressKeyRequests = true;
+}
+
 mtx::events::collections::TimelineEvents *
 EventStore::get(std::string id, std::string_view related_to, bool decrypt, bool resolve_edits)
 {
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index d9bb86cb108fe4dd27812a336019fb8e4a588864..7c40410262256546ba254aac7633a5f196a4a0f6 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -115,6 +115,7 @@ public slots:
         void addPending(mtx::events::collections::TimelineEvents event);
         void receivedSessionKey(const std::string &session_id);
         void clearTimeline();
+        void enableKeyRequests(bool suppressKeyRequests_);
 
 private:
         std::vector<mtx::events::collections::TimelineEvents> edits(const std::string &event_id);
@@ -142,4 +143,5 @@ private:
         std::string current_txn;
         int current_txn_error_count = 0;
         bool noMoreMessages         = false;
+        bool suppressKeyRequests    = true;
 };
diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp
index 0f980c6cb5fb558509d10744f1d9abf581af1e36..7f59b11296d95581e594f3f900cb07582942c4b0 100644
--- a/src/timeline/RoomlistModel.cpp
+++ b/src/timeline/RoomlistModel.cpp
@@ -51,6 +51,7 @@ RoomlistModel::roleNames() const
           {IsInvite, "isInvite"},
           {IsSpace, "isSpace"},
           {Tags, "tags"},
+          {ParentSpaces, "parentSpaces"},
         };
 }
 
@@ -84,8 +85,9 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                         case Roles::NotificationCount:
                                 return room->notificationCount();
                         case Roles::IsInvite:
-                        case Roles::IsSpace:
                                 return false;
+                        case Roles::IsSpace:
+                                return room->isSpace();
                         case Roles::Tags: {
                                 auto info = cache::singleRoomInfo(roomid.toStdString());
                                 QStringList list;
@@ -93,6 +95,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                                         list.push_back(QString::fromStdString(t));
                                 return list;
                         }
+                        case Roles::ParentSpaces: {
+                                auto parents =
+                                  cache::client()->getParentRoomIds(roomid.toStdString());
+                                QStringList list;
+                                for (const auto &t : parents)
+                                        list.push_back(QString::fromStdString(t));
+                                return list;
+                        }
                         default:
                                 return {};
                         }
@@ -122,6 +132,14 @@ RoomlistModel::data(const QModelIndex &index, int role) const
                                 return false;
                         case Roles::Tags:
                                 return QStringList();
+                        case Roles::ParentSpaces: {
+                                auto parents =
+                                  cache::client()->getParentRoomIds(roomid.toStdString());
+                                QStringList list;
+                                for (const auto &t : parents)
+                                        list.push_back(QString::fromStdString(t));
+                                return list;
+                        }
                         default:
                                 return {};
                         }
@@ -412,7 +430,9 @@ enum NotificationImportance : short
         AllEventsRead      = 0,
         NewMessage         = 1,
         NewMentions        = 2,
-        Invite             = 3
+        Invite             = 3,
+        SubSpace           = 4,
+        CurrentSpace       = 5,
 };
 }
 
@@ -422,7 +442,13 @@ FilteredRoomlistModel::calculateImportance(const QModelIndex &idx) const
         // Returns the degree of importance of the unread messages in the room.
         // If sorting by importance is disabled in settings, this only ever
         // returns ImportanceDisabled or Invite
-        if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
+        if (sourceModel()->data(idx, RoomlistModel::IsSpace).toBool()) {
+                if (filterType == FilterBy::Space &&
+                    filterStr == sourceModel()->data(idx, RoomlistModel::RoomId).toString())
+                        return CurrentSpace;
+                else
+                        return SubSpace;
+        } else if (sourceModel()->data(idx, RoomlistModel::IsInvite).toBool()) {
                 return Invite;
         } else if (!this->sortByImportance) {
                 return ImportanceDisabled;
@@ -505,6 +531,12 @@ bool
 FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) const
 {
         if (filterType == FilterBy::Nothing) {
+                if (sourceModel()
+                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+                      .toBool()) {
+                        return false;
+                }
+
                 if (!hiddenTags.empty()) {
                         auto tags =
                           sourceModel()
@@ -516,19 +548,86 @@ FilteredRoomlistModel::filterAcceptsRow(int sourceRow, const QModelIndex &) cons
                                         return false;
                 }
 
+                if (!hiddenSpaces.empty()) {
+                        auto parents =
+                          sourceModel()
+                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+                            .toStringList();
+                        for (const auto &t : parents)
+                                if (hiddenSpaces.contains(t))
+                                        return false;
+                }
+
                 return true;
         } else if (filterType == FilterBy::Tag) {
+                if (sourceModel()
+                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+                      .toBool()) {
+                        return false;
+                }
+
                 auto tags = sourceModel()
                               ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
                               .toStringList();
 
                 if (!tags.contains(filterStr))
                         return false;
-                else if (!hiddenTags.empty()) {
+
+                if (!hiddenTags.empty()) {
                         for (const auto &t : tags)
                                 if (t != filterStr && hiddenTags.contains(t))
                                         return false;
                 }
+
+                if (!hiddenSpaces.empty()) {
+                        auto parents =
+                          sourceModel()
+                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+                            .toStringList();
+                        for (const auto &t : parents)
+                                if (hiddenSpaces.contains(t))
+                                        return false;
+                }
+
+                return true;
+        } else if (filterType == FilterBy::Space) {
+                if (filterStr == sourceModel()
+                                   ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::RoomId)
+                                   .toString())
+                        return true;
+
+                auto parents =
+                  sourceModel()
+                    ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::ParentSpaces)
+                    .toStringList();
+
+                if (!parents.contains(filterStr))
+                        return false;
+
+                if (!hiddenTags.empty()) {
+                        auto tags =
+                          sourceModel()
+                            ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::Tags)
+                            .toStringList();
+
+                        for (const auto &t : tags)
+                                if (hiddenTags.contains(t))
+                                        return false;
+                }
+
+                if (!hiddenSpaces.empty()) {
+                        for (const auto &t : parents)
+                                if (hiddenSpaces.contains(t))
+                                        return false;
+                }
+
+                if (sourceModel()
+                      ->data(sourceModel()->index(sourceRow, 0), RoomlistModel::IsSpace)
+                      .toBool() &&
+                    !parents.contains(filterStr)) {
+                        return false;
+                }
+
                 return true;
         } else {
                 return true;
@@ -582,7 +681,7 @@ FilteredRoomlistModel::previousRoom()
         if (r) {
                 int idx = roomidToIndex(r->roomId());
                 idx--;
-                if (idx > 0) {
+                if (idx >= 0) {
                         setCurrentRoom(
                           data(index(idx, 0), RoomlistModel::Roles::RoomId).toString());
                 }
diff --git a/src/timeline/RoomlistModel.h b/src/timeline/RoomlistModel.h
index b024488611ace19ecd334209fe9807d2633326c9..d6cbb462b570eda16c360a70658be8f2493ba2ca 100644
--- a/src/timeline/RoomlistModel.h
+++ b/src/timeline/RoomlistModel.h
@@ -38,6 +38,7 @@ public:
                 IsInvite,
                 IsSpace,
                 Tags,
+                ParentSpaces,
         };
 
         RoomlistModel(TimelineViewManager *parent = nullptr);
@@ -134,6 +135,9 @@ public slots:
                 if (tagId.startsWith("tag:")) {
                         filterType = FilterBy::Tag;
                         filterStr  = tagId.mid(4);
+                } else if (tagId.startsWith("space:")) {
+                        filterType = FilterBy::Space;
+                        filterStr  = tagId.mid(6);
                 } else {
                         filterType = FilterBy::Nothing;
                         filterStr.clear();
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 99547b15b496acb69d6b333e7f1e2603472dd58b..067f219aaa86fefd9ce76344bdf628a2166c5ef9 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -320,6 +320,10 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
 {
         lastMessage_.timestamp = 0;
 
+        if (auto create =
+              cache::client()->getStateEvent<mtx::events::state::Create>(room_id.toStdString()))
+                this->isSpace_ = create->content.type == mtx::events::state::room_type::space;
+
         connect(
           this,
           &TimelineModel::redactionFailed,
@@ -375,6 +379,7 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
         connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
                 this->updateFlowEventId(event_id);
         });
+
         // When a message is sent, check if the current edit/reply relates to that message,
         // and update the event_id so that it points to the sent message and not the pending one.
         connect(&events,
@@ -391,6 +396,11 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
                         }
                 });
 
+        connect(manager_,
+                &TimelineViewManager::initialSyncChanged,
+                &events,
+                &EventStore::enableKeyRequests);
+
         showEventTimer.callOnTimeout(this, &TimelineModel::scrollTimerEvent);
 }
 
@@ -770,6 +780,7 @@ TimelineModel::syncState(const mtx::responses::State &s)
                 } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
                         emit roomAvatarUrlChanged();
                         emit roomNameChanged();
+                        emit roomMemberCountChanged();
                 }
         }
 }
@@ -826,6 +837,7 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
                 } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
                         emit roomAvatarUrlChanged();
                         emit roomNameChanged();
+                        emit roomMemberCountChanged();
                 }
         }
         updateLastMessage();
@@ -1931,3 +1943,9 @@ TimelineModel::roomTopic() const
                 return utils::replaceEmoji(utils::linkifyMessage(
                   QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
 }
+
+int
+TimelineModel::roomMemberCount() const
+{
+        return (int)cache::client()->memberCount(room_id_.toStdString());
+}
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 3ebbe120db3de93a0ddb68df065acc27a3dd04a1..3392d4746f4e7b98b4d5e34e9e00acfb858a91cb 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -161,6 +161,8 @@ class TimelineModel : public QAbstractListModel
         Q_PROPERTY(QString roomName READ roomName NOTIFY roomNameChanged)
         Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
         Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
+        Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
+        Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
         Q_PROPERTY(InputBar *input READ input CONSTANT)
         Q_PROPERTY(Permissions *permissions READ permissions NOTIFY permissionsChanged)
 
@@ -262,6 +264,8 @@ public:
         RelatedInfo relatedInfo(QString id);
 
         DescInfo lastMessage() const { return lastMessage_; }
+        bool isSpace() const { return isSpace_; }
+        int roomMemberCount() const;
 
 public slots:
         void setCurrentIndex(int index);
@@ -348,6 +352,7 @@ signals:
         void roomNameChanged();
         void roomTopicChanged();
         void roomAvatarUrlChanged();
+        void roomMemberCountChanged();
         void permissionsChanged();
         void forwardToRoom(mtx::events::collections::TimelineEvents *e, QString roomId);
 
@@ -366,9 +371,6 @@ private:
 
         QString room_id_;
 
-        bool decryptDescription     = true;
-        bool m_paginationInProgress = false;
-
         QString currentId, currentReadId;
         QString reply_, edit_;
         QString textBeforeEdit, replyBeforeEdit;
@@ -388,6 +390,10 @@ private:
         friend struct SendMessageVisitor;
 
         int notification_count = 0, highlight_count = 0;
+
+        bool decryptDescription     = true;
+        bool m_paginationInProgress = false;
+        bool isSpace_               = false;
 };
 
 template<class T>
diff --git a/src/ui/Theme.cpp b/src/ui/Theme.cpp
index 261193936ae1b04350ef1e050d0bdb436bc9cbd2..732a044385ee4f9629c701dfdb10793019fa2308 100644
--- a/src/ui/Theme.cpp
+++ b/src/ui/Theme.cpp
@@ -47,7 +47,7 @@ Theme::paletteFromTheme(std::string_view theme)
                 darkActive.setColor(QPalette::ToolTipBase, darkActive.base().color());
                 darkActive.setColor(QPalette::ToolTipText, darkActive.text().color());
                 darkActive.setColor(QPalette::Link, QColor("#38a3d8"));
-                darkActive.setColor(QPalette::ButtonText, "#727274");
+                darkActive.setColor(QPalette::ButtonText, "#828284");
                 return darkActive;
         } else {
                 return original;
diff --git a/third_party/SingleApplication-3.3.0/.github/FUNDING.yml b/third_party/SingleApplication-3.3.0/.github/FUNDING.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3ca4d97aa50ff5f95126eb11d2ca3d24215e1a37
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: itay-grudev
diff --git a/third_party/SingleApplication-3.3.0/.github/workflows/build-cmake.yml b/third_party/SingleApplication-3.3.0/.github/workflows/build-cmake.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6344b504842201eae4d28f253613b6000b387e43
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/.github/workflows/build-cmake.yml
@@ -0,0 +1,56 @@
+name: "CI: Build Test"
+
+on: [push, pull_request]
+
+jobs:
+  build:
+
+    strategy:
+      matrix:
+        qt_version: [5.12.6, 5.13.2, 5.14.0, 5.15.0, 6.0.0]
+        platform: [ubuntu-20.04, windows-latest, macos-latest]
+        include:
+          - qt_version: 6.0.0
+            additional_arguments: -D QT_DEFAULT_MAJOR_VERSION=6
+          - platform: ubuntu-20.04
+            CXXFLAGS: -Wall -Wextra -pedantic -Werror
+          - platform: macos-latest
+            CXXFLAGS: -Wall -Wextra -pedantic -Werror
+          - platform: windows-latest
+            CXXFLAGS: /W4 /WX
+
+    runs-on: ${{ matrix.platform }}
+    env:
+      CXXFLAGS: ${{ matrix.CXXFLAGS }}
+
+    steps:
+    - uses: actions/checkout@v2.3.4
+
+    - name: Install Qt
+      uses: jurplel/install-qt-action@v2.11.1
+      with:
+        version: ${{ matrix.qt_version }}
+
+    - name: cmake
+      run: cmake . ${{ matrix.additional_arguments }}
+
+    - name: cmake build
+      run: cmake --build .
+
+    - name: Build example - basic (cmake)
+      working-directory: examples/basic/
+      run: |
+        cmake . ${{ matrix.additional_arguments }}
+        cmake --build .
+
+    - name: Build example - calculator (cmake)
+      working-directory: examples/calculator/
+      run: |
+        cmake . ${{ matrix.additional_arguments }}
+        cmake --build .
+
+    - name: Build example - sending_arguments (cmake)
+      working-directory: examples/sending_arguments/
+      run: |
+        cmake . ${{ matrix.additional_arguments }}
+        cmake --build .
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/.gitignore b/third_party/SingleApplication-3.3.0/.gitignore
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/.gitignore
rename to third_party/SingleApplication-3.3.0/.gitignore
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md b/third_party/SingleApplication-3.3.0/CHANGELOG.md
similarity index 98%
rename from third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md
rename to third_party/SingleApplication-3.3.0/CHANGELOG.md
index e2ba290e2a1cdaa30946224ceb7d6ed40ad5a780..51669b90c010c98d968edfac9bfd4ec526b3775d 100644
--- a/third_party/SingleApplication-3.2.0-dc8042b/CHANGELOG.md
+++ b/third_party/SingleApplication-3.3.0/CHANGELOG.md
@@ -3,6 +3,12 @@ Changelog
 
 If by accident I have forgotten to credit someone in the CHANGELOG, email me and I will fix it.
 
+
+__3.3.0__
+---------
+
+* Fixed message fragmentation issue causing crashes and incorrectly and inconsistently received messages. - _Nils Jeisecke_
+
 __3.2.0__
 ---------
 
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt b/third_party/SingleApplication-3.3.0/CMakeLists.txt
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/CMakeLists.txt
rename to third_party/SingleApplication-3.3.0/CMakeLists.txt
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/LICENSE b/third_party/SingleApplication-3.3.0/LICENSE
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/LICENSE
rename to third_party/SingleApplication-3.3.0/LICENSE
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/README.md b/third_party/SingleApplication-3.3.0/README.md
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/README.md
rename to third_party/SingleApplication-3.3.0/README.md
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/SingleApplication b/third_party/SingleApplication-3.3.0/SingleApplication
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/SingleApplication
rename to third_party/SingleApplication-3.3.0/SingleApplication
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/Windows.md b/third_party/SingleApplication-3.3.0/Windows.md
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/Windows.md
rename to third_party/SingleApplication-3.3.0/Windows.md
diff --git a/third_party/SingleApplication-3.3.0/examples/basic/CMakeLists.txt b/third_party/SingleApplication-3.3.0/examples/basic/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c1429230c07e2ed57be763d56b97e0732b029304
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/basic/CMakeLists.txt
@@ -0,0 +1,12 @@
+cmake_minimum_required(VERSION 3.7.0)
+
+project(basic LANGUAGES CXX)
+
+# SingleApplication base class
+set(QAPPLICATION_CLASS QCoreApplication)
+add_subdirectory(../.. SingleApplication)
+
+add_executable(basic main.cpp)
+
+target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication)
+
diff --git a/third_party/SingleApplication-3.3.0/examples/basic/basic.pro b/third_party/SingleApplication-3.3.0/examples/basic/basic.pro
new file mode 100755
index 0000000000000000000000000000000000000000..b7af16cf66d2610f41e3b603910b37deb704a3b7
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/basic/basic.pro
@@ -0,0 +1,5 @@
+# Single Application implementation
+include(../../singleapplication.pri)
+DEFINES += QAPPLICATION_CLASS=QCoreApplication
+
+SOURCES += main.cpp
diff --git a/third_party/SingleApplication-3.3.0/examples/basic/main.cpp b/third_party/SingleApplication-3.3.0/examples/basic/main.cpp
new file mode 100755
index 0000000000000000000000000000000000000000..b2092c6db9ec52052471a579bd4ff6cc49976a5f
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/basic/main.cpp
@@ -0,0 +1,10 @@
+#include <singleapplication.h>
+
+int main(int argc, char *argv[])
+{
+    SingleApplication app( argc, argv );
+
+    qWarning() << "Started a new instance";
+
+    return app.exec();
+}
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/CMakeLists.txt b/third_party/SingleApplication-3.3.0/examples/calculator/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..82305f0470e700d9494eb04299e41d65f24a4f4b
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/CMakeLists.txt
@@ -0,0 +1,21 @@
+cmake_minimum_required(VERSION 3.7.0)
+
+project(calculator LANGUAGES CXX)
+
+set(CMAKE_AUTOMOC ON)
+
+# SingleApplication base class
+set(QAPPLICATION_CLASS QApplication)
+add_subdirectory(../.. SingleApplication)
+
+find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED)
+
+add_executable(${PROJECT_NAME}
+    button.h
+    calculator.h
+    button.cpp
+    calculator.cpp
+    main.cpp
+)
+
+target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication)
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/button.cpp b/third_party/SingleApplication-3.3.0/examples/calculator/button.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d6cca0a07d1e3189b70151a276640ad677080d4d
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/button.cpp
@@ -0,0 +1,73 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of The Qt Company Ltd nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtWidgets>
+
+#include "button.h"
+
+//! [0]
+Button::Button(const QString &text, QWidget *parent)
+    : QToolButton(parent)
+{
+    setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
+    setText(text);
+}
+//! [0]
+
+//! [1]
+QSize Button::sizeHint() const
+//! [1] //! [2]
+{
+    QSize size = QToolButton::sizeHint();
+    size.rheight() += 20;
+    size.rwidth() = qMax(size.width(), size.height());
+    return size;
+}
+//! [2]
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/button.h b/third_party/SingleApplication-3.3.0/examples/calculator/button.h
new file mode 100644
index 0000000000000000000000000000000000000000..2c014c7b91afc1af766999778a4cc069fcf1a245
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/button.h
@@ -0,0 +1,68 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of The Qt Company Ltd nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef BUTTON_H
+#define BUTTON_H
+
+#include <QToolButton>
+
+//! [0]
+class Button : public QToolButton
+{
+    Q_OBJECT
+
+public:
+    explicit Button(const QString &text, QWidget *parent = 0);
+
+    QSize sizeHint() const Q_DECL_OVERRIDE;
+};
+//! [0]
+
+#endif
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/calculator.cpp b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..3d34c2a743d96a438ec6c1b6b4efe63119f7b13b
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.cpp
@@ -0,0 +1,406 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of The Qt Company Ltd nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtWidgets>
+
+#include <cmath>
+
+#include "button.h"
+#include "calculator.h"
+
+//! [0]
+Calculator::Calculator(QWidget *parent)
+    : QWidget(parent)
+{
+    sumInMemory = 0.0;
+    sumSoFar = 0.0;
+    factorSoFar = 0.0;
+    waitingForOperand = true;
+//! [0]
+
+//! [1]
+    display = new QLineEdit("0");
+//! [1] //! [2]
+    display->setReadOnly(true);
+    display->setAlignment(Qt::AlignRight);
+    display->setMaxLength(15);
+
+    QFont font = display->font();
+    font.setPointSize(font.pointSize() + 8);
+    display->setFont(font);
+//! [2]
+
+//! [4]
+    for (int i = 0; i < NumDigitButtons; ++i) {
+        digitButtons[i] = createButton(QString::number(i), SLOT(digitClicked()));
+    }
+
+    Button *pointButton = createButton(".", SLOT(pointClicked()));
+    Button *changeSignButton = createButton("\302\261", SLOT(changeSignClicked()));
+
+    Button *backspaceButton = createButton("Backspace", SLOT(backspaceClicked()));
+    Button *clearButton = createButton("Clear", SLOT(clear()));
+    Button *clearAllButton = createButton("Clear All", SLOT(clearAll()));
+
+    Button *clearMemoryButton = createButton("MC", SLOT(clearMemory()));
+    Button *readMemoryButton = createButton("MR", SLOT(readMemory()));
+    Button *setMemoryButton = createButton("MS", SLOT(setMemory()));
+    Button *addToMemoryButton = createButton("M+", SLOT(addToMemory()));
+
+    Button *divisionButton = createButton("\303\267", SLOT(multiplicativeOperatorClicked()));
+    Button *timesButton = createButton("\303\227", SLOT(multiplicativeOperatorClicked()));
+    Button *minusButton = createButton("-", SLOT(additiveOperatorClicked()));
+    Button *plusButton = createButton("+", SLOT(additiveOperatorClicked()));
+
+    Button *squareRootButton = createButton("Sqrt", SLOT(unaryOperatorClicked()));
+    Button *powerButton = createButton("x\302\262", SLOT(unaryOperatorClicked()));
+    Button *reciprocalButton = createButton("1/x", SLOT(unaryOperatorClicked()));
+    Button *equalButton = createButton("=", SLOT(equalClicked()));
+//! [4]
+
+//! [5]
+    QGridLayout *mainLayout = new QGridLayout;
+//! [5] //! [6]
+    mainLayout->setSizeConstraint(QLayout::SetFixedSize);
+    mainLayout->addWidget(display, 0, 0, 1, 6);
+    mainLayout->addWidget(backspaceButton, 1, 0, 1, 2);
+    mainLayout->addWidget(clearButton, 1, 2, 1, 2);
+    mainLayout->addWidget(clearAllButton, 1, 4, 1, 2);
+
+    mainLayout->addWidget(clearMemoryButton, 2, 0);
+    mainLayout->addWidget(readMemoryButton, 3, 0);
+    mainLayout->addWidget(setMemoryButton, 4, 0);
+    mainLayout->addWidget(addToMemoryButton, 5, 0);
+
+    for (int i = 1; i < NumDigitButtons; ++i) {
+        int row = ((9 - i) / 3) + 2;
+        int column = ((i - 1) % 3) + 1;
+        mainLayout->addWidget(digitButtons[i], row, column);
+    }
+
+    mainLayout->addWidget(digitButtons[0], 5, 1);
+    mainLayout->addWidget(pointButton, 5, 2);
+    mainLayout->addWidget(changeSignButton, 5, 3);
+
+    mainLayout->addWidget(divisionButton, 2, 4);
+    mainLayout->addWidget(timesButton, 3, 4);
+    mainLayout->addWidget(minusButton, 4, 4);
+    mainLayout->addWidget(plusButton, 5, 4);
+
+    mainLayout->addWidget(squareRootButton, 2, 5);
+    mainLayout->addWidget(powerButton, 3, 5);
+    mainLayout->addWidget(reciprocalButton, 4, 5);
+    mainLayout->addWidget(equalButton, 5, 5);
+    setLayout(mainLayout);
+
+    setWindowTitle("Calculator");
+}
+//! [6]
+
+//! [7]
+void Calculator::digitClicked()
+{
+    Button *clickedButton = qobject_cast<Button *>(sender());
+    int digitValue = clickedButton->text().toInt();
+    if (display->text() == "0" && digitValue == 0.0)
+        return;
+
+    if (waitingForOperand) {
+        display->clear();
+        waitingForOperand = false;
+    }
+    display->setText(display->text() + QString::number(digitValue));
+}
+//! [7]
+
+//! [8]
+void Calculator::unaryOperatorClicked()
+//! [8] //! [9]
+{
+    Button *clickedButton = qobject_cast<Button *>(sender());
+    QString clickedOperator = clickedButton->text();
+    double operand = display->text().toDouble();
+    double result = 0.0;
+
+    if (clickedOperator == "Sqrt") {
+        if (operand < 0.0) {
+            abortOperation();
+            return;
+        }
+        result = std::sqrt(operand);
+    } else if (clickedOperator == "x\302\262") {
+        result = std::pow(operand, 2.0);
+    } else if (clickedOperator == "1/x") {
+        if (operand == 0.0) {
+            abortOperation();
+            return;
+        }
+        result = 1.0 / operand;
+    }
+    display->setText(QString::number(result));
+    waitingForOperand = true;
+}
+//! [9]
+
+//! [10]
+void Calculator::additiveOperatorClicked()
+//! [10] //! [11]
+{
+    Button *clickedButton = qobject_cast<Button *>(sender());
+    QString clickedOperator = clickedButton->text();
+    double operand = display->text().toDouble();
+
+//! [11] //! [12]
+    if (!pendingMultiplicativeOperator.isEmpty()) {
+//! [12] //! [13]
+        if (!calculate(operand, pendingMultiplicativeOperator)) {
+            abortOperation();
+            return;
+        }
+        display->setText(QString::number(factorSoFar));
+        operand = factorSoFar;
+        factorSoFar = 0.0;
+        pendingMultiplicativeOperator.clear();
+    }
+
+//! [13] //! [14]
+    if (!pendingAdditiveOperator.isEmpty()) {
+//! [14] //! [15]
+        if (!calculate(operand, pendingAdditiveOperator)) {
+            abortOperation();
+            return;
+        }
+        display->setText(QString::number(sumSoFar));
+    } else {
+        sumSoFar = operand;
+    }
+
+//! [15] //! [16]
+    pendingAdditiveOperator = clickedOperator;
+//! [16] //! [17]
+    waitingForOperand = true;
+}
+//! [17]
+
+//! [18]
+void Calculator::multiplicativeOperatorClicked()
+{
+    Button *clickedButton = qobject_cast<Button *>(sender());
+    QString clickedOperator = clickedButton->text();
+    double operand = display->text().toDouble();
+
+    if (!pendingMultiplicativeOperator.isEmpty()) {
+        if (!calculate(operand, pendingMultiplicativeOperator)) {
+            abortOperation();
+            return;
+        }
+        display->setText(QString::number(factorSoFar));
+    } else {
+        factorSoFar = operand;
+    }
+
+    pendingMultiplicativeOperator = clickedOperator;
+    waitingForOperand = true;
+}
+//! [18]
+
+//! [20]
+void Calculator::equalClicked()
+{
+    double operand = display->text().toDouble();
+
+    if (!pendingMultiplicativeOperator.isEmpty()) {
+        if (!calculate(operand, pendingMultiplicativeOperator)) {
+            abortOperation();
+            return;
+        }
+        operand = factorSoFar;
+        factorSoFar = 0.0;
+        pendingMultiplicativeOperator.clear();
+    }
+    if (!pendingAdditiveOperator.isEmpty()) {
+        if (!calculate(operand, pendingAdditiveOperator)) {
+            abortOperation();
+            return;
+        }
+        pendingAdditiveOperator.clear();
+    } else {
+        sumSoFar = operand;
+    }
+
+    display->setText(QString::number(sumSoFar));
+    sumSoFar = 0.0;
+    waitingForOperand = true;
+}
+//! [20]
+
+//! [22]
+void Calculator::pointClicked()
+{
+    if (waitingForOperand)
+        display->setText("0");
+    if (!display->text().contains('.'))
+        display->setText(display->text() + ".");
+    waitingForOperand = false;
+}
+//! [22]
+
+//! [24]
+void Calculator::changeSignClicked()
+{
+    QString text = display->text();
+    double value = text.toDouble();
+
+    if (value > 0.0) {
+        text.prepend("-");
+    } else if (value < 0.0) {
+        text.remove(0, 1);
+    }
+    display->setText(text);
+}
+//! [24]
+
+//! [26]
+void Calculator::backspaceClicked()
+{
+    if (waitingForOperand)
+        return;
+
+    QString text = display->text();
+    text.chop(1);
+    if (text.isEmpty()) {
+        text = "0";
+        waitingForOperand = true;
+    }
+    display->setText(text);
+}
+//! [26]
+
+//! [28]
+void Calculator::clear()
+{
+    if (waitingForOperand)
+        return;
+
+    display->setText("0");
+    waitingForOperand = true;
+}
+//! [28]
+
+//! [30]
+void Calculator::clearAll()
+{
+    sumSoFar = 0.0;
+    factorSoFar = 0.0;
+    pendingAdditiveOperator.clear();
+    pendingMultiplicativeOperator.clear();
+    display->setText("0");
+    waitingForOperand = true;
+}
+//! [30]
+
+//! [32]
+void Calculator::clearMemory()
+{
+    sumInMemory = 0.0;
+}
+
+void Calculator::readMemory()
+{
+    display->setText(QString::number(sumInMemory));
+    waitingForOperand = true;
+}
+
+void Calculator::setMemory()
+{
+    equalClicked();
+    sumInMemory = display->text().toDouble();
+}
+
+void Calculator::addToMemory()
+{
+    equalClicked();
+    sumInMemory += display->text().toDouble();
+}
+//! [32]
+//! [34]
+Button *Calculator::createButton(const QString &text, const char *member)
+{
+    Button *button = new Button(text);
+    connect(button, SIGNAL(clicked()), this, member);
+    return button;
+}
+//! [34]
+
+//! [36]
+void Calculator::abortOperation()
+{
+    clearAll();
+    display->setText("####");
+}
+//! [36]
+
+//! [38]
+bool Calculator::calculate(double rightOperand, const QString &pendingOperator)
+{
+    if (pendingOperator == "+") {
+        sumSoFar += rightOperand;
+    } else if (pendingOperator == "-") {
+        sumSoFar -= rightOperand;
+    } else if (pendingOperator == "\303\227") {
+        factorSoFar *= rightOperand;
+    } else if (pendingOperator == "\303\267") {
+        if (rightOperand == 0.0)
+            return false;
+        factorSoFar /= rightOperand;
+    }
+    return true;
+}
+//! [38]
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/calculator.h b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.h
new file mode 100644
index 0000000000000000000000000000000000000000..250a2f3e77bbd4851cf86348e24397eee55ff257
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.h
@@ -0,0 +1,117 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of The Qt Company Ltd nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef CALCULATOR_H
+#define CALCULATOR_H
+
+#include <QWidget>
+
+QT_BEGIN_NAMESPACE
+class QLineEdit;
+QT_END_NAMESPACE
+class Button;
+
+//! [0]
+class Calculator : public QWidget
+{
+    Q_OBJECT
+
+public:
+    Calculator(QWidget *parent = 0);
+
+private slots:
+    void digitClicked();
+    void unaryOperatorClicked();
+    void additiveOperatorClicked();
+    void multiplicativeOperatorClicked();
+    void equalClicked();
+    void pointClicked();
+    void changeSignClicked();
+    void backspaceClicked();
+    void clear();
+    void clearAll();
+    void clearMemory();
+    void readMemory();
+    void setMemory();
+    void addToMemory();
+//! [0]
+
+//! [1]
+private:
+//! [1] //! [2]
+    Button *createButton(const QString &text, const char *member);
+    void abortOperation();
+    bool calculate(double rightOperand, const QString &pendingOperator);
+//! [2]
+
+//! [3]
+    double sumInMemory;
+//! [3] //! [4]
+    double sumSoFar;
+//! [4] //! [5]
+    double factorSoFar;
+//! [5] //! [6]
+    QString pendingAdditiveOperator;
+//! [6] //! [7]
+    QString pendingMultiplicativeOperator;
+//! [7] //! [8]
+    bool waitingForOperand;
+//! [8]
+
+//! [9]
+    QLineEdit *display;
+//! [9] //! [10]
+
+    enum { NumDigitButtons = 10 };
+    Button *digitButtons[NumDigitButtons];
+};
+//! [10]
+
+#endif
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/calculator.pro b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.pro
new file mode 100644
index 0000000000000000000000000000000000000000..8f132609c547f9cbf46fb7cd1276f613eabb5a7c
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/calculator.pro
@@ -0,0 +1,11 @@
+QT += widgets
+
+HEADERS = button.h \
+    calculator.h
+SOURCES = button.cpp \
+    calculator.cpp \
+    main.cpp
+
+# Single Application implementation
+include(../../singleapplication.pri)
+DEFINES += QAPPLICATION_CLASS=QApplication
diff --git a/third_party/SingleApplication-3.3.0/examples/calculator/main.cpp b/third_party/SingleApplication-3.3.0/examples/calculator/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..d45438f483028023889f911d007ead25b4831601
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/calculator/main.cpp
@@ -0,0 +1,71 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the examples of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** BSD License Usage
+** Alternatively, you may use this file under the terms of the BSD license
+** as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of The Qt Company Ltd nor the names of its
+**     contributors may be used to endorse or promote products derived
+**     from this software without specific prior written permission.
+**
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QApplication>
+
+#include <singleapplication.h>
+
+#include "calculator.h"
+
+int main(int argc, char *argv[])
+{
+    SingleApplication app(argc, argv);
+
+    Calculator calc;
+
+    QObject::connect( &app, &SingleApplication::instanceStarted, [ &calc ]() {
+        calc.raise();
+        calc.activateWindow();
+    });
+
+    calc.show();
+
+    return app.exec();
+}
diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/CMakeLists.txt b/third_party/SingleApplication-3.3.0/examples/sending_arguments/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2cc5597572f79a3e63dc5ebdf069d8de0a9e7f96
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/CMakeLists.txt
@@ -0,0 +1,20 @@
+cmake_minimum_required(VERSION 3.7.0)
+
+project(sending_arguments LANGUAGES CXX)
+
+set(CMAKE_AUTOMOC ON)
+
+# SingleApplication base class
+set(QAPPLICATION_CLASS QCoreApplication)
+add_subdirectory(../.. SingleApplication)
+
+find_package(Qt${QT_DEFAULT_MAJOR_VERSION} COMPONENTS Core REQUIRED)
+
+add_executable(${PROJECT_NAME}
+    main.cpp
+    messagereceiver.cpp
+    messagereceiver.h
+    main.cpp
+)
+
+target_link_libraries(${PROJECT_NAME} SingleApplication::SingleApplication)
diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/main.cpp b/third_party/SingleApplication-3.3.0/examples/sending_arguments/main.cpp
new file mode 100755
index 0000000000000000000000000000000000000000..a9d34dd97ad671d96e3cef454c84bebe0a1c97fb
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/main.cpp
@@ -0,0 +1,28 @@
+#include <singleapplication.h>
+#include "messagereceiver.h"
+
+int main(int argc, char *argv[])
+{
+    // Allow secondary instances
+    SingleApplication app( argc, argv, true );
+
+    MessageReceiver msgReceiver;
+
+    // If this is a secondary instance
+    if( app.isSecondary() ) {
+        app.sendMessage( app.arguments().join(' ').toUtf8() );
+        qDebug() << "App already running.";
+        qDebug() << "Primary instance PID: " << app.primaryPid();
+        qDebug() << "Primary instance user: " << app.primaryUser();
+        return 0;
+    } else {
+        QObject::connect(
+            &app,
+            &SingleApplication::receivedMessage,
+            &msgReceiver,
+            &MessageReceiver::receivedMessage
+        );
+    }
+
+    return app.exec();
+}
diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.cpp b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..0560b0726b536b179c83386c29b80864f58a29ce
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.cpp
@@ -0,0 +1,12 @@
+#include <QDebug>
+#include "messagereceiver.h"
+
+MessageReceiver::MessageReceiver(QObject *parent) : QObject(parent)
+{
+}
+
+void MessageReceiver::receivedMessage(int instanceId, QByteArray message)
+{
+    qDebug() << "Received message from instance: " << instanceId;
+    qDebug() << "Message Text: " << message;
+}
diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.h b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.h
new file mode 100644
index 0000000000000000000000000000000000000000..50a970c8fbd6811f00321ff2608e3ae316a95cd7
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/messagereceiver.h
@@ -0,0 +1,15 @@
+#ifndef MESSAGERECEIVER_H
+#define MESSAGERECEIVER_H
+
+#include <QObject>
+
+class MessageReceiver : public QObject
+{
+    Q_OBJECT
+public:
+    explicit MessageReceiver(QObject *parent = 0);
+public slots:
+    void receivedMessage( int instanceId, QByteArray message );
+};
+
+#endif // MESSAGERECEIVER_H
diff --git a/third_party/SingleApplication-3.3.0/examples/sending_arguments/sending_arguments.pro b/third_party/SingleApplication-3.3.0/examples/sending_arguments/sending_arguments.pro
new file mode 100755
index 0000000000000000000000000000000000000000..897636a9b64656acd81f333f0d258fa5c48da36e
--- /dev/null
+++ b/third_party/SingleApplication-3.3.0/examples/sending_arguments/sending_arguments.pro
@@ -0,0 +1,9 @@
+# Single Application implementation
+include(../../singleapplication.pri)
+DEFINES += QAPPLICATION_CLASS=QCoreApplication
+
+SOURCES += main.cpp \
+    messagereceiver.cpp
+
+HEADERS += \
+    messagereceiver.h
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp b/third_party/SingleApplication-3.3.0/singleapplication.cpp
similarity index 92%
rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp
rename to third_party/SingleApplication-3.3.0/singleapplication.cpp
index 276ceee9247e1a80f369cedb0cc92e346ce578e4..09e264ef51f667ce67efc3db055da82a09a79cba 100644
--- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.cpp
+++ b/third_party/SingleApplication-3.3.0/singleapplication.cpp
@@ -36,7 +36,7 @@
  * @param options Optional flags to toggle specific behaviour
  * @param timeout Maximum time blocking functions are allowed during app load
  */
-SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, QString userData )
+SingleApplication::SingleApplication( int &argc, char *argv[], bool allowSecondary, Options options, int timeout, const QString &userData )
     : app_t( argc, argv ), d_ptr( new SingleApplicationPrivate( this ) )
 {
     Q_D( SingleApplication );
@@ -172,9 +172,9 @@ SingleApplication::~SingleApplication()
  * Checks if the current application instance is primary.
  * @return Returns true if the instance is primary, false otherwise.
  */
-bool SingleApplication::isPrimary()
+bool SingleApplication::isPrimary() const
 {
-    Q_D( SingleApplication );
+    Q_D( const SingleApplication );
     return d->server != nullptr;
 }
 
@@ -182,9 +182,9 @@ bool SingleApplication::isPrimary()
  * Checks if the current application instance is secondary.
  * @return Returns true if the instance is secondary, false otherwise.
  */
-bool SingleApplication::isSecondary()
+bool SingleApplication::isSecondary() const
 {
-    Q_D( SingleApplication );
+    Q_D( const SingleApplication );
     return d->server == nullptr;
 }
 
@@ -194,9 +194,9 @@ bool SingleApplication::isSecondary()
  * only incremented afterwards.
  * @return Returns a unique instance id.
  */
-quint32 SingleApplication::instanceId()
+quint32 SingleApplication::instanceId() const
 {
-    Q_D( SingleApplication );
+    Q_D( const SingleApplication );
     return d->instanceNumber;
 }
 
@@ -206,9 +206,9 @@ quint32 SingleApplication::instanceId()
  * specific APIs.
  * @return Returns the primary instance PID.
  */
-qint64 SingleApplication::primaryPid()
+qint64 SingleApplication::primaryPid() const
 {
-    Q_D( SingleApplication );
+    Q_D( const SingleApplication );
     return d->primaryPid();
 }
 
@@ -216,9 +216,9 @@ qint64 SingleApplication::primaryPid()
  * Returns the username the primary instance is running as.
  * @return Returns the username the primary instance is running as.
  */
-QString SingleApplication::primaryUser()
+QString SingleApplication::primaryUser() const
 {
-    Q_D( SingleApplication );
+    Q_D( const SingleApplication );
     return d->primaryUser();
 }
 
@@ -226,7 +226,7 @@ QString SingleApplication::primaryUser()
  * Returns the username the current instance is running as.
  * @return Returns the username the current instance is running as.
  */
-QString SingleApplication::currentUser()
+QString SingleApplication::currentUser() const
 {
     return SingleApplicationPrivate::getUsername();
 }
@@ -248,10 +248,7 @@ bool SingleApplication::sendMessage( const QByteArray &message, int timeout )
     if( ! d->connectToPrimary( timeout,  SingleApplicationPrivate::Reconnect ) )
       return false;
 
-    d->socket->write( message );
-    bool dataWritten = d->socket->waitForBytesWritten( timeout );
-    d->socket->flush();
-    return dataWritten;
+    return d->writeConfirmedMessage( timeout, message );
 }
 
 /**
@@ -267,8 +264,8 @@ void SingleApplication::abortSafely()
     ::exit( EXIT_FAILURE );
 }
 
-QStringList SingleApplication::userData()
+QStringList SingleApplication::userData() const
 {
-    Q_D( SingleApplication );
+    Q_D( const SingleApplication );
     return d->appData();
 }
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h b/third_party/SingleApplication-3.3.0/singleapplication.h
similarity index 95%
rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h
rename to third_party/SingleApplication-3.3.0/singleapplication.h
index d39a661433bcf43c83ea95c236d8ac5787184d24..91cabf93e858cfe444d0905e20e76376a7244417 100644
--- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.h
+++ b/third_party/SingleApplication-3.3.0/singleapplication.h
@@ -85,44 +85,44 @@ public:
      * Usually 4*timeout would be the worst case (fail) scenario.
      * @see See the corresponding QAPPLICATION_CLASS constructor for reference
      */
-    explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, QString userData = QString() );
+    explicit SingleApplication( int &argc, char *argv[], bool allowSecondary = false, Options options = Mode::User, int timeout = 1000, const QString &userData = {} );
     ~SingleApplication() override;
 
     /**
      * @brief Returns if the instance is the primary instance
      * @returns {bool}
      */
-    bool isPrimary();
+    bool isPrimary() const;
 
     /**
      * @brief Returns if the instance is a secondary instance
      * @returns {bool}
      */
-    bool isSecondary();
+    bool isSecondary() const;
 
     /**
      * @brief Returns a unique identifier for the current instance
      * @returns {qint32}
      */
-    quint32 instanceId();
+    quint32 instanceId() const;
 
     /**
      * @brief Returns the process ID (PID) of the primary instance
      * @returns {qint64}
      */
-    qint64 primaryPid();
+    qint64 primaryPid() const;
 
     /**
      * @brief Returns the username of the user running the primary instance
      * @returns {QString}
      */
-    QString primaryUser();
+    QString primaryUser() const;
 
     /**
      * @brief Returns the username of the current user
      * @returns {QString}
      */
-    QString currentUser();
+    QString currentUser() const;
 
     /**
      * @brief Sends a message to the primary instance. Returns true on success.
@@ -137,7 +137,7 @@ public:
      * @brief Get the set user data.
      * @returns {QStringList}
      */
-    QStringList userData();
+    QStringList userData() const;
 
 Q_SIGNALS:
     void instanceStarted();
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication.pri b/third_party/SingleApplication-3.3.0/singleapplication.pri
similarity index 100%
rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication.pri
rename to third_party/SingleApplication-3.3.0/singleapplication.pri
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp b/third_party/SingleApplication-3.3.0/singleapplication_p.cpp
similarity index 85%
rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp
rename to third_party/SingleApplication-3.3.0/singleapplication_p.cpp
index 1ab58c239f0c0d84b03a14d931b87ef34759cff5..133972825040f003e4de1a39b57c22115bfe01e1 100644
--- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.cpp
+++ b/third_party/SingleApplication-3.3.0/singleapplication_p.cpp
@@ -263,20 +263,46 @@ bool SingleApplicationPrivate::connectToPrimary( int msecs, ConnectionType conne
 #endif
     writeStream << checksum;
 
-    // The header indicates the message length that follows
+    return writeConfirmedMessage( static_cast<int>(msecs - time.elapsed()), initMsg );
+}
+
+void SingleApplicationPrivate::writeAck( QLocalSocket *sock ) {
+    sock->putChar('\n');
+}
+
+bool SingleApplicationPrivate::writeConfirmedMessage (int msecs, const QByteArray &msg)
+{
+    QElapsedTimer time;
+    time.start();
+
+    // Frame 1: The header indicates the message length that follows
     QByteArray header;
     QDataStream headerStream(&header, QIODevice::WriteOnly);
 
 #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
     headerStream.setVersion(QDataStream::Qt_5_6);
 #endif
-    headerStream << static_cast <quint64>( initMsg.length() );
+    headerStream << static_cast <quint64>( msg.length() );
 
-    socket->write( header );
-    socket->write( initMsg );
-    bool result = socket->waitForBytesWritten( static_cast<int>(msecs - time.elapsed()) );
+    if( ! writeConfirmedFrame( static_cast<int>(msecs - time.elapsed()), header ))
+        return false;
+
+    // Frame 2: The message
+    return writeConfirmedFrame( static_cast<int>(msecs - time.elapsed()), msg );
+}
+
+bool SingleApplicationPrivate::writeConfirmedFrame( int msecs, const QByteArray &msg )
+{
+    socket->write( msg );
     socket->flush();
-    return result;
+
+    bool result = socket->waitForReadyRead( msecs ); // await ack byte
+    if (result) {
+        socket->read( 1 );
+        return true;
+    }
+
+    return false;
 }
 
 quint16 SingleApplicationPrivate::blockChecksum() const
@@ -321,32 +347,36 @@ void SingleApplicationPrivate::slotConnectionEstablished()
     QLocalSocket *nextConnSocket = server->nextPendingConnection();
     connectionMap.insert(nextConnSocket, ConnectionInfo());
 
-    QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose,
+    QObject::connect(nextConnSocket, &QLocalSocket::aboutToClose, this,
         [nextConnSocket, this](){
             auto &info = connectionMap[nextConnSocket];
-            Q_EMIT this->slotClientConnectionClosed( nextConnSocket, info.instanceId );
+            this->slotClientConnectionClosed( nextConnSocket, info.instanceId );
         }
     );
 
-    QObject::connect(nextConnSocket, &QLocalSocket::disconnected,
+    QObject::connect(nextConnSocket, &QLocalSocket::disconnected, nextConnSocket, &QLocalSocket::deleteLater);
+
+    QObject::connect(nextConnSocket, &QLocalSocket::destroyed, this,
         [nextConnSocket, this](){
             connectionMap.remove(nextConnSocket);
-            nextConnSocket->deleteLater();
         }
     );
 
-    QObject::connect(nextConnSocket, &QLocalSocket::readyRead,
+    QObject::connect(nextConnSocket, &QLocalSocket::readyRead, this,
         [nextConnSocket, this](){
             auto &info = connectionMap[nextConnSocket];
             switch(info.stage){
-            case StageHeader:
-                readInitMessageHeader(nextConnSocket);
+            case StageInitHeader:
+                readMessageHeader( nextConnSocket, StageInitBody );
                 break;
-            case StageBody:
+            case StageInitBody:
                 readInitMessageBody(nextConnSocket);
                 break;
-            case StageConnected:
-                Q_EMIT this->slotDataAvailable( nextConnSocket, info.instanceId );
+            case StageConnectedHeader:
+                readMessageHeader( nextConnSocket, StageConnectedBody );
+                break;
+            case StageConnectedBody:
+                this->slotDataAvailable( nextConnSocket, info.instanceId );
                 break;
             default:
                 break;
@@ -355,7 +385,7 @@ void SingleApplicationPrivate::slotConnectionEstablished()
     );
 }
 
-void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
+void SingleApplicationPrivate::readMessageHeader( QLocalSocket *sock, SingleApplicationPrivate::ConnectionStage nextStage )
 {
     if (!connectionMap.contains( sock )){
         return;
@@ -375,29 +405,35 @@ void SingleApplicationPrivate::readInitMessageHeader( QLocalSocket *sock )
     quint64 msgLen = 0;
     headerStream >> msgLen;
     ConnectionInfo &info = connectionMap[sock];
-    info.stage = StageBody;
+    info.stage = nextStage;
     info.msgLen = msgLen;
 
-    if ( sock->bytesAvailable() >= (qint64) msgLen ){
-        readInitMessageBody( sock );
-    }
+    writeAck( sock );
 }
 
-void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
+bool SingleApplicationPrivate::isFrameComplete( QLocalSocket *sock )
 {
-    Q_Q(SingleApplication);
-
     if (!connectionMap.contains( sock )){
-        return;
+        return false;
     }
 
     ConnectionInfo &info = connectionMap[sock];
     if( sock->bytesAvailable() < ( qint64 )info.msgLen ){
-        return;
+        return false;
     }
 
+    return true;
+}
+
+void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
+{
+    Q_Q(SingleApplication);
+
+    if( !isFrameComplete( sock ) )
+        return;
+
     // Read the message body
-    QByteArray msgBytes = sock->read(info.msgLen);
+    QByteArray msgBytes = sock->readAll();
     QDataStream readStream(msgBytes);
 
 #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0))
@@ -437,8 +473,9 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
         return;
     }
 
+    ConnectionInfo &info = connectionMap[sock];
     info.instanceId = instanceId;
-    info.stage = StageConnected;
+    info.stage = StageConnectedHeader;
 
     if( connectionType == NewInstance ||
         ( connectionType == SecondaryInstance &&
@@ -447,21 +484,28 @@ void SingleApplicationPrivate::readInitMessageBody( QLocalSocket *sock )
         Q_EMIT q->instanceStarted();
     }
 
-    if (sock->bytesAvailable() > 0){
-        Q_EMIT this->slotDataAvailable( sock, instanceId );
-    }
+    writeAck( sock );
 }
 
 void SingleApplicationPrivate::slotDataAvailable( QLocalSocket *dataSocket, quint32 instanceId )
 {
     Q_Q(SingleApplication);
+
+    if ( !isFrameComplete( dataSocket ) )
+        return;
+
     Q_EMIT q->receivedMessage( instanceId, dataSocket->readAll() );
+
+    writeAck( dataSocket );
+
+    ConnectionInfo &info = connectionMap[dataSocket];
+    info.stage = StageConnectedHeader;
 }
 
 void SingleApplicationPrivate::slotClientConnectionClosed( QLocalSocket *closedSocket, quint32 instanceId )
 {
     if( closedSocket->bytesAvailable() > 0 )
-        Q_EMIT slotDataAvailable( closedSocket, instanceId  );
+        slotDataAvailable( closedSocket, instanceId  );
 }
 
 void SingleApplicationPrivate::randomSleep()
@@ -470,7 +514,7 @@ void SingleApplicationPrivate::randomSleep()
     QThread::msleep( QRandomGenerator::global()->bounded( 8u, 18u ));
 #else
     qsrand( QDateTime::currentMSecsSinceEpoch() % std::numeric_limits<uint>::max() );
-    QThread::msleep( 8 + static_cast <unsigned long>( static_cast <float>( qrand() ) / RAND_MAX * 10 ));
+    QThread::msleep( qrand() % 11 + 8);
 #endif
 }
 
diff --git a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h b/third_party/SingleApplication-3.3.0/singleapplication_p.h
similarity index 88%
rename from third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h
rename to third_party/SingleApplication-3.3.0/singleapplication_p.h
index c49a46ddce4511e7e964e4c0dc3661bfb15e1b4c..58507cf3ea647e29de6adf3d61e490d2d676e747 100644
--- a/third_party/SingleApplication-3.2.0-dc8042b/singleapplication_p.h
+++ b/third_party/SingleApplication-3.3.0/singleapplication_p.h
@@ -61,9 +61,10 @@ public:
         Reconnect = 3
     };
     enum ConnectionStage : quint8 {
-        StageHeader = 0,
-        StageBody = 1,
-        StageConnected = 2,
+        StageInitHeader = 0,
+        StageInitBody = 1,
+        StageConnectedHeader = 2,
+        StageConnectedBody = 3,
     };
     Q_DECLARE_PUBLIC(SingleApplication)
 
@@ -79,8 +80,12 @@ public:
     quint16 blockChecksum() const;
     qint64 primaryPid() const;
     QString primaryUser() const;
-    void readInitMessageHeader(QLocalSocket *socket);
+    bool isFrameComplete(QLocalSocket *sock);
+    void readMessageHeader(QLocalSocket *socket, ConnectionStage nextStage);
     void readInitMessageBody(QLocalSocket *socket);
+    void writeAck(QLocalSocket *sock);
+    bool writeConfirmedFrame(int msecs, const QByteArray &msg);
+    bool writeConfirmedMessage(int msecs, const QByteArray &msg);
     static void randomSleep();
     void addAppData(const QString &data);
     QStringList appData() const;