diff --git a/resources/qml/ChatPage.qml b/resources/qml/ChatPage.qml new file mode 100644 index 00000000..a02f0ca9 --- /dev/null +++ b/resources/qml/ChatPage.qml @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.9 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.3 +import im.nheko 1.0 + +Rectangle { + id: chatPage + + color: Nheko.colors.window + + SplitView { + anchors.fill: parent + + Rectangle { + SplitView.minimumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2 + SplitView.preferredWidth: Nheko.avatarSize + Nheko.paddingSmall * 2 + SplitView.maximumWidth: Nheko.avatarSize + Nheko.paddingSmall * 2 + color: "blue" + } + + Rectangle { + SplitView.minimumWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2 + SplitView.preferredWidth: Nheko.avatarSize * 3 + Nheko.paddingSmall * 2 + SplitView.maximumWidth: Nheko.avatarSize * 7 + Nheko.paddingSmall * 2 + color: "red" + } + + TimelineView { + id: timeline + + SplitView.fillWidth: true + SplitView.minimumWidth: 400 + } + + } + + PrivacyScreen { + anchors.fill: parent + visible: Settings.privacyScreen + screenTimeout: Settings.privacyScreenTimeout + timelineRoot: timeline + } + +} diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 1ec18540..59bfe94d 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -21,7 +21,7 @@ Popup { modal: true palette: Nheko.colors parent: Overlay.overlay - width: implicitWidth >= (timelineRoot.width * 0.8) ? implicitWidth : (timelineRoot.width * 0.8) + width: implicitWidth >= (timelineView.width * 0.8) ? implicitWidth : (timelineView.width * 0.8) height: implicitHeight + completerPopup.height + padding * 2 leftPadding: 10 rightPadding: 10 diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml new file mode 100644 index 00000000..35b81a1f --- /dev/null +++ b/resources/qml/Root.qml @@ -0,0 +1,260 @@ +// SPDX-FileCopyrightText: 2021 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./delegates" +import "./device-verification" +import "./emoji" +import "./voip" +import Qt.labs.platform 1.1 as Platform +import QtGraphicalEffects 1.0 +import QtQuick 2.9 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.3 +import QtQuick.Window 2.2 +import im.nheko 1.0 +import im.nheko.EmojiModel 1.0 + +Page { + id: timelineRoot + + palette: Nheko.colors + + FontMetrics { + id: fontMetrics + } + + EmojiPicker { + id: emojiPopup + + colors: palette + model: TimelineManager.completerFor("allemoji", "") + } + + Component { + id: userProfileComponent + + UserProfile { + } + + } + + Component { + id: roomSettingsComponent + + RoomSettings { + } + + } + + Component { + id: mobileCallInviteDialog + + CallInvite { + } + + } + + Component { + id: quickSwitcherComponent + + QuickSwitcher { + } + + } + + Component { + id: forwardCompleterComponent + + ForwardCompleter { + } + + } + + Shortcut { + sequence: "Ctrl+K" + onActivated: { + var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); + TimelineManager.focusTimeline(); + quickSwitch.open(); + } + } + + Platform.Menu { + id: messageContextMenu + + property string eventId + property string link + property string text + property int eventType + property bool isEncrypted + property bool isEditable + property bool isSender + + function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { + eventId = eventId_; + eventType = eventType_; + isEncrypted = isEncrypted_; + isEditable = isEditable_; + isSender = isSender_; + if (text_) + text = text_; + else + text = ""; + if (link_) + link = link_; + else + link = ""; + if (showAt_) + open(showAt_); + else + open(); + } + + Platform.MenuItem { + visible: messageContextMenu.text + enabled: visible + text: qsTr("Copy") + onTriggered: Clipboard.text = messageContextMenu.text + } + + Platform.MenuItem { + visible: messageContextMenu.link + enabled: visible + text: qsTr("Copy link location") + onTriggered: Clipboard.text = messageContextMenu.link + } + + Platform.MenuItem { + id: reactionOption + + visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false + text: qsTr("React") + onTriggered: emojiPopup.show(null, function(emoji) { + TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji); + }) + } + + Platform.MenuItem { + visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false + text: qsTr("Reply") + onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) + enabled: visible + text: qsTr("Edit") + onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + text: qsTr("Read receipts") + onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage + text: qsTr("Forward") + onTriggered: { + var forwardMess = forwardCompleterComponent.createObject(timelineRoot); + forwardMess.setMessageEventId(messageContextMenu.eventId); + forwardMess.open(); + } + } + + Platform.MenuItem { + text: qsTr("Mark as read") + } + + Platform.MenuItem { + text: qsTr("View raw message") + onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId) + } + + Platform.MenuItem { + // TODO(Nico): Fix this still being iterated over, when using keyboard to select options + visible: messageContextMenu.isEncrypted + enabled: visible + text: qsTr("View decrypted raw message") + onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender + text: qsTr("Remove message") + onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + enabled: visible + text: qsTr("Save as") + onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + enabled: visible + text: qsTr("Open in external program") + onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId) + } + + Platform.MenuItem { + visible: messageContextMenu.eventId + enabled: visible + text: qsTr("Copy link to event") + onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId) + } + + } + + Component { + id: deviceVerificationDialog + + DeviceVerification { + } + + } + + Connections { + target: TimelineManager + onNewDeviceVerificationRequest: { + var dialog = deviceVerificationDialog.createObject(timelineRoot, { + "flow": flow + }); + dialog.show(); + } + onOpenProfile: { + var userProfile = userProfileComponent.createObject(timelineRoot, { + "profile": profile + }); + userProfile.show(); + } + } + + Connections { + target: TimelineManager.timeline + onOpenRoomSettingsDialog: { + var roomSettings = roomSettingsComponent.createObject(timelineRoot, { + "roomSettings": settings + }); + roomSettings.show(); + } + } + + Connections { + target: CallManager + onNewInviteState: { + if (CallManager.haveCallInvite && Settings.mobileMode) { + var dialog = mobileCallInviteDialog.createObject(msgView); + dialog.open(); + } + } + } + + ChatPage { + anchors.fill: parent + } + +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index a848cb49..0d0e286d 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -9,370 +9,123 @@ import "./voip" import Qt.labs.platform 1.1 as Platform import QtGraphicalEffects 1.0 import QtQuick 2.9 -import QtQuick.Controls 2.3 +import QtQuick.Controls 2.13 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 import im.nheko 1.0 import im.nheko.EmojiModel 1.0 -Page { - id: timelineRoot +Item { + id: timelineView - palette: Nheko.colors - - FontMetrics { - id: fontMetrics + Label { + visible: !TimelineManager.timeline && !TimelineManager.isInitialSync + anchors.centerIn: parent + text: qsTr("No room open") + font.pointSize: 24 + color: Nheko.colors.text } - EmojiPicker { - id: emojiPopup - - colors: palette - model: TimelineManager.completerFor("allemoji", "") + BusyIndicator { + visible: running + anchors.centerIn: parent + running: TimelineManager.isInitialSync + height: 200 + width: 200 + z: 3 } - Component { - id: userProfileComponent + ColumnLayout { + id: timelineLayout - UserProfile { - } - - } - - Component { - id: roomSettingsComponent - - RoomSettings { - } - - } - - Component { - id: mobileCallInviteDialog - - CallInvite { - } - - } - - Component { - id: quickSwitcherComponent - - QuickSwitcher { - } - - } - - Component { - id: forwardCompleterComponent - - ForwardCompleter { - } - - } - - Shortcut { - sequence: "Ctrl+K" - onActivated: { - var quickSwitch = quickSwitcherComponent.createObject(timelineRoot); - TimelineManager.focusTimeline(); - quickSwitch.open(); - } - } - - Platform.Menu { - id: messageContextMenu - - property string eventId - property string link - property string text - property int eventType - property bool isEncrypted - property bool isEditable - property bool isSender - - function show(eventId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) { - eventId = eventId_; - eventType = eventType_; - isEncrypted = isEncrypted_; - isEditable = isEditable_; - isSender = isSender_; - if (text_) - text = text_; - else - text = ""; - if (link_) - link = link_; - else - link = ""; - if (showAt_) - open(showAt_); - else - open(); - } - - Platform.MenuItem { - visible: messageContextMenu.text - enabled: visible - text: qsTr("Copy") - onTriggered: Clipboard.text = messageContextMenu.text - } - - Platform.MenuItem { - visible: messageContextMenu.link - enabled: visible - text: qsTr("Copy link location") - onTriggered: Clipboard.text = messageContextMenu.link - } - - Platform.MenuItem { - id: reactionOption - - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.Reaction) : false - text: qsTr("React") - onTriggered: emojiPopup.show(null, function(emoji) { - TimelineManager.queueReactionMessage(messageContextMenu.eventId, emoji); - }) - } - - Platform.MenuItem { - visible: TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false - text: qsTr("Reply") - onTriggered: TimelineManager.timeline.replyAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.isEditable && (TimelineManager.timeline ? TimelineManager.timeline.permissions.canSend(MtxEvent.TextMessage) : false) - enabled: visible - text: qsTr("Edit") - onTriggered: TimelineManager.timeline.editAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - text: qsTr("Read receipts") - onTriggered: TimelineManager.timeline.readReceiptsAction(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage - text: qsTr("Forward") - onTriggered: { - var forwardMess = forwardCompleterComponent.createObject(timelineRoot); - forwardMess.setMessageEventId(messageContextMenu.eventId); - forwardMess.open(); - } - } - - Platform.MenuItem { - text: qsTr("Mark as read") - } - - Platform.MenuItem { - text: qsTr("View raw message") - onTriggered: TimelineManager.timeline.viewRawMessage(messageContextMenu.eventId) - } - - Platform.MenuItem { - // TODO(Nico): Fix this still being iterated over, when using keyboard to select options - visible: messageContextMenu.isEncrypted - enabled: visible - text: qsTr("View decrypted raw message") - onTriggered: TimelineManager.timeline.viewDecryptedRawMessage(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: (TimelineManager.timeline ? TimelineManager.timeline.permissions.canRedact() : false) || messageContextMenu.isSender - text: qsTr("Remove message") - onTriggered: TimelineManager.timeline.redactEvent(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker - enabled: visible - text: qsTr("Save as") - onTriggered: TimelineManager.timeline.saveMedia(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker - enabled: visible - text: qsTr("Open in external program") - onTriggered: TimelineManager.timeline.openMedia(messageContextMenu.eventId) - } - - Platform.MenuItem { - visible: messageContextMenu.eventId - enabled: visible - text: qsTr("Copy link to event") - onTriggered: TimelineManager.timeline.copyLinkToEvent(messageContextMenu.eventId) - } - - } - - Rectangle { + visible: TimelineManager.timeline != null anchors.fill: parent - color: Nheko.colors.window - - Component { - id: deviceVerificationDialog - - DeviceVerification { - } + spacing: 0 + TopBar { } - Connections { - target: TimelineManager - onNewDeviceVerificationRequest: { - var dialog = deviceVerificationDialog.createObject(timelineRoot, { - "flow": flow - }); - dialog.show(); - } - onOpenProfile: { - var userProfile = userProfileComponent.createObject(timelineRoot, { - "profile": profile - }); - userProfile.show(); - } + Rectangle { + Layout.fillWidth: true + height: 1 + z: 3 + color: Nheko.colors.mid } - Connections { - target: TimelineManager.timeline - onOpenRoomSettingsDialog: { - var roomSettings = roomSettingsComponent.createObject(timelineRoot, { - "roomSettings": settings - }); - roomSettings.show(); - } - } + Rectangle { + id: msgView + + Layout.fillWidth: true + Layout.fillHeight: true + color: Nheko.colors.base + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + StackLayout { + id: stackLayout + + currentIndex: 0 + + Connections { + function onActiveTimelineChanged() { + stackLayout.currentIndex = 0; + } + + target: TimelineManager + } + + MessageView { + Layout.fillWidth: true + implicitHeight: msgView.height - typingIndicator.height + } + + Loader { + source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" + onLoaded: TimelineManager.setVideoCallItem() + } - Connections { - target: CallManager - onNewInviteState: { - if (CallManager.haveCallInvite && Settings.mobileMode) { - var dialog = mobileCallInviteDialog.createObject(msgView); - dialog.open(); } + + TypingIndicator { + id: typingIndicator + } + } + } - Label { - visible: !TimelineManager.timeline && !TimelineManager.isInitialSync - anchors.centerIn: parent - text: qsTr("No room open") - font.pointSize: 24 - color: Nheko.colors.text - } + CallInviteBar { + id: callInviteBar - BusyIndicator { - visible: running - anchors.centerIn: parent - running: TimelineManager.isInitialSync - height: 200 - width: 200 + Layout.fillWidth: true z: 3 } - ColumnLayout { - id: timelineLayout - - visible: TimelineManager.timeline != null - anchors.fill: parent - spacing: 0 - - TopBar { - } - - Rectangle { - Layout.fillWidth: true - height: 1 - z: 3 - color: Nheko.colors.mid - } - - Rectangle { - id: msgView - - Layout.fillWidth: true - Layout.fillHeight: true - color: Nheko.colors.base - - ColumnLayout { - anchors.fill: parent - spacing: 0 - - StackLayout { - id: stackLayout - - currentIndex: 0 - - Connections { - function onActiveTimelineChanged() { - stackLayout.currentIndex = 0; - } - - target: TimelineManager - } - - MessageView { - Layout.fillWidth: true - Layout.fillHeight: true - } - - Loader { - source: CallManager.isOnCall && CallManager.callType != CallType.VOICE ? "voip/VideoCall.qml" : "" - onLoaded: TimelineManager.setVideoCallItem() - } - - } - - TypingIndicator { - } - - } - - } - - CallInviteBar { - id: callInviteBar - - Layout.fillWidth: true - z: 3 - } - - ActiveCallBar { - Layout.fillWidth: true - z: 3 - } - - Rectangle { - Layout.fillWidth: true - z: 3 - height: 1 - color: Nheko.colors.mid - } - - ReplyPopup { - } - - MessageInput { - } - + ActiveCallBar { + Layout.fillWidth: true + z: 3 } - NhekoDropArea { - anchors.fill: parent - roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" + Rectangle { + Layout.fillWidth: true + z: 3 + height: 1 + color: Nheko.colors.mid + } + + ReplyPopup { + } + + MessageInput { } } - PrivacyScreen { + NhekoDropArea { anchors.fill: parent - visible: Settings.privacyScreen - screenTimeout: Settings.privacyScreenTimeout - timelineRoot: timelineLayout + roomid: TimelineManager.timeline ? TimelineManager.timeline.roomId() : "" } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 704af3fe..ce8e779c 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -9,10 +9,10 @@ Item { property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? parent.width : model.data.width) property double tempHeight: tempWidth * model.data.proportionalHeight property double divisor: model.isReply ? 5 : 3 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + property bool tooHigh: tempHeight > timelineView.height / divisor - height: Math.round(tooHigh ? timelineRoot.height / divisor : tempHeight) - width: Math.round(tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth) + height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight) + width: Math.round(tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth) Image { id: blurhash diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index 223c2a34..0234495d 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -29,11 +29,11 @@ Rectangle { property double tempWidth: Math.min(parent ? parent.width : undefined, model.data.width < 1 ? 400 : model.data.width) property double tempHeight: tempWidth * model.data.proportionalHeight property double divisor: model.isReply ? 4 : 2 - property bool tooHigh: tempHeight > timelineRoot.height / divisor + property bool tooHigh: tempHeight > timelineView.height / divisor visible: model.data.type == MtxEvent.VideoMessage - height: tooHigh ? timelineRoot.height / divisor : tempHeight - width: tooHigh ? (timelineRoot.height / divisor) / model.data.proportionalHeight : tempWidth + height: tooHigh ? timelineView.height / divisor : tempHeight + width: tooHigh ? (timelineView.height / divisor) / model.data.proportionalHeight : tempWidth Image { anchors.fill: parent diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 810ee3d4..ae622480 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -11,7 +11,7 @@ MatrixText { text: "" + formatted.replace("
", "
")
     width: parent ? parent.width : undefined
-    height: isReply ? Math.round(Math.min(timelineRoot.height / 8, implicitHeight)) : undefined
+    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && model.data.isOnlyEmoji > 0 && model.data.isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize
diff --git a/resources/res.qrc b/resources/res.qrc
index 304493b6..8105e966 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -123,6 +123,8 @@
     
         qtquickcontrols2.conf
 
+        qml/Root.qml
+        qml/ChatPage.qml
         qml/TimelineView.qml
         qml/Avatar.qml
         qml/Completer.qml
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index b407a128..e8e57fd8 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -257,7 +257,7 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
         view->engine()->addImageProvider("MxcImage", imgProvider);
         view->engine()->addImageProvider("colorimage", colorImgProvider);
         view->engine()->addImageProvider("blurhash", blurhashProvider);
-        view->setSource(QUrl("qrc:///qml/TimelineView.qml"));
+        view->setSource(QUrl("qrc:///qml/Root.qml"));
 
         connect(parent, &ChatPage::themeChanged, this, &TimelineViewManager::updateColorPalette);
         connect(parent,
diff --git a/src/ui/NhekoGlobalObject.h b/src/ui/NhekoGlobalObject.h
index 9875507e..d952c266 100644
--- a/src/ui/NhekoGlobalObject.h
+++ b/src/ui/NhekoGlobalObject.h
@@ -14,6 +14,9 @@ class Nheko : public QObject
         Q_PROPERTY(QPalette colors READ colors NOTIFY colorsChanged)
         Q_PROPERTY(QPalette inactiveColors READ inactiveColors NOTIFY colorsChanged)
         Q_PROPERTY(int avatarSize READ avatarSize CONSTANT)
+        Q_PROPERTY(int paddingSmall READ paddingSmall CONSTANT)
+        Q_PROPERTY(int paddingMedium READ paddingMedium CONSTANT)
+        Q_PROPERTY(int paddingLarge READ paddingLarge CONSTANT)
 
 public:
         Nheko();
@@ -23,6 +26,10 @@ public:
 
         int avatarSize() const { return 40; }
 
+        int paddingSmall() const { return 4; }
+        int paddingMedium() const { return 8; }
+        int paddingLarge() const { return 20; }
+
         Q_INVOKABLE void openLink(QString link) const;
 
 signals: