summary refs log tree commit diff
path: root/resources/qml
diff options
context:
space:
mode:
authorJoe <rubberduckie3554@gmail.com>2021-07-24 18:26:25 -0400
committerJoe <rubberduckie3554@gmail.com>2021-07-24 18:26:25 -0400
commit3f567a8da7f41a2ce094f15340e39ea8aec55fb3 (patch)
tree42b6f59f0fa36ad738133198333cccf0c4100621 /resources/qml
parentRemove 'respond to key requests' functionality (diff)
parentFix edge case that could lead to no new one time keys being uploaded (diff)
downloadnheko-3f567a8da7f41a2ce094f15340e39ea8aec55fb3.tar.xz
Merge master and fix conflicts
Diffstat (limited to 'resources/qml')
-rw-r--r--resources/qml/Completer.qml2
-rw-r--r--resources/qml/InviteDialog.qml159
-rw-r--r--resources/qml/MatrixText.qml8
-rw-r--r--resources/qml/MatrixTextField.qml4
-rw-r--r--resources/qml/MessageInput.qml27
-rw-r--r--resources/qml/MessageView.qml31
-rw-r--r--resources/qml/RoomList.qml20
-rw-r--r--resources/qml/RoomMembers.qml148
-rw-r--r--resources/qml/RoomSettings.qml39
-rw-r--r--resources/qml/Root.qml72
-rw-r--r--resources/qml/TimelineRow.qml41
-rw-r--r--resources/qml/TimelineView.qml12
-rw-r--r--resources/qml/TopBar.qml12
-rw-r--r--resources/qml/delegates/MessageDelegate.qml13
-rw-r--r--resources/qml/delegates/Reply.qml2
-rw-r--r--resources/qml/dialogs/ImagePackSettingsDialog.qml309
-rw-r--r--resources/qml/emoji/EmojiButton.qml23
-rw-r--r--resources/qml/emoji/EmojiPicker.qml1
-rw-r--r--resources/qml/emoji/StickerPicker.qml180
-rw-r--r--resources/qml/voip/PlaceCall.qml4
-rw-r--r--resources/qml/voip/ScreenShare.qml2
21 files changed, 1006 insertions, 103 deletions
diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml
index 333fb11d..00fc3216 100644
--- a/resources/qml/Completer.qml
+++ b/resources/qml/Completer.qml
@@ -70,7 +70,7 @@ Popup {
     onCompleterNameChanged: {
         if (completerName) {
             if (completerName == "user")
-                completer = TimelineManager.completerFor(completerName, room.roomId());
+                completer = TimelineManager.completerFor(completerName, room.roomId);
             else
                 completer = TimelineManager.completerFor(completerName);
             completer.setSearchString("");
diff --git a/resources/qml/InviteDialog.qml b/resources/qml/InviteDialog.qml
new file mode 100644
index 00000000..50287ad5
--- /dev/null
+++ b/resources/qml/InviteDialog.qml
@@ -0,0 +1,159 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: inviteDialogRoot
+
+    property string roomId
+    property string plainRoomName
+    property InviteesModel invitees
+
+    function addInvite() {
+        if (inviteeEntry.isValidMxid) {
+            invitees.addUser(inviteeEntry.text);
+            inviteeEntry.clear();
+        }
+    }
+
+    function cleanUpAndClose() {
+        if (inviteeEntry.isValidMxid)
+            addInvite();
+
+        invitees.accept();
+        close();
+    }
+
+    title: qsTr("Invite users to %1").arg(plainRoomName)
+    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
+    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+    height: 380
+    width: 340
+    palette: Nheko.colors
+    color: Nheko.colors.window
+
+    Shortcut {
+        sequence: "Ctrl+Enter"
+        onActivated: cleanUpAndClose()
+    }
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: inviteDialogRoot.close()
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingMedium
+        spacing: Nheko.paddingMedium
+
+        Label {
+            text: qsTr("User ID to invite")
+            Layout.fillWidth: true
+            color: Nheko.colors.text
+        }
+
+        RowLayout {
+            spacing: Nheko.paddingMedium
+
+            MatrixTextField {
+                id: inviteeEntry
+
+                property bool isValidMxid: text.match("@.+?:.{3,}")
+
+                backgroundColor: Nheko.colors.window
+                placeholderText: qsTr("@joe:matrix.org", "Example user id. The name 'joe' can be localized however you want.")
+                Layout.fillWidth: true
+                onAccepted: {
+                    if (isValidMxid)
+                        addInvite();
+
+                }
+                Component.onCompleted: forceActiveFocus()
+                Keys.onShortcutOverride: event.accepted = ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers & Qt.ControlModifier))
+                Keys.onPressed: {
+                    if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && (event.modifiers === Qt.ControlModifier))
+                        cleanUpAndClose();
+
+                }
+            }
+
+            Button {
+                text: qsTr("Add")
+                enabled: inviteeEntry.isValidMxid
+                onClicked: addInvite()
+            }
+
+        }
+
+        ListView {
+            id: inviteesList
+
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            model: invitees
+
+            delegate: RowLayout {
+                spacing: Nheko.paddingMedium
+
+                Avatar {
+                    width: Nheko.avatarSize
+                    height: Nheko.avatarSize
+                    userid: model.mxid
+                    url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                    displayName: model.displayName
+                    onClicked: TimelineManager.openGlobalUserProfile(model.mxid)
+                }
+
+                ColumnLayout {
+                    spacing: Nheko.paddingSmall
+
+                    Label {
+                        text: model.displayName
+                        color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
+                        font.pointSize: fontMetrics.font.pointSize
+                    }
+
+                    Label {
+                        text: model.mxid
+                        color: Nheko.colors.buttonText
+                        font.pointSize: fontMetrics.font.pointSize * 0.9
+                    }
+
+                    Item {
+                        Layout.fillHeight: true
+                        Layout.fillWidth: true
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        id: buttons
+
+        Button {
+            text: qsTr("Invite")
+            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+            enabled: invitees.count > 0
+            onClicked: cleanUpAndClose()
+        }
+
+        Button {
+            text: qsTr("Cancel")
+            DialogButtonBox.buttonRole: DialogButtonBox.DestructiveRole
+            onClicked: inviteDialogRoot.close()
+        }
+
+    }
+
+}
diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml
index 9129b154..35e5f7e7 100644
--- a/resources/qml/MatrixText.qml
+++ b/resources/qml/MatrixText.qml
@@ -8,6 +8,7 @@ import im.nheko 1.0
 
 TextEdit {
     id: r
+
     textFormat: TextEdit.RichText
     readOnly: true
     focus: false
@@ -19,14 +20,13 @@ TextEdit {
     onLinkActivated: Nheko.openLink(link)
     ToolTip.visible: hoveredLink
     ToolTip.text: hoveredLink
+    Component.onCompleted: {
+        TimelineManager.fixImageRendering(r.textDocument, r);
+    }
 
     CursorShape {
         anchors.fill: parent
         cursorShape: hoveredLink ? Qt.PointingHandCursor : Qt.ArrowCursor
     }
 
-    Component.onCompleted: {
-        TimelineManager.fixImageRendering(r.textDocument, r)
-    }
-
 }
diff --git a/resources/qml/MatrixTextField.qml b/resources/qml/MatrixTextField.qml
index 3c660bac..80732b27 100644
--- a/resources/qml/MatrixTextField.qml
+++ b/resources/qml/MatrixTextField.qml
@@ -10,6 +10,8 @@ import im.nheko 1.0
 TextField {
     id: input
 
+    property alias backgroundColor: backgroundRect.color
+
     palette: Nheko.colors
     color: Nheko.colors.text
 
@@ -62,6 +64,8 @@ TextField {
     }
 
     background: Rectangle {
+        id: backgroundRect
+
         color: Nheko.colors.base
     }
 
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index 24f9b0e8..58d71a4e 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -2,6 +2,7 @@
 //
 // SPDX-License-Identifier: GPL-3.0-or-later
 
+import "./emoji"
 import "./voip"
 import QtQuick 2.12
 import QtQuick.Controls 2.3
@@ -87,7 +88,7 @@ Rectangle {
             Layout.alignment: Qt.AlignBottom // | Qt.AlignHCenter
             Layout.maximumHeight: Window.height / 4
             Layout.minimumHeight: Settings.fontSize
-            implicitWidth: inputBar.width - 4 * (22 + 16) - 24
+            implicitWidth: inputBar.width - 5 * (22 + 16) - 24
 
             TextArea {
                 id: messageInput
@@ -320,6 +321,30 @@ Rectangle {
         }
 
         ImageButton {
+            id: stickerButton
+
+            Layout.alignment: Qt.AlignRight | Qt.AlignBottom
+            Layout.margins: 8
+            hoverEnabled: true
+            width: 22
+            height: 22
+            image: ":/icons/icons/ui/sticky-note-solid.svg"
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Stickers")
+            onClicked: stickerPopup.visible ? stickerPopup.close() : stickerPopup.show(stickerButton, room.roomId, function(row) {
+                room.input.sticker(stickerPopup.model.sourceModel, row);
+                TimelineManager.focusMessageInput();
+            })
+
+            StickerPicker {
+                id: stickerPopup
+
+                colors: Nheko.colors
+            }
+
+        }
+
+        ImageButton {
             id: emojiButton
 
             Layout.alignment: Qt.AlignRight | Qt.AlignBottom
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 33dff122..50cbd371 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -92,16 +92,20 @@ ScrollView {
                     }
                 }
 
-                EmojiButton {
+                ImageButton {
                     id: reactButton
 
                     visible: chat.model ? chat.model.permissions.canSend(MtxEvent.Reaction) : false
                     width: 16
                     hoverEnabled: true
+                    image: ":/icons/icons/ui/smile.png"
                     ToolTip.visible: hovered
                     ToolTip.text: qsTr("React")
-                    emojiPicker: emojiPopup
-                    event_id: row.model ? row.model.eventId : ""
+                    onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, function(emoji) {
+                        var event_id = row.model ? row.model.eventId : "";
+                        room.input.reaction(event_id, emoji);
+                        TimelineManager.focusMessageInput();
+                    })
                 }
 
                 ImageButton {
@@ -337,6 +341,7 @@ ScrollView {
             required property var timestamp
             required property int status
             required property int index
+            required property int relatedEventCacheBuster
             required property string previousMessageUserId
             required property string day
             required property string previousMessageDay
@@ -442,6 +447,7 @@ ScrollView {
                 trustlevel: wrapper.trustlevel
                 timestamp: wrapper.timestamp
                 status: wrapper.status
+                relatedEventCacheBuster: wrapper.relatedEventCacheBuster
                 y: section.visible && section.active ? section.y + section.height : 0
 
                 HoverHandler {
@@ -471,12 +477,23 @@ ScrollView {
 
         }
 
-        footer: Spinner {
+        footer: Item {
             anchors.horizontalCenter: parent.horizontalCenter
-            running: chat.model && chat.model.paginationInProgress
-            foreground: Nheko.colors.mid
+            anchors.margins: Nheko.paddingLarge
             visible: chat.model && chat.model.paginationInProgress
-            z: 3
+            // hacky, but works
+            height: loadingSpinner.height + 2 * Nheko.paddingLarge
+
+            Spinner {
+                id: loadingSpinner
+
+                anchors.centerIn: parent
+                anchors.margins: Nheko.paddingLarge
+                running: chat.model && chat.model.paginationInProgress
+                foreground: Nheko.colors.mid
+                z: 3
+            }
+
         }
 
     }
diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml
index a1ce8d7e..2be5fe92 100644
--- a/resources/qml/RoomList.qml
+++ b/resources/qml/RoomList.qml
@@ -33,8 +33,8 @@ Page {
 
         Connections {
             onActiveTimelineChanged: {
-                roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId()), ListView.Contain);
-                console.log("Test" + Rooms.currentRoom.roomId() + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId()));
+                roomlist.positionViewAtIndex(Rooms.roomidToIndex(Rooms.currentRoom.roomId), ListView.Contain);
+                console.log("Test" + Rooms.currentRoom.roomId + " " + Rooms.roomidToIndex(Rooms.currentRoom.roomId));
             }
             target: TimelineManager
         }
@@ -61,9 +61,19 @@ Page {
                 }
             }
 
+            Platform.MessageDialog {
+                id: leaveRoomDialog
+
+                title: qsTr("Leave Room")
+                text: qsTr("Are you sure you want to leave this room?")
+                modality: Qt.Modal
+                onAccepted: Rooms.leave(roomContextMenu.roomid)
+                buttons: Dialog.Ok | Dialog.Cancel
+            }
+
             Platform.MenuItem {
                 text: qsTr("Leave room")
-                onTriggered: Rooms.leave(roomContextMenu.roomid)
+                onTriggered: leaveRoomDialog.open()
             }
 
             Platform.MenuSeparator {
@@ -133,7 +143,7 @@ Page {
             states: [
                 State {
                     name: "highlight"
-                    when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == roomId)
+                    when: hovered.hovered && !((Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId)
 
                     PropertyChanges {
                         target: roomItem
@@ -147,7 +157,7 @@ Page {
                 },
                 State {
                     name: "selected"
-                    when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId()) || Rooms.currentRoomPreview.roomid == roomId
+                    when: (Rooms.currentRoom && roomId == Rooms.currentRoom.roomId) || Rooms.currentRoomPreview.roomid == roomId
 
                     PropertyChanges {
                         target: roomItem
diff --git a/resources/qml/RoomMembers.qml b/resources/qml/RoomMembers.qml
new file mode 100644
index 00000000..641a08be
--- /dev/null
+++ b/resources/qml/RoomMembers.qml
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "./ui"
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import QtQuick.Window 2.12
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: roomMembersRoot
+
+    property MemberList members
+
+    title: qsTr("Members of %1").arg(members.roomName)
+    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
+    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+    height: 650
+    width: 420
+    minimumHeight: 420
+    palette: Nheko.colors
+    color: Nheko.colors.window
+
+    Shortcut {
+        sequence: StandardKey.Cancel
+        onActivated: roomMembersRoot.close()
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        anchors.margins: Nheko.paddingMedium
+        spacing: Nheko.paddingMedium
+
+        Avatar {
+            id: roomAvatar
+
+            width: 130
+            height: width
+            displayName: members.roomName
+            Layout.alignment: Qt.AlignHCenter
+            url: members.avatarUrl.replace("mxc://", "image://MxcImage/")
+            onClicked: TimelineManager.openRoomSettings(members.roomId)
+        }
+
+        ElidedLabel {
+            font.pixelSize: fontMetrics.font.pixelSize * 2
+            fullText: qsTr("%n people in %1", "Summary above list of members", members.memberCount).arg(members.roomName)
+            Layout.alignment: Qt.AlignHCenter
+            elideWidth: parent.width - Nheko.paddingMedium
+        }
+
+        ImageButton {
+            Layout.alignment: Qt.AlignHCenter
+            image: ":/icons/icons/ui/add-square-button.png"
+            hoverEnabled: true
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Invite more people")
+            onClicked: TimelineManager.openInviteUsers(members.roomId)
+        }
+
+        ScrollView {
+            palette: Nheko.colors
+            padding: Nheko.paddingMedium
+            ScrollBar.horizontal.visible: false
+            Layout.fillHeight: true
+            Layout.minimumHeight: 200
+            Layout.fillWidth: true
+
+            ListView {
+                id: memberList
+
+                clip: true
+                spacing: Nheko.paddingMedium
+                boundsBehavior: Flickable.StopAtBounds
+                model: members
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                    enabled: !Settings.mobileMode
+                }
+
+                delegate: RowLayout {
+                    spacing: Nheko.paddingMedium
+
+                    Avatar {
+                        width: Nheko.avatarSize
+                        height: Nheko.avatarSize
+                        userid: model.mxid
+                        url: model.avatarUrl.replace("mxc://", "image://MxcImage/")
+                        displayName: model.displayName
+                        onClicked: Rooms.currentRoom.openUserProfile(model.mxid)
+                    }
+
+                    ColumnLayout {
+                        spacing: Nheko.paddingSmall
+
+                        Label {
+                            text: model.displayName
+                            color: TimelineManager.userColor(model ? model.mxid : "", Nheko.colors.window)
+                            font.pointSize: fontMetrics.font.pointSize
+                        }
+
+                        Label {
+                            text: model.mxid
+                            color: Nheko.colors.buttonText
+                            font.pointSize: fontMetrics.font.pointSize * 0.9
+                        }
+
+                        Item {
+                            Layout.fillHeight: true
+                            Layout.fillWidth: true
+                        }
+
+                    }
+
+                }
+
+                footer: Item {
+                    width: parent.width
+                    visible: (members.numUsersLoaded < members.memberCount) && members.loadingMoreMembers
+                    // use the default height if it's visible, otherwise no height at all
+                    height: membersLoadingSpinner.height
+                    anchors.margins: Nheko.paddingMedium
+
+                    Spinner {
+                        id: membersLoadingSpinner
+
+                        anchors.centerIn: parent
+                        height: visible ? 35 : 0
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        standardButtons: DialogButtonBox.Ok
+        onAccepted: roomMembersRoot.close()
+    }
+
+}
diff --git a/resources/qml/RoomSettings.qml b/resources/qml/RoomSettings.qml
index 4b06401a..b8e527a5 100644
--- a/resources/qml/RoomSettings.qml
+++ b/resources/qml/RoomSettings.qml
@@ -4,7 +4,7 @@
 
 import "./ui"
 import Qt.labs.platform 1.1 as Platform
-import QtQuick 2.9
+import QtQuick 2.15
 import QtQuick.Controls 2.3
 import QtQuick.Layouts 1.2
 import QtQuick.Window 2.3
@@ -98,13 +98,23 @@ ApplicationWindow {
 
             MatrixText {
                 text: roomSettings.roomName
-                font.pixelSize: 24
+                font.pixelSize: fontMetrics.font.pixelSize * 2
                 Layout.alignment: Qt.AlignHCenter
             }
 
             MatrixText {
                 text: qsTr("%1 member(s)").arg(roomSettings.memberCount)
                 Layout.alignment: Qt.AlignHCenter
+
+                TapHandler {
+                    onTapped: TimelineManager.openRoomMembers(roomSettings.roomId)
+                }
+
+                CursorShape {
+                    cursorShape: Qt.PointingHandCursor
+                    anchors.fill: parent
+                }
+
             }
 
         }
@@ -209,7 +219,7 @@ ApplicationWindow {
                 title: qsTr("End-to-End Encryption")
                 text: qsTr("Encryption is currently experimental and things might break unexpectedly. <br>
                             Please take note that it can't be disabled afterwards.")
-                modality: Qt.NonModal
+                modality: Qt.Modal
                 onAccepted: {
                     if (roomSettings.isEncryptionEnabled)
                         return ;
@@ -222,6 +232,17 @@ ApplicationWindow {
                 buttons: Dialog.Ok | Dialog.Cancel
             }
 
+            MatrixText {
+                text: qsTr("Sticker & Emote Settings")
+            }
+
+            Button {
+                text: qsTr("Change")
+                ToolTip.text: qsTr("Change what packs are enabled, remove packs or create new ones")
+                onClicked: TimelineManager.openImagePackSettings(roomSettings.roomId)
+                Layout.alignment: Qt.AlignRight
+            }
+
             Item {
                 // for adding extra space between sections
                 Layout.fillWidth: true
@@ -247,7 +268,7 @@ ApplicationWindow {
 
             MatrixText {
                 text: roomSettings.roomId
-                font.pixelSize: 14
+                font.pixelSize: fontMetrics.font.pixelSize * 1.2
                 Layout.alignment: Qt.AlignRight
             }
 
@@ -257,16 +278,16 @@ ApplicationWindow {
 
             MatrixText {
                 text: roomSettings.roomVersion
-                font.pixelSize: 14
+                font.pixelSize: fontMetrics.font.pixelSize * 1.2
                 Layout.alignment: Qt.AlignRight
             }
 
         }
 
-        Button {
-            Layout.alignment: Qt.AlignRight
-            text: qsTr("OK")
-            onClicked: close()
+        DialogButtonBox {
+            Layout.fillWidth: true
+            standardButtons: DialogButtonBox.Ok
+            onAccepted: close()
         }
 
     }
diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml
index 5316e20d..1793d9bc 100644
--- a/resources/qml/Root.qml
+++ b/resources/qml/Root.qml
@@ -4,6 +4,7 @@
 
 import "./delegates"
 import "./device-verification"
+import "./dialogs"
 import "./emoji"
 import "./voip"
 import Qt.labs.platform 1.1 as Platform
@@ -48,6 +49,14 @@ Page {
     }
 
     Component {
+        id: roomMembersComponent
+
+        RoomMembers {
+        }
+
+    }
+
+    Component {
         id: mobileCallInviteDialog
 
         CallInvite {
@@ -63,6 +72,30 @@ Page {
 
     }
 
+    Component {
+        id: deviceVerificationDialog
+
+        DeviceVerification {
+        }
+
+    }
+
+    Component {
+        id: inviteDialog
+
+        InviteDialog {
+        }
+
+    }
+
+    Component {
+        id: packSettingsComponent
+
+        ImagePackSettingsDialog {
+        }
+
+    }
+
     Shortcut {
         sequence: "Ctrl+K"
         onActivated: {
@@ -82,14 +115,6 @@ Page {
         onActivated: Rooms.previousRoom()
     }
 
-    Component {
-        id: deviceVerificationDialog
-
-        DeviceVerification {
-        }
-
-    }
-
     Connections {
         target: TimelineManager
         onNewDeviceVerificationRequest: {
@@ -104,6 +129,12 @@ Page {
             });
             userProfile.show();
         }
+        onShowImagePackSettings: {
+            var packSet = packSettingsComponent.createObject(timelineRoot, {
+                "packlist": packlist
+            });
+            packSet.show();
+        }
     }
 
     Connections {
@@ -116,6 +147,31 @@ Page {
         }
     }
 
+    Connections {
+        target: TimelineManager
+        onOpenRoomMembersDialog: {
+            var membersDialog = roomMembersComponent.createObject(timelineRoot, {
+                "members": members,
+                "roomName": Rooms.currentRoom.roomName
+            });
+            membersDialog.show();
+        }
+        onOpenRoomSettingsDialog: {
+            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
+                "roomSettings": settings
+            });
+            roomSettings.show();
+        }
+        onOpenInviteUsersDialog: {
+            var dialog = inviteDialog.createObject(timelineRoot, {
+                "roomId": Rooms.currentRoom.roomId,
+                "plainRoomName": Rooms.currentRoom.plainRoomName,
+                "invitees": invitees
+            });
+            dialog.show();
+        }
+    }
+
     ChatPage {
         anchors.fill: parent
     }
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 58e367a0..755ab503 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -40,6 +40,7 @@ Item {
     required property int trustlevel
     required property var timestamp
     required property int status
+    required property int relatedEventCacheBuster
 
     anchors.left: parent.left
     anchors.right: parent.right
@@ -86,29 +87,30 @@ Item {
             // fancy reply, if this is a reply
             Reply {
                 function fromModel(role) {
-                    return replyTo != "" ? room.dataById(replyTo, role) : null;
+                    return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null;
                 }
 
                 visible: replyTo
-                userColor: TimelineManager.userColor(userId, Nheko.colors.base)
-                blurhash: fromModel(Room.Blurhash) ?? ""
-                body: fromModel(Room.Body) ?? ""
-                formattedBody: fromModel(Room.FormattedBody) ?? ""
+                userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, Nheko.colors.base)
+                blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? ""
+                body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? ""
+                formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? ""
                 eventId: fromModel(Room.EventId) ?? ""
-                filename: fromModel(Room.Filename) ?? ""
-                filesize: fromModel(Room.Filesize) ?? ""
-                proportionalHeight: fromModel(Room.ProportionalHeight) ?? 1
-                type: fromModel(Room.Type) ?? MtxEvent.UnknownMessage
-                typeString: fromModel(Room.TypeString) ?? ""
-                url: fromModel(Room.Url) ?? ""
-                originalWidth: fromModel(Room.OriginalWidth) ?? 0
-                isOnlyEmoji: fromModel(Room.IsOnlyEmoji) ?? false
-                userId: fromModel(Room.UserId) ?? ""
-                userName: fromModel(Room.UserName) ?? ""
-                thumbnailUrl: fromModel(Room.ThumbnailUrl) ?? ""
-                roomTopic: fromModel(Room.RoomTopic) ?? ""
-                roomName: fromModel(Room.RoomName) ?? ""
-                callType: fromModel(Room.CallType) ?? ""
+                filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? ""
+                filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? ""
+                proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1
+                type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage
+                typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? ""
+                url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? ""
+                originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0
+                isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false
+                userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? ""
+                userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? ""
+                thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? ""
+                roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? ""
+                roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? ""
+                callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
+                relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
             }
 
             // actual message content
@@ -134,6 +136,7 @@ Item {
                 roomTopic: r.roomTopic
                 roomName: r.roomName
                 callType: r.callType
+                relatedEventCacheBuster: r.relatedEventCacheBuster
                 isReply: false
             }
 
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 148a5817..c5cc69a6 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -246,17 +246,7 @@ Item {
 
     NhekoDropArea {
         anchors.fill: parent
-        roomid: room ? room.roomId() : ""
-    }
-
-    Connections {
-        target: room
-        onOpenRoomSettingsDialog: {
-            var roomSettings = roomSettingsComponent.createObject(timelineRoot, {
-                "roomSettings": settings
-            });
-            roomSettings.show();
-        }
+        roomid: room ? room.roomId : ""
     }
 
 }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 58aba0c7..8543d02a 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -24,7 +24,7 @@ Rectangle {
     TapHandler {
         onSingleTapped: {
             if (room)
-                room.openRoomSettings();
+                TimelineManager.openRoomSettings(room.roomId);
 
             eventPoint.accepted = true;
         }
@@ -66,7 +66,7 @@ Rectangle {
             displayName: roomName
             onClicked: {
                 if (room)
-                    room.openRoomSettings();
+                    TimelineManager.openRoomSettings(room.roomId);
 
             }
         }
@@ -111,22 +111,22 @@ Rectangle {
                 Platform.MenuItem {
                     visible: room ? room.permissions.canInvite() : false
                     text: qsTr("Invite users")
-                    onTriggered: TimelineManager.openInviteUsersDialog()
+                    onTriggered: TimelineManager.openInviteUsers(room.roomId)
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Members")
-                    onTriggered: TimelineManager.openMemberListDialog(room.roomId())
+                    onTriggered: TimelineManager.openRoomMembers(room.roomId)
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Leave room")
-                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId())
+                    onTriggered: TimelineManager.openLeaveRoomDialog(room.roomId)
                 }
 
                 Platform.MenuItem {
                     text: qsTr("Settings")
-                    onTriggered: room.openRoomSettings()
+                    onTriggered: TimelineManager.openRoomSettings(room.roomId)
                 }
 
             }
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 0b060629..a98c2a8b 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -29,6 +29,7 @@ Item {
     required property string roomTopic
     required property string roomName
     required property string callType
+    required property int relatedEventCacheBuster
 
     height: chooser.childrenRect.height
 
@@ -231,7 +232,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId())
+                formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId)
             }
 
         }
@@ -301,7 +302,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: room.formatPowerLevelEvent(d.eventId)
+                formatted: d.relatedEventCacheBuster, room.formatPowerLevelEvent(d.eventId)
             }
 
         }
@@ -313,7 +314,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: room.formatJoinRuleEvent(d.eventId)
+                formatted: d.relatedEventCacheBuster, room.formatJoinRuleEvent(d.eventId)
             }
 
         }
@@ -325,7 +326,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: room.formatHistoryVisibilityEvent(d.eventId)
+                formatted: d.relatedEventCacheBuster, room.formatHistoryVisibilityEvent(d.eventId)
             }
 
         }
@@ -337,7 +338,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: room.formatGuestAccessEvent(d.eventId)
+                formatted: d.relatedEventCacheBuster, room.formatGuestAccessEvent(d.eventId)
             }
 
         }
@@ -349,7 +350,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
-                formatted: room.formatMemberEvent(d.eventId)
+                formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
             }
 
         }
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 3a188d78..75e3d617 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -30,6 +30,7 @@ Item {
     property string roomTopic
     property string roomName
     property string callType
+    property int relatedEventCacheBuster
 
     width: parent.width
     height: replyContainer.height
@@ -95,6 +96,7 @@ Item {
             roomTopic: r.roomTopic
             roomName: r.roomName
             callType: r.callType
+            relatedEventCacheBuster: r.relatedEventCacheBuster
             enabled: false
             width: parent.width
             isReply: true
diff --git a/resources/qml/dialogs/ImagePackSettingsDialog.qml b/resources/qml/dialogs/ImagePackSettingsDialog.qml
new file mode 100644
index 00000000..c4b4a885
--- /dev/null
+++ b/resources/qml/dialogs/ImagePackSettingsDialog.qml
@@ -0,0 +1,309 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import ".."
+import "../components"
+import QtQuick 2.12
+import QtQuick.Controls 2.12
+import QtQuick.Layouts 1.12
+import im.nheko 1.0
+
+ApplicationWindow {
+    id: win
+
+    property ImagePackListModel packlist
+    property int avatarSize: Math.ceil(fontMetrics.lineSpacing * 2.3)
+    property SingleImagePackModel currentPack: packlist.packAt(currentPackIndex)
+    property int currentPackIndex: 0
+    readonly property int stickerDim: 128
+    readonly property int stickerDimPad: 128 + Nheko.paddingSmall
+
+    title: qsTr("Image pack settings")
+    x: MainWindow.x + (MainWindow.width / 2) - (width / 2)
+    y: MainWindow.y + (MainWindow.height / 2) - (height / 2)
+    height: 400
+    width: 600
+    palette: Nheko.colors
+    color: Nheko.colors.base
+    modality: Qt.NonModal
+    flags: Qt.Dialog
+
+    AdaptiveLayout {
+        id: adaptiveView
+
+        anchors.fill: parent
+        singlePageMode: false
+        pageIndex: 0
+
+        AdaptiveLayoutElement {
+            id: packlistC
+
+            visible: Settings.groupView
+            minimumWidth: 200
+            collapsedWidth: 200
+            preferredWidth: 300
+            maximumWidth: 300
+
+            ListView {
+                model: packlist
+                clip: true
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                    enabled: !Settings.mobileMode
+                }
+
+                delegate: Rectangle {
+                    id: packItem
+
+                    property color background: Nheko.colors.window
+                    property color importantText: Nheko.colors.text
+                    property color unimportantText: Nheko.colors.buttonText
+                    property color bubbleBackground: Nheko.colors.highlight
+                    property color bubbleText: Nheko.colors.highlightedText
+                    required property string displayName
+                    required property string avatarUrl
+                    required property bool fromAccountData
+                    required property bool fromCurrentRoom
+                    required property int index
+
+                    color: background
+                    height: avatarSize + 2 * Nheko.paddingMedium
+                    width: ListView.view.width
+                    state: "normal"
+                    states: [
+                        State {
+                            name: "highlight"
+                            when: hovered.hovered && !(index == currentPackIndex)
+
+                            PropertyChanges {
+                                target: packItem
+                                background: Nheko.colors.dark
+                                importantText: Nheko.colors.brightText
+                                unimportantText: Nheko.colors.brightText
+                                bubbleBackground: Nheko.colors.highlight
+                                bubbleText: Nheko.colors.highlightedText
+                            }
+
+                        },
+                        State {
+                            name: "selected"
+                            when: index == currentPackIndex
+
+                            PropertyChanges {
+                                target: packItem
+                                background: Nheko.colors.highlight
+                                importantText: Nheko.colors.highlightedText
+                                unimportantText: Nheko.colors.highlightedText
+                                bubbleBackground: Nheko.colors.highlightedText
+                                bubbleText: Nheko.colors.highlight
+                            }
+
+                        }
+                    ]
+
+                    TapHandler {
+                        margin: -Nheko.paddingSmall
+                        onSingleTapped: currentPackIndex = index
+                    }
+
+                    HoverHandler {
+                        id: hovered
+                    }
+
+                    RowLayout {
+                        spacing: Nheko.paddingMedium
+                        anchors.fill: parent
+                        anchors.margins: Nheko.paddingMedium
+
+                        Avatar {
+                            // In the future we could show an online indicator by setting the userid for the avatar
+                            //userid: Nheko.currentUser.userid
+
+                            id: avatar
+
+                            enabled: false
+                            Layout.alignment: Qt.AlignVCenter
+                            height: avatarSize
+                            width: avatarSize
+                            url: avatarUrl.replace("mxc://", "image://MxcImage/")
+                            displayName: packItem.displayName
+                        }
+
+                        ColumnLayout {
+                            id: textContent
+
+                            Layout.alignment: Qt.AlignLeft
+                            Layout.fillWidth: true
+                            Layout.minimumWidth: 100
+                            width: parent.width - avatar.width
+                            Layout.preferredWidth: parent.width - avatar.width
+                            spacing: Nheko.paddingSmall
+
+                            RowLayout {
+                                Layout.fillWidth: true
+                                spacing: 0
+
+                                ElidedLabel {
+                                    Layout.alignment: Qt.AlignBottom
+                                    color: packItem.importantText
+                                    elideWidth: textContent.width - Nheko.paddingMedium
+                                    fullText: displayName
+                                    textFormat: Text.PlainText
+                                }
+
+                                Item {
+                                    Layout.fillWidth: true
+                                }
+
+                            }
+
+                            RowLayout {
+                                Layout.fillWidth: true
+                                spacing: 0
+
+                                ElidedLabel {
+                                    color: packItem.unimportantText
+                                    font.pixelSize: fontMetrics.font.pixelSize * 0.9
+                                    elideWidth: textContent.width - Nheko.paddingSmall
+                                    fullText: {
+                                        if (fromAccountData)
+                                            return qsTr("Private pack");
+                                        else if (fromCurrentRoom)
+                                            return qsTr("Pack from this room");
+                                        else
+                                            return qsTr("Globally enabled pack");
+                                    }
+                                    textFormat: Text.PlainText
+                                }
+
+                                Item {
+                                    Layout.fillWidth: true
+                                }
+
+                            }
+
+                        }
+
+                    }
+
+                }
+
+            }
+
+        }
+
+        AdaptiveLayoutElement {
+            id: packinfoC
+
+            Rectangle {
+                color: Nheko.colors.window
+
+                ColumnLayout {
+                    id: packinfo
+
+                    property string packName: currentPack ? currentPack.packname : ""
+                    property string avatarUrl: currentPack ? currentPack.avatarUrl : ""
+
+                    anchors.fill: parent
+                    anchors.margins: Nheko.paddingLarge
+                    spacing: Nheko.paddingLarge
+
+                    Avatar {
+                        url: packinfo.avatarUrl.replace("mxc://", "image://MxcImage/")
+                        displayName: packinfo.packName
+                        height: 100
+                        width: 100
+                        Layout.alignment: Qt.AlignHCenter
+                        enabled: false
+                    }
+
+                    MatrixText {
+                        text: packinfo.packName
+                        font.pixelSize: 24
+                        Layout.alignment: Qt.AlignHCenter
+                    }
+
+                    GridLayout {
+                        Layout.alignment: Qt.AlignHCenter
+                        visible: currentPack && currentPack.roomid != ""
+                        columns: 2
+                        rowSpacing: Nheko.paddingMedium
+
+                        MatrixText {
+                            text: qsTr("Enable globally")
+                        }
+
+                        ToggleButton {
+                            ToolTip.text: qsTr("Enables this pack to be used in all rooms")
+                            checked: currentPack ? currentPack.isGloballyEnabled : false
+                            onClicked: currentPack.isGloballyEnabled = !currentPack.isGloballyEnabled
+                            Layout.alignment: Qt.AlignRight
+                        }
+
+                    }
+
+                    GridView {
+                        Layout.fillHeight: true
+                        Layout.fillWidth: true
+                        model: currentPack
+                        cellWidth: stickerDimPad
+                        cellHeight: stickerDimPad
+                        boundsBehavior: Flickable.StopAtBounds
+                        clip: true
+                        currentIndex: -1 // prevent sorting from stealing focus
+                        cacheBuffer: 500
+
+                        ScrollHelper {
+                            flickable: parent
+                            anchors.fill: parent
+                            enabled: !Settings.mobileMode
+                        }
+
+                        // Individual emoji
+                        delegate: AbstractButton {
+                            width: stickerDim
+                            height: stickerDim
+                            hoverEnabled: true
+                            ToolTip.text: ":" + model.shortcode + ": - " + model.body
+                            ToolTip.visible: hovered
+
+                            contentItem: Image {
+                                height: stickerDim
+                                width: stickerDim
+                                source: model.url.replace("mxc://", "image://MxcImage/")
+                                fillMode: Image.PreserveAspectFit
+                            }
+
+                            background: Rectangle {
+                                anchors.fill: parent
+                                color: hovered ? Nheko.colors.highlight : 'transparent'
+                                radius: 5
+                            }
+
+                        }
+
+                    }
+
+                }
+
+            }
+
+        }
+
+    }
+
+    footer: DialogButtonBox {
+        id: buttons
+
+        Button {
+            text: qsTr("Close")
+            DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
+            onClicked: win.close()
+        }
+
+    }
+
+}
diff --git a/resources/qml/emoji/EmojiButton.qml b/resources/qml/emoji/EmojiButton.qml
deleted file mode 100644
index 5f4d23d3..00000000
--- a/resources/qml/emoji/EmojiButton.qml
+++ /dev/null
@@ -1,23 +0,0 @@
-// SPDX-FileCopyrightText: 2021 Nheko Contributors
-//
-// SPDX-License-Identifier: GPL-3.0-or-later
-
-import "../"
-import QtQuick 2.10
-import QtQuick.Controls 2.1
-import im.nheko 1.0
-import im.nheko.EmojiModel 1.0
-
-ImageButton {
-    id: emojiButton
-
-    property var colors: currentActivePalette
-    property var emojiPicker
-    property string event_id
-
-    image: ":/icons/icons/ui/smile.png"
-    onClicked: emojiPicker.visible ? emojiPicker.close() : emojiPicker.show(emojiButton, function(emoji) {
-        room.input.reaction(event_id, emoji);
-        TimelineManager.focusMessageInput();
-    })
-}
diff --git a/resources/qml/emoji/EmojiPicker.qml b/resources/qml/emoji/EmojiPicker.qml
index 6f10a230..354e340c 100644
--- a/resources/qml/emoji/EmojiPicker.qml
+++ b/resources/qml/emoji/EmojiPicker.qml
@@ -130,6 +130,7 @@ Menu {
                 boundsBehavior: Flickable.StopAtBounds
                 clip: true
                 currentIndex: -1 // prevent sorting from stealing focus
+                cacheBuffer: 500
 
                 // Individual emoji
                 delegate: AbstractButton {
diff --git a/resources/qml/emoji/StickerPicker.qml b/resources/qml/emoji/StickerPicker.qml
new file mode 100644
index 00000000..3731a948
--- /dev/null
+++ b/resources/qml/emoji/StickerPicker.qml
@@ -0,0 +1,180 @@
+// SPDX-FileCopyrightText: 2021 Nheko Contributors
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+import "../"
+import QtGraphicalEffects 1.0
+import QtQuick 2.9
+import QtQuick.Controls 2.3
+import QtQuick.Layouts 1.3
+import im.nheko 1.0
+import im.nheko.EmojiModel 1.0
+
+Menu {
+    id: stickerPopup
+
+    property var callback
+    property var colors
+    property string roomid
+    property alias model: gridView.model
+    property var textArea
+    property real highlightHue: Nheko.colors.highlight.hslHue
+    property real highlightSat: Nheko.colors.highlight.hslSaturation
+    property real highlightLight: Nheko.colors.highlight.hslLightness
+    readonly property int stickerDim: 128
+    readonly property int stickerDimPad: 128 + Nheko.paddingSmall
+    readonly property int stickersPerRow: 3
+
+    function show(showAt, roomid_, callback) {
+        console.debug("Showing sticker picker");
+        roomid = roomid_;
+        stickerPopup.callback = callback;
+        popup(showAt ? showAt : null);
+    }
+
+    margins: 0
+    bottomPadding: 1
+    leftPadding: 1
+    rightPadding: 1
+    modal: true
+    focus: true
+    closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+    width: stickersPerRow * stickerDimPad + 20
+
+    Rectangle {
+        color: Nheko.colors.window
+        height: columnView.implicitHeight + 4
+        width: stickersPerRow * stickerDimPad + 20
+
+        ColumnLayout {
+            id: columnView
+
+            spacing: 0
+            anchors.leftMargin: 3
+            anchors.rightMargin: 3
+            anchors.bottom: parent.bottom
+            anchors.left: parent.left
+            anchors.right: parent.right
+            anchors.topMargin: 2
+
+            // Search field
+            TextField {
+                id: emojiSearch
+
+                Layout.topMargin: 3
+                Layout.preferredWidth: stickersPerRow * stickerDimPad + 20 - 6
+                palette: Nheko.colors
+                background: null
+                placeholderTextColor: Nheko.colors.buttonText
+                color: Nheko.colors.text
+                placeholderText: qsTr("Search")
+                selectByMouse: true
+                rightPadding: clearSearch.width
+                onTextChanged: searchTimer.restart()
+                onVisibleChanged: {
+                    if (visible)
+                        forceActiveFocus();
+
+                }
+
+                Timer {
+                    id: searchTimer
+
+                    interval: 350 // tweak as needed?
+                    onTriggered: stickerPopup.model.searchString = emojiSearch.text
+                }
+
+                ToolButton {
+                    id: clearSearch
+
+                    visible: emojiSearch.text !== ''
+                    icon.source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
+                    focusPolicy: Qt.NoFocus
+                    onClicked: emojiSearch.clear()
+                    hoverEnabled: true
+                    background: null
+
+                    anchors {
+                        verticalCenter: parent.verticalCenter
+                        right: parent.right
+                    }
+                    // clear the default hover effects.
+
+                    Image {
+                        height: parent.height - 2 * Nheko.paddingSmall
+                        width: height
+                        source: "image://colorimage/:/icons/icons/ui/round-remove-button.png?" + (clearSearch.hovered ? Nheko.colors.highlight : Nheko.colors.buttonText)
+
+                        anchors {
+                            verticalCenter: parent.verticalCenter
+                            right: parent.right
+                            margins: Nheko.paddingSmall
+                        }
+
+                    }
+
+                }
+
+            }
+
+            // emoji grid
+            GridView {
+                id: gridView
+
+                model: roomid ? TimelineManager.completerFor("stickers", roomid) : null
+                Layout.preferredHeight: cellHeight * 3.5
+                Layout.preferredWidth: stickersPerRow * stickerDimPad + 20
+                Layout.leftMargin: 4
+                cellWidth: stickerDimPad
+                cellHeight: stickerDimPad
+                boundsBehavior: Flickable.StopAtBounds
+                clip: true
+                currentIndex: -1 // prevent sorting from stealing focus
+                cacheBuffer: 500
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                    enabled: !Settings.mobileMode
+                }
+
+                // Individual emoji
+                delegate: AbstractButton {
+                    width: stickerDim
+                    height: stickerDim
+                    hoverEnabled: true
+                    ToolTip.text: ":" + model.shortcode + ": - " + model.body
+                    ToolTip.visible: hovered
+                    // TODO: maybe add favorites at some point?
+                    onClicked: {
+                        console.debug("Picked " + model.shortcode);
+                        stickerPopup.close();
+                        callback(model.originalRow);
+                    }
+
+                    contentItem: Image {
+                        height: stickerDim
+                        width: stickerDim
+                        source: model.url.replace("mxc://", "image://MxcImage/")
+                        fillMode: Image.PreserveAspectFit
+                    }
+
+                    background: Rectangle {
+                        anchors.fill: parent
+                        color: hovered ? Nheko.colors.highlight : 'transparent'
+                        radius: 5
+                    }
+
+                }
+
+                ScrollBar.vertical: ScrollBar {
+                    id: emojiScroll
+                }
+
+            }
+
+        }
+
+    }
+
+}
diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml
index 5f564853..97932cc9 100644
--- a/resources/qml/voip/PlaceCall.qml
+++ b/resources/qml/voip/PlaceCall.qml
@@ -88,7 +88,7 @@ Popup {
                 onClicked: {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
-                        CallManager.sendInvite(room.roomId(), CallType.VOICE);
+                        CallManager.sendInvite(room.roomId, CallType.VOICE);
                         close();
                     }
                 }
@@ -102,7 +102,7 @@ Popup {
                     if (buttonLayout.validateMic()) {
                         Settings.microphone = micCombo.currentText;
                         Settings.camera = cameraCombo.currentText;
-                        CallManager.sendInvite(room.roomId(), CallType.VIDEO);
+                        CallManager.sendInvite(room.roomId, CallType.VIDEO);
                         close();
                     }
                 }
diff --git a/resources/qml/voip/ScreenShare.qml b/resources/qml/voip/ScreenShare.qml
index a10057b2..8cd43b1c 100644
--- a/resources/qml/voip/ScreenShare.qml
+++ b/resources/qml/voip/ScreenShare.qml
@@ -136,7 +136,7 @@ Popup {
                         Settings.screenSharePiP = pipCheckBox.checked;
                         Settings.screenShareRemoteVideo = remoteVideoCheckBox.checked;
                         Settings.screenShareHideCursor = hideCursorCheckBox.checked;
-                        CallManager.sendInvite(room.roomId(), CallType.SCREEN, windowCombo.currentIndex);
+                        CallManager.sendInvite(room.roomId, CallType.SCREEN, windowCombo.currentIndex);
                         close();
                     }
                 }