diff --git a/.travis.yml b/.travis.yml index eec3229..49c6eb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,21 +63,21 @@ matrix: env: - CXX=g++-8 - CC=gcc-8 - - QT_PKG=59 + - QT_PKG=510 addons: apt: sources: - ubuntu-toolchain-r-test - - sourceline: 'ppa:beineri/opt-qt597-xenial' + - sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial' packages: - g++-8 - ninja-build - - qt59base - - qt59tools - - qt59svg - - qt59multimedia - - qt59quickcontrols2 - - qt59graphicaleffects + - qt510base + - qt510tools + - qt510svg + - qt510multimedia + - qt510quickcontrols2 + - qt510graphicaleffects - liblmdb-dev - libgl1-mesa-dev # needed for missing gl.h - os: linux @@ -85,23 +85,23 @@ matrix: env: - CXX=clang++-6.0 - CC=clang-6.0 - - QT_PKG=59 + - QT_PKG=510 addons: apt: sources: - ubuntu-toolchain-r-test - llvm-toolchain-xenial-6.0 - - sourceline: 'ppa:beineri/opt-qt597-xenial' + - sourceline: 'ppa:beineri/opt-qt-5.10.1-xenial' packages: - clang++-6.0 - g++-7 - ninja-build - - qt59base - - qt59tools - - qt59svg - - qt59multimedia - - qt59quickcontrols2 - - qt59graphicaleffects + - qt510base + - qt510tools + - qt510svg + - qt510multimedia + - qt510quickcontrols2 + - qt510graphicaleffects - liblmdb-dev - libgl1-mesa-dev # needed for missing gl.h - os: linux diff --git a/CMakeLists.txt b/CMakeLists.txt index 938fbc8..2f83a86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -142,9 +142,9 @@ if (APPLE) endif(APPLE) if (Qt5Widgets_FOUND) - if (Qt5Widgets_VERSION VERSION_LESS 5.9.0) + if (Qt5Widgets_VERSION VERSION_LESS 5.10.0) message(STATUS "Qt version ${Qt5Widgets_VERSION}") - message(WARNING "Minimum supported Qt5 version is 5.9!") + message(WARNING "Minimum supported Qt5 version is 5.10!") endif() endif(Qt5Widgets_FOUND) @@ -239,7 +239,6 @@ set(SRC_FILES src/dialogs/ReCaptcha.cpp src/dialogs/ReadReceipts.cpp src/dialogs/RoomSettings.cpp - src/dialogs/UserProfile.cpp # Emoji src/emoji/Category.cpp @@ -278,6 +277,7 @@ set(SRC_FILES src/ui/ToggleButton.cpp src/ui/Theme.cpp src/ui/ThemeManager.cpp + src/ui/UserProfile.cpp src/AvatarProvider.cpp src/BlurhashProvider.cpp @@ -287,6 +287,7 @@ set(SRC_FILES src/ColorImageProvider.cpp src/CommunitiesList.cpp src/CommunitiesListItem.cpp + src/DeviceVerificationFlow.cpp src/EventAccessors.cpp src/InviteeItem.cpp src/Logging.cpp @@ -339,7 +340,7 @@ if(USE_BUNDLED_MTXCLIENT) FetchContent_Declare( MatrixClient GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git - GIT_TAG 21a55ba65d0712a441fbef2af2ede66771430247 + GIT_TAG ad5575bc24089dc385e97d9ace026414b618775c ) FetchContent_MakeAvailable(MatrixClient) else() @@ -451,7 +452,6 @@ qt5_wrap_cpp(MOC_HEADERS src/dialogs/ReCaptcha.h src/dialogs/ReadReceipts.h src/dialogs/RoomSettings.h - src/dialogs/UserProfile.h # Emoji src/emoji/Category.h @@ -487,6 +487,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ui/ToggleButton.h src/ui/Theme.h src/ui/ThemeManager.h + src/ui/UserProfile.h src/notifications/Manager.h @@ -497,6 +498,7 @@ qt5_wrap_cpp(MOC_HEADERS src/ChatPage.h src/CommunitiesList.h src/CommunitiesListItem.h + src/DeviceVerificationFlow.h src/InviteeItem.h src/LoginPage.h src/MainWindow.h diff --git a/README.md b/README.md index 2d24165..92b5546 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ sudo pacman -S qt5-base \ ##### Gentoo Linux ```bash -sudo emerge -a ">=dev-qt/qtgui-5.9.0" media-libs/fontconfig +sudo emerge -a ">=dev-qt/qtgui-5.10.0" media-libs/fontconfig ``` ##### Ubuntu 20.04 diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json index 842017f..59f4fa4 100644 --- a/io.github.NhekoReborn.Nheko.json +++ b/io.github.NhekoReborn.Nheko.json @@ -73,9 +73,9 @@ ], "sources": [ { - "sha256": "3dbcbfd8c07e25f5e0d662b194d3a7772ef214358c49ada23c044c4747ce8b19", + "sha256": "5197b3147cfcfaa67dd564db7b878e4a4b3d9f3443801722b3915cdeced656cb", "type": "archive", - "url": "https://github.com/gabime/spdlog/archive/v1.1.0.tar.gz" + "url": "https://github.com/gabime/spdlog/archive/v1.8.1.tar.gz" } ] }, @@ -146,7 +146,7 @@ "name": "mtxclient", "sources": [ { - "commit": "21a55ba65d0712a441fbef2af2ede66771430247", + "commit": "ad5575bc24089dc385e97d9ace026414b618775c", "type": "git", "url": "https://github.com/Nheko-Reborn/mtxclient.git" } diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml index 8d837c2..6148462 100644 --- a/resources/qml/ActiveCallBar.qml +++ b/resources/qml/ActiveCallBar.qml @@ -6,7 +6,7 @@ import im.nheko 1.0 Rectangle { id: activeCallBar - visible: timelineManager.callState != WebRTCState.DISCONNECTED + visible: TimelineManager.callState != WebRTCState.DISCONNECTED color: "#2ECC71" implicitHeight: rowLayout.height + 8 @@ -21,13 +21,13 @@ Rectangle { width: avatarSize height: avatarSize - url: timelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") - displayName: timelineManager.callPartyName + url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") + displayName: TimelineManager.callPartyName } Label { font.pointSize: fontMetrics.font.pointSize * 1.1 - text: " " + timelineManager.callPartyName + " " + text: " " + TimelineManager.callPartyName + " " } Image { @@ -42,7 +42,7 @@ Rectangle { } Connections { - target: timelineManager + target: TimelineManager function onCallStateChanged(state) { switch (state) { case WebRTCState.INITIATING: @@ -69,7 +69,7 @@ Rectangle { id: callTimer property int startTime interval: 1000 - running: timelineManager.callState == WebRTCState.CONNECTED + running: TimelineManager.callState == WebRTCState.CONNECTED repeat: true onTriggered: { var d = new Date() @@ -93,15 +93,15 @@ Rectangle { width: 24 height: 24 buttonTextColor: "#000000" - image: timelineManager.isMicMuted ? + image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png" hoverEnabled: true ToolTip.visible: hovered - ToolTip.text: timelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") + ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic") - onClicked: timelineManager.toggleMicMute() + onClicked: TimelineManager.toggleMicMute() } Item { diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml index c030c84..df3dd08 100644 --- a/resources/qml/Avatar.qml +++ b/resources/qml/Avatar.qml @@ -2,11 +2,13 @@ import QtQuick 2.6 import QtQuick.Controls 2.3 import QtGraphicalEffects 1.0 +import im.nheko 1.0 + Rectangle { id: avatar width: 48 height: 48 - radius: settings.avatarCircles ? height/2 : 3 + radius: Settings.avatarCircles ? height/2 : 3 property alias url: img.source property string userid @@ -14,7 +16,7 @@ Rectangle { Label { anchors.fill: parent - text: timelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") + text: TimelineManager.escapeEmoji(displayName ? String.fromCodePoint(displayName.codePointAt(0)) : "") textFormat: Text.RichText font.pixelSize: avatar.height/2 verticalAlignment: Text.AlignVCenter @@ -40,7 +42,7 @@ Rectangle { anchors.fill: parent width: avatar.width height: avatar.height - radius: settings.avatarCircles ? height/2 : 3 + radius: Settings.avatarCircles ? height/2 : 3 } } @@ -54,8 +56,8 @@ Rectangle { height: avatar.height / 6 width: height - radius: settings.avatarCircles ? height / 2 : height / 4 - color: switch (timelineManager.userPresence(userid)) { + radius: Settings.avatarCircles ? height / 2 : height / 4 + color: switch (TimelineManager.userPresence(userid)) { case "online": return "#00cc66" case "unavailable": return "#ff9933" case "offline": // return "#a82353" don't show anything if offline, since it is confusing, if presence is disabled diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 1da223d..2921416 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -1,6 +1,8 @@ import QtQuick 2.5 import QtQuick.Controls 2.3 +import im.nheko 1.0 + TextEdit { textFormat: TextEdit.RichText readOnly: true @@ -11,13 +13,13 @@ TextEdit { onLinkActivated: { if (/^https:\/\/matrix.to\/#\/(@.*)$/.test(link)) chat.model.openUserProfile(/^https:\/\/matrix.to\/#\/(@.*)$/.exec(link)[1]) - else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) timelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) + else if (/^https:\/\/matrix.to\/#\/(![^\/]*)$/.test(link)) TimelineManager.setHistoryView(/^https:\/\/matrix.to\/#\/(!.*)$/.exec(link)[1]) else if (/^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.test(link)) { var match = /^https:\/\/matrix.to\/#\/(![^\/]*)\/(\$.*)$/.exec(link) - timelineManager.setHistoryView(match[1]) + TimelineManager.setHistoryView(match[1]) chat.positionViewAtIndex(chat.model.idToIndex(match[2]), ListView.Contain) } - else timelineManager.openLink(link) + else TimelineManager.openLink(link) } MouseArea { diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index c109175..ec46f7e 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -1,6 +1,8 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 +import im.nheko 1.0 + // This class is for showing Reactions in the timeline row, not for // adding new reactions via the emoji picker Flow { @@ -12,7 +14,6 @@ Flow { property real highlightLight: colors.highlight.hslLightness property string eventId - property string roomId anchors.left: parent.left anchors.right: parent.right @@ -33,8 +34,8 @@ Flow { ToolTip.text: modelData.users onClicked: { - console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + " in room " + reactionFlow.roomId + ". selfReactedEvent: " + modelData.selfReactedEvent) - timelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key) + console.debug("Picked " + modelData.key + "in response to " + reactionFlow.eventId + ". selfReactedEvent: " + modelData.selfReactedEvent) + TimelineManager.queueReactionMessage(reactionFlow.eventId, modelData.key) } @@ -46,7 +47,7 @@ Flow { TextMetrics { id: textMetrics - font.family: settings.emojiFont + font.family: Settings.emojiFont elide: Text.ElideRight elideWidth: 150 text: modelData.key @@ -56,7 +57,7 @@ Flow { anchors.baseline: reactionCounter.baseline id: reactionText text: textMetrics.elidedText + (textMetrics.elidedText == modelData.key ? "" : "…") - font.family: settings.emojiFont + font.family: Settings.emojiFont color: reaction.hovered ? colors.highlight : colors.text maximumLineCount: 1 } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index a38a4d3..c026d82 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -29,7 +29,7 @@ Item { } } Rectangle { - color: (settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" + color: (Settings.messageHoverHighlight && parent.containsMouse) ? colors.base : "transparent" anchors.fill: row } RowLayout { @@ -48,8 +48,8 @@ Item { // fancy reply, if this is a reply Reply { visible: model.replyTo - modelData: chat.model.getDump(model.replyTo, model.id) - userColor: timelineManager.userColor(modelData.userId, colors.window) + modelData: chat.model.getDump(model.replyTo,model.id) + userColor: TimelineManager.userColor(modelData.userId, colors.window) } // actual message content @@ -64,7 +64,6 @@ Item { Reactions { id: reactionRow reactions: model.reactions - roomId: model.roomId eventId: model.id } } @@ -84,7 +83,7 @@ Item { width: 16 } EmojiButton { - visible: settings.buttonsInTimeline + visible: Settings.buttonsInTimeline Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 width: 16 @@ -96,7 +95,7 @@ Item { event_id: model.id } ImageButton { - visible: settings.buttonsInTimeline + visible: Settings.buttonsInTimeline Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 width: 16 @@ -112,7 +111,7 @@ Item { onClicked: chat.model.replyAction(model.id) } ImageButton { - visible: settings.buttonsInTimeline + visible: Settings.buttonsInTimeline Layout.alignment: Qt.AlignRight | Qt.AlignTop Layout.preferredHeight: 16 width: 16 diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 07c5e1a..5c9ca34 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -9,6 +9,7 @@ import im.nheko.EmojiModel 1.0 import "./delegates" import "./emoji" +import "./device-verification" Page { id: timelineRoot @@ -90,7 +91,7 @@ Page { visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker height: visible ? implicitHeight : 0 text: qsTr("Save as") - onTriggered: timelineManager.timeline.saveMedia(messageContextMenu.eventId) + onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) } } @@ -98,8 +99,27 @@ Page { anchors.fill: parent color: colors.window + Component { + id: deviceVerificationDialog + DeviceVerification {} + } + Connections { + target: TimelineManager + function onNewDeviceVerificationRequest(flow,transactionId,userId,deviceId,isRequest) { + var dialog = deviceVerificationDialog.createObject(timelineRoot, {flow: flow}); + dialog.show(); + } + } + Connections { + target: TimelineManager.timeline + function onOpenProfile(profile) { + var userProfile = userProfileComponent.createObject(timelineRoot,{profile: profile}); + userProfile.show(); + } + } + Label { - visible: !timelineManager.timeline && !timelineManager.isInitialSync + visible: !TimelineManager.timeline && !TimelineManager.isInitialSync anchors.centerIn: parent text: qsTr("No room open") font.pointSize: 24 @@ -109,7 +129,7 @@ Page { BusyIndicator { visible: running anchors.centerIn: parent - running: timelineManager.isInitialSync + running: TimelineManager.isInitialSync height: 200 width: 200 z: 3 @@ -128,7 +148,7 @@ Page { MouseArea { anchors.fill: parent - onClicked: timelineManager.openRoomSettings(); + onClicked: TimelineManager.openRoomSettings(); } GridLayout { @@ -149,14 +169,14 @@ Page { Layout.rowSpan: 2 Layout.alignment: Qt.AlignVCenter - visible: timelineManager.isNarrowView + visible: TimelineManager.isNarrowView image: ":/icons/icons/ui/angle-pointing-to-left.png" ToolTip.visible: hovered ToolTip.text: qsTr("Back to room list") - onClicked: timelineManager.backToRooms() + onClicked: TimelineManager.backToRooms() } Avatar { @@ -173,7 +193,7 @@ Page { MouseArea { anchors.fill: parent - onClicked: timelineManager.openRoomSettings(); + onClicked: TimelineManager.openRoomSettings(); } } @@ -181,6 +201,7 @@ Page { Layout.fillWidth: true Layout.column: 2 Layout.row: 0 + color: colors.text font.pointSize: fontMetrics.font.pointSize * 1.1 @@ -188,7 +209,7 @@ Page { MouseArea { anchors.fill: parent - onClicked: timelineManager.openRoomSettings(); + onClicked: TimelineManager.openRoomSettings(); } } MatrixText { @@ -220,19 +241,19 @@ Page { id: roomOptionsMenu MenuItem { text: qsTr("Invite users") - onTriggered: timelineManager.openInviteUsersDialog(); + onTriggered: TimelineManager.openInviteUsersDialog(); } MenuItem { text: qsTr("Members") - onTriggered: timelineManager.openMemberListDialog(); + onTriggered: TimelineManager.openMemberListDialog(); } MenuItem { text: qsTr("Leave room") - onTriggered: timelineManager.openLeaveRoomDialog(); + onTriggered: TimelineManager.openLeaveRoomDialog(); } MenuItem { text: qsTr("Settings") - onTriggered: timelineManager.openRoomSettings(); + onTriggered: TimelineManager.openRoomSettings(); } } } @@ -242,14 +263,14 @@ Page { ListView { id: chat - visible: !!timelineManager.timeline + visible: TimelineManager.timeline != null cacheBuffer: 400 Layout.fillWidth: true Layout.fillHeight: true - model: timelineManager.timeline + model: TimelineManager.timeline boundsBehavior: Flickable.StopAtBounds @@ -293,7 +314,7 @@ Page { onCountChanged: if (atYEnd) model.currentIndex = 0 // Mark last event as read, since we are at the bottom - property int delegateMaxWidth: (settings.timelineMaxWidth > 100 && (parent.width - settings.timelineMaxWidth) > scrollbar.width*2) ? settings.timelineMaxWidth : (parent.width - scrollbar.width*2) + property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width*2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width*2) delegate: Item { // This would normally be previousSection, but our model's order is inverted. @@ -333,6 +354,11 @@ Page { } } + Component{ + id: userProfileComponent + UserProfile{} + } + section { property: "section" } @@ -369,6 +395,7 @@ Page { color: colors.base } } + Row { height: userName.height spacing: 8 @@ -390,26 +417,18 @@ Page { Label { id: userName - text: timelineManager.escapeEmoji(modelData.userName) - color: timelineManager.userColor(modelData.userId, colors.window) + text: TimelineManager.escapeEmoji(modelData.userName) + color: TimelineManager.userColor(modelData.userId, colors.window) textFormat: Text.RichText MouseArea { anchors.fill: parent - onClicked: chat.model.openUserProfile(section.split(" ")[0]) + Layout.alignment: Qt.AlignHCenter + onClicked: chat.model.openUserProfile(modelData.userId) cursorShape: Qt.PointingHandCursor propagateComposedEvents: true } } - - Label { - color: colors.buttonText - text: timelineManager.userStatus(modelData.userId) - textFormat: Text.PlainText - elide: Text.ElideRight - width: chat.delegateMaxWidth - parent.spacing*2 - userName.implicitWidth - avatarSize - font.italic: true - } } } } @@ -475,7 +494,7 @@ Page { anchors.bottom: parent.bottom modelData: chat.model ? chat.model.getDump(chat.model.reply, chat.model.id) : {} - userColor: timelineManager.userColor(modelData.userId, colors.window) + userColor: TimelineManager.userColor(modelData.userId, colors.window) } ImageButton { diff --git a/resources/qml/UserProfile.qml b/resources/qml/UserProfile.qml new file mode 100644 index 0000000..562dd4f --- /dev/null +++ b/resources/qml/UserProfile.qml @@ -0,0 +1,172 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.3 + +import im.nheko 1.0 + +import "./device-verification" + +ApplicationWindow{ + property var profile + + id: userProfileDialog + height: 650 + width: 420 + minimumHeight: 420 + + palette: colors + + Component { + id: deviceVerificationDialog + DeviceVerification {} + } + + ColumnLayout{ + id: contentL + + anchors.fill: parent + anchors.margins: 10 + + spacing: 10 + + Avatar { + url: profile.avatarUrl.replace("mxc://", "image://MxcImage/") + height: 130 + width: 130 + displayName: profile.displayName + userid: profile.userid + Layout.alignment: Qt.AlignHCenter + } + + Label { + text: profile.displayName + fontSizeMode: Text.HorizontalFit + font.pixelSize: 20 + color: TimelineManager.userColor(profile.userid, colors.window) + font.bold: true + Layout.alignment: Qt.AlignHCenter + } + + MatrixText { + text: profile.userid + font.pixelSize: 15 + Layout.alignment: Qt.AlignHCenter + } + + Button { + id: verifyUserButton + text: qsTr("Verify") + Layout.alignment: Qt.AlignHCenter + enabled: !profile.isUserVerified + visible: !profile.isUserVerified + + onClicked: profile.verify() + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 + + ImageButton { + image:":/icons/icons/ui/do-not-disturb-rounded-sign.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Ban the user") + onClicked: profile.banUser() + } + // ImageButton{ + // image:":/icons/icons/ui/volume-off-indicator.png" + // Layout.margins: { + // left: 5 + // right: 5 + // } + // ToolTip.visible: hovered + // ToolTip.text: qsTr("Ignore messages from this user") + // onClicked : { + // profile.ignoreUser() + // } + // } + ImageButton{ + image:":/icons/icons/ui/black-bubble-speech.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Start a private chat") + onClicked: profile.startChat() + } + ImageButton{ + image:":/icons/icons/ui/round-remove-button.png" + hoverEnabled: true + ToolTip.visible: hovered + ToolTip.text: qsTr("Kick the user") + onClicked: profile.kickUser() + } + } + + ListView{ + id: devicelist + + Layout.fillHeight: true + Layout.minimumHeight: 200 + Layout.fillWidth: true + + clip: true + spacing: 8 + boundsBehavior: Flickable.StopAtBounds + + model: profile.deviceList + + delegate: RowLayout{ + width: devicelist.width + spacing: 4 + + ColumnLayout{ + spacing: 0 + Text{ + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft + + elide: Text.ElideRight + font.bold: true + color: colors.text + text: model.deviceId + } + Text{ + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + + elide: Text.ElideRight + color: colors.text + text: model.deviceName + } + } + + Image{ + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + + source: ((model.verificationStatus == VerificationStatus.VERIFIED)?"image://colorimage/:/icons/icons/ui/lock.png?green": + ((model.verificationStatus == VerificationStatus.UNVERIFIED)?"image://colorimage/:/icons/icons/ui/unlock.png?yellow": + "image://colorimage/:/icons/icons/ui/unlock.png?red")) + } + Button{ + id: verifyButton + text: (model.verificationStatus != VerificationStatus.VERIFIED)?"Verify":"Unverify" + onClicked: { + if(model.verificationStatus == VerificationStatus.VERIFIED){ + profile.unverify(model.deviceId) + }else{ + profile.verify(model.deviceId); + } + } + } + } + } + } + + footer: DialogButtonBox { + standardButtons: DialogButtonBox.Ok + + onAccepted: userProfileDialog.close() + } +} diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 464fa83..7a2588f 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -1,6 +1,8 @@ import QtQuick 2.6 import QtQuick.Layouts 1.2 +import im.nheko 1.0 + Item { height: row.height + 24 width: parent ? parent.width : undefined @@ -29,7 +31,7 @@ Item { } MouseArea { anchors.fill: parent - onClicked: timelineManager.timeline.saveMedia(model.data.id) + onClicked: TimelineManager.timeline.saveMedia(model.data.id) cursorShape: Qt.PointingHandCursor } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 3885dda..b5c51c2 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -36,7 +36,7 @@ Item { MouseArea { enabled: model.data.type == MtxEvent.ImageMessage && img.status == Image.Ready anchors.fill: parent - onClicked: timelineManager.openImageOverlay(model.data.url, model.data.id) + onClicked: TimelineManager.openImageOverlay(model.data.url, model.data.id) } } } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 5b69264..a5cb2ae 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -36,8 +36,8 @@ Item { DelegateChoice { roleValue: MtxEvent.EmoteMessage NoticeMessage { - formatted: timelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody - color: timelineManager.userColor(modelData.userId, colors.window) + formatted: TimelineManager.escapeEmoji(modelData.userName) + " " + model.data.formattedBody + color: TimelineManager.userColor(modelData.userId, colors.window) } } DelegateChoice { @@ -128,31 +128,85 @@ Item { // TODO: make a more complex formatter for the power levels. roleValue: MtxEvent.PowerLevels NoticeMessage { - text: timelineManager.timeline.formatPowerLevelEvent(model.data.id) + text: TimelineManager.timeline.formatPowerLevelEvent(model.data.id) } } DelegateChoice { roleValue: MtxEvent.RoomJoinRules NoticeMessage { - text: timelineManager.timeline.formatJoinRuleEvent(model.data.id) + text: TimelineManager.timeline.formatJoinRuleEvent(model.data.id) } } DelegateChoice { roleValue: MtxEvent.RoomHistoryVisibility NoticeMessage { - text: timelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) + text: TimelineManager.timeline.formatHistoryVisibilityEvent(model.data.id) } } DelegateChoice { roleValue: MtxEvent.RoomGuestAccess NoticeMessage { - text: timelineManager.timeline.formatGuestAccessEvent(model.data.id) + text: TimelineManager.timeline.formatGuestAccessEvent(model.data.id) } } DelegateChoice { roleValue: MtxEvent.Member NoticeMessage { - text: timelineManager.timeline.formatMemberEvent(model.data.id); + text: TimelineManager.timeline.formatMemberEvent(model.data.id); + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationRequest + NoticeMessage { + text: "KeyVerificationRequest"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationStart + NoticeMessage { + text: "KeyVerificationStart"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationReady + NoticeMessage { + text: "KeyVerificationReady"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationCancel + NoticeMessage { + text: "KeyVerificationCancel"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationKey + NoticeMessage { + text: "KeyVerificationKey"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationMac + NoticeMessage { + text: "KeyVerificationMac"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationDone + NoticeMessage { + text: "KeyVerificationDone"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationDone + NoticeMessage { + text: "KeyVerificationDone"; + } + } + DelegateChoice { + roleValue: MtxEvent.KeyVerificationAccept + NoticeMessage { + text: "KeyVerificationAccept"; } } DelegateChoice { diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 8d2fa8a..893325b 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -106,7 +106,7 @@ Rectangle { anchors.fill: parent onClicked: { switch (button.state) { - case "": timelineManager.timeline.cacheMedia(model.data.id); break; + case "": TimelineManager.timeline.cacheMedia(model.data.id); break; case "stopped": media.play(); console.log("play"); button.state = "playing" @@ -127,7 +127,7 @@ Rectangle { } Connections { - target: timelineManager.timeline + target: TimelineManager.timeline onMediaCached: { if (mxcUrl == model.data.url) { media.source = "file://" + cacheUrl diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 36a6d37..43fc281 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -3,6 +3,8 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.2 import QtQuick.Window 2.2 +import im.nheko 1.0 + Item { id: replyComponent @@ -26,7 +28,7 @@ Item { anchors.bottom: replyContainer.bottom width: 4 - color: timelineManager.userColor(reply.modelData.userId, colors.window) + color: TimelineManager.userColor(reply.modelData.userId, colors.window) } Column { @@ -37,7 +39,7 @@ Item { Text { id: userName - text: timelineManager.escapeEmoji(reply.modelData.userName) + text: TimelineManager.escapeEmoji(reply.modelData.userName) color: replyComponent.userColor textFormat: Text.RichText diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index cc2d2da..99ff932 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -1,10 +1,12 @@ import ".." +import im.nheko 1.0 + MatrixText { property string formatted: model.data.formattedBody text: "" + formatted.replace("
", "
")
 	width: parent ? parent.width : undefined
 	height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
 	clip: true
-	font.pointSize: (settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? settings.fontSize * 3 : settings.fontSize
+	font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
 }
diff --git a/resources/qml/device-verification/AwaitingVerificationConfirmation.qml b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
new file mode 100644
index 0000000..cd8ccfd
--- /dev/null
+++ b/resources/qml/device-verification/AwaitingVerificationConfirmation.qml
@@ -0,0 +1,39 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+import im.nheko 1.0
+
+Pane {
+	property string title: qsTr("Awaiting Confirmation")
+	ColumnLayout {
+		spacing: 16
+		Label {
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+			wrapMode: Text.Wrap
+			id: content
+			text: qsTr("Waiting for other side to complete verification.")
+			color:colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		BusyIndicator {
+			Layout.alignment: Qt.AlignHCenter
+		}
+		RowLayout {
+			Button {
+				Layout.alignment: Qt.AlignLeft
+				text: qsTr("Cancel")
+
+				onClicked: { 
+					flow.cancel();
+					dialog.close();
+				}
+			}
+			Item {
+				Layout.fillWidth: true
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/DeviceVerification.qml b/resources/qml/device-verification/DeviceVerification.qml
new file mode 100644
index 0000000..2e8f750
--- /dev/null
+++ b/resources/qml/device-verification/DeviceVerification.qml
@@ -0,0 +1,97 @@
+import QtQuick 2.10
+import QtQuick.Controls 2.10
+import QtQuick.Window 2.10
+
+import im.nheko 1.0
+
+ApplicationWindow {
+	property var flow
+
+	onClosing: TimelineManager.removeVerificationFlow(flow)
+
+	title: stack.currentItem.title
+	id: dialog
+
+	flags: Qt.Dialog
+
+	palette: colors
+
+	height: stack.implicitHeight
+	width: stack.implicitWidth
+
+	StackView {
+		id: stack
+		initialItem: newVerificationRequest
+		implicitWidth: currentItem.implicitWidth
+		implicitHeight: currentItem.implicitHeight
+	}
+
+	Component{
+		id: newVerificationRequest
+		NewVerificationRequest {}
+	}
+
+	Component {
+		id: waiting
+		Waiting {}
+	}
+
+	Component {
+		id: success
+		Success {}
+	}
+
+	Component {
+		id: failed
+		Failed {}
+	}
+
+	Component {
+		id: digitVerification
+		DigitVerification {}
+	}
+
+	Component {
+		id: emojiVerification
+		EmojiVerification {}
+	}
+
+	Item {
+	state: flow.state
+
+	states: [
+		State {
+			name: "PromptStartVerification"
+			StateChangeScript { script: stack.replace(newVerificationRequest) }
+		},
+		State {
+			name: "CompareEmoji"
+			StateChangeScript { script: stack.replace(emojiVerification) }
+		},
+		State {
+			name: "CompareNumber"
+			StateChangeScript { script: stack.replace(digitVerification) }
+		},
+		State {
+			name: "WaitingForKeys"
+			StateChangeScript { script: stack.replace(waiting) }
+		},
+		State {
+			name: "WaitingForOtherToAccept"
+			StateChangeScript { script: stack.replace(waiting) }
+		},
+		State {
+			name: "WaitingForMac"
+			StateChangeScript { script: stack.replace(waiting) }
+		},
+		State {
+			name: "Success"
+			StateChangeScript { script: stack.replace(success) }
+		},
+		State {
+			name: "Failed"
+			StateChangeScript { script: stack.replace(failed); }
+		}
+	]
+}
+}
diff --git a/resources/qml/device-verification/DigitVerification.qml b/resources/qml/device-verification/DigitVerification.qml
new file mode 100644
index 0000000..ff878a5
--- /dev/null
+++ b/resources/qml/device-verification/DigitVerification.qml
@@ -0,0 +1,60 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+import im.nheko 1.0
+
+Pane {
+	property string title: qsTr("Verification Code")
+
+	ColumnLayout {
+		spacing: 16
+		Label {
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+			wrapMode: Text.Wrap
+			text: qsTr("Please verify the following digits. You should see the same numbers on both sides. If they differ, please press 'They do not match!' to abort verification!")
+			color:colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		RowLayout {
+			Layout.alignment: Qt.AlignHCenter
+			Label {
+				font.pixelSize: Qt.application.font.pixelSize * 2
+				text: flow.sasList[0]
+				color:colors.text
+			}
+			Label {
+				font.pixelSize: Qt.application.font.pixelSize * 2
+				text: flow.sasList[1]
+				color:colors.text
+			}
+			Label {
+				font.pixelSize: Qt.application.font.pixelSize * 2
+				text: flow.sasList[2]
+				color:colors.text
+			}
+		}
+		RowLayout {
+			Button {
+				Layout.alignment: Qt.AlignLeft
+				text: qsTr("They do not match!")
+
+				onClicked: {
+					flow.cancel();
+					dialog.close();
+				}
+			}
+			Item {
+				Layout.fillWidth: true
+			}
+			Button {
+				Layout.alignment: Qt.AlignRight
+				text: qsTr("They match!")
+
+				onClicked: flow.next();
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/EmojiElement.qml b/resources/qml/device-verification/EmojiElement.qml
new file mode 100644
index 0000000..7e36459
--- /dev/null
+++ b/resources/qml/device-verification/EmojiElement.qml
@@ -0,0 +1,26 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+Rectangle {
+	color: "red"
+	implicitHeight: Qt.application.font.pixelSize * 4
+	implicitWidth: col.width
+	height: Qt.application.font.pixelSize * 4
+	width: col.width
+	ColumnLayout {
+		id: col
+		anchors.bottom: parent.bottom
+		property var emoji: emojis.mapping[Math.floor(Math.random()*64)]
+		Label {
+			height: font.pixelSize * 2
+			Layout.alignment: Qt.AlignHCenter
+			text: col.emoji.emoji
+			font.pixelSize: Qt.application.font.pixelSize * 2
+		}
+		Label {
+			Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+			text: col.emoji.description
+		}
+	}
+}
diff --git a/resources/qml/device-verification/EmojiVerification.qml b/resources/qml/device-verification/EmojiVerification.qml
new file mode 100644
index 0000000..ed7727a
--- /dev/null
+++ b/resources/qml/device-verification/EmojiVerification.qml
@@ -0,0 +1,140 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+import im.nheko 1.0
+
+Pane {
+	property string title: qsTr("Verification Code")
+
+	ColumnLayout {
+		spacing: 16
+		Label {
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+			wrapMode: Text.Wrap
+			text: qsTr("Please verify the following emoji. You should see the same emoji on both sides. If they differ, please press 'They do not match!' to abort verification!")
+			color:colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		RowLayout {
+			Layout.alignment: Qt.AlignHCenter
+			id: emojis
+			property var mapping: [
+				{"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
+				{"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
+				{"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
+				{"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
+				{"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
+				{"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
+				{"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
+				{"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
+				{"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
+				{"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
+				{"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
+				{"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
+				{"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
+				{"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
+				{"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
+				{"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
+				{"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
+				{"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
+				{"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
+				{"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
+				{"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
+				{"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
+				{"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
+				{"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
+				{"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
+				{"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
+				{"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
+				{"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
+				{"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
+				{"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
+				{"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
+				{"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
+				{"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
+				{"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
+				{"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
+				{"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
+				{"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
+				{"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
+				{"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
+				{"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
+				{"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
+				{"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
+				{"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
+				{"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
+				{"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
+				{"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
+				{"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
+				{"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
+				{"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
+				{"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
+				{"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
+				{"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
+				{"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
+				{"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
+				{"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
+				{"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
+				{"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
+				{"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
+				{"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
+				{"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
+				{"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
+				{"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
+				{"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
+				{"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
+			]
+			Repeater {
+				id: repeater
+				model: 7
+				delegate: Rectangle {
+					color: "transparent"
+					implicitHeight: Qt.application.font.pixelSize * 8
+					implicitWidth: col.width
+					ColumnLayout {
+						id: col
+						Layout.fillWidth: true
+						anchors.bottom: parent.bottom
+						property var emoji: emojis.mapping[flow.sasList[index]]
+						Label {
+							//height: font.pixelSize * 2
+							Layout.alignment: Qt.AlignHCenter
+							text: col.emoji.emoji
+							font.pixelSize: Qt.application.font.pixelSize * 2
+							font.family: Settings.emojiFont
+							color:colors.text
+						}
+						Label {
+							Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom
+							text: col.emoji.description
+							color:colors.text
+						}
+					}
+				}
+			}
+		}
+		RowLayout {
+			Button {
+				Layout.alignment: Qt.AlignLeft
+				text: qsTr("They do not match!")
+
+				onClicked: {  
+					flow.cancel();
+					dialog.close();
+				}
+			}
+			Item {
+				Layout.fillWidth: true
+			}
+			Button {
+				Layout.alignment: Qt.AlignRight
+				text: qsTr("They match!")
+
+				onClicked: flow.next()
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/Failed.qml b/resources/qml/device-verification/Failed.qml
new file mode 100644
index 0000000..fcff789
--- /dev/null
+++ b/resources/qml/device-verification/Failed.qml
@@ -0,0 +1,44 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+import im.nheko 1.0
+
+Pane {
+	property string title: qsTr("Verification failed")
+	ColumnLayout {
+		spacing: 16
+		Text {
+			id: content
+
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+
+			wrapMode: Text.Wrap
+			text: switch (flow.error) {
+				case DeviceVerificationFlow.UnknownMethod: return qsTr("Other client does not support our verification protocol.")
+				case DeviceVerificationFlow.MismatchedCommitment:
+				case DeviceVerificationFlow.MismatchedSAS:
+				case DeviceVerificationFlow.KeyMismatch: return qsTr("Key mismatch detected!")
+				case DeviceVerificationFlow.Timeout: return qsTr("Device verification timed out.")
+				case DeviceVerificationFlow.User: return qsTr("Other party canceled the verification.")
+				case DeviceVerificationFlow.OutOfOrder: return qsTr("Device verification timed out.")
+				default: return "Unknown verification error.";
+			}
+			color:colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		RowLayout {
+			Item {
+				Layout.fillWidth: true
+			}
+			Button {
+				Layout.alignment: Qt.AlignRight
+				text: qsTr("Close")
+
+				onClicked: dialog.close()
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/NewVerificationRequest.qml b/resources/qml/device-verification/NewVerificationRequest.qml
new file mode 100644
index 0000000..bd25bb9
--- /dev/null
+++ b/resources/qml/device-verification/NewVerificationRequest.qml
@@ -0,0 +1,44 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+import im.nheko 1.0
+
+Pane {
+	property string title: flow.sender ? qsTr("Send Device Verification Request") : qsTr("Recieved Device Verification Request")
+
+	ColumnLayout {
+		spacing: 16
+		Label {
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+			wrapMode: Text.Wrap
+			text: flow.sender ?
+			    qsTr("To ensure that no malicious user can eavesdrop on your encrypted communications, you can verify this device.")
+			    : qsTr("The device was requested to be verified")
+			color:colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		RowLayout {
+			Button {
+				Layout.alignment: Qt.AlignLeft
+				text: flow.sender ? qsTr("Cancel") : qsTr("Deny")
+
+				onClicked: { 
+					flow.cancel();
+					dialog.close();
+				}
+			}
+			Item {
+				Layout.fillWidth: true
+			}
+			Button {
+				Layout.alignment: Qt.AlignRight
+				text: flow.sender ? qsTr("Start verification") : qsTr("Accept")
+
+				onClicked: flow.next();
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/Success.qml b/resources/qml/device-verification/Success.qml
new file mode 100644
index 0000000..b17b293
--- /dev/null
+++ b/resources/qml/device-verification/Success.qml
@@ -0,0 +1,31 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+Pane {
+	property string title: qsTr("Successful Verification")
+	ColumnLayout {
+		spacing: 16
+		Label {
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+			wrapMode: Text.Wrap
+			id: content
+			text: qsTr("Verification successful! Both sides verified their devices!")
+			color:colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		RowLayout {
+			Item {
+				Layout.fillWidth: true
+			}
+			Button {
+				Layout.alignment: Qt.AlignRight
+				text: qsTr("Close")
+
+				onClicked: dialog.close();
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/Waiting.qml b/resources/qml/device-verification/Waiting.qml
new file mode 100644
index 0000000..38abf76
--- /dev/null
+++ b/resources/qml/device-verification/Waiting.qml
@@ -0,0 +1,45 @@
+import QtQuick 2.3
+import QtQuick.Controls 2.10
+import QtQuick.Layouts 1.10
+
+import im.nheko 1.0
+
+Pane {
+	property string title: qsTr("Waiting for other party")
+	ColumnLayout {
+		spacing: 16
+		Label {
+			Layout.maximumWidth: 400
+			Layout.fillHeight: true
+			Layout.fillWidth: true
+			wrapMode: Text.Wrap
+			id: content
+			text: switch (flow.state) {
+				case "WaitingForOtherToAccept": return qsTr("Waiting for other side to accept the verification request.")
+				case "WaitingForKeys": return qsTr("Waiting for other side to continue the verification request.")
+				case "WaitingForMac": return qsTr("Waiting for other side to complete the verification request.")
+			}
+
+			color: colors.text
+			verticalAlignment: Text.AlignVCenter
+		}
+		BusyIndicator {
+			Layout.alignment: Qt.AlignHCenter
+			palette: colors
+		}
+		RowLayout {
+			Button {
+				Layout.alignment: Qt.AlignLeft
+				text: qsTr("Cancel")
+
+				onClicked: { 
+					flow.cancel();
+					dialog.close();
+				}
+			}
+			Item {
+				Layout.fillWidth: true
+			}
+		}
+	}
+}
diff --git a/resources/qml/device-verification/sas-emoji.json b/resources/qml/device-verification/sas-emoji.json
new file mode 100644
index 0000000..060fbd4
--- /dev/null
+++ b/resources/qml/device-verification/sas-emoji.json
@@ -0,0 +1,66 @@
+[
+    {"number": 0, "emoji": "🐶", "description": "Dog", "unicode": "U+1F436"},
+    {"number": 1, "emoji": "🐱", "description": "Cat", "unicode": "U+1F431"},
+    {"number": 2, "emoji": "🦁", "description": "Lion", "unicode": "U+1F981"},
+    {"number": 3, "emoji": "🐎", "description": "Horse", "unicode": "U+1F40E"},
+    {"number": 4, "emoji": "🦄", "description": "Unicorn", "unicode": "U+1F984"},
+    {"number": 5, "emoji": "🐷", "description": "Pig", "unicode": "U+1F437"},
+    {"number": 6, "emoji": "🐘", "description": "Elephant", "unicode": "U+1F418"},
+    {"number": 7, "emoji": "🐰", "description": "Rabbit", "unicode": "U+1F430"},
+    {"number": 8, "emoji": "🐼", "description": "Panda", "unicode": "U+1F43C"},
+    {"number": 9, "emoji": "🐓", "description": "Rooster", "unicode": "U+1F413"},
+    {"number": 10, "emoji": "🐧", "description": "Penguin", "unicode": "U+1F427"},
+    {"number": 11, "emoji": "🐢", "description": "Turtle", "unicode": "U+1F422"},
+    {"number": 12, "emoji": "🐟", "description": "Fish", "unicode": "U+1F41F"},
+    {"number": 13, "emoji": "🐙", "description": "Octopus", "unicode": "U+1F419"},
+    {"number": 14, "emoji": "🦋", "description": "Butterfly", "unicode": "U+1F98B"},
+    {"number": 15, "emoji": "🌷", "description": "Flower", "unicode": "U+1F337"},
+    {"number": 16, "emoji": "🌳", "description": "Tree", "unicode": "U+1F333"},
+    {"number": 17, "emoji": "🌵", "description": "Cactus", "unicode": "U+1F335"},
+    {"number": 18, "emoji": "🍄", "description": "Mushroom", "unicode": "U+1F344"},
+    {"number": 19, "emoji": "🌏", "description": "Globe", "unicode": "U+1F30F"},
+    {"number": 20, "emoji": "🌙", "description": "Moon", "unicode": "U+1F319"},
+    {"number": 21, "emoji": "☁️", "description": "Cloud", "unicode": "U+2601U+FE0F"},
+    {"number": 22, "emoji": "🔥", "description": "Fire", "unicode": "U+1F525"},
+    {"number": 23, "emoji": "🍌", "description": "Banana", "unicode": "U+1F34C"},
+    {"number": 24, "emoji": "🍎", "description": "Apple", "unicode": "U+1F34E"},
+    {"number": 25, "emoji": "🍓", "description": "Strawberry", "unicode": "U+1F353"},
+    {"number": 26, "emoji": "🌽", "description": "Corn", "unicode": "U+1F33D"},
+    {"number": 27, "emoji": "🍕", "description": "Pizza", "unicode": "U+1F355"},
+    {"number": 28, "emoji": "🎂", "description": "Cake", "unicode": "U+1F382"},
+    {"number": 29, "emoji": "❤️", "description": "Heart", "unicode": "U+2764U+FE0F"},
+    {"number": 30, "emoji": "😀", "description": "Smiley", "unicode": "U+1F600"},
+    {"number": 31, "emoji": "🤖", "description": "Robot", "unicode": "U+1F916"},
+    {"number": 32, "emoji": "🎩", "description": "Hat", "unicode": "U+1F3A9"},
+    {"number": 33, "emoji": "👓", "description": "Glasses", "unicode": "U+1F453"},
+    {"number": 34, "emoji": "🔧", "description": "Spanner", "unicode": "U+1F527"},
+    {"number": 35, "emoji": "🎅", "description": "Santa", "unicode": "U+1F385"},
+    {"number": 36, "emoji": "👍", "description": "Thumbs Up", "unicode": "U+1F44D"},
+    {"number": 37, "emoji": "☂️", "description": "Umbrella", "unicode": "U+2602U+FE0F"},
+    {"number": 38, "emoji": "⌛", "description": "Hourglass", "unicode": "U+231B"},
+    {"number": 39, "emoji": "⏰", "description": "Clock", "unicode": "U+23F0"},
+    {"number": 40, "emoji": "🎁", "description": "Gift", "unicode": "U+1F381"},
+    {"number": 41, "emoji": "💡", "description": "Light Bulb", "unicode": "U+1F4A1"},
+    {"number": 42, "emoji": "📕", "description": "Book", "unicode": "U+1F4D5"},
+    {"number": 43, "emoji": "✏️", "description": "Pencil", "unicode": "U+270FU+FE0F"},
+    {"number": 44, "emoji": "📎", "description": "Paperclip", "unicode": "U+1F4CE"},
+    {"number": 45, "emoji": "✂️", "description": "Scissors", "unicode": "U+2702U+FE0F"},
+    {"number": 46, "emoji": "🔒", "description": "Lock", "unicode": "U+1F512"},
+    {"number": 47, "emoji": "🔑", "description": "Key", "unicode": "U+1F511"},
+    {"number": 48, "emoji": "🔨", "description": "Hammer", "unicode": "U+1F528"},
+    {"number": 49, "emoji": "☎️", "description": "Telephone", "unicode": "U+260EU+FE0F"},
+    {"number": 50, "emoji": "🏁", "description": "Flag", "unicode": "U+1F3C1"},
+    {"number": 51, "emoji": "🚂", "description": "Train", "unicode": "U+1F682"},
+    {"number": 52, "emoji": "🚲", "description": "Bicycle", "unicode": "U+1F6B2"},
+    {"number": 53, "emoji": "✈️", "description": "Aeroplane", "unicode": "U+2708U+FE0F"},
+    {"number": 54, "emoji": "🚀", "description": "Rocket", "unicode": "U+1F680"},
+    {"number": 55, "emoji": "🏆", "description": "Trophy", "unicode": "U+1F3C6"},
+    {"number": 56, "emoji": "⚽", "description": "Ball", "unicode": "U+26BD"},
+    {"number": 57, "emoji": "🎸", "description": "Guitar", "unicode": "U+1F3B8"},
+    {"number": 58, "emoji": "🎺", "description": "Trumpet", "unicode": "U+1F3BA"},
+    {"number": 59, "emoji": "🔔", "description": "Bell", "unicode": "U+1F514"},
+    {"number": 60, "emoji": "⚓", "description": "Anchor", "unicode": "U+2693"},
+    {"number": 61, "emoji": "🎧", "description": "Headphones", "unicode": "U+1F3A7"},
+    {"number": 62, "emoji": "📁", "description": "Folder", "unicode": "U+1F4C1"},
+    {"number": 63, "emoji": "📌", "description": "Pin", "unicode": "U+1F4CC"}
+]
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index f75221d..cbb77be 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -73,7 +73,7 @@ Popup {
                 contentItem: Text {
                     horizontalAlignment: Text.AlignHCenter
                     verticalAlignment: Text.AlignVCenter
-                    font.family: settings.emojiFont
+                    font.family: Settings.emojiFont
                     
                     font.pixelSize: 36
                     text: model.unicode
@@ -104,7 +104,7 @@ Popup {
                 onClicked: {
                     console.debug("Picked " + model.unicode + "in response to " + emojiPopup.event_id)
                     emojiPopup.close()
-                    timelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
+                    TimelineManager.queueReactionMessage(emojiPopup.event_id, model.unicode)
                 }
             }
 
diff --git a/resources/res.qrc b/resources/res.qrc
index 63a4043..87216e3 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -132,6 +132,7 @@
         qml/TimelineRow.qml
         qml/emoji/EmojiButton.qml
         qml/emoji/EmojiPicker.qml
+        qml/UserProfile.qml
         qml/delegates/MessageDelegate.qml
         qml/delegates/TextMessage.qml
         qml/delegates/NoticeMessage.qml
@@ -141,6 +142,13 @@
         qml/delegates/Pill.qml
         qml/delegates/Placeholder.qml
         qml/delegates/Reply.qml
+        qml/device-verification/Waiting.qml
+        qml/device-verification/DeviceVerification.qml
+        qml/device-verification/DigitVerification.qml
+        qml/device-verification/EmojiVerification.qml
+        qml/device-verification/NewVerificationRequest.qml
+        qml/device-verification/Failed.qml
+        qml/device-verification/Success.qml
     
     
         media/ring.ogg
diff --git a/src/Cache.cpp b/src/Cache.cpp
index 98fe64c..08b6f15 100644
--- a/src/Cache.cpp
+++ b/src/Cache.cpp
@@ -31,8 +31,10 @@
 
 #include "Cache.h"
 #include "Cache_p.h"
+#include "ChatPage.h"
 #include "EventAccessors.h"
 #include "Logging.h"
+#include "MatrixClient.h"
 #include "Olm.h"
 #include "Utils.h"
 
@@ -89,6 +91,7 @@ Q_DECLARE_METATYPE(RoomMember)
 Q_DECLARE_METATYPE(mtx::responses::Timeline)
 Q_DECLARE_METATYPE(RoomSearchResult)
 Q_DECLARE_METATYPE(RoomInfo)
+Q_DECLARE_METATYPE(mtx::responses::QueryKeys)
 
 namespace {
 std::unique_ptr instance_ = nullptr;
@@ -153,6 +156,7 @@ Cache::Cache(const QString &userId, QObject *parent)
   , localUserId_{userId}
 {
         setup();
+        connect(this, &Cache::userKeysUpdate, this, &Cache::updateUserKeys, Qt::QueuedConnection);
 }
 
 void
@@ -368,6 +372,25 @@ Cache::updateOutboundMegolmSession(const std::string &room_id, int message_index
         txn.commit();
 }
 
+void
+Cache::dropOutboundMegolmSession(const std::string &room_id)
+{
+        using namespace mtx::crypto;
+
+        if (!outboundMegolmSessionExists(room_id))
+                return;
+
+        {
+                std::unique_lock lock(session_storage.group_outbound_mtx);
+                session_storage.group_outbound_session_data.erase(room_id);
+                session_storage.group_outbound_sessions.erase(room_id);
+
+                auto txn = lmdb::txn::begin(env_);
+                lmdb::dbi_del(txn, outboundMegolmSessionDb_, lmdb::val(room_id), nullptr);
+                txn.commit();
+        }
+}
+
 void
 Cache::saveOutboundMegolmSession(const std::string &room_id,
                                  const OutboundGroupSessionData &data,
@@ -683,11 +706,14 @@ Cache::nextBatchToken() const
         auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
         lmdb::val token;
 
-        lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
+        auto result = lmdb::dbi_get(txn, syncStateDb_, NEXT_BATCH_KEY, token);
 
         txn.commit();
 
-        return std::string(token.data(), token.size());
+        if (result)
+                return std::string(token.data(), token.size());
+        else
+                return "";
 }
 
 void
@@ -995,6 +1021,8 @@ Cache::saveState(const mtx::responses::Sync &res)
         using namespace mtx::events;
         auto user_id = this->localUserId_.toStdString();
 
+        auto currentBatchToken = nextBatchToken();
+
         auto txn = lmdb::txn::begin(env_);
 
         setNextBatchToken(txn, res.next_batch);
@@ -1012,6 +1040,8 @@ Cache::saveState(const mtx::responses::Sync &res)
                           ev);
         }
 
+        auto userKeyCacheDb = getUserKeysDb(txn);
+
         // Save joined rooms
         for (const auto &room : res.rooms.join) {
                 auto statesdb  = getStatesDb(txn, room.first);
@@ -1085,6 +1115,9 @@ Cache::saveState(const mtx::responses::Sync &res)
 
         savePresence(txn, res.presence);
 
+        markUserKeysOutOfDate(txn, userKeyCacheDb, res.device_lists.changed, currentBatchToken);
+        deleteUserKeys(txn, userKeyCacheDb, res.device_lists.left);
+
         removeLeftRooms(txn, res.rooms.leave);
 
         txn.commit();
@@ -3073,6 +3106,378 @@ Cache::statusMessage(const std::string &user_id)
         return status_msg;
 }
 
+void
+to_json(json &j, const UserKeyCache &info)
+{
+        j["device_keys"]       = info.device_keys;
+        j["master_keys"]       = info.master_keys;
+        j["user_signing_keys"] = info.user_signing_keys;
+        j["self_signing_keys"] = info.self_signing_keys;
+        j["updated_at"]        = info.updated_at;
+        j["last_changed"]      = info.last_changed;
+}
+
+void
+from_json(const json &j, UserKeyCache &info)
+{
+        info.device_keys = j.value("device_keys", std::map{});
+        info.master_keys = j.value("master_keys", mtx::crypto::CrossSigningKeys{});
+        info.user_signing_keys = j.value("user_signing_keys", mtx::crypto::CrossSigningKeys{});
+        info.self_signing_keys = j.value("self_signing_keys", mtx::crypto::CrossSigningKeys{});
+        info.updated_at        = j.value("updated_at", "");
+        info.last_changed      = j.value("last_changed", "");
+}
+
+std::optional
+Cache::userKeys(const std::string &user_id)
+{
+        lmdb::val keys;
+
+        try {
+                auto txn = lmdb::txn::begin(env_, nullptr, MDB_RDONLY);
+                auto db  = getUserKeysDb(txn);
+                auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), keys);
+
+                if (res) {
+                        return json::parse(std::string_view(keys.data(), keys.size()))
+                          .get();
+                } else {
+                        return {};
+                }
+        } catch (std::exception &) {
+                return {};
+        }
+}
+
+void
+Cache::updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
+{
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getUserKeysDb(txn);
+
+        std::map updates;
+
+        for (const auto &[user, keys] : keyQuery.device_keys)
+                updates[user].device_keys = keys;
+        for (const auto &[user, keys] : keyQuery.master_keys)
+                updates[user].master_keys = keys;
+        for (const auto &[user, keys] : keyQuery.user_signing_keys)
+                updates[user].user_signing_keys = keys;
+        for (const auto &[user, keys] : keyQuery.self_signing_keys)
+                updates[user].self_signing_keys = keys;
+
+        for (auto &[user, update] : updates) {
+                lmdb::val oldKeys;
+                auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys);
+
+                if (res) {
+                        auto last_changed =
+                          json::parse(std::string_view(oldKeys.data(), oldKeys.size()))
+                            .get()
+                            .last_changed;
+                        // skip if we are tracking this and expect it to be up to date with the last
+                        // sync token
+                        if (!last_changed.empty() && last_changed != sync_token)
+                                continue;
+                }
+                lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(update).dump()));
+        }
+
+        txn.commit();
+
+        std::map tmp;
+        const auto local_user = utils::localUser().toStdString();
+
+        {
+                std::unique_lock lock(verification_storage.verification_storage_mtx);
+                for (auto &[user_id, update] : updates) {
+                        (void)update;
+                        if (user_id == local_user) {
+                                std::swap(tmp, verification_storage.status);
+                        } else {
+                                verification_storage.status.erase(user_id);
+                        }
+                }
+        }
+        for (auto &[user_id, update] : updates) {
+                (void)update;
+                if (user_id == local_user) {
+                        for (const auto &[user, status] : tmp) {
+                                (void)status;
+                                emit verificationStatusChanged(user);
+                        }
+                } else {
+                        emit verificationStatusChanged(user_id);
+                }
+        }
+}
+
+void
+Cache::deleteUserKeys(lmdb::txn &txn, lmdb::dbi &db, const std::vector &user_ids)
+{
+        for (const auto &user_id : user_ids)
+                lmdb::dbi_del(txn, db, lmdb::val(user_id), nullptr);
+}
+
+void
+Cache::markUserKeysOutOfDate(lmdb::txn &txn,
+                             lmdb::dbi &db,
+                             const std::vector &user_ids,
+                             const std::string &sync_token)
+{
+        mtx::requests::QueryKeys query;
+        query.token = sync_token;
+
+        for (const auto &user : user_ids) {
+                lmdb::val oldKeys;
+                auto res = lmdb::dbi_get(txn, db, lmdb::val(user), oldKeys);
+
+                if (!res)
+                        continue;
+
+                auto cacheEntry =
+                  json::parse(std::string_view(oldKeys.data(), oldKeys.size())).get();
+                cacheEntry.last_changed = sync_token;
+                lmdb::dbi_put(txn, db, lmdb::val(user), lmdb::val(json(cacheEntry).dump()));
+
+                query.device_keys[user] = {};
+        }
+
+        if (!query.device_keys.empty())
+                http::client()->query_keys(query,
+                                           [this, sync_token](const mtx::responses::QueryKeys &keys,
+                                                              mtx::http::RequestErr err) {
+                                                   if (err) {
+                                                           nhlog::net()->warn(
+                                                             "failed to query device keys: {} {}",
+                                                             err->matrix_error.error,
+                                                             static_cast(err->status_code));
+                                                           return;
+                                                   }
+
+                                                   emit userKeysUpdate(sync_token, keys);
+                                           });
+}
+
+void
+to_json(json &j, const VerificationCache &info)
+{
+        j["device_verified"] = info.device_verified;
+        j["device_blocked"]  = info.device_blocked;
+}
+
+void
+from_json(const json &j, VerificationCache &info)
+{
+        info.device_verified = j.at("device_verified").get>();
+        info.device_blocked  = j.at("device_blocked").get>();
+}
+
+std::optional
+Cache::verificationCache(const std::string &user_id)
+{
+        lmdb::val verifiedVal;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getVerificationDb(txn);
+
+        try {
+                VerificationCache verified_state;
+                auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), verifiedVal);
+                if (res) {
+                        verified_state =
+                          json::parse(std::string_view(verifiedVal.data(), verifiedVal.size()));
+                        return verified_state;
+                } else {
+                        return {};
+                }
+        } catch (std::exception &) {
+                return {};
+        }
+}
+
+void
+Cache::markDeviceVerified(const std::string &user_id, const std::string &key)
+{
+        lmdb::val val;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getVerificationDb(txn);
+
+        try {
+                VerificationCache verified_state;
+                auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
+                if (res) {
+                        verified_state = json::parse(std::string_view(val.data(), val.size()));
+                }
+
+                for (const auto &device : verified_state.device_verified)
+                        if (device == key)
+                                return;
+
+                verified_state.device_verified.push_back(key);
+                lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
+                txn.commit();
+        } catch (std::exception &) {
+        }
+
+        const auto local_user = utils::localUser().toStdString();
+        std::map tmp;
+        {
+                std::unique_lock lock(verification_storage.verification_storage_mtx);
+                if (user_id == local_user) {
+                        std::swap(tmp, verification_storage.status);
+                } else {
+                        verification_storage.status.erase(user_id);
+                }
+        }
+        if (user_id == local_user) {
+                for (const auto &[user, status] : tmp) {
+                        (void)status;
+                        emit verificationStatusChanged(user);
+                }
+        } else {
+                emit verificationStatusChanged(user_id);
+        }
+}
+
+void
+Cache::markDeviceUnverified(const std::string &user_id, const std::string &key)
+{
+        lmdb::val val;
+
+        auto txn = lmdb::txn::begin(env_);
+        auto db  = getVerificationDb(txn);
+
+        try {
+                VerificationCache verified_state;
+                auto res = lmdb::dbi_get(txn, db, lmdb::val(user_id), val);
+                if (res) {
+                        verified_state = json::parse(std::string_view(val.data(), val.size()));
+                }
+
+                verified_state.device_verified.erase(
+                  std::remove(verified_state.device_verified.begin(),
+                              verified_state.device_verified.end(),
+                              key),
+                  verified_state.device_verified.end());
+
+                lmdb::dbi_put(txn, db, lmdb::val(user_id), lmdb::val(json(verified_state).dump()));
+                txn.commit();
+        } catch (std::exception &) {
+        }
+
+        const auto local_user = utils::localUser().toStdString();
+        std::map tmp;
+        {
+                std::unique_lock lock(verification_storage.verification_storage_mtx);
+                if (user_id == local_user) {
+                        std::swap(tmp, verification_storage.status);
+                } else {
+                        verification_storage.status.erase(user_id);
+                }
+        }
+        if (user_id == local_user) {
+                for (const auto &[user, status] : tmp) {
+                        (void)status;
+                        emit verificationStatusChanged(user);
+                }
+        } else {
+                emit verificationStatusChanged(user_id);
+        }
+}
+
+VerificationStatus
+Cache::verificationStatus(const std::string &user_id)
+{
+        std::unique_lock lock(verification_storage.verification_storage_mtx);
+        if (verification_storage.status.count(user_id))
+                return verification_storage.status.at(user_id);
+
+        VerificationStatus status;
+
+        if (auto verifCache = verificationCache(user_id)) {
+                status.verified_devices = verifCache->device_verified;
+        }
+
+        const auto local_user = utils::localUser().toStdString();
+
+        if (user_id == local_user)
+                status.verified_devices.push_back(http::client()->device_id());
+
+        verification_storage.status[user_id] = status;
+
+        auto verifyAtLeastOneSig = [](const auto &toVerif,
+                                      const std::map &keys,
+                                      const std::string &keyOwner) {
+                if (!toVerif.signatures.count(keyOwner))
+                        return false;
+
+                for (const auto &[key_id, signature] : toVerif.signatures.at(keyOwner)) {
+                        if (!keys.count(key_id))
+                                continue;
+
+                        if (mtx::crypto::ed25519_verify_signature(
+                              keys.at(key_id), json(toVerif), signature))
+                                return true;
+                }
+                return false;
+        };
+
+        try {
+                // for local user verify this device_key -> our master_key -> our self_signing_key
+                // -> our device_keys
+                //
+                // for other user verify this device_key -> our master_key -> our user_signing_key
+                // -> their master_key -> their self_signing_key -> their device_keys
+                //
+                // This means verifying the other user adds 2 extra steps,verifying our user_signing
+                // key and their master key
+                auto ourKeys   = userKeys(local_user);
+                auto theirKeys = userKeys(user_id);
+                if (!ourKeys || !theirKeys)
+                        return status;
+
+                if (!mtx::crypto::ed25519_verify_signature(
+                      olm::client()->identity_keys().ed25519,
+                      json(ourKeys->master_keys),
+                      ourKeys->master_keys.signatures.at(local_user)
+                        .at("ed25519:" + http::client()->device_id())))
+                        return status;
+
+                auto master_keys = ourKeys->master_keys.keys;
+
+                if (user_id != local_user) {
+                        if (!verifyAtLeastOneSig(
+                              ourKeys->user_signing_keys, master_keys, local_user))
+                                return status;
+
+                        if (!verifyAtLeastOneSig(
+                              theirKeys->master_keys, ourKeys->user_signing_keys.keys, local_user))
+                                return status;
+
+                        master_keys = theirKeys->master_keys.keys;
+                }
+
+                status.user_verified = true;
+
+                if (!verifyAtLeastOneSig(theirKeys->self_signing_keys, master_keys, user_id))
+                        return status;
+
+                for (const auto &[device, device_key] : theirKeys->device_keys) {
+                        (void)device;
+                        if (verifyAtLeastOneSig(
+                              device_key, theirKeys->self_signing_keys.keys, user_id))
+                                status.verified_devices.push_back(device_key.device_id);
+                }
+
+                verification_storage.status[user_id] = status;
+                return status;
+        } catch (std::exception &) {
+                return status;
+        }
+}
+
 void
 to_json(json &j, const RoomInfo &info)
 {
@@ -3195,6 +3600,7 @@ init(const QString &user_id)
         qRegisterMetaType>();
         qRegisterMetaType>();
         qRegisterMetaType>();
+        qRegisterMetaType();
 
         instance_ = std::make_unique(user_id);
 }
@@ -3262,6 +3668,37 @@ populateMembers()
         instance_->populateMembers();
 }
 
+// user cache stores user keys
+std::optional
+userKeys(const std::string &user_id)
+{
+        return instance_->userKeys(user_id);
+}
+void
+updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery)
+{
+        instance_->updateUserKeys(sync_token, keyQuery);
+}
+
+// device & user verification cache
+std::optional
+verificationStatus(const std::string &user_id)
+{
+        return instance_->verificationStatus(user_id);
+}
+
+void
+markDeviceVerified(const std::string &user_id, const std::string &device)
+{
+        instance_->markDeviceVerified(user_id, device);
+}
+
+void
+markDeviceUnverified(const std::string &user_id, const std::string &device)
+{
+        instance_->markDeviceUnverified(user_id, device);
+}
+
 std::vector
 joinedRooms()
 {
@@ -3595,6 +4032,11 @@ updateOutboundMegolmSession(const std::string &room_id, int message_index)
 {
         instance_->updateOutboundMegolmSession(room_id, message_index);
 }
+void
+dropOutboundMegolmSession(const std::string &room_id)
+{
+        instance_->dropOutboundMegolmSession(room_id);
+}
 
 void
 importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys)
diff --git a/src/Cache.h b/src/Cache.h
index b527562..cd96708 100644
--- a/src/Cache.h
+++ b/src/Cache.h
@@ -60,6 +60,20 @@ presenceState(const std::string &user_id);
 std::string
 statusMessage(const std::string &user_id);
 
+// user cache stores user keys
+std::optional
+userKeys(const std::string &user_id);
+void
+updateUserKeys(const std::string &sync_token, const mtx::responses::QueryKeys &keyQuery);
+
+// device & user verification cache
+std::optional
+verificationStatus(const std::string &user_id);
+void
+markDeviceVerified(const std::string &user_id, const std::string &device);
+void
+markDeviceUnverified(const std::string &user_id, const std::string &device);
+
 //! Load saved data for the display names & avatars.
 void
 populateMembers();
@@ -255,6 +269,8 @@ bool
 outboundMegolmSessionExists(const std::string &room_id) noexcept;
 void
 updateOutboundMegolmSession(const std::string &room_id, int message_index);
+void
+dropOutboundMegolmSession(const std::string &room_id);
 
 void
 importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
diff --git a/src/CacheCryptoStructs.h b/src/CacheCryptoStructs.h
index 14c9c86..935d649 100644
--- a/src/CacheCryptoStructs.h
+++ b/src/CacheCryptoStructs.h
@@ -65,3 +65,53 @@ struct OlmSessionStorage
         std::mutex group_outbound_mtx;
         std::mutex group_inbound_mtx;
 };
+
+//! Verification status of a single user
+struct VerificationStatus
+{
+        //! True, if the users master key is verified
+        bool user_verified = false;
+        //! List of all devices marked as verified
+        std::vector verified_devices;
+};
+
+//! In memory cache of verification status
+struct VerificationStorage
+{
+        //! mapping of user to verification status
+        std::map status;
+        std::mutex verification_storage_mtx;
+};
+
+// this will store the keys of the user with whom a encrypted room is shared with
+struct UserKeyCache
+{
+        //! Device id to device keys
+        std::map device_keys;
+        //! corss signing keys
+        mtx::crypto::CrossSigningKeys master_keys, user_signing_keys, self_signing_keys;
+        //! Sync token when nheko last fetched the keys
+        std::string updated_at;
+        //! Sync token when the keys last changed. updated != last_changed means they are outdated.
+        std::string last_changed;
+};
+
+void
+to_json(nlohmann::json &j, const UserKeyCache &info);
+void
+from_json(const nlohmann::json &j, UserKeyCache &info);
+
+// the reason these are stored in a seperate cache rather than storing it in the user cache is
+// UserKeyCache stores only keys of users with which encrypted room is shared
+struct VerificationCache
+{
+        //! list of verified device_ids with device-verification
+        std::vector device_verified;
+        //! list of devices the user blocks
+        std::vector device_blocked;
+};
+
+void
+to_json(nlohmann::json &j, const VerificationCache &info);
+void
+from_json(const nlohmann::json &j, VerificationCache &info);
diff --git a/src/Cache_p.h b/src/Cache_p.h
index c57995a..b3f4c58 100644
--- a/src/Cache_p.h
+++ b/src/Cache_p.h
@@ -54,6 +54,23 @@ public:
         mtx::presence::PresenceState presenceState(const std::string &user_id);
         std::string statusMessage(const std::string &user_id);
 
+        // user cache stores user keys
+        std::optional userKeys(const std::string &user_id);
+        void updateUserKeys(const std::string &sync_token,
+                            const mtx::responses::QueryKeys &keyQuery);
+        void markUserKeysOutOfDate(lmdb::txn &txn,
+                                   lmdb::dbi &db,
+                                   const std::vector &user_ids,
+                                   const std::string &sync_token);
+        void deleteUserKeys(lmdb::txn &txn,
+                            lmdb::dbi &db,
+                            const std::vector &user_ids);
+
+        // device & user verification cache
+        VerificationStatus verificationStatus(const std::string &user_id);
+        void markDeviceVerified(const std::string &user_id, const std::string &device);
+        void markDeviceUnverified(const std::string &user_id, const std::string &device);
+
         static void removeDisplayName(const QString &room_id, const QString &user_id);
         static void removeAvatarUrl(const QString &room_id, const QString &user_id);
 
@@ -233,6 +250,7 @@ public:
         OutboundGroupSessionDataRef getOutboundMegolmSession(const std::string &room_id);
         bool outboundMegolmSessionExists(const std::string &room_id) noexcept;
         void updateOutboundMegolmSession(const std::string &room_id, int message_index);
+        void dropOutboundMegolmSession(const std::string &room_id);
 
         void importSessionKeys(const mtx::crypto::ExportedSessionKeys &keys);
         mtx::crypto::ExportedSessionKeys exportSessionKeys();
@@ -262,6 +280,9 @@ signals:
         void newReadReceipts(const QString &room_id, const std::vector &event_ids);
         void roomReadStatus(const std::map &status);
         void removeNotification(const QString &room_id, const QString &event_id);
+        void userKeysUpdate(const std::string &sync_token,
+                            const mtx::responses::QueryKeys &keyQuery);
+        void verificationStatusChanged(const std::string &userid);
 
 private:
         //! Save an invited room.
@@ -527,6 +548,16 @@ private:
                 return lmdb::dbi::open(txn, "presence", MDB_CREATE);
         }
 
+        lmdb::dbi getUserKeysDb(lmdb::txn &txn)
+        {
+                return lmdb::dbi::open(txn, "user_key", MDB_CREATE);
+        }
+
+        lmdb::dbi getVerificationDb(lmdb::txn &txn)
+        {
+                return lmdb::dbi::open(txn, "verified", MDB_CREATE);
+        }
+
         //! Retrieves or creates the database that stores the open OLM sessions between our device
         //! and the given curve25519 key which represents another device.
         //!
@@ -545,6 +576,8 @@ private:
                 return QString::fromStdString(event.state_key);
         }
 
+        std::optional verificationCache(const std::string &user_id);
+
         void setNextBatchToken(lmdb::txn &txn, const std::string &token);
         void setNextBatchToken(lmdb::txn &txn, const QString &token);
 
@@ -569,6 +602,7 @@ private:
         static QHash AvatarUrls;
 
         OlmSessionStorage session_storage;
+        VerificationStorage verification_storage;
 };
 
 namespace cache {
diff --git a/src/ChatPage.cpp b/src/ChatPage.cpp
index 59ae7ef..8e93c0f 100644
--- a/src/ChatPage.cpp
+++ b/src/ChatPage.cpp
@@ -26,6 +26,7 @@
 #include "Cache.h"
 #include "Cache_p.h"
 #include "ChatPage.h"
+#include "DeviceVerificationFlow.h"
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MainWindow.h"
@@ -159,6 +160,10 @@ ChatPage::ChatPage(QSharedPointer userSettings, QWidget *parent)
                 view_manager_,
                 &TimelineViewManager::clearCurrentRoomTimeline);
 
+        connect(text_input_, &TextInputWidget::rotateMegolmSession, this, [this]() {
+                cache::dropOutboundMegolmSession(current_room_.toStdString());
+        });
+
         connect(
           new QShortcut(QKeySequence("Ctrl+Down"), this), &QShortcut::activated, this, [this]() {
                   if (isVisible())
@@ -1456,6 +1461,46 @@ ChatPage::initiateLogout()
         emit showOverlayProgressBar();
 }
 
+void
+ChatPage::query_keys(const std::string &user_id,
+                     std::function cb)
+{
+        auto cache_ = cache::userKeys(user_id);
+
+        if (cache_.has_value()) {
+                if (!cache_->updated_at.empty() && cache_->updated_at == cache_->last_changed) {
+                        cb(cache_.value(), {});
+                        return;
+                }
+        }
+
+        mtx::requests::QueryKeys req;
+        req.device_keys[user_id] = {};
+
+        std::string last_changed;
+        if (cache_)
+                last_changed = cache_->last_changed;
+        req.token = last_changed;
+
+        http::client()->query_keys(req,
+                                   [cb, user_id, last_changed](const mtx::responses::QueryKeys &res,
+                                                               mtx::http::RequestErr err) {
+                                           if (err) {
+                                                   nhlog::net()->warn(
+                                                     "failed to query device keys: {},{}",
+                                                     err->matrix_error.errcode,
+                                                     static_cast(err->status_code));
+                                                   cb({}, err);
+                                                   return;
+                                           }
+
+                                           cache::updateUserKeys(last_changed, res);
+
+                                           auto keys = cache::userKeys(user_id);
+                                           cb(keys.value_or(UserKeyCache{}), err);
+                                   });
+}
+
 template
 void
 ChatPage::connectCallMessage()
diff --git a/src/ChatPage.h b/src/ChatPage.h
index 6b3916a..f0e12ab 100644
--- a/src/ChatPage.h
+++ b/src/ChatPage.h
@@ -19,6 +19,7 @@
 
 #include 
 #include 
+#include 
 #include 
 
 #include 
@@ -34,6 +35,7 @@
 #include 
 #include 
 
+#include "CacheCryptoStructs.h"
 #include "CacheStructs.h"
 #include "CallManager.h"
 #include "CommunitiesList.h"
@@ -50,6 +52,8 @@ class TextInputWidget;
 class TimelineViewManager;
 class UserInfoWidget;
 class UserSettings;
+class NotificationsManager;
+class TimelineModel;
 
 constexpr int CONSENSUS_TIMEOUT      = 1000;
 constexpr int SHOW_CONTENT_TIMEOUT   = 3000;
@@ -85,6 +89,8 @@ public:
         //! Show the room/group list (if it was visible).
         void showSideBars();
         void initiateLogout();
+        void query_keys(const std::string &req,
+                        std::function cb);
         void focusMessageInput();
 
         QString status() const;
@@ -161,6 +167,24 @@ signals:
         void themeChanged();
         void decryptSidebarChanged();
 
+        //! Signals for device verificaiton
+        void receivedDeviceVerificationAccept(
+          const mtx::events::msg::KeyVerificationAccept &message);
+        void receivedDeviceVerificationRequest(
+          const mtx::events::msg::KeyVerificationRequest &message,
+          std::string sender);
+        void receivedRoomDeviceVerificationRequest(
+          const mtx::events::RoomEvent &message,
+          TimelineModel *model);
+        void receivedDeviceVerificationCancel(
+          const mtx::events::msg::KeyVerificationCancel &message);
+        void receivedDeviceVerificationKey(const mtx::events::msg::KeyVerificationKey &message);
+        void receivedDeviceVerificationMac(const mtx::events::msg::KeyVerificationMac &message);
+        void receivedDeviceVerificationStart(const mtx::events::msg::KeyVerificationStart &message,
+                                             std::string sender);
+        void receivedDeviceVerificationReady(const mtx::events::msg::KeyVerificationReady &message);
+        void receivedDeviceVerificationDone(const mtx::events::msg::KeyVerificationDone &message);
+
 private slots:
         void showUnreadMessageNotification(int count);
         void logout();
diff --git a/src/DeviceVerificationFlow.cpp b/src/DeviceVerificationFlow.cpp
new file mode 100644
index 0000000..aa1a960
--- /dev/null
+++ b/src/DeviceVerificationFlow.cpp
@@ -0,0 +1,794 @@
+#include "DeviceVerificationFlow.h"
+
+#include "Cache.h"
+#include "ChatPage.h"
+#include "Logging.h"
+#include "timeline/TimelineModel.h"
+
+#include 
+#include 
+#include 
+
+static constexpr int TIMEOUT = 2 * 60 * 1000; // 2 minutes
+
+namespace msgs = mtx::events::msg;
+
+static mtx::events::msg::KeyVerificationMac
+key_verification_mac(mtx::crypto::SAS *sas,
+                     mtx::identifiers::User sender,
+                     const std::string &senderDevice,
+                     mtx::identifiers::User receiver,
+                     const std::string &receiverDevice,
+                     const std::string &transactionId,
+                     std::map keys);
+
+DeviceVerificationFlow::DeviceVerificationFlow(QObject *,
+                                               DeviceVerificationFlow::Type flow_type,
+                                               TimelineModel *model,
+                                               QString userID,
+                                               QString deviceId_)
+  : sender(false)
+  , type(flow_type)
+  , deviceId(deviceId_)
+  , model_(model)
+{
+        timeout = new QTimer(this);
+        timeout->setSingleShot(true);
+        this->sas           = olm::client()->sas_init();
+        this->isMacVerified = false;
+
+        auto user_id   = userID.toStdString();
+        this->toClient = mtx::identifiers::parse(user_id);
+        ChatPage::instance()->query_keys(
+          user_id, [user_id, this](const UserKeyCache &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to query device keys: {},{}",
+                                             err->matrix_error.errcode,
+                                             static_cast(err->status_code));
+                          return;
+                  }
+
+                  if (!this->deviceId.isEmpty() &&
+                      (res.device_keys.find(deviceId.toStdString()) == res.device_keys.end())) {
+                          nhlog::net()->warn("no devices retrieved {}", user_id);
+                          return;
+                  }
+
+                  this->their_keys = res;
+          });
+
+        ChatPage::instance()->query_keys(
+          http::client()->user_id().to_string(),
+          [this](const UserKeyCache &res, mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to query device keys: {},{}",
+                                             err->matrix_error.errcode,
+                                             static_cast(err->status_code));
+                          return;
+                  }
+
+                  if (res.master_keys.keys.empty())
+                          return;
+
+                  if (auto status =
+                        cache::verificationStatus(http::client()->user_id().to_string());
+                      status && status->user_verified)
+                          this->our_trusted_master_key = res.master_keys.keys.begin()->second;
+          });
+
+        if (model) {
+                connect(this->model_,
+                        &TimelineModel::updateFlowEventId,
+                        this,
+                        [this](std::string event_id_) {
+                                this->relation.rel_type = mtx::common::RelationType::Reference;
+                                this->relation.event_id = event_id_;
+                                this->transaction_id    = event_id_;
+                        });
+        }
+
+        connect(timeout, &QTimer::timeout, this, [this]() {
+                if (state_ != Success && state_ != Failed)
+                        this->cancelVerification(DeviceVerificationFlow::Error::Timeout);
+        });
+
+        connect(ChatPage::instance(),
+                &ChatPage::receivedDeviceVerificationStart,
+                this,
+                &DeviceVerificationFlow::handleStartMessage);
+        connect(ChatPage::instance(),
+                &ChatPage::receivedDeviceVerificationAccept,
+                this,
+                [this](const mtx::events::msg::KeyVerificationAccept &msg) {
+                        if (msg.transaction_id.has_value()) {
+                                if (msg.transaction_id.value() != this->transaction_id)
+                                        return;
+                        } else if (msg.relates_to.has_value()) {
+                                if (msg.relates_to.value().event_id != this->relation.event_id)
+                                        return;
+                        }
+                        if ((msg.key_agreement_protocol == "curve25519-hkdf-sha256") &&
+                            (msg.hash == "sha256") &&
+                            (msg.message_authentication_code == "hkdf-hmac-sha256")) {
+                                this->commitment = msg.commitment;
+                                if (std::find(msg.short_authentication_string.begin(),
+                                              msg.short_authentication_string.end(),
+                                              mtx::events::msg::SASMethods::Emoji) !=
+                                    msg.short_authentication_string.end()) {
+                                        this->method = mtx::events::msg::SASMethods::Emoji;
+                                } else {
+                                        this->method = mtx::events::msg::SASMethods::Decimal;
+                                }
+                                this->mac_method = msg.message_authentication_code;
+                                this->sendVerificationKey();
+                        } else {
+                                this->cancelVerification(
+                                  DeviceVerificationFlow::Error::UnknownMethod);
+                        }
+                });
+
+        connect(ChatPage::instance(),
+                &ChatPage::receivedDeviceVerificationCancel,
+                this,
+                [this](const mtx::events::msg::KeyVerificationCancel &msg) {
+                        if (msg.transaction_id.has_value()) {
+                                if (msg.transaction_id.value() != this->transaction_id)
+                                        return;
+                        } else if (msg.relates_to.has_value()) {
+                                if (msg.relates_to.value().event_id != this->relation.event_id)
+                                        return;
+                        }
+                        error_ = User;
+                        emit errorChanged();
+                        setState(Failed);
+                });
+
+        connect(ChatPage::instance(),
+                &ChatPage::receivedDeviceVerificationKey,
+                this,
+                [this](const mtx::events::msg::KeyVerificationKey &msg) {
+                        if (msg.transaction_id.has_value()) {
+                                if (msg.transaction_id.value() != this->transaction_id)
+                                        return;
+                        } else if (msg.relates_to.has_value()) {
+                                if (msg.relates_to.value().event_id != this->relation.event_id)
+                                        return;
+                        }
+
+                        if (sender) {
+                                if (state_ != WaitingForOtherToAccept) {
+                                        this->cancelVerification(OutOfOrder);
+                                        return;
+                                }
+                        } else {
+                                if (state_ != WaitingForKeys) {
+                                        this->cancelVerification(OutOfOrder);
+                                        return;
+                                }
+                        }
+
+                        this->sas->set_their_key(msg.key);
+                        std::string info;
+                        if (this->sender == true) {
+                                info = "MATRIX_KEY_VERIFICATION_SAS|" +
+                                       http::client()->user_id().to_string() + "|" +
+                                       http::client()->device_id() + "|" + this->sas->public_key() +
+                                       "|" + this->toClient.to_string() + "|" +
+                                       this->deviceId.toStdString() + "|" + msg.key + "|" +
+                                       this->transaction_id;
+                        } else {
+                                info = "MATRIX_KEY_VERIFICATION_SAS|" + this->toClient.to_string() +
+                                       "|" + this->deviceId.toStdString() + "|" + msg.key + "|" +
+                                       http::client()->user_id().to_string() + "|" +
+                                       http::client()->device_id() + "|" + this->sas->public_key() +
+                                       "|" + this->transaction_id;
+                        }
+
+                        nhlog::ui()->info("Info is: '{}'", info);
+
+                        if (this->sender == false) {
+                                this->sendVerificationKey();
+                        } else {
+                                if (this->commitment !=
+                                    mtx::crypto::bin2base64_unpadded(
+                                      mtx::crypto::sha256(msg.key + this->canonical_json.dump()))) {
+                                        this->cancelVerification(
+                                          DeviceVerificationFlow::Error::MismatchedCommitment);
+                                        return;
+                                }
+                        }
+
+                        if (this->method == mtx::events::msg::SASMethods::Emoji) {
+                                this->sasList = this->sas->generate_bytes_emoji(info);
+                                setState(CompareEmoji);
+                        } else if (this->method == mtx::events::msg::SASMethods::Decimal) {
+                                this->sasList = this->sas->generate_bytes_decimal(info);
+                                setState(CompareNumber);
+                        }
+                });
+
+        connect(
+          ChatPage::instance(),
+          &ChatPage::receivedDeviceVerificationMac,
+          this,
+          [this](const mtx::events::msg::KeyVerificationMac &msg) {
+                  if (msg.transaction_id.has_value()) {
+                          if (msg.transaction_id.value() != this->transaction_id)
+                                  return;
+                  } else if (msg.relates_to.has_value()) {
+                          if (msg.relates_to.value().event_id != this->relation.event_id)
+                                  return;
+                  }
+
+                  std::map key_list;
+                  std::string key_string;
+                  for (const auto &mac : msg.mac) {
+                          for (const auto &[deviceid, key] : their_keys.device_keys) {
+                                  (void)deviceid;
+                                  if (key.keys.count(mac.first))
+                                          key_list[mac.first] = key.keys.at(mac.first);
+                          }
+
+                          if (their_keys.master_keys.keys.count(mac.first))
+                                  key_list[mac.first] = their_keys.master_keys.keys[mac.first];
+                          if (their_keys.user_signing_keys.keys.count(mac.first))
+                                  key_list[mac.first] =
+                                    their_keys.user_signing_keys.keys[mac.first];
+                          if (their_keys.self_signing_keys.keys.count(mac.first))
+                                  key_list[mac.first] =
+                                    their_keys.self_signing_keys.keys[mac.first];
+                  }
+                  auto macs = key_verification_mac(sas.get(),
+                                                   toClient,
+                                                   this->deviceId.toStdString(),
+                                                   http::client()->user_id(),
+                                                   http::client()->device_id(),
+                                                   this->transaction_id,
+                                                   key_list);
+
+                  for (const auto &[key, mac] : macs.mac) {
+                          if (mac != msg.mac.at(key)) {
+                                  this->cancelVerification(
+                                    DeviceVerificationFlow::Error::KeyMismatch);
+                                  return;
+                          }
+                  }
+
+                  if (msg.keys == macs.keys) {
+                          mtx::requests::KeySignaturesUpload req;
+                          if (utils::localUser().toStdString() == this->toClient.to_string()) {
+                                  // self verification, sign master key with device key, if we
+                                  // verified it
+                                  for (const auto &mac : msg.mac) {
+                                          if (their_keys.master_keys.keys.count(mac.first)) {
+                                                  json j = their_keys.master_keys;
+                                                  j.erase("signatures");
+                                                  j.erase("unsigned");
+                                                  mtx::crypto::CrossSigningKeys master_key = j;
+                                                  master_key
+                                                    .signatures[utils::localUser().toStdString()]
+                                                               ["ed25519:" +
+                                                                http::client()->device_id()] =
+                                                    olm::client()->sign_message(j.dump());
+                                                  req.signatures[utils::localUser().toStdString()]
+                                                                [master_key.keys.at(mac.first)] =
+                                                    master_key;
+                                          }
+                                  }
+                                  // TODO(Nico): Sign their device key with self signing key
+                          } else {
+                                  // TODO(Nico): Sign their master key with user signing key
+                          }
+
+                          if (!req.signatures.empty()) {
+                                  http::client()->keys_signatures_upload(
+                                    req,
+                                    [](const mtx::responses::KeySignaturesUpload &res,
+                                       mtx::http::RequestErr err) {
+                                            if (err) {
+                                                    nhlog::net()->error(
+                                                      "failed to upload signatures: {},{}",
+                                                      err->matrix_error.errcode,
+                                                      static_cast(err->status_code));
+                                            }
+
+                                            for (const auto &[user_id, tmp] : res.errors)
+                                                    for (const auto &[key_id, e] : tmp)
+                                                            nhlog::net()->error(
+                                                              "signature error for user {} and key "
+                                                              "id {}: {}, {}",
+                                                              user_id,
+                                                              key_id,
+                                                              e.errcode,
+                                                              e.error);
+                                    });
+                          }
+
+                          this->isMacVerified = true;
+                          this->acceptDevice();
+                  } else {
+                          this->cancelVerification(DeviceVerificationFlow::Error::KeyMismatch);
+                  }
+          });
+
+        connect(ChatPage::instance(),
+                &ChatPage::receivedDeviceVerificationReady,
+                this,
+                [this](const mtx::events::msg::KeyVerificationReady &msg) {
+                        if (!sender) {
+                                if (msg.from_device != http::client()->device_id()) {
+                                        error_ = User;
+                                        emit errorChanged();
+                                        setState(Failed);
+                                }
+
+                                return;
+                        }
+
+                        if (msg.transaction_id.has_value()) {
+                                if (msg.transaction_id.value() != this->transaction_id)
+                                        return;
+                        } else if ((msg.relates_to.has_value() && sender)) {
+                                if (msg.relates_to.value().event_id != this->relation.event_id)
+                                        return;
+                                else {
+                                        this->deviceId = QString::fromStdString(msg.from_device);
+                                }
+                        }
+                        this->startVerificationRequest();
+                });
+
+        connect(ChatPage::instance(),
+                &ChatPage::receivedDeviceVerificationDone,
+                this,
+                [this](const mtx::events::msg::KeyVerificationDone &msg) {
+                        if (msg.transaction_id.has_value()) {
+                                if (msg.transaction_id.value() != this->transaction_id)
+                                        return;
+                        } else if (msg.relates_to.has_value()) {
+                                if (msg.relates_to.value().event_id != this->relation.event_id)
+                                        return;
+                        }
+                        nhlog::ui()->info("Flow done on other side");
+                });
+
+        timeout->start(TIMEOUT);
+}
+
+QString
+DeviceVerificationFlow::state()
+{
+        switch (state_) {
+        case PromptStartVerification:
+                return "PromptStartVerification";
+        case CompareEmoji:
+                return "CompareEmoji";
+        case CompareNumber:
+                return "CompareNumber";
+        case WaitingForKeys:
+                return "WaitingForKeys";
+        case WaitingForOtherToAccept:
+                return "WaitingForOtherToAccept";
+        case WaitingForMac:
+                return "WaitingForMac";
+        case Success:
+                return "Success";
+        case Failed:
+                return "Failed";
+        default:
+                return "";
+        }
+}
+
+void
+DeviceVerificationFlow::next()
+{
+        if (sender) {
+                switch (state_) {
+                case PromptStartVerification:
+                        sendVerificationRequest();
+                        break;
+                case CompareEmoji:
+                case CompareNumber:
+                        sendVerificationMac();
+                        break;
+                case WaitingForKeys:
+                case WaitingForOtherToAccept:
+                case WaitingForMac:
+                case Success:
+                case Failed:
+                        nhlog::db()->error("verification: Invalid state transition!");
+                        break;
+                }
+        } else {
+                switch (state_) {
+                case PromptStartVerification:
+                        if (canonical_json.is_null())
+                                sendVerificationReady();
+                        else // legacy path without request and ready
+                                acceptVerificationRequest();
+                        break;
+                case CompareEmoji:
+                        [[fallthrough]];
+                case CompareNumber:
+                        sendVerificationMac();
+                        break;
+                case WaitingForKeys:
+                case WaitingForOtherToAccept:
+                case WaitingForMac:
+                case Success:
+                case Failed:
+                        nhlog::db()->error("verification: Invalid state transition!");
+                        break;
+                }
+        }
+}
+
+QString
+DeviceVerificationFlow::getUserId()
+{
+        return QString::fromStdString(this->toClient.to_string());
+}
+
+QString
+DeviceVerificationFlow::getDeviceId()
+{
+        return this->deviceId;
+}
+
+bool
+DeviceVerificationFlow::getSender()
+{
+        return this->sender;
+}
+
+std::vector
+DeviceVerificationFlow::getSasList()
+{
+        return this->sasList;
+}
+
+void
+DeviceVerificationFlow::setEventId(std::string event_id_)
+{
+        this->relation.rel_type = mtx::common::RelationType::Reference;
+        this->relation.event_id = event_id_;
+        this->transaction_id    = event_id_;
+}
+
+void
+DeviceVerificationFlow::handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg,
+                                           std::string)
+{
+        if (msg.transaction_id.has_value()) {
+                if (msg.transaction_id.value() != this->transaction_id)
+                        return;
+        } else if (msg.relates_to.has_value()) {
+                if (msg.relates_to.value().event_id != this->relation.event_id)
+                        return;
+        }
+        if ((std::find(msg.key_agreement_protocols.begin(),
+                       msg.key_agreement_protocols.end(),
+                       "curve25519-hkdf-sha256") != msg.key_agreement_protocols.end()) &&
+            (std::find(msg.hashes.begin(), msg.hashes.end(), "sha256") != msg.hashes.end()) &&
+            (std::find(msg.message_authentication_codes.begin(),
+                       msg.message_authentication_codes.end(),
+                       "hkdf-hmac-sha256") != msg.message_authentication_codes.end())) {
+                if (std::find(msg.short_authentication_string.begin(),
+                              msg.short_authentication_string.end(),
+                              mtx::events::msg::SASMethods::Emoji) !=
+                    msg.short_authentication_string.end()) {
+                        this->method = mtx::events::msg::SASMethods::Emoji;
+                } else if (std::find(msg.short_authentication_string.begin(),
+                                     msg.short_authentication_string.end(),
+                                     mtx::events::msg::SASMethods::Decimal) !=
+                           msg.short_authentication_string.end()) {
+                        this->method = mtx::events::msg::SASMethods::Decimal;
+                } else {
+                        this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
+                        return;
+                }
+                if (!sender)
+                        this->canonical_json = nlohmann::json(msg);
+                else {
+                        if (utils::localUser().toStdString() < this->toClient.to_string()) {
+                                this->canonical_json = nlohmann::json(msg);
+                        }
+                }
+
+                if (state_ != PromptStartVerification)
+                        this->acceptVerificationRequest();
+        } else {
+                this->cancelVerification(DeviceVerificationFlow::Error::UnknownMethod);
+        }
+}
+
+//! accepts a verification
+void
+DeviceVerificationFlow::acceptVerificationRequest()
+{
+        mtx::events::msg::KeyVerificationAccept req;
+
+        req.method                      = mtx::events::msg::VerificationMethods::SASv1;
+        req.key_agreement_protocol      = "curve25519-hkdf-sha256";
+        req.hash                        = "sha256";
+        req.message_authentication_code = "hkdf-hmac-sha256";
+        if (this->method == mtx::events::msg::SASMethods::Emoji)
+                req.short_authentication_string = {mtx::events::msg::SASMethods::Emoji};
+        else if (this->method == mtx::events::msg::SASMethods::Decimal)
+                req.short_authentication_string = {mtx::events::msg::SASMethods::Decimal};
+        req.commitment = mtx::crypto::bin2base64_unpadded(
+          mtx::crypto::sha256(this->sas->public_key() + this->canonical_json.dump()));
+
+        send(req);
+        setState(WaitingForKeys);
+}
+//! responds verification request
+void
+DeviceVerificationFlow::sendVerificationReady()
+{
+        mtx::events::msg::KeyVerificationReady req;
+
+        req.from_device = http::client()->device_id();
+        req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
+
+        send(req);
+        setState(WaitingForKeys);
+}
+//! accepts a verification
+void
+DeviceVerificationFlow::sendVerificationDone()
+{
+        mtx::events::msg::KeyVerificationDone req;
+
+        send(req);
+}
+//! starts the verification flow
+void
+DeviceVerificationFlow::startVerificationRequest()
+{
+        mtx::events::msg::KeyVerificationStart req;
+
+        req.from_device                  = http::client()->device_id();
+        req.method                       = mtx::events::msg::VerificationMethods::SASv1;
+        req.key_agreement_protocols      = {"curve25519-hkdf-sha256"};
+        req.hashes                       = {"sha256"};
+        req.message_authentication_codes = {"hkdf-hmac-sha256"};
+        req.short_authentication_string  = {mtx::events::msg::SASMethods::Decimal,
+                                           mtx::events::msg::SASMethods::Emoji};
+
+        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
+                mtx::requests::ToDeviceMessages body;
+                req.transaction_id   = this->transaction_id;
+                this->canonical_json = nlohmann::json(req);
+        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
+                req.relates_to       = this->relation;
+                this->canonical_json = nlohmann::json(req);
+        }
+        send(req);
+        setState(WaitingForOtherToAccept);
+}
+//! sends a verification request
+void
+DeviceVerificationFlow::sendVerificationRequest()
+{
+        mtx::events::msg::KeyVerificationRequest req;
+
+        req.from_device = http::client()->device_id();
+        req.methods     = {mtx::events::msg::VerificationMethods::SASv1};
+
+        if (this->type == DeviceVerificationFlow::Type::ToDevice) {
+                QDateTime currentTime = QDateTime::currentDateTimeUtc();
+
+                req.timestamp = (uint64_t)currentTime.toMSecsSinceEpoch();
+
+        } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
+                req.to      = this->toClient.to_string();
+                req.msgtype = "m.key.verification.request";
+                req.body = "User is requesting to verify keys with you. However, your client does "
+                           "not support this method, so you will need to use the legacy method of "
+                           "key verification.";
+        }
+
+        send(req);
+        setState(WaitingForOtherToAccept);
+}
+//! cancels a verification flow
+void
+DeviceVerificationFlow::cancelVerification(DeviceVerificationFlow::Error error_code)
+{
+        mtx::events::msg::KeyVerificationCancel req;
+
+        if (error_code == DeviceVerificationFlow::Error::UnknownMethod) {
+                req.code   = "m.unknown_method";
+                req.reason = "unknown method received";
+        } else if (error_code == DeviceVerificationFlow::Error::MismatchedCommitment) {
+                req.code   = "m.mismatched_commitment";
+                req.reason = "commitment didn't match";
+        } else if (error_code == DeviceVerificationFlow::Error::MismatchedSAS) {
+                req.code   = "m.mismatched_sas";
+                req.reason = "sas didn't match";
+        } else if (error_code == DeviceVerificationFlow::Error::KeyMismatch) {
+                req.code   = "m.key_match";
+                req.reason = "keys did not match";
+        } else if (error_code == DeviceVerificationFlow::Error::Timeout) {
+                req.code   = "m.timeout";
+                req.reason = "timed out";
+        } else if (error_code == DeviceVerificationFlow::Error::User) {
+                req.code   = "m.user";
+                req.reason = "user cancelled the verification";
+        } else if (error_code == DeviceVerificationFlow::Error::OutOfOrder) {
+                req.code   = "m.unexpected_message";
+                req.reason = "received messages out of order";
+        }
+
+        this->error_ = error_code;
+        emit errorChanged();
+        this->setState(Failed);
+
+        send(req);
+}
+//! sends the verification key
+void
+DeviceVerificationFlow::sendVerificationKey()
+{
+        mtx::events::msg::KeyVerificationKey req;
+
+        req.key = this->sas->public_key();
+
+        send(req);
+}
+
+mtx::events::msg::KeyVerificationMac
+key_verification_mac(mtx::crypto::SAS *sas,
+                     mtx::identifiers::User sender,
+                     const std::string &senderDevice,
+                     mtx::identifiers::User receiver,
+                     const std::string &receiverDevice,
+                     const std::string &transactionId,
+                     std::map keys)
+{
+        mtx::events::msg::KeyVerificationMac req;
+
+        std::string info = "MATRIX_KEY_VERIFICATION_MAC" + sender.to_string() + senderDevice +
+                           receiver.to_string() + receiverDevice + transactionId;
+
+        std::string key_list;
+        bool first = true;
+        for (const auto &[key_id, key] : keys) {
+                req.mac[key_id] = sas->calculate_mac(key, info + key_id);
+
+                if (!first)
+                        key_list += ",";
+                key_list += key_id;
+                first = false;
+        }
+
+        req.keys = sas->calculate_mac(key_list, info + "KEY_IDS");
+
+        return req;
+}
+
+//! sends the mac of the keys
+void
+DeviceVerificationFlow::sendVerificationMac()
+{
+        std::map key_list;
+        key_list["ed25519:" + http::client()->device_id()] = olm::client()->identity_keys().ed25519;
+
+        // send our master key, if we trust it
+        if (!this->our_trusted_master_key.empty())
+                key_list["ed25519:" + our_trusted_master_key] = our_trusted_master_key;
+
+        mtx::events::msg::KeyVerificationMac req =
+          key_verification_mac(sas.get(),
+                               http::client()->user_id(),
+                               http::client()->device_id(),
+                               this->toClient,
+                               this->deviceId.toStdString(),
+                               this->transaction_id,
+                               key_list);
+
+        send(req);
+
+        setState(WaitingForMac);
+        acceptDevice();
+}
+//! Completes the verification flow
+void
+DeviceVerificationFlow::acceptDevice()
+{
+        if (!isMacVerified) {
+                setState(WaitingForMac);
+        } else if (state_ == WaitingForMac) {
+                cache::markDeviceVerified(this->toClient.to_string(), this->deviceId.toStdString());
+                this->sendVerificationDone();
+                setState(Success);
+        }
+}
+
+void
+DeviceVerificationFlow::unverify()
+{
+        cache::markDeviceUnverified(this->toClient.to_string(), this->deviceId.toStdString());
+
+        emit refreshProfile();
+}
+
+QSharedPointer
+DeviceVerificationFlow::NewInRoomVerification(QObject *parent_,
+                                              TimelineModel *timelineModel_,
+                                              const mtx::events::msg::KeyVerificationRequest &msg,
+                                              QString other_user_,
+                                              QString event_id_)
+{
+        QSharedPointer flow(
+          new DeviceVerificationFlow(parent_,
+                                     Type::RoomMsg,
+                                     timelineModel_,
+                                     other_user_,
+                                     QString::fromStdString(msg.from_device)));
+
+        flow->setEventId(event_id_.toStdString());
+
+        if (std::find(msg.methods.begin(),
+                      msg.methods.end(),
+                      mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
+                flow->cancelVerification(UnknownMethod);
+        }
+
+        return flow;
+}
+QSharedPointer
+DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
+                                                const mtx::events::msg::KeyVerificationRequest &msg,
+                                                QString other_user_,
+                                                QString txn_id_)
+{
+        QSharedPointer flow(new DeviceVerificationFlow(
+          parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
+        flow->transaction_id = txn_id_.toStdString();
+
+        if (std::find(msg.methods.begin(),
+                      msg.methods.end(),
+                      mtx::events::msg::VerificationMethods::SASv1) == msg.methods.end()) {
+                flow->cancelVerification(UnknownMethod);
+        }
+
+        return flow;
+}
+QSharedPointer
+DeviceVerificationFlow::NewToDeviceVerification(QObject *parent_,
+                                                const mtx::events::msg::KeyVerificationStart &msg,
+                                                QString other_user_,
+                                                QString txn_id_)
+{
+        QSharedPointer flow(new DeviceVerificationFlow(
+          parent_, Type::ToDevice, nullptr, other_user_, QString::fromStdString(msg.from_device)));
+        flow->transaction_id = txn_id_.toStdString();
+
+        flow->handleStartMessage(msg, "");
+
+        return flow;
+}
+QSharedPointer
+DeviceVerificationFlow::InitiateUserVerification(QObject *parent_,
+                                                 TimelineModel *timelineModel_,
+                                                 QString userid)
+{
+        QSharedPointer flow(
+          new DeviceVerificationFlow(parent_, Type::RoomMsg, timelineModel_, userid, ""));
+        flow->sender = true;
+        return flow;
+}
+QSharedPointer
+DeviceVerificationFlow::InitiateDeviceVerification(QObject *parent_, QString userid, QString device)
+{
+        QSharedPointer flow(
+          new DeviceVerificationFlow(parent_, Type::ToDevice, nullptr, userid, device));
+
+        flow->sender         = true;
+        flow->transaction_id = http::client()->generate_txn_id();
+
+        return flow;
+}
diff --git a/src/DeviceVerificationFlow.h b/src/DeviceVerificationFlow.h
new file mode 100644
index 0000000..70b5d9b
--- /dev/null
+++ b/src/DeviceVerificationFlow.h
@@ -0,0 +1,235 @@
+#pragma once
+
+#include 
+
+#include 
+
+#include "CacheCryptoStructs.h"
+#include "Logging.h"
+#include "MatrixClient.h"
+#include "Olm.h"
+#include "timeline/TimelineModel.h"
+
+class QTimer;
+
+using sas_ptr = std::unique_ptr;
+
+// clang-format off
+/*
+ * Stolen from fluffy chat :D
+ *
+ *      State         |   +-------------+                    +-----------+                                  |
+ *                    |   | AliceDevice |                    | BobDevice |                                  |
+ *                    |   | (sender)    |                    |           |                                  |
+ *                    |   +-------------+                    +-----------+                                  |
+ * promptStartVerify  |         |                                 |                                         |
+ *                    |      o  | (m.key.verification.request)    |                                         |
+ *                    |      p  |-------------------------------->| (ASK FOR VERIFICATION REQUEST)          |
+ * waitForOtherAccept |      t  |                                 |                                         | promptStartVerify
+ * &&                 |      i  |      (m.key.verification.ready) |                                         |
+ * no commitment      |      o  |<--------------------------------|                                         |
+ * &&                 |      n  |                                 |                                         |
+ * no canonical_json  |      a  |      (m.key.verification.start) |                                         | waitingForKeys
+ *                    |      l  |<--------------------------------| Not sending to prevent the glare resolve| && no commitment
+ *                    |         |                                 |                                         | && no canonical_json
+ *                    |         | m.key.verification.start        |                                         |
+ * waitForOtherAccept |         |-------------------------------->| (IF NOT ALREADY ASKED,                  |
+ * &&                 |         |                                 |  ASK FOR VERIFICATION REQUEST)          | promptStartVerify, if not accepted
+ * canonical_json     |         |       m.key.verification.accept |                                         |
+ *                    |         |<--------------------------------|                                         |
+ * waitForOtherAccept |         |                                 |                                         | waitingForKeys
+ * &&                 |         | m.key.verification.key          |                                         | && canonical_json
+ * commitment         |         |-------------------------------->|                                         | && commitment
+ *                    |         |                                 |                                         |
+ *                    |         |          m.key.verification.key |                                         |
+ *                    |         |<--------------------------------|                                         |
+ * compareEmoji/Number|         |                                 |                                         | compareEmoji/Number
+ *                    |         |     COMPARE EMOJI / NUMBERS     |                                         |
+ *                    |         |                                 |                                         |
+ * waitingForMac      |         |     m.key.verification.mac      |                                         | waitingForMac
+ *                    | success |<------------------------------->|  success                                |
+ *                    |         |                                 |                                         |
+ * success/fail       |         |         m.key.verification.done |                                         | success/fail
+ *                    |         |<------------------------------->|                                         |
+ */
+// clang-format on
+class DeviceVerificationFlow : public QObject
+{
+        Q_OBJECT
+        Q_PROPERTY(QString state READ state NOTIFY stateChanged)
+        Q_PROPERTY(Error error READ error NOTIFY errorChanged)
+        Q_PROPERTY(QString userId READ getUserId CONSTANT)
+        Q_PROPERTY(QString deviceId READ getDeviceId CONSTANT)
+        Q_PROPERTY(bool sender READ getSender CONSTANT)
+        Q_PROPERTY(std::vector sasList READ getSasList CONSTANT)
+
+public:
+        enum State
+        {
+                PromptStartVerification,
+                WaitingForOtherToAccept,
+                WaitingForKeys,
+                CompareEmoji,
+                CompareNumber,
+                WaitingForMac,
+                Success,
+                Failed,
+        };
+        Q_ENUM(State)
+
+        enum Type
+        {
+                ToDevice,
+                RoomMsg
+        };
+
+        enum Error
+        {
+                UnknownMethod,
+                MismatchedCommitment,
+                MismatchedSAS,
+                KeyMismatch,
+                Timeout,
+                User,
+                OutOfOrder,
+        };
+        Q_ENUM(Error)
+
+        static QSharedPointer NewInRoomVerification(
+          QObject *parent_,
+          TimelineModel *timelineModel_,
+          const mtx::events::msg::KeyVerificationRequest &msg,
+          QString other_user_,
+          QString event_id_);
+        static QSharedPointer NewToDeviceVerification(
+          QObject *parent_,
+          const mtx::events::msg::KeyVerificationRequest &msg,
+          QString other_user_,
+          QString txn_id_);
+        static QSharedPointer NewToDeviceVerification(
+          QObject *parent_,
+          const mtx::events::msg::KeyVerificationStart &msg,
+          QString other_user_,
+          QString txn_id_);
+        static QSharedPointer
+        InitiateUserVerification(QObject *parent_, TimelineModel *timelineModel_, QString userid);
+        static QSharedPointer InitiateDeviceVerification(QObject *parent,
+                                                                                 QString userid,
+                                                                                 QString device);
+
+        // getters
+        QString state();
+        Error error() { return error_; }
+        QString getUserId();
+        QString getDeviceId();
+        bool getSender();
+        std::vector getSasList();
+        QString transactionId() { return QString::fromStdString(this->transaction_id); }
+        // setters
+        void setDeviceId(QString deviceID);
+        void setEventId(std::string event_id);
+
+        void callback_fn(const UserKeyCache &res, mtx::http::RequestErr err, std::string user_id);
+
+public slots:
+        //! unverifies a device
+        void unverify();
+        //! Continues the flow
+        void next();
+        //! Cancel the flow
+        void cancel() { cancelVerification(User); }
+
+signals:
+        void refreshProfile();
+        void stateChanged();
+        void errorChanged();
+
+private:
+        DeviceVerificationFlow(QObject *,
+                               DeviceVerificationFlow::Type flow_type,
+                               TimelineModel *model,
+                               QString userID,
+                               QString deviceId_);
+        void setState(State state)
+        {
+                if (state != state_) {
+                        state_ = state;
+                        emit stateChanged();
+                }
+        }
+
+        void handleStartMessage(const mtx::events::msg::KeyVerificationStart &msg, std::string);
+        //! sends a verification request
+        void sendVerificationRequest();
+        //! accepts a verification request
+        void sendVerificationReady();
+        //! completes the verification flow();
+        void sendVerificationDone();
+        //! accepts a verification
+        void acceptVerificationRequest();
+        //! starts the verification flow
+        void startVerificationRequest();
+        //! cancels a verification flow
+        void cancelVerification(DeviceVerificationFlow::Error error_code);
+        //! sends the verification key
+        void sendVerificationKey();
+        //! sends the mac of the keys
+        void sendVerificationMac();
+        //! Completes the verification flow
+        void acceptDevice();
+
+        std::string transaction_id;
+
+        bool sender;
+        Type type;
+        mtx::identifiers::User toClient;
+        QString deviceId;
+
+        // public part of our master key, when trusted or empty
+        std::string our_trusted_master_key;
+
+        mtx::events::msg::SASMethods method = mtx::events::msg::SASMethods::Emoji;
+        QTimer *timeout                     = nullptr;
+        sas_ptr sas;
+        std::string mac_method;
+        std::string commitment;
+        nlohmann::json canonical_json;
+
+        std::vector sasList;
+        UserKeyCache their_keys;
+        TimelineModel *model_;
+        mtx::common::RelatesTo relation;
+
+        State state_ = PromptStartVerification;
+        Error error_ = UnknownMethod;
+
+        bool isMacVerified = false;
+
+        template
+        void send(T msg)
+        {
+                if (this->type == DeviceVerificationFlow::Type::ToDevice) {
+                        mtx::requests::ToDeviceMessages body;
+                        msg.transaction_id                           = this->transaction_id;
+                        body[this->toClient][deviceId.toStdString()] = msg;
+
+                        http::client()->send_to_device(
+                          this->transaction_id, body, [](mtx::http::RequestErr err) {
+                                  if (err)
+                                          nhlog::net()->warn(
+                                            "failed to send verification to_device message: {} {}",
+                                            err->matrix_error.error,
+                                            static_cast(err->status_code));
+                          });
+                } else if (this->type == DeviceVerificationFlow::Type::RoomMsg && model_) {
+                        if constexpr (!std::is_same_v)
+                                msg.relates_to = this->relation;
+                        (model_)->sendMessageEvent(msg, mtx::events::to_device_content_to_type);
+                }
+
+                nhlog::net()->debug(
+                  "Sent verification step: {} in state: {}",
+                  mtx::events::to_string(mtx::events::to_device_content_to_type),
+                  state().toStdString());
+        }
+};
diff --git a/src/EventAccessors.cpp b/src/EventAccessors.cpp
index 88612b1..b62be9a 100644
--- a/src/EventAccessors.cpp
+++ b/src/EventAccessors.cpp
@@ -39,8 +39,15 @@ struct EventMsgType
         template
         mtx::events::MessageType operator()(const mtx::events::Event &e)
         {
-                if constexpr (is_detected::value)
-                        return mtx::events::getMessageType(e.content.msgtype);
+                if constexpr (is_detected::value) {
+                        if constexpr (std::is_same_v,
+                                                     std::remove_cv_t>)
+                                return mtx::events::getMessageType(e.content.msgtype.value());
+                        else if constexpr (std::is_same_v<
+                                             std::string,
+                                             std::remove_cv_t>)
+                                return mtx::events::getMessageType(e.content.msgtype);
+                }
                 return mtx::events::MessageType::Unknown;
         }
 };
@@ -97,8 +104,15 @@ struct EventBody
         template
         std::string operator()(const mtx::events::Event &e)
         {
-                if constexpr (is_detected::value)
-                        return e.content.body;
+                if constexpr (is_detected::value) {
+                        if constexpr (std::is_same_v,
+                                                     std::remove_cv_t>)
+                                return e.content.body ? e.content.body.value() : "";
+                        else if constexpr (std::is_same_v<
+                                             std::string,
+                                             std::remove_cv_t>)
+                                return e.content.body;
+                }
                 return "";
         }
 };
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 330ba2a..b6ad8bb 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -328,15 +328,6 @@ MainWindow::hasActiveUser()
                settings.contains("auth/user_id");
 }
 
-void
-MainWindow::openUserProfile(const QString &user_id, const QString &room_id)
-{
-        auto dialog = new dialogs::UserProfile(this);
-        dialog->init(user_id, room_id);
-
-        showDialog(dialog);
-}
-
 void
 MainWindow::openRoomSettings(const QString &room_id)
 {
diff --git a/src/MainWindow.h b/src/MainWindow.h
index 4f54a19..e66f299 100644
--- a/src/MainWindow.h
+++ b/src/MainWindow.h
@@ -25,7 +25,6 @@
 #include 
 
 #include "UserSettingsPage.h"
-#include "dialogs/UserProfile.h"
 #include "ui/OverlayModal.h"
 
 #include "jdenticoninterface.h"
@@ -76,7 +75,6 @@ public:
         void openLogoutDialog();
         void openRoomSettings(const QString &room_id);
         void openMemberListDialog(const QString &room_id);
-        void openUserProfile(const QString &user_id, const QString &room_id);
         void openReadReceiptsDialog(const QString &event_id);
 
         void hideOverlay();
diff --git a/src/Olm.cpp b/src/Olm.cpp
index e38e9ef..f4cb220 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -1,9 +1,12 @@
+#include 
 #include 
 
 #include "Olm.h"
 
 #include "Cache.h"
 #include "Cache_p.h"
+#include "ChatPage.h"
+#include "DeviceVerificationFlow.h"
 #include "Logging.h"
 #include "MatrixClient.h"
 #include "Utils.h"
@@ -28,7 +31,6 @@ handle_to_device_messages(const std::vectorinfo("received {} to_device messages", msgs.size());
         nlohmann::json j_msg;
 
@@ -53,6 +55,10 @@ handle_to_device_messages(const std::vectorwarn("validation error for olm message: {} {}",
                                                       e.what(),
                                                       j_msg.dump(2));
+
+                                nhlog::crypto()->warn("validation error for olm message: {} {}",
+                                                      e.what(),
+                                                      j_msg.dump(2));
                         }
 
                 } else if (msg_type == to_string(mtx::events::EventType::RoomKeyRequest)) {
@@ -71,6 +77,43 @@ handle_to_device_messages(const std::vector>(msg);
+                        ChatPage::instance()->receivedDeviceVerificationAccept(message.content);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationRequest)) {
+                        auto message = std::get<
+                          mtx::events::DeviceEvent>(msg);
+                        ChatPage::instance()->receivedDeviceVerificationRequest(message.content,
+                                                                                message.sender);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationCancel)) {
+                        auto message = std::get<
+                          mtx::events::DeviceEvent>(msg);
+                        ChatPage::instance()->receivedDeviceVerificationCancel(message.content);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationKey)) {
+                        auto message =
+                          std::get>(
+                            msg);
+                        ChatPage::instance()->receivedDeviceVerificationKey(message.content);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationMac)) {
+                        auto message =
+                          std::get>(
+                            msg);
+                        ChatPage::instance()->receivedDeviceVerificationMac(message.content);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationStart)) {
+                        auto message = std::get<
+                          mtx::events::DeviceEvent>(msg);
+                        ChatPage::instance()->receivedDeviceVerificationStart(message.content,
+                                                                              message.sender);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationReady)) {
+                        auto message = std::get<
+                          mtx::events::DeviceEvent>(msg);
+                        ChatPage::instance()->receivedDeviceVerificationReady(message.content);
+                } else if (msg_type == to_string(mtx::events::EventType::KeyVerificationDone)) {
+                        auto message =
+                          std::get>(
+                            msg);
+                        ChatPage::instance()->receivedDeviceVerificationDone(message.content);
                 } else {
                         nhlog::crypto()->warn("unhandled event: {}", j_msg.dump(2));
                 }
@@ -95,23 +138,76 @@ handle_olm_message(const OlmMessage &msg)
 
                 auto payload = try_olm_decryption(msg.sender_key, cipher.second);
 
-                if (!payload.is_null()) {
-                        nhlog::crypto()->debug("decrypted olm payload: {}", payload.dump(2));
-                        create_inbound_megolm_session(msg.sender, msg.sender_key, payload);
-                        return;
+                if (payload.is_null()) {
+                        // Check for PRE_KEY message
+                        if (cipher.second.type == 0) {
+                                payload = handle_pre_key_olm_message(
+                                  msg.sender, msg.sender_key, cipher.second);
+                        } else {
+                                nhlog::crypto()->error("Undecryptable olm message!");
+                                continue;
+                        }
                 }
 
-                // Not a PRE_KEY message
-                if (cipher.second.type != 0) {
-                        // TODO: log that it should have matched something
-                        return;
+                if (!payload.is_null()) {
+                        std::string msg_type = payload["type"];
+
+                        if (msg_type == to_string(mtx::events::EventType::KeyVerificationAccept)) {
+                                ChatPage::instance()->receivedDeviceVerificationAccept(
+                                  payload["content"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationRequest)) {
+                                ChatPage::instance()->receivedDeviceVerificationRequest(
+                                  payload["content"], payload["sender"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationCancel)) {
+                                ChatPage::instance()->receivedDeviceVerificationCancel(
+                                  payload["content"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationKey)) {
+                                ChatPage::instance()->receivedDeviceVerificationKey(
+                                  payload["content"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationMac)) {
+                                ChatPage::instance()->receivedDeviceVerificationMac(
+                                  payload["content"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationStart)) {
+                                ChatPage::instance()->receivedDeviceVerificationStart(
+                                  payload["content"], payload["sender"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationReady)) {
+                                ChatPage::instance()->receivedDeviceVerificationReady(
+                                  payload["content"]);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::KeyVerificationDone)) {
+                                ChatPage::instance()->receivedDeviceVerificationDone(
+                                  payload["content"]);
+                                return;
+                        } else if (msg_type == to_string(mtx::events::EventType::RoomKey)) {
+                                mtx::events::DeviceEvent roomKey =
+                                  payload;
+                                create_inbound_megolm_session(roomKey, msg.sender_key);
+                                return;
+                        } else if (msg_type ==
+                                   to_string(mtx::events::EventType::ForwardedRoomKey)) {
+                                mtx::events::DeviceEvent
+                                  roomKey = payload;
+                                import_inbound_megolm_session(roomKey);
+                                return;
+                        }
                 }
-
-                handle_pre_key_olm_message(msg.sender, msg.sender_key, cipher.second);
         }
 }
 
-void
+nlohmann::json
 handle_pre_key_olm_message(const std::string &sender,
                            const std::string &sender_key,
                            const mtx::events::msg::OlmCipherContent &content)
@@ -129,14 +225,14 @@ handle_pre_key_olm_message(const std::string &sender,
         } catch (const mtx::crypto::olm_exception &e) {
                 nhlog::crypto()->critical(
                   "failed to create inbound session with {}: {}", sender, e.what());
-                return;
+                return {};
         }
 
         if (!mtx::crypto::matches_inbound_session_from(
               inbound_session.get(), sender_key, content.body)) {
                 nhlog::crypto()->warn("inbound olm session doesn't match sender's key ({})",
                                       sender);
-                return;
+                return {};
         }
 
         mtx::crypto::BinaryBuf output;
@@ -146,7 +242,7 @@ handle_pre_key_olm_message(const std::string &sender,
         } catch (const mtx::crypto::olm_exception &e) {
                 nhlog::crypto()->critical(
                   "failed to decrypt olm message {}: {}", content.body, e.what());
-                return;
+                return {};
         }
 
         auto plaintext = json::parse(std::string((char *)output.data(), output.size()));
@@ -159,7 +255,7 @@ handle_pre_key_olm_message(const std::string &sender,
                   "failed to save inbound olm session from {}: {}", sender, e.what());
         }
 
-        create_inbound_megolm_session(sender, sender_key, plaintext);
+        return plaintext;
 }
 
 mtx::events::msg::Encrypted
@@ -169,10 +265,15 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
 
         // relations shouldn't be encrypted...
         mtx::common::ReplyRelatesTo relation;
+        mtx::common::RelatesTo r_relation;
+
         if (body["content"].contains("m.relates_to") &&
             body["content"]["m.relates_to"].contains("m.in_reply_to")) {
                 relation = body["content"]["m.relates_to"];
                 body["content"].erase("m.relates_to");
+        } else if (body["content"]["m.relates_to"].contains("event_id")) {
+                r_relation = body["content"]["m.relates_to"];
+                body["content"].erase("m.relates_to");
         }
 
         // Always check before for existence.
@@ -181,12 +282,13 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
 
         // Prepare the m.room.encrypted event.
         msg::Encrypted data;
-        data.ciphertext = std::string((char *)payload.data(), payload.size());
-        data.sender_key = olm::client()->identity_keys().curve25519;
-        data.session_id = res.data.session_id;
-        data.device_id  = device_id;
-        data.algorithm  = MEGOLM_ALGO;
-        data.relates_to = relation;
+        data.ciphertext   = std::string((char *)payload.data(), payload.size());
+        data.sender_key   = olm::client()->identity_keys().curve25519;
+        data.session_id   = res.data.session_id;
+        data.device_id    = device_id;
+        data.algorithm    = MEGOLM_ALGO;
+        data.relates_to   = relation;
+        data.r_relates_to = r_relation;
 
         auto message_index = olm_outbound_group_session_message_index(res.session);
         nhlog::crypto()->debug("next message_index {}", message_index);
@@ -229,10 +331,12 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
                 }
 
                 try {
-                        return json::parse(std::string((char *)text.data(), text.size()));
+                        return json::parse(std::string_view((char *)text.data(), text.size()));
                 } catch (const json::exception &e) {
-                        nhlog::crypto()->critical("failed to parse the decrypted session msg: {}",
-                                                  e.what());
+                        nhlog::crypto()->critical(
+                          "failed to parse the decrypted session msg: {} {}",
+                          e.what(),
+                          std::string_view((char *)text.data(), text.size()));
                 }
         }
 
@@ -240,39 +344,54 @@ try_olm_decryption(const std::string &sender_key, const mtx::events::msg::OlmCip
 }
 
 void
-create_inbound_megolm_session(const std::string &sender,
-                              const std::string &sender_key,
-                              const nlohmann::json &payload)
+create_inbound_megolm_session(const mtx::events::DeviceEvent &roomKey,
+                              const std::string &sender_key)
 {
-        std::string room_id, session_id, session_key;
+        MegolmSessionIndex index;
+        index.room_id    = roomKey.content.room_id;
+        index.session_id = roomKey.content.session_id;
+        index.sender_key = sender_key;
 
         try {
-                room_id     = payload.at("content").at("room_id");
-                session_id  = payload.at("content").at("session_id");
-                session_key = payload.at("content").at("session_key");
-        } catch (const nlohmann::json::exception &e) {
-                nhlog::crypto()->critical(
-                  "failed to parse plaintext olm message: {} {}", e.what(), payload.dump(2));
+                auto megolm_session =
+                  olm::client()->init_inbound_group_session(roomKey.content.session_key);
+                cache::saveInboundMegolmSession(index, std::move(megolm_session));
+        } catch (const lmdb::error &e) {
+                nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
+                return;
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
                 return;
         }
 
+        nhlog::crypto()->info(
+          "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
+}
+
+void
+import_inbound_megolm_session(
+  const mtx::events::DeviceEvent &roomKey)
+{
         MegolmSessionIndex index;
-        index.room_id    = room_id;
-        index.session_id = session_id;
-        index.sender_key = sender_key;
+        index.room_id    = roomKey.content.room_id;
+        index.session_id = roomKey.content.session_id;
+        index.sender_key = roomKey.content.sender_key;
 
         try {
-                auto megolm_session = olm::client()->init_inbound_group_session(session_key);
+                auto megolm_session =
+                  olm::client()->import_inbound_group_session(roomKey.content.session_key);
                 cache::saveInboundMegolmSession(index, std::move(megolm_session));
         } catch (const lmdb::error &e) {
                 nhlog::crypto()->critical("failed to save inbound megolm session: {}", e.what());
                 return;
         } catch (const mtx::crypto::olm_exception &e) {
-                nhlog::crypto()->critical("failed to create inbound megolm session: {}", e.what());
+                nhlog::crypto()->critical("failed to import inbound megolm session: {}", e.what());
                 return;
         }
 
-        nhlog::crypto()->info("established inbound megolm session ({}, {})", room_id, sender);
+        // TODO(Nico): Reload messages encrypted with this key.
+        nhlog::crypto()->info(
+          "established inbound megolm session ({}, {})", roomKey.content.room_id, roomKey.sender);
 }
 
 void
@@ -373,6 +492,10 @@ handle_key_request_message(const mtx::events::DeviceEventwarn("requested session not found in room: {}",
                                       req.content.room_id);
+
+                nhlog::crypto()->warn("requested session not found in room: {}",
+                                      req.content.room_id);
+
                 return;
         }
 
@@ -477,9 +600,11 @@ send_megolm_key_to_device(const std::string &user_id,
                                     ->create_room_key_event(UserId(user_id), pks.ed25519, payload)
                                     .dump();
 
+                  mtx::requests::ClaimKeys claim_keys;
+                  claim_keys.one_time_keys[user_id][device_id] = mtx::crypto::SIGNED_CURVE25519;
+
                   http::client()->claim_keys(
-                    user_id,
-                    {device_id},
+                    claim_keys,
                     [room_key, user_id, device_id, pks](const mtx::responses::ClaimKeys &res,
                                                         mtx::http::RequestErr err) {
                             if (err) {
diff --git a/src/Olm.h b/src/Olm.h
index 87f4e3e..7b97039 100644
--- a/src/Olm.h
+++ b/src/Olm.h
@@ -71,11 +71,13 @@ handle_olm_message(const OlmMessage &msg);
 
 //! Establish a new inbound megolm session with the decrypted payload from olm.
 void
-create_inbound_megolm_session(const std::string &sender,
-                              const std::string &sender_key,
-                              const nlohmann::json &payload);
-
+create_inbound_megolm_session(const mtx::events::DeviceEvent &roomKey,
+                              const std::string &sender_key);
 void
+import_inbound_megolm_session(
+  const mtx::events::DeviceEvent &roomKey);
+
+nlohmann::json
 handle_pre_key_olm_message(const std::string &sender,
                            const std::string &sender_key,
                            const mtx::events::msg::OlmCipherContent &content);
diff --git a/src/TextInputWidget.cpp b/src/TextInputWidget.cpp
index f0d23e2..22e8aaf 100644
--- a/src/TextInputWidget.cpp
+++ b/src/TextInputWidget.cpp
@@ -709,6 +709,8 @@ TextInputWidget::command(QString command, QString args)
                 emit sendTextMessage("ノ┬─┬ノ ︵ ( \\o°o)\\");
         } else if (command == "clear-timeline") {
                 emit clearRoomTimeline();
+        } else if (command == "rotate-megolm-session") {
+                emit rotateMegolmSession();
         }
 }
 
diff --git a/src/TextInputWidget.h b/src/TextInputWidget.h
index f4ca980..7cc73e9 100644
--- a/src/TextInputWidget.h
+++ b/src/TextInputWidget.h
@@ -186,6 +186,7 @@ signals:
         void sendBanRoomRequest(const QString &userid, const QString &reason);
         void sendUnbanRoomRequest(const QString &userid, const QString &reason);
         void changeRoomNick(const QString &displayname);
+        void rotateMegolmSession();
 
         void startedTyping();
         void stoppedTyping();
diff --git a/src/WebRTCSession.cpp b/src/WebRTCSession.cpp
index 9c8c032..1c11f75 100644
--- a/src/WebRTCSession.cpp
+++ b/src/WebRTCSession.cpp
@@ -418,7 +418,6 @@ getPayloadType(const std::string &sdp, const std::string &name)
         }
         return -1;
 }
-
 }
 
 bool
diff --git a/src/dialogs/AcceptCall.cpp b/src/dialogs/AcceptCall.cpp
index be1eb0c..2b47b7d 100644
--- a/src/dialogs/AcceptCall.cpp
+++ b/src/dialogs/AcceptCall.cpp
@@ -131,5 +131,4 @@ AcceptCall::AcceptCall(const QString &caller,
                 emit close();
         });
 }
-
 }
diff --git a/src/dialogs/AcceptCall.h b/src/dialogs/AcceptCall.h
index 909605d..5db8fcf 100644
--- a/src/dialogs/AcceptCall.h
+++ b/src/dialogs/AcceptCall.h
@@ -33,5 +33,4 @@ private:
         QPushButton *rejectBtn_;
         std::vector audioDevices_;
 };
-
 }
diff --git a/src/dialogs/PlaceCall.cpp b/src/dialogs/PlaceCall.cpp
index 4e70370..8acdbe8 100644
--- a/src/dialogs/PlaceCall.cpp
+++ b/src/dialogs/PlaceCall.cpp
@@ -100,5 +100,4 @@ PlaceCall::PlaceCall(const QString &callee,
                 emit close();
         });
 }
-
 }
diff --git a/src/dialogs/PlaceCall.h b/src/dialogs/PlaceCall.h
index 5a1e982..e178afc 100644
--- a/src/dialogs/PlaceCall.h
+++ b/src/dialogs/PlaceCall.h
@@ -33,5 +33,4 @@ private:
         QPushButton *cancelBtn_;
         std::vector audioDevices_;
 };
-
 }
diff --git a/src/emoji/EmojiModel.h b/src/emoji/EmojiModel.h
index 8d43e00..88bacde 100644
--- a/src/emoji/EmojiModel.h
+++ b/src/emoji/EmojiModel.h
@@ -60,5 +60,4 @@ private:
         EmojiCategory category_ = EmojiCategory::Search;
         emoji::Provider emoji_provider_;
 };
-
 }
\ No newline at end of file
diff --git a/src/emoji/EmojiSearchModel.h b/src/emoji/EmojiSearchModel.h
index 1ff5f4e..13a0393 100644
--- a/src/emoji/EmojiSearchModel.h
+++ b/src/emoji/EmojiSearchModel.h
@@ -33,5 +33,4 @@ private:
                 return shortname.replace(" ", "-").replace(":", "-").replace("--", "-").toLower();
         }
 };
-
 }
diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp
index 749d409..d3c5c3f 100644
--- a/src/timeline/EventStore.cpp
+++ b/src/timeline/EventStore.cpp
@@ -5,6 +5,7 @@
 
 #include "Cache.h"
 #include "Cache_p.h"
+#include "ChatPage.h"
 #include "EventAccessors.h"
 #include "Logging.h"
 #include "MatrixClient.h"
@@ -53,9 +54,8 @@ EventStore::EventStore(std::string room_id, QObject *)
           &EventStore::oldMessagesRetrieved,
           this,
           [this](const mtx::responses::Messages &res) {
-                  //
                   uint64_t newFirst = cache::client()->saveOldMessages(room_id_, res);
-                  if (newFirst == first && !res.chunk.empty())
+                  if (newFirst == first)
                           fetchMore();
                   else {
                           emit beginInsertRows(toExternalIdx(newFirst),
@@ -97,8 +97,8 @@ EventStore::EventStore(std::string room_id, QObject *)
                                     room_id_,
                                     txn_id,
                                     e.content,
-                                    [this, txn_id](const mtx::responses::EventId &event_id,
-                                                   mtx::http::RequestErr err) {
+                                    [this, txn_id, e](const mtx::responses::EventId &event_id,
+                                                      mtx::http::RequestErr err) {
                                             if (err) {
                                                     const int status_code =
                                                       static_cast(err->status_code);
@@ -110,7 +110,21 @@ EventStore::EventStore(std::string room_id, QObject *)
                                                     emit messageFailed(txn_id);
                                                     return;
                                             }
+
                                             emit messageSent(txn_id, event_id.event_id.to_string());
+                                            if constexpr (std::is_same_v<
+                                                            decltype(e.content),
+                                                            mtx::events::msg::Encrypted>) {
+                                                    auto event =
+                                                      decryptEvent({room_id_, e.event_id}, e);
+                                                    if (auto dec =
+                                                          std::get_if>(event)) {
+                                                            emit updateFlowEventId(
+                                                              event_id.event_id.to_string());
+                                                    }
+                                            }
                                     });
                   },
                   event->data);
@@ -265,9 +279,78 @@ EventStore::handleSync(const mtx::responses::Timeline &events)
                                 emit dataChanged(toExternalIdx(*idx), toExternalIdx(*idx));
                         }
                 }
+
+                // decrypting and checking some encrypted messages
+                if (auto encrypted =
+                      std::get_if>(
+                        &event)) {
+                        mtx::events::collections::TimelineEvents *d_event =
+                          decryptEvent({room_id_, encrypted->event_id}, *encrypted);
+                        if (std::visit(
+                              [](auto e) { return (e.sender != utils::localUser().toStdString()); },
+                              *d_event)) {
+                                handle_room_verification(*d_event);
+                        }
+                        // else {
+                        //        // only the key.verification.ready sent by localuser's other
+                        //        device
+                        //        // is of significance as it is used for detecting accepted request
+                        //        if (std::get_if>(d_event)) {
+                        //                auto msg = std::get_if>(d_event);
+                        //                ChatPage::instance()->receivedDeviceVerificationReady(
+                        //                  msg->content);
+                        //        }
+                        //}
+                }
         }
 }
 
+namespace {
+template
+struct overloaded : Ts...
+{
+        using Ts::operator()...;
+};
+template
+overloaded(Ts...) -> overloaded;
+}
+
+void
+EventStore::handle_room_verification(mtx::events::collections::TimelineEvents event)
+{
+        std::visit(
+          overloaded{
+            [this](const mtx::events::RoomEvent &msg) {
+                    emit startDMVerification(msg);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationCancel(msg.content);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationAccept(msg.content);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationKey(msg.content);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationMac(msg.content);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationReady(msg.content);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationDone(msg.content);
+            },
+            [](const mtx::events::RoomEvent &msg) {
+                    ChatPage::instance()->receivedDeviceVerificationStart(msg.content, msg.sender);
+            },
+            [](const auto &) {},
+          },
+          event);
+}
+
 QVariantList
 EventStore::reactions(const std::string &event_id)
 {
@@ -289,13 +372,14 @@ EventStore::reactions(const std::string &event_id)
                         continue;
 
                 if (auto reaction = std::get_if>(
-                      related_event)) {
-                        auto &agg = aggregation[reaction->content.relates_to.key];
+                      related_event);
+                    reaction && reaction->content.relates_to.key) {
+                        auto &agg = aggregation[reaction->content.relates_to.key.value()];
 
                         if (agg.count == 0) {
                                 Reaction temp{};
                                 temp.key_ =
-                                  QString::fromStdString(reaction->content.relates_to.key);
+                                  QString::fromStdString(reaction->content.relates_to.key.value());
                                 reactions.push_back(temp);
                         }
 
@@ -407,11 +491,12 @@ EventStore::decryptEvent(const IdIndex &idx,
 
         auto decryptionResult = olm::decryptEvent(index, e);
 
+        mtx::events::RoomEvent dummy;
+        dummy.origin_server_ts = e.origin_server_ts;
+        dummy.event_id         = e.event_id;
+        dummy.sender           = e.sender;
+
         if (decryptionResult.error) {
-                mtx::events::RoomEvent dummy;
-                dummy.origin_server_ts = e.origin_server_ts;
-                dummy.event_id         = e.event_id;
-                dummy.sender           = e.sender;
                 switch (*decryptionResult.error) {
                 case olm::DecryptionErrorCode::MissingSession:
                         dummy.content.body =
@@ -484,6 +569,66 @@ EventStore::decryptEvent(const IdIndex &idx,
                 return asCacheEntry(std::move(dummy));
         }
 
+        std::string msg_str;
+        try {
+                auto session = cache::client()->getInboundMegolmSession(index);
+                auto res     = olm::client()->decrypt_group_message(session, e.content.ciphertext);
+                msg_str      = std::string((char *)res.data.data(), res.data.size());
+        } catch (const lmdb::error &e) {
+                nhlog::db()->critical("failed to retrieve megolm session with index ({}, {}, {})",
+                                      index.room_id,
+                                      index.session_id,
+                                      index.sender_key,
+                                      e.what());
+                dummy.content.body =
+                  tr("-- Decryption Error (failed to retrieve megolm keys from db) --",
+                     "Placeholder, when the message can't be decrypted, because the DB "
+                     "access "
+                     "failed.")
+                    .toStdString();
+                return asCacheEntry(std::move(dummy));
+        } catch (const mtx::crypto::olm_exception &e) {
+                nhlog::crypto()->critical("failed to decrypt message with index ({}, {}, {}): {}",
+                                          index.room_id,
+                                          index.session_id,
+                                          index.sender_key,
+                                          e.what());
+                dummy.content.body =
+                  tr("-- Decryption Error (%1) --",
+                     "Placeholder, when the message can't be decrypted. In this case, the "
+                     "Olm "
+                     "decrytion returned an error, which is passed as %1.")
+                    .arg(e.what())
+                    .toStdString();
+                return asCacheEntry(std::move(dummy));
+        }
+
+        // Add missing fields for the event.
+        json body                = json::parse(msg_str);
+        body["event_id"]         = e.event_id;
+        body["sender"]           = e.sender;
+        body["origin_server_ts"] = e.origin_server_ts;
+        body["unsigned"]         = e.unsigned_data;
+
+        // relations are unencrypted in content...
+        if (json old_ev = e; old_ev["content"].count("m.relates_to") != 0)
+                body["content"]["m.relates_to"] = old_ev["content"]["m.relates_to"];
+
+        json event_array = json::array();
+        event_array.push_back(body);
+
+        std::vector temp_events;
+        mtx::responses::utils::parse_timeline_events(event_array, temp_events);
+
+        if (temp_events.size() == 1) {
+                auto encInfo = mtx::accessors::file(temp_events[0]);
+
+                if (encInfo)
+                        emit newEncryptedImage(encInfo.value());
+
+                return asCacheEntry(std::move(temp_events[0]));
+        }
+
         auto encInfo = mtx::accessors::file(decryptionResult.event.value());
         if (encInfo)
                 emit newEncryptedImage(encInfo.value());
@@ -515,7 +660,8 @@ EventStore::get(std::string_view id, std::string_view related_to, bool decrypt)
                                           mtx::http::RequestErr err) {
                                   if (err) {
                                           nhlog::net()->error(
-                                            "Failed to retrieve event with id {}, which was "
+                                            "Failed to retrieve event with id {}, which "
+                                            "was "
                                             "requested to show the replyTo for event {}",
                                             relatedTo,
                                             id);
diff --git a/src/timeline/EventStore.h b/src/timeline/EventStore.h
index d4353a1..954e271 100644
--- a/src/timeline/EventStore.h
+++ b/src/timeline/EventStore.h
@@ -98,6 +98,9 @@ signals:
         void processPending();
         void messageSent(std::string txn_id, std::string event_id);
         void messageFailed(std::string txn_id);
+        void startDMVerification(
+          const mtx::events::RoomEvent &msg);
+        void updateFlowEventId(std::string event_id);
 
 public slots:
         void addPending(mtx::events::collections::TimelineEvents event);
@@ -107,6 +110,7 @@ private:
         mtx::events::collections::TimelineEvents *decryptEvent(
           const IdIndex &idx,
           const mtx::events::EncryptedEvent &e);
+        void handle_room_verification(mtx::events::collections::TimelineEvents event);
 
         std::string room_id_;
 
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 32e9f92..359e95b 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -116,7 +116,46 @@ struct RoomEventType
         {
                 return qml_mtx_events::EventType::VideoMessage;
         }
-
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationRequest;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationStart;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationMac;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationAccept;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationReady;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationCancel;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationKey;
+        }
+        qml_mtx_events::EventType operator()(
+          const mtx::events::Event &)
+        {
+                return qml_mtx_events::EventType::KeyVerificationDone;
+        }
         qml_mtx_events::EventType operator()(const mtx::events::Event &)
         {
                 return qml_mtx_events::EventType::Redacted;
@@ -211,6 +250,15 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
         connect(&events, &EventStore::newEncryptedImage, this, &TimelineModel::newEncryptedImage);
         connect(
           &events, &EventStore::fetchedMore, this, [this]() { setPaginationInProgress(false); });
+        connect(&events,
+                &EventStore::startDMVerification,
+                this,
+                [this](mtx::events::RoomEvent msg) {
+                        ChatPage::instance()->receivedRoomDeviceVerificationRequest(msg, this);
+                });
+        connect(&events, &EventStore::updateFlowEventId, this, [this](std::string event_id) {
+                this->updateFlowEventId(event_id);
+        });
 }
 
 QHash
@@ -743,9 +791,9 @@ TimelineModel::viewDecryptedRawMessage(QString id) const
 }
 
 void
-TimelineModel::openUserProfile(QString userid) const
+TimelineModel::openUserProfile(QString userid)
 {
-        MainWindow::instance()->openUserProfile(userid, room_id_);
+        emit openProfile(new UserProfile(room_id_, userid, manager_, this));
 }
 
 void
@@ -846,18 +894,18 @@ TimelineModel::markEventsAsRead(const std::vector &event_ids)
         }
 }
 
+template
 void
-TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
-                                         nlohmann::json content,
-                                         mtx::events::EventType eventType)
+TimelineModel::sendEncryptedMessage(mtx::events::RoomEvent msg, mtx::events::EventType eventType)
 {
         const auto room_id = room_id_.toStdString();
 
         using namespace mtx::events;
         using namespace mtx::identifiers;
 
-        json doc = {
-          {"type", mtx::events::to_string(eventType)}, {"content", content}, {"room_id", room_id}};
+        json doc = {{"type", mtx::events::to_string(eventType)},
+                    {"content", json(msg.content)},
+                    {"room_id", room_id}};
 
         try {
                 // Check if we have already an outbound megolm session then we can use.
@@ -865,7 +913,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
                         mtx::events::EncryptedEvent event;
                         event.content =
                           olm::encrypt_group_message(room_id, http::client()->device_id(), doc);
-                        event.event_id         = txn_id;
+                        event.event_id         = msg.event_id;
                         event.room_id          = room_id;
                         event.sender           = http::client()->user_id().to_string();
                         event.type             = mtx::events::EventType::RoomEncrypted;
@@ -893,32 +941,43 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
                 OutboundGroupSessionData session_data;
                 session_data.session_id    = session_id;
                 session_data.session_key   = session_key;
-                session_data.message_index = 0; // TODO Update me
+                session_data.message_index = 0;
                 cache::saveOutboundMegolmSession(
                   room_id, session_data, std::move(outbound_session));
 
+                {
+                        MegolmSessionIndex index;
+                        index.room_id    = room_id;
+                        index.session_id = session_id;
+                        index.sender_key = olm::client()->identity_keys().curve25519;
+                        auto megolm_session =
+                          olm::client()->init_inbound_group_session(session_key);
+                        cache::saveInboundMegolmSession(index, std::move(megolm_session));
+                }
+
                 const auto members = cache::roomMembers(room_id);
                 nhlog::ui()->info("retrieved {} members for {}", members.size(), room_id);
 
-                auto keeper = std::make_shared([room_id, doc, txn_id, this]() {
-                        try {
-                                mtx::events::EncryptedEvent event;
-                                event.content = olm::encrypt_group_message(
-                                  room_id, http::client()->device_id(), doc);
-                                event.event_id         = txn_id;
-                                event.room_id          = room_id;
-                                event.sender           = http::client()->user_id().to_string();
-                                event.type             = mtx::events::EventType::RoomEncrypted;
-                                event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
-
-                                emit this->addPendingMessageToStore(event);
-                        } catch (const lmdb::error &e) {
-                                nhlog::db()->critical("failed to save megolm outbound session: {}",
-                                                      e.what());
-                                emit ChatPage::instance()->showNotification(
-                                  tr("Failed to encrypt event, sending aborted!"));
-                        }
-                });
+                auto keeper =
+                  std::make_shared([room_id, doc, txn_id = msg.event_id, this]() {
+                          try {
+                                  mtx::events::EncryptedEvent event;
+                                  event.content = olm::encrypt_group_message(
+                                    room_id, http::client()->device_id(), doc);
+                                  event.event_id         = txn_id;
+                                  event.room_id          = room_id;
+                                  event.sender           = http::client()->user_id().to_string();
+                                  event.type             = mtx::events::EventType::RoomEncrypted;
+                                  event.origin_server_ts = QDateTime::currentMSecsSinceEpoch();
+
+                                  emit this->addPendingMessageToStore(event);
+                          } catch (const lmdb::error &e) {
+                                  nhlog::db()->critical(
+                                    "failed to save megolm outbound session: {}", e.what());
+                                  emit ChatPage::instance()->showNotification(
+                                    tr("Failed to encrypt event, sending aborted!"));
+                          }
+                  });
 
                 mtx::requests::QueryKeys req;
                 for (const auto &member : members)
@@ -926,7 +985,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
 
                 http::client()->query_keys(
                   req,
-                  [keeper = std::move(keeper), megolm_payload, txn_id, this](
+                  [keeper = std::move(keeper), megolm_payload, txn_id = msg.event_id, this](
                     const mtx::responses::QueryKeys &res, mtx::http::RequestErr err) {
                           if (err) {
                                   nhlog::net()->warn("failed to query device keys: {} {}",
@@ -937,19 +996,23 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
                                   return;
                           }
 
-                          for (const auto &user : res.device_keys) {
-                                  // Mapping from a device_id with valid identity keys to the
-                                  // generated room_key event used for sharing the megolm session.
-                                  std::map room_key_msgs;
-                                  std::map deviceKeys;
+                          mtx::requests::ClaimKeys claim_keys;
 
-                                  room_key_msgs.clear();
-                                  deviceKeys.clear();
+                          // Mapping from user id to a device_id with valid identity keys to the
+                          // generated room_key event used for sharing the megolm session.
+                          std::map> room_key_msgs;
+                          std::map> deviceKeys;
 
+                          for (const auto &user : res.device_keys) {
                                   for (const auto &dev : user.second) {
                                           const auto user_id   = ::UserId(dev.second.user_id);
                                           const auto device_id = DeviceId(dev.second.device_id);
 
+                                          if (user_id.get() ==
+                                                http::client()->user_id().to_string() &&
+                                              device_id.get() == http::client()->device_id())
+                                                  continue;
+
                                           const auto device_keys = dev.second.keys;
                                           const auto curveKey    = "curve25519:" + device_id.get();
                                           const auto edKey       = "ed25519:" + device_id.get();
@@ -968,7 +1031,7 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
 
                                           try {
                                                   if (!mtx::crypto::verify_identity_signature(
-                                                        json(dev.second), device_id, user_id)) {
+                                                        dev.second, device_id, user_id)) {
                                                           nhlog::crypto()->warn(
                                                             "failed to verify identity keys: {}",
                                                             json(dev.second).dump(2));
@@ -991,42 +1054,25 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
                                                               user_id, pks.ed25519, megolm_payload)
                                                             .dump();
 
-                                          room_key_msgs.emplace(device_id, room_key);
-                                          deviceKeys.emplace(device_id, pks);
-                                  }
-
-                                  std::vector valid_devices;
-                                  valid_devices.reserve(room_key_msgs.size());
-                                  for (auto const &d : room_key_msgs) {
-                                          valid_devices.push_back(d.first);
+                                          room_key_msgs[user_id].emplace(device_id, room_key);
+                                          deviceKeys[user_id].emplace(device_id, pks);
+                                          claim_keys.one_time_keys[user.first][device_id] =
+                                            mtx::crypto::SIGNED_CURVE25519;
 
-                                          nhlog::net()->info("{}", d.first);
-                                          nhlog::net()->info("  curve25519 {}",
-                                                             deviceKeys.at(d.first).curve25519);
-                                          nhlog::net()->info("  ed25519 {}",
-                                                             deviceKeys.at(d.first).ed25519);
+                                          nhlog::net()->info("{}", device_id.get());
+                                          nhlog::net()->info("  curve25519 {}", pks.curve25519);
+                                          nhlog::net()->info("  ed25519 {}", pks.ed25519);
                                   }
-
-                                  nhlog::net()->info(
-                                    "sending claim request for user {} with {} devices",
-                                    user.first,
-                                    valid_devices.size());
-
-                                  http::client()->claim_keys(
-                                    user.first,
-                                    valid_devices,
-                                    std::bind(&TimelineModel::handleClaimedKeys,
-                                              this,
-                                              keeper,
-                                              room_key_msgs,
-                                              deviceKeys,
-                                              user.first,
-                                              std::placeholders::_1,
-                                              std::placeholders::_2));
-
-                                  // TODO: Wait before sending the next batch of requests.
-                                  std::this_thread::sleep_for(std::chrono::milliseconds(500));
                           }
+
+                          http::client()->claim_keys(claim_keys,
+                                                     std::bind(&TimelineModel::handleClaimedKeys,
+                                                               this,
+                                                               keeper,
+                                                               room_key_msgs,
+                                                               deviceKeys,
+                                                               std::placeholders::_1,
+                                                               std::placeholders::_2));
                   });
 
                 // TODO: Let the user know about the errors.
@@ -1044,12 +1090,12 @@ TimelineModel::sendEncryptedMessageEvent(const std::string &txn_id,
 }
 
 void
-TimelineModel::handleClaimedKeys(std::shared_ptr keeper,
-                                 const std::map &room_keys,
-                                 const std::map &pks,
-                                 const std::string &user_id,
-                                 const mtx::responses::ClaimKeys &res,
-                                 mtx::http::RequestErr err)
+TimelineModel::handleClaimedKeys(
+  std::shared_ptr keeper,
+  const std::map> &room_keys,
+  const std::map> &pks,
+  const mtx::responses::ClaimKeys &res,
+  mtx::http::RequestErr err)
 {
         if (err) {
                 nhlog::net()->warn("claim keys error: {} {} {}",
@@ -1059,65 +1105,59 @@ TimelineModel::handleClaimedKeys(std::shared_ptr keeper,
                 return;
         }
 
-        nhlog::net()->debug("claimed keys for {}", user_id);
-
-        if (res.one_time_keys.size() == 0) {
-                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
-                return;
-        }
-
-        if (res.one_time_keys.find(user_id) == res.one_time_keys.end()) {
-                nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
-                return;
-        }
+        // Payload with all the to_device message to be sent.
+        nlohmann::json body;
 
-        auto retrieved_devices = res.one_time_keys.at(user_id);
+        for (const auto &[user_id, retrieved_devices] : res.one_time_keys) {
+                nhlog::net()->debug("claimed keys for {}", user_id);
+                if (retrieved_devices.size() == 0) {
+                        nhlog::net()->debug("no one-time keys found for user_id: {}", user_id);
+                        return;
+                }
 
-        // Payload with all the to_device message to be sent.
-        json body;
-        body["messages"][user_id] = json::object();
+                for (const auto &rd : retrieved_devices) {
+                        const auto device_id = rd.first;
 
-        for (const auto &rd : retrieved_devices) {
-                const auto device_id = rd.first;
-                nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
+                        nhlog::net()->debug("{} : \n {}", device_id, rd.second.dump(2));
 
-                // TODO: Verify signatures
-                auto otk = rd.second.begin()->at("key");
+                        if (rd.second.empty() || !rd.second.begin()->contains("key")) {
+                                nhlog::net()->warn("Skipping device {} as it has no key.",
+                                                   device_id);
+                                continue;
+                        }
 
-                if (pks.find(device_id) == pks.end()) {
-                        nhlog::net()->critical("couldn't find public key for device: {}",
-                                               device_id);
-                        continue;
-                }
+                        // TODO: Verify signatures
+                        auto otk = rd.second.begin()->at("key");
 
-                auto id_key = pks.at(device_id).curve25519;
-                auto s      = olm::client()->create_outbound_session(id_key, otk);
+                        auto id_key = pks.at(user_id).at(device_id).curve25519;
+                        auto s      = olm::client()->create_outbound_session(id_key, otk);
 
-                if (room_keys.find(device_id) == room_keys.end()) {
-                        nhlog::net()->critical("couldn't find m.room_key for device: {}",
-                                               device_id);
-                        continue;
-                }
+                        auto device_msg = olm::client()->create_olm_encrypted_content(
+                          s.get(),
+                          room_keys.at(user_id).at(device_id),
+                          pks.at(user_id).at(device_id).curve25519);
 
-                auto device_msg = olm::client()->create_olm_encrypted_content(
-                  s.get(), room_keys.at(device_id), pks.at(device_id).curve25519);
+                        try {
+                                cache::saveOlmSession(id_key, std::move(s));
+                        } catch (const lmdb::error &e) {
+                                nhlog::db()->critical("failed to save outbound olm session: {}",
+                                                      e.what());
+                        } catch (const mtx::crypto::olm_exception &e) {
+                                nhlog::crypto()->critical(
+                                  "failed to pickle outbound olm session: {}", e.what());
+                        }
 
-                try {
-                        cache::saveOlmSession(id_key, std::move(s));
-                } catch (const lmdb::error &e) {
-                        nhlog::db()->critical("failed to save outbound olm session: {}", e.what());
-                } catch (const mtx::crypto::olm_exception &e) {
-                        nhlog::crypto()->critical("failed to pickle outbound olm session: {}",
-                                                  e.what());
+                        body["messages"][user_id][device_id] = device_msg;
                 }
 
-                body["messages"][user_id][device_id] = device_msg;
+                nhlog::net()->info("send_to_device: {}", user_id);
         }
 
-        nhlog::net()->info("send_to_device: {}", user_id);
-
         http::client()->send_to_device(
-          "m.room.encrypted", body, [keeper](mtx::http::RequestErr err) {
+          mtx::events::to_string(mtx::events::EventType::RoomEncrypted),
+          http::client()->generate_txn_id(),
+          body,
+          [keeper](mtx::http::RequestErr err) {
                   if (err) {
                           nhlog::net()->warn("failed to send "
                                              "send_to_device "
@@ -1143,8 +1183,7 @@ struct SendMessageVisitor
                         if (encInfo)
                                 emit model_->newEncryptedImage(encInfo.value());
 
-                        model_->sendEncryptedMessageEvent(
-                          msg.event_id, nlohmann::json(msg.content), Event);
+                        model_->sendEncryptedMessage(msg, Event);
                 } else {
                         msg.type = Event;
                         emit model_->addPendingMessageToStore(msg);
@@ -1199,6 +1238,54 @@ struct SendMessageVisitor
                   event);
         }
 
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
+        void operator()(const mtx::events::RoomEvent &msg)
+        {
+                sendRoomEvent(msg);
+        }
+
         TimelineModel *model_;
 };
 
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 6daaac1..3234a20 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -10,6 +10,7 @@
 
 #include "CacheCryptoStructs.h"
 #include "EventStore.h"
+#include "ui/UserProfile.h"
 
 namespace mtx::http {
 using RequestErr = const std::optional &;
@@ -87,6 +88,14 @@ enum EventType
         VideoMessage,
         Redacted,
         UnknownMessage,
+        KeyVerificationRequest,
+        KeyVerificationStart,
+        KeyVerificationMac,
+        KeyVerificationAccept,
+        KeyVerificationCancel,
+        KeyVerificationKey,
+        KeyVerificationDone,
+        KeyVerificationReady
 };
 Q_ENUM_NS(EventType)
 
@@ -199,7 +208,7 @@ public:
 
         Q_INVOKABLE void viewRawMessage(QString id) const;
         Q_INVOKABLE void viewDecryptedRawMessage(QString id) const;
-        Q_INVOKABLE void openUserProfile(QString userid) const;
+        Q_INVOKABLE void openUserProfile(QString userid);
         Q_INVOKABLE void replyAction(QString id);
         Q_INVOKABLE void readReceiptsAction(QString id) const;
         Q_INVOKABLE void redactEvent(QString id);
@@ -275,23 +284,25 @@ signals:
         void paginationInProgressChanged(const bool);
         void newCallEvent(const mtx::events::collections::TimelineEvents &event);
 
+        void openProfile(UserProfile *profile);
+
         void newMessageToSend(mtx::events::collections::TimelineEvents event);
         void addPendingMessageToStore(mtx::events::collections::TimelineEvents event);
+        void updateFlowEventId(std::string event_id);
 
         void roomNameChanged();
         void roomTopicChanged();
         void roomAvatarUrlChanged();
 
 private:
-        void sendEncryptedMessageEvent(const std::string &txn_id,
-                                       nlohmann::json content,
-                                       mtx::events::EventType);
-        void handleClaimedKeys(std::shared_ptr keeper,
-                               const std::map &room_key,
-                               const std::map &pks,
-                               const std::string &user_id,
-                               const mtx::responses::ClaimKeys &res,
-                               mtx::http::RequestErr err);
+        template
+        void sendEncryptedMessage(mtx::events::RoomEvent msg, mtx::events::EventType eventType);
+        void handleClaimedKeys(
+          std::shared_ptr keeper,
+          const std::map> &room_keys,
+          const std::map> &pks,
+          const mtx::responses::ClaimKeys &res,
+          mtx::http::RequestErr err);
         void readEvent(const std::string &id);
 
         void setPaginationInProgress(const bool paginationInProgress);
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 2b453e5..7c81ca8 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -4,6 +4,7 @@
 #include 
 #include 
 #include 
+#include 
 #include 
 
 #include "BlurhashProvider.h"
@@ -19,7 +20,12 @@
 #include "emoji/EmojiModel.h"
 #include "emoji/Provider.h"
 
+#include  //only for debugging
+
 Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
+Q_DECLARE_METATYPE(std::vector)
+
+namespace msgs = mtx::events::msg;
 
 void
 TimelineViewManager::updateEncryptedDescriptions()
@@ -65,9 +71,13 @@ TimelineViewManager::userColor(QString id, QColor background)
 QString
 TimelineViewManager::userPresence(QString id) const
 {
-        return QString::fromStdString(
-          mtx::presence::to_string(cache::presenceState(id.toStdString())));
+        if (id.isEmpty())
+                return "";
+        else
+                return QString::fromStdString(
+                  mtx::presence::to_string(cache::presenceState(id.toStdString())));
 }
+
 QString
 TimelineViewManager::userStatus(QString id) const
 {
@@ -83,15 +93,52 @@ TimelineViewManager::TimelineViewManager(QSharedPointer userSettin
   , callManager_(callManager)
   , settings(userSettings)
 {
+        qRegisterMetaType();
+        qRegisterMetaType();
+        qRegisterMetaType();
+        qRegisterMetaType();
+        qRegisterMetaType();
+        qRegisterMetaType();
+        qRegisterMetaType();
+        qRegisterMetaType();
+
         qmlRegisterUncreatableMetaObject(qml_mtx_events::staticMetaObject,
                                          "im.nheko",
                                          1,
                                          0,
                                          "MtxEvent",
                                          "Can't instantiate enum!");
+        qmlRegisterUncreatableMetaObject(verification::staticMetaObject,
+                                         "im.nheko",
+                                         1,
+                                         0,
+                                         "VerificationStatus",
+                                         "Can't instantiate enum!");
+
         qmlRegisterType("im.nheko", 1, 0, "DelegateChoice");
         qmlRegisterType("im.nheko", 1, 0, "DelegateChooser");
+        qmlRegisterUncreatableType(
+          "im.nheko", 1, 0, "DeviceVerificationFlow", "Can't create verification flow from QML!");
+        qmlRegisterUncreatableType(
+          "im.nheko",
+          1,
+          0,
+          "UserProfileModel",
+          "UserProfile needs to be instantiated on the C++ side");
+
+        static auto self = this;
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "TimelineManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  return self;
+          });
+        qmlRegisterSingletonType(
+          "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  return self->settings.data();
+          });
+
         qRegisterMetaType();
+        qRegisterMetaType>();
+
         qmlRegisterType("im.nheko.EmojiModel", 1, 0, "EmojiModel");
         qmlRegisterType("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
         qmlRegisterUncreatableType(
@@ -123,8 +170,6 @@ TimelineViewManager::TimelineViewManager(QSharedPointer userSettin
         });
 #endif
         container->setMinimumSize(200, 200);
-        view->rootContext()->setContextProperty("timelineManager", this);
-        view->rootContext()->setContextProperty("settings", settings.data());
         updateColorPalette();
         view->engine()->addImageProvider("MxcImage", imgProvider);
         view->engine()->addImageProvider("colorimage", colorImgProvider);
@@ -136,6 +181,57 @@ TimelineViewManager::TimelineViewManager(QSharedPointer userSettin
                 &ChatPage::decryptSidebarChanged,
                 this,
                 &TimelineViewManager::updateEncryptedDescriptions);
+        connect(
+          dynamic_cast(parent),
+          &ChatPage::receivedRoomDeviceVerificationRequest,
+          this,
+          [this](const mtx::events::RoomEvent &message,
+                 TimelineModel *model) {
+                  auto event_id = QString::fromStdString(message.event_id);
+                  if (!this->dvList.contains(event_id)) {
+                          if (auto flow = DeviceVerificationFlow::NewInRoomVerification(
+                                this,
+                                model,
+                                message.content,
+                                QString::fromStdString(message.sender),
+                                event_id)) {
+                                  dvList[event_id] = flow;
+                                  emit newDeviceVerificationRequest(flow.data());
+                          }
+                  }
+          });
+        connect(dynamic_cast(parent),
+                &ChatPage::receivedDeviceVerificationRequest,
+                this,
+                [this](const mtx::events::msg::KeyVerificationRequest &msg, std::string sender) {
+                        if (!msg.transaction_id)
+                                return;
+
+                        auto txnid = QString::fromStdString(msg.transaction_id.value());
+                        if (!this->dvList.contains(txnid)) {
+                                if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
+                                      this, msg, QString::fromStdString(sender), txnid)) {
+                                        dvList[txnid] = flow;
+                                        emit newDeviceVerificationRequest(flow.data());
+                                }
+                        }
+                });
+        connect(dynamic_cast(parent),
+                &ChatPage::receivedDeviceVerificationStart,
+                this,
+                [this](const mtx::events::msg::KeyVerificationStart &msg, std::string sender) {
+                        if (!msg.transaction_id)
+                                return;
+
+                        auto txnid = QString::fromStdString(msg.transaction_id.value());
+                        if (!this->dvList.contains(txnid)) {
+                                if (auto flow = DeviceVerificationFlow::NewToDeviceVerification(
+                                      this, msg, QString::fromStdString(sender), txnid)) {
+                                        dvList[txnid] = flow;
+                                        emit newDeviceVerificationRequest(flow.data());
+                                }
+                        }
+                });
         connect(parent, &ChatPage::loggedOut, this, [this]() {
                 isInitialSync_ = true;
                 emit initialSyncChanged(true);
@@ -284,6 +380,58 @@ TimelineViewManager::openRoomSettings() const
         MainWindow::instance()->openRoomSettings(timeline_->roomId());
 }
 
+void
+TimelineViewManager::verifyUser(QString userid)
+{
+        auto joined_rooms = cache::joinedRooms();
+        auto room_infos   = cache::getRoomInfo(joined_rooms);
+
+        for (std::string room_id : joined_rooms) {
+                if ((room_infos[QString::fromStdString(room_id)].member_count == 2) &&
+                    cache::isRoomEncrypted(room_id)) {
+                        auto room_members = cache::roomMembers(room_id);
+                        if (std::find(room_members.begin(),
+                                      room_members.end(),
+                                      (userid).toStdString()) != room_members.end()) {
+                                auto model = models.value(QString::fromStdString(room_id));
+                                auto flow  = DeviceVerificationFlow::InitiateUserVerification(
+                                  this, model.data(), userid);
+                                connect(model.data(),
+                                        &TimelineModel::updateFlowEventId,
+                                        this,
+                                        [this, flow](std::string eventId) {
+                                                dvList[QString::fromStdString(eventId)] = flow;
+                                        });
+                                emit newDeviceVerificationRequest(flow.data());
+                                return;
+                        }
+                }
+        }
+
+        emit ChatPage::instance()->showNotification(
+          tr("No share room with this user found. Create an "
+             "encrypted room with this user and try again."));
+}
+
+void
+TimelineViewManager::removeVerificationFlow(DeviceVerificationFlow *flow)
+{
+        for (auto it = dvList.keyValueBegin(); it != dvList.keyValueEnd(); ++it) {
+                if ((*it).second == flow) {
+                        dvList.remove((*it).first);
+                        return;
+                }
+        }
+}
+
+void
+TimelineViewManager::verifyDevice(QString userid, QString deviceid)
+{
+        auto flow = DeviceVerificationFlow::InitiateDeviceVerification(this, userid, deviceid);
+        this->dvList[flow->transactionId()] = flow;
+        emit newDeviceVerificationRequest(flow.data());
+}
+
 void
 TimelineViewManager::updateReadReceipts(const QString &room_id,
                                         const std::vector &event_ids)
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 9ff9ada..9a2a646 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -11,6 +11,7 @@
 
 #include "Cache.h"
 #include "CallManager.h"
+#include "DeviceVerificationFlow.h"
 #include "Logging.h"
 #include "TimelineModel.h"
 #include "Utils.h"
@@ -71,6 +72,10 @@ public:
         Q_INVOKABLE void openMemberListDialog() const;
         Q_INVOKABLE void openLeaveRoomDialog() const;
         Q_INVOKABLE void openRoomSettings() const;
+        Q_INVOKABLE void removeVerificationFlow(DeviceVerificationFlow *flow);
+
+        void verifyUser(QString userid);
+        void verifyDevice(QString userid, QString deviceid);
 
 signals:
         void clearRoomMessageCount(QString roomid);
@@ -79,6 +84,7 @@ signals:
         void initialSyncChanged(bool isInitialSync);
         void replyingEventChanged(QString replyingEvent);
         void replyClosed();
+        void newDeviceVerificationRequest(DeviceVerificationFlow *flow);
         void inviteUsers(QStringList users);
         void showRoomList();
         void narrowViewChanged();
@@ -172,4 +178,14 @@ private:
 
         QSharedPointer settings;
         QHash userColors;
+
+        QHash> dvList;
 };
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationAccept)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationCancel)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationDone)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationKey)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationMac)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationReady)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationRequest)
+Q_DECLARE_METATYPE(mtx::events::msg::KeyVerificationStart)
diff --git a/src/ui/UserProfile.cpp b/src/ui/UserProfile.cpp
new file mode 100644
index 0000000..2bb0370
--- /dev/null
+++ b/src/ui/UserProfile.cpp
@@ -0,0 +1,227 @@
+#include "UserProfile.h"
+#include "Cache_p.h"
+#include "ChatPage.h"
+#include "DeviceVerificationFlow.h"
+#include "Logging.h"
+#include "Utils.h"
+#include "mtx/responses/crypto.hpp"
+#include "timeline/TimelineModel.h"
+#include "timeline/TimelineViewManager.h"
+
+UserProfile::UserProfile(QString roomid,
+                         QString userid,
+                         TimelineViewManager *manager_,
+                         TimelineModel *parent)
+  : QObject(parent)
+  , roomid_(roomid)
+  , userid_(userid)
+  , manager(manager_)
+  , model(parent)
+{
+        fetchDeviceList(this->userid_);
+
+        connect(cache::client(),
+                &Cache::verificationStatusChanged,
+                this,
+                [this](const std::string &user_id) {
+                        if (user_id != this->userid_.toStdString())
+                                return;
+
+                        auto status = cache::verificationStatus(user_id);
+                        if (!status)
+                                return;
+                        this->isUserVerified = status->user_verified;
+                        emit userStatusChanged();
+
+                        for (auto &deviceInfo : deviceList_.deviceList_) {
+                                deviceInfo.verification_status =
+                                  std::find(status->verified_devices.begin(),
+                                            status->verified_devices.end(),
+                                            deviceInfo.device_id.toStdString()) ==
+                                      status->verified_devices.end()
+                                    ? verification::UNVERIFIED
+                                    : verification::VERIFIED;
+                        }
+                        deviceList_.reset(deviceList_.deviceList_);
+                });
+}
+
+QHash
+DeviceInfoModel::roleNames() const
+{
+        return {
+          {DeviceId, "deviceId"},
+          {DeviceName, "deviceName"},
+          {VerificationStatus, "verificationStatus"},
+        };
+}
+
+QVariant
+DeviceInfoModel::data(const QModelIndex &index, int role) const
+{
+        if (!index.isValid() || index.row() >= (int)deviceList_.size() || index.row() < 0)
+                return {};
+
+        switch (role) {
+        case DeviceId:
+                return deviceList_[index.row()].device_id;
+        case DeviceName:
+                return deviceList_[index.row()].display_name;
+        case VerificationStatus:
+                return QVariant::fromValue(deviceList_[index.row()].verification_status);
+        default:
+                return {};
+        }
+}
+
+void
+DeviceInfoModel::reset(const std::vector &deviceList)
+{
+        beginResetModel();
+        this->deviceList_ = std::move(deviceList);
+        endResetModel();
+}
+
+DeviceInfoModel *
+UserProfile::deviceList()
+{
+        return &this->deviceList_;
+}
+
+QString
+UserProfile::userid()
+{
+        return this->userid_;
+}
+
+QString
+UserProfile::displayName()
+{
+        return cache::displayName(roomid_, userid_);
+}
+
+QString
+UserProfile::avatarUrl()
+{
+        return cache::avatarUrl(roomid_, userid_);
+}
+
+bool
+UserProfile::getUserStatus()
+{
+        return isUserVerified;
+}
+
+void
+UserProfile::fetchDeviceList(const QString &userID)
+{
+        auto localUser = utils::localUser();
+
+        ChatPage::instance()->query_keys(
+          userID.toStdString(),
+          [other_user_id = userID.toStdString(), this](const UserKeyCache &other_user_keys,
+                                                       mtx::http::RequestErr err) {
+                  if (err) {
+                          nhlog::net()->warn("failed to query device keys: {},{}",
+                                             err->matrix_error.errcode,
+                                             static_cast(err->status_code));
+                          return;
+                  }
+
+                  // Finding if the User is Verified or not based on the Signatures
+                  ChatPage::instance()->query_keys(
+                    utils::localUser().toStdString(),
+                    [other_user_id, other_user_keys, this](const UserKeyCache &res,
+                                                           mtx::http::RequestErr err) {
+                            using namespace mtx;
+                            std::string local_user_id = utils::localUser().toStdString();
+
+                            if (err) {
+                                    nhlog::net()->warn("failed to query device keys: {},{}",
+                                                       err->matrix_error.errcode,
+                                                       static_cast(err->status_code));
+                                    return;
+                            }
+
+                            if (res.device_keys.empty()) {
+                                    nhlog::net()->warn("no devices retrieved {}", local_user_id);
+                                    return;
+                            }
+
+                            std::vector deviceInfo;
+                            auto devices = other_user_keys.device_keys;
+                            auto verificationStatus =
+                              cache::client()->verificationStatus(other_user_id);
+
+                            isUserVerified = verificationStatus.user_verified;
+                            emit userStatusChanged();
+
+                            for (const auto &d : devices) {
+                                    auto device = d.second;
+                                    verification::Status verified =
+                                      verification::Status::UNVERIFIED;
+
+                                    if (std::find(verificationStatus.verified_devices.begin(),
+                                                  verificationStatus.verified_devices.end(),
+                                                  device.device_id) !=
+                                          verificationStatus.verified_devices.end() &&
+                                        mtx::crypto::verify_identity_signature(
+                                          device,
+                                          DeviceId(device.device_id),
+                                          UserId(other_user_id)))
+                                            verified = verification::Status::VERIFIED;
+
+                                    deviceInfo.push_back(
+                                      {QString::fromStdString(d.first),
+                                       QString::fromStdString(
+                                         device.unsigned_info.device_display_name),
+                                       verified});
+                            }
+
+                            this->deviceList_.queueReset(std::move(deviceInfo));
+                    });
+          });
+}
+
+void
+UserProfile::banUser()
+{
+        ChatPage::instance()->banUser(this->userid_, "");
+}
+
+// void ignoreUser(){
+
+// }
+
+void
+UserProfile::kickUser()
+{
+        ChatPage::instance()->kickUser(this->userid_, "");
+}
+
+void
+UserProfile::startChat()
+{
+        mtx::requests::CreateRoom req;
+        req.preset     = mtx::requests::Preset::PrivateChat;
+        req.visibility = mtx::requests::Visibility::Private;
+        if (utils::localUser() != this->userid_)
+                req.invite = {this->userid_.toStdString()};
+        emit ChatPage::instance()->createRoom(req);
+}
+
+void
+UserProfile::verify(QString device)
+{
+        if (!device.isEmpty())
+                manager->verifyDevice(userid_, device);
+        else {
+                manager->verifyUser(userid_);
+        }
+}
+
+void
+UserProfile::unverify(QString device)
+{
+        cache::markDeviceUnverified(userid_.toStdString(), device.toStdString());
+}
diff --git a/src/ui/UserProfile.h b/src/ui/UserProfile.h
new file mode 100644
index 0000000..77b2232
--- /dev/null
+++ b/src/ui/UserProfile.h
@@ -0,0 +1,123 @@
+#pragma once
+
+#include 
+#include 
+#include 
+#include 
+
+#include "MatrixClient.h"
+
+namespace verification {
+Q_NAMESPACE
+
+enum Status
+{
+        VERIFIED,
+        UNVERIFIED,
+        BLOCKED
+};
+Q_ENUM_NS(Status)
+}
+
+class DeviceVerificationFlow;
+class TimelineModel;
+class TimelineViewManager;
+
+class DeviceInfo
+{
+public:
+        DeviceInfo(const QString deviceID,
+                   const QString displayName,
+                   verification::Status verification_status_)
+          : device_id(deviceID)
+          , display_name(displayName)
+          , verification_status(verification_status_)
+        {}
+        DeviceInfo()
+          : verification_status(verification::UNVERIFIED)
+        {}
+
+        QString device_id;
+        QString display_name;
+
+        verification::Status verification_status;
+};
+
+class DeviceInfoModel : public QAbstractListModel
+{
+        Q_OBJECT
+public:
+        enum Roles
+        {
+                DeviceId,
+                DeviceName,
+                VerificationStatus,
+        };
+
+        explicit DeviceInfoModel(QObject *parent = nullptr)
+        {
+                (void)parent;
+                connect(this, &DeviceInfoModel::queueReset, this, &DeviceInfoModel::reset);
+        };
+        QHash roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override
+        {
+                (void)parent;
+                return (int)deviceList_.size();
+        }
+        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+signals:
+        void queueReset(const std::vector &deviceList);
+public slots:
+        void reset(const std::vector &deviceList);
+
+private:
+        std::vector deviceList_;
+
+        friend class UserProfile;
+};
+
+class UserProfile : public QObject
+{
+        Q_OBJECT
+        Q_PROPERTY(QString displayName READ displayName CONSTANT)
+        Q_PROPERTY(QString userid READ userid CONSTANT)
+        Q_PROPERTY(QString avatarUrl READ avatarUrl CONSTANT)
+        Q_PROPERTY(DeviceInfoModel *deviceList READ deviceList CONSTANT)
+        Q_PROPERTY(bool isUserVerified READ getUserStatus NOTIFY userStatusChanged)
+public:
+        UserProfile(QString roomid,
+                    QString userid,
+                    TimelineViewManager *manager_,
+                    TimelineModel *parent = nullptr);
+
+        DeviceInfoModel *deviceList();
+
+        QString userid();
+        QString displayName();
+        QString avatarUrl();
+        bool getUserStatus();
+
+        Q_INVOKABLE void verify(QString device = "");
+        Q_INVOKABLE void unverify(QString device = "");
+        Q_INVOKABLE void fetchDeviceList(const QString &userID);
+        Q_INVOKABLE void banUser();
+        // Q_INVOKABLE void ignoreUser();
+        Q_INVOKABLE void kickUser();
+        Q_INVOKABLE void startChat();
+
+signals:
+        void userStatusChanged();
+
+private:
+        QString roomid_, userid_;
+        DeviceInfoModel deviceList_;
+        bool isUserVerified = false;
+        TimelineViewManager *manager;
+        TimelineModel *model;
+
+        void callback_fn(const mtx::responses::QueryKeys &res,
+                         mtx::http::RequestErr err,
+                         std::string user_id);
+};