From 04b47d68829dcdbb5bbde615b0e344a74be86598 Mon Sep 17 00:00:00 2001 From: Malte E Date: Thu, 3 Feb 2022 20:26:20 +0100 Subject: [PATCH 01/18] prepare code for message bubbles and dynamic message layout --- resources/qml/TimelineRow.qml | 156 +++++++++++--------- resources/qml/delegates/MessageDelegate.qml | 9 +- resources/qml/delegates/Placeholder.qml | 2 +- resources/qml/delegates/Reply.qml | 14 +- resources/qml/delegates/TextMessage.qml | 4 +- 5 files changed, 106 insertions(+), 79 deletions(-) diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index dd1b3a0f..1d9a036a 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -71,27 +71,39 @@ Item { gesturePolicy: TapHandler.ReleaseWithinBounds } - RowLayout { + Item { id: row anchors.rightMargin: 1 anchors.leftMargin: Nheko.avatarSize + 16 anchors.left: parent.left anchors.right: parent.right - - Column { - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: 4 - Layout.topMargin: 1 - Layout.bottomMargin: 1 + height: msg.height+reactionRow.height+2 + GridLayout { + id: msg + anchors { + right: parent.right + left: parent.left + top: parent.top + topMargin: 1 + bottomMargin: 1 + } + rowSpacing: 0 + columnSpacing: 0 + columns: 2 + rows: 2 // fancy reply, if this is a reply Reply { + Layout.row: 0 + Layout.column: 0 + Layout.fillWidth: true + Layout.margins: 0 + id: reply + function fromModel(role) { return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; } - visible: replyTo userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, Nheko.colors.base) blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" @@ -118,9 +130,12 @@ Item { // actual message content MessageDelegate { + Layout.row: 1 + Layout.column: 0 + Layout.fillWidth: true + Layout.margins: 2 id: contentItem - width: parent.width blurhash: r.blurhash body: r.body formattedBody: r.formattedBody @@ -144,67 +159,76 @@ Item { isReply: false } - Reactions { - id: reactionRow + RowLayout { + Layout.column: 1 + Layout.row: 0 + Layout.rowSpan: 2 + Layout.alignment: Qt.AlignTop | Qt.AlignRight - reactions: r.reactions - eventId: r.eventId + StatusIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + width: 16 + status: r.status + eventId: r.eventId + } + + Image { + visible: isEdited || eventId == chat.model.edit + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + height: 16 + width: 16 + sourceSize.width: 16 * Screen.devicePixelRatio + sourceSize.height: 16 * Screen.devicePixelRatio + source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) + ToolTip.visible: editHovered.hovered + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Edited") + + HoverHandler { + id: editHovered + } + + } + + EncryptionIndicator { + visible: room.isEncrypted + encrypted: isEncrypted + trust: trustlevel + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredHeight: 16 + Layout.preferredWidth: 16 + } + + Label { + Layout.alignment: Qt.AlignRight | Qt.AlignTop + text: timestamp.toLocaleTimeString(Locale.ShortFormat) + width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) + color: Nheko.inactiveColors.text + ToolTip.visible: ma.hovered + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) + + HoverHandler { + id: ma + } + + } } - } - StatusIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - width: 16 - status: r.status + Reactions { + anchors { + top: msg.bottom + left: parent.left + } + + id: reactionRow + + reactions: r.reactions eventId: r.eventId } - - Image { - visible: isEdited || eventId == chat.model.edit - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - Layout.preferredWidth: 16 - height: 16 - width: 16 - sourceSize.width: 16 * Screen.devicePixelRatio - sourceSize.height: 16 * Screen.devicePixelRatio - source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText) - ToolTip.visible: editHovered.hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Edited") - - HoverHandler { - id: editHovered - } - - } - - EncryptionIndicator { - visible: room.isEncrypted - encrypted: isEncrypted - trust: trustlevel - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredHeight: 16 - Layout.preferredWidth: 16 - } - - Label { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - text: timestamp.toLocaleTimeString(Locale.ShortFormat) - width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth) - color: Nheko.inactiveColors.text - ToolTip.visible: ma.hovered - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) - - HoverHandler { - id: ma - } - - } - } - } diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index c0266c2c..ad6e7580 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -13,7 +13,7 @@ Item { required property bool isReply property alias child: chooser.child - property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width +// property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width required property double proportionalHeight required property int type required property string typeString @@ -35,14 +35,17 @@ Item { required property int encryptionError required property int relatedEventCacheBuster - height: chooser.child ? chooser.child.height : Nheko.paddingLarge + Layout.preferredHeight: chooser.child ? chooser.child.height : Nheko.paddingLarge DelegateChooser { id: chooser //role: "type" //< not supported in our custom implementation, have to use roleValue roleValue: type - anchors.fill: parent + //anchors.fill: parent + + anchors.left: parent.left + anchors.right: parent.right DelegateChoice { roleValue: MtxEvent.UnknownMessage diff --git a/resources/qml/delegates/Placeholder.qml b/resources/qml/delegates/Placeholder.qml index 19e48393..f63e62f5 100644 --- a/resources/qml/delegates/Placeholder.qml +++ b/resources/qml/delegates/Placeholder.qml @@ -10,6 +10,6 @@ MatrixText { required property string typeString text: qsTr("unimplemented event: ") + typeString - width: parent.width +// width: parent.width color: Nheko.inactiveColors.text } diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index d148f858..efa959a0 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -35,7 +35,7 @@ Item { property int encryptionError property int relatedEventCacheBuster - width: parent.width + Layout.preferredHeight: replyContainer.height height: replyContainer.height CursorShape { @@ -52,12 +52,12 @@ Item { color: TimelineManager.userColor(userId, Nheko.colors.base) } - Column { + ColumnLayout { id: replyContainer anchors.left: colorLine.right - anchors.leftMargin: 4 width: parent.width - 8 + spacing: 0 TapHandler { acceptedButtons: Qt.LeftButton @@ -80,6 +80,7 @@ Item { } Text { + Layout.leftMargin: 4 id: userName_ text: TimelineManager.escapeEmoji(userName) @@ -94,8 +95,8 @@ Item { } MessageDelegate { + Layout.leftMargin: 4 id: reply - blurhash: r.blurhash body: r.body formattedBody: r.formattedBody @@ -118,7 +119,7 @@ Item { encryptionError: r.encryptionError // This is disabled so that left clicking the reply goes to its location enabled: false - width: parent.width + Layout.fillWidth: true isReply: true } @@ -128,8 +129,7 @@ Item { id: backgroundItem z: -1 - height: replyContainer.height - width: Math.min(Math.max(reply.implicitWidth, userName_.implicitWidth) + 8 + 4, parent.width) + anchors.fill: replyContainer color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.1) } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index eea8cd1e..c9b8a882 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -33,8 +33,8 @@ MatrixText { blockquote { margin-left: 1em; } " + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
-    width: parent.width
-    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
+//    width: parent.width
+    //height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize

From 3c8c02e80265e9ac285756c803b938c016b1597c Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Fri, 4 Feb 2022 21:20:25 +0100
Subject: [PATCH 02/18] Revert removal of width and height in TextMessage and
 implicitWidth in MessageDelegate

---
 resources/qml/delegates/MessageDelegate.qml | 2 +-
 resources/qml/delegates/TextMessage.qml     | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index ad6e7580..8c74bfe8 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -13,7 +13,7 @@ Item {
 
     required property bool isReply
     property alias child: chooser.child
-//    property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
+    property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
     required property double proportionalHeight
     required property int type
     required property string typeString
diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index c9b8a882..eea8cd1e 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -33,8 +33,8 @@ MatrixText {
     blockquote { margin-left: 1em; }
     
     " + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
-//    width: parent.width
-    //height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
+    width: parent.width
+    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize

From 3f7c15c7afeb029223a7507387a95752428bb689 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Fri, 4 Feb 2022 23:12:30 +0100
Subject: [PATCH 03/18] Add message bubbles

---
 resources/qml/TimelineRow.qml     | 26 +++++++++++++++++++++-----
 resources/qml/delegates/Reply.qml |  4 +++-
 src/UserSettingsPage.cpp          | 30 ++++++++++++++++++++++++++++++
 src/UserSettingsPage.h            |  6 ++++++
 4 files changed, 60 insertions(+), 6 deletions(-)

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 1d9a036a..876fc800 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -75,10 +75,19 @@ Item {
         id: row
 
         anchors.rightMargin: 1
-        anchors.leftMargin: Nheko.avatarSize + 16
+        anchors.leftMargin: Nheko.avatarSize + 12
         anchors.left: parent.left
         anchors.right: parent.right
-        height: msg.height+reactionRow.height+2
+        height: msg.height+(reactionRow.height> 0 ? reactionRow.height-4 : 0)
+        Rectangle {
+            anchors.fill: msg
+            property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
+            property color bgColor: Nheko.colors.base
+            color: Qt.rgba(userColor.r*0.1+bgColor.r*0.9,userColor.g*0.1+bgColor.g*0.9,userColor.b*0.1+bgColor.b*0.9,1) //TimelineManager.userColor(userId, Nheko.colors.base)
+            radius: 4
+            visible: Settings.bubbles
+        }
+
         GridLayout {
             id: msg
             anchors {
@@ -98,7 +107,9 @@ Item {
                 Layout.row: 0
                 Layout.column: 0
                 Layout.fillWidth: true
-                Layout.margins: 0
+                Layout.margins: visible? 4 : 0
+                Layout.bottomMargin: 0
+                Layout.topMargin: visible? (Settings.bubbles? 4 : 2) : 0
                 id: reply
 
                 function fromModel(role) {
@@ -133,7 +144,10 @@ Item {
                 Layout.row: 1
                 Layout.column: 0
                 Layout.fillWidth: true
-                Layout.margins: 2
+                Layout.leftMargin: 4
+                Layout.rightMargin: 4
+                Layout.topMargin: reply.visible ? 2 : 4
+                Layout.bottomMargin: Settings.bubbles? 4: 2
                 id: contentItem
 
                 blurhash: r.blurhash
@@ -164,6 +178,8 @@ Item {
                 Layout.row: 0
                 Layout.rowSpan: 2
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
+                Layout.topMargin: 4
+                Layout.rightMargin: Settings.bubbles? 4 : 0
 
                 StatusIndicator {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
@@ -221,7 +237,7 @@ Item {
 
         Reactions {
             anchors {
-                top: msg.bottom
+                bottom: parent.bottom
                 left: parent.left
             }
 
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index efa959a0..0d4ff041 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -130,7 +130,9 @@ Item {
 
         z: -1
         anchors.fill: replyContainer
-        color: Qt.rgba(userColor.r, userColor.g, userColor.b, 0.1)
+        property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
+        property color bgColor: Nheko.colors.base
+        color: Qt.rgba(userColor.r*0.1+bgColor.r*0.9,userColor.g*0.1+bgColor.g*0.9,userColor.b*0.1+bgColor.b*0.9,1) // alpha makes this mix with the bubble color
     }
 
 }
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index a0aa8f84..993b9a01 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -70,6 +70,7 @@ UserSettings::load(std::optional profile)
     enlargeEmojiOnlyMessages_ =
       settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
     markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
+    bubbles_ = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
     animateImagesOnHover_ =
       settings.value(QStringLiteral("user/animate_images_on_hover"), false).toBool();
     typingNotifications_ =
@@ -242,6 +243,16 @@ UserSettings::setMarkdown(bool state)
     save();
 }
 
+void
+UserSettings::setBubbles(bool state)
+{
+    if (state == bubbles_)
+        return;
+    bubbles_ = state;
+    emit bubblesChanged(state);
+    save();
+}
+
 void
 UserSettings::setAnimateImagesOnHover(bool state)
 {
@@ -696,6 +707,7 @@ UserSettings::save()
     settings.setValue(QStringLiteral("read_receipts"), readReceipts_);
     settings.setValue(QStringLiteral("group_view"), groupView_);
     settings.setValue(QStringLiteral("markdown_enabled"), markdown_);
+    settings.setValue(QStringLiteral("bubbles_enabled"), bubbles_);
     settings.setValue(QStringLiteral("animate_images_on_hover"), animateImagesOnHover_);
     settings.setValue(QStringLiteral("desktop_notifications"), hasDesktopNotifications_);
     settings.setValue(QStringLiteral("alert_on_notification"), hasAlertOnNotification_);
@@ -796,6 +808,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr("Group's sidebar");
         case Markdown:
             return tr("Send messages as Markdown");
+        case Bubbles:
+            return tr("Enable Message bubbles");
         case AnimateImagesOnHover:
             return tr("Play animated images only on hover");
         case TypingNotifications:
@@ -916,6 +930,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return i->groupView();
         case Markdown:
             return i->markdown();
+        case Bubbles:
+            return i->bubbles();
         case AnimateImagesOnHover:
             return i->animateImagesOnHover();
         case TypingNotifications:
@@ -1042,6 +1058,9 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr(
               "Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
               "text.");
+        case Bubbles:
+            return tr(
+              "Messages received a bubble background.");
         case AnimateImagesOnHover:
             return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.");
         case TypingNotifications:
@@ -1158,6 +1177,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case StartInTray:
         case GroupView:
         case Markdown:
+        case Bubbles:
         case AnimateImagesOnHover:
         case TypingNotifications:
         case SortByImportance:
@@ -1375,6 +1395,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
             } else
                 return false;
         }
+        case Bubbles: {
+            if (value.userType() == QMetaType::Bool) {
+                i->setBubbles(value.toBool());
+                return true;
+            } else
+                return false;
+        }
         case AnimateImagesOnHover: {
             if (value.userType() == QMetaType::Bool) {
                 i->setAnimateImagesOnHover(value.toBool());
@@ -1737,6 +1764,9 @@ UserSettingsModel::UserSettingsModel(QObject *p)
     connect(s.get(), &UserSettings::markdownChanged, this, [this]() {
         emit dataChanged(index(Markdown), index(Markdown), {Value});
     });
+    connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
+        emit dataChanged(index(Bubbles), index(Bubbles), {Value});
+    });
 
     connect(s.get(), &UserSettings::groupViewStateChanged, this, [this]() {
         emit dataChanged(index(GroupView), index(GroupView), {Value});
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index e9b8763d..e5d22f6a 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -40,6 +40,7 @@ class UserSettings : public QObject
     Q_PROPERTY(bool startInTray READ startInTray WRITE setStartInTray NOTIFY startInTrayChanged)
     Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged)
     Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
+    Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
     Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
                  NOTIFY animateImagesOnHoverChanged)
     Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications NOTIFY
@@ -139,6 +140,7 @@ public:
     void setEmojiFontFamily(QString family);
     void setGroupView(bool state);
     void setMarkdown(bool state);
+    void setBubbles(bool state);
     void setAnimateImagesOnHover(bool state);
     void setReadReceipts(bool state);
     void setTypingNotifications(bool state);
@@ -190,6 +192,7 @@ public:
     bool privacyScreen() const { return privacyScreen_; }
     int privacyScreenTimeout() const { return privacyScreenTimeout_; }
     bool markdown() const { return markdown_; }
+    bool bubbles() const { return bubbles_; }
     bool animateImagesOnHover() const { return animateImagesOnHover_; }
     bool typingNotifications() const { return typingNotifications_; }
     bool sortByImportance() const { return sortByImportance_; }
@@ -247,6 +250,7 @@ signals:
     void trayChanged(bool state);
     void startInTrayChanged(bool state);
     void markdownChanged(bool state);
+    void bubblesChanged(bool state);
     void animateImagesOnHoverChanged(bool state);
     void typingNotificationsChanged(bool state);
     void buttonInTimelineChanged(bool state);
@@ -302,6 +306,7 @@ private:
     bool startInTray_;
     bool groupView_;
     bool markdown_;
+    bool bubbles_;
     bool animateImagesOnHover_;
     bool typingNotifications_;
     bool sortByImportance_;
@@ -380,6 +385,7 @@ class UserSettingsModel : public QAbstractListModel
         ReadReceipts,
         ButtonsInTimeline,
         Markdown,
+        Bubbles,
 
         SidebarSection,
         GroupView,

From f5a693ac031c526774bc89b111e2771b6ef9eff9 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Sat, 5 Feb 2022 14:12:51 +0100
Subject: [PATCH 04/18] place metadata below message when the Layout is narrow
 (<350)

---
 resources/qml/TimelineRow.qml | 39 ++++++++++++++++++-----------------
 src/UserSettingsPage.cpp      |  5 ++---
 2 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 876fc800..95dbec4e 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -97,10 +97,11 @@ Item {
                 topMargin: 1
                 bottomMargin: 1
             }
+            property bool narrowLayout: (row.width < 350) && Settings.bubbles
             rowSpacing: 0
             columnSpacing: 0
-            columns: 2
-            rows: 2
+            columns: narrowLayout? 1 : 2
+            rows: narrowLayout? 3 : 2
 
             // fancy reply, if this is a reply
             Reply {
@@ -147,7 +148,7 @@ Item {
                 Layout.leftMargin: 4
                 Layout.rightMargin: 4
                 Layout.topMargin: reply.visible ? 2 : 4
-                Layout.bottomMargin: Settings.bubbles? 4: 2
+                Layout.bottomMargin: Settings.bubbles? (msg.narrowLayout? 0 : 4) : 2
                 id: contentItem
 
                 blurhash: r.blurhash
@@ -174,17 +175,17 @@ Item {
             }
 
             RowLayout {
-                Layout.column: 1
-                Layout.row: 0
-                Layout.rowSpan: 2
+                Layout.column: msg.narrowLayout? 0 : 1
+                Layout.row: msg.narrowLayout? 2 : 0
+                Layout.rowSpan: msg.narrowLayout? 1 : 2
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
-                Layout.topMargin: 4
+                Layout.topMargin: msg.narrowLayout? 0 : 4
                 Layout.rightMargin: Settings.bubbles? 4 : 0
-
+                property double scaling: msg.narrowLayout? 0.75 : 1
                 StatusIndicator {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
-                    Layout.preferredHeight: 16
-                    width: 16
+                    Layout.preferredHeight: 16*parent.scaling
+                    width: 16*parent.scaling
                     status: r.status
                     eventId: r.eventId
                 }
@@ -192,12 +193,12 @@ Item {
                 Image {
                     visible: isEdited || eventId == chat.model.edit
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
-                    Layout.preferredHeight: 16
-                    Layout.preferredWidth: 16
-                    height: 16
-                    width: 16
-                    sourceSize.width: 16 * Screen.devicePixelRatio
-                    sourceSize.height: 16 * Screen.devicePixelRatio
+                    Layout.preferredHeight: 16*parent.scaling
+                    Layout.preferredWidth: 16*parent.scaling
+                    height: 16*parent.scaling
+                    width: 16*parent.scaling
+                    sourceSize.width: 16 * Screen.devicePixelRatio*parent.scaling
+                    sourceSize.height: 16 * Screen.devicePixelRatio*parent.scaling
                     source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText)
                     ToolTip.visible: editHovered.hovered
                     ToolTip.delay: Nheko.tooltipDelay
@@ -214,8 +215,8 @@ Item {
                     encrypted: isEncrypted
                     trust: trustlevel
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
-                    Layout.preferredHeight: 16
-                    Layout.preferredWidth: 16
+                    Layout.preferredHeight: 16*parent.scaling
+                    Layout.preferredWidth: 16*parent.scaling
                 }
 
                 Label {
@@ -226,7 +227,7 @@ Item {
                     ToolTip.visible: ma.hovered
                     ToolTip.delay: Nheko.tooltipDelay
                     ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate)
-
+                    font.pointSize: 10*parent.scaling
                     HoverHandler {
                         id: ma
                     }
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 993b9a01..c8ce19a7 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -70,7 +70,7 @@ UserSettings::load(std::optional profile)
     enlargeEmojiOnlyMessages_ =
       settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
     markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
-    bubbles_ = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
+    bubbles_  = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
     animateImagesOnHover_ =
       settings.value(QStringLiteral("user/animate_images_on_hover"), false).toBool();
     typingNotifications_ =
@@ -1059,8 +1059,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
               "Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
               "text.");
         case Bubbles:
-            return tr(
-              "Messages received a bubble background.");
+            return tr("Messages received a bubble background.");
         case AnimateImagesOnHover:
             return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.");
         case TypingNotifications:

From 9d194cc2e666cf8ff3141bac847f20423995d0ca Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Sat, 5 Feb 2022 21:53:21 +0100
Subject: [PATCH 05/18] clean up margin setting

---
 resources/qml/TimelineRow.qml     | 54 ++++++++++++++-----------------
 resources/qml/delegates/Reply.qml |  2 +-
 src/UserSettingsPage.cpp          |  5 +--
 3 files changed, 29 insertions(+), 32 deletions(-)

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 95dbec4e..c221d43f 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -46,7 +46,7 @@ Item {
 
     anchors.left: parent.left
     anchors.right: parent.right
-    height: row.height
+    height: row.height+reactionRow.height+(Settings.bubbles? 8 : 4)
 
     Rectangle {
         color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? Nheko.colors.alternateBase : "transparent"
@@ -71,16 +71,20 @@ Item {
         gesturePolicy: TapHandler.ReleaseWithinBounds
     }
 
-    Item {
+    Control {
         id: row
 
         anchors.rightMargin: 1
-        anchors.leftMargin: Nheko.avatarSize + 12
+        anchors.leftMargin: Nheko.avatarSize + 12 // align bubble with section header
         anchors.left: parent.left
         anchors.right: parent.right
-        height: msg.height+(reactionRow.height> 0 ? reactionRow.height-4 : 0)
-        Rectangle {
-            anchors.fill: msg
+        height: msg.height
+        topInset: -4
+        bottomInset: -4
+        leftInset: -4
+        rightInset: -4
+        background: Rectangle {
+            //anchors.fill: msg
             property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
             property color bgColor: Nheko.colors.base
             color: Qt.rgba(userColor.r*0.1+bgColor.r*0.9,userColor.g*0.1+bgColor.g*0.9,userColor.b*0.1+bgColor.b*0.9,1) //TimelineManager.userColor(userId, Nheko.colors.base)
@@ -94,12 +98,10 @@ Item {
                 right: parent.right
                 left: parent.left
                 top: parent.top
-                topMargin: 1
-                bottomMargin: 1
             }
             property bool narrowLayout: (row.width < 350) && Settings.bubbles
             rowSpacing: 0
-            columnSpacing: 0
+            columnSpacing: 2
             columns: narrowLayout? 1 : 2
             rows: narrowLayout? 3 : 2
 
@@ -108,9 +110,7 @@ Item {
                 Layout.row: 0
                 Layout.column: 0
                 Layout.fillWidth: true
-                Layout.margins: visible? 4 : 0
-                Layout.bottomMargin: 0
-                Layout.topMargin: visible? (Settings.bubbles? 4 : 2) : 0
+                Layout.bottomMargin: visible? 2 : 0
                 id: reply
 
                 function fromModel(role) {
@@ -145,10 +145,6 @@ Item {
                 Layout.row: 1
                 Layout.column: 0
                 Layout.fillWidth: true
-                Layout.leftMargin: 4
-                Layout.rightMargin: 4
-                Layout.topMargin: reply.visible ? 2 : 4
-                Layout.bottomMargin: Settings.bubbles? (msg.narrowLayout? 0 : 4) : 2
                 id: contentItem
 
                 blurhash: r.blurhash
@@ -178,9 +174,9 @@ Item {
                 Layout.column: msg.narrowLayout? 0 : 1
                 Layout.row: msg.narrowLayout? 2 : 0
                 Layout.rowSpan: msg.narrowLayout? 1 : 2
+                Layout.bottomMargin: msg.narrowLayout? -4 : 0
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
-                Layout.topMargin: msg.narrowLayout? 0 : 4
-                Layout.rightMargin: Settings.bubbles? 4 : 0
+
                 property double scaling: msg.narrowLayout? 0.75 : 1
                 StatusIndicator {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
@@ -235,17 +231,17 @@ Item {
                 }
             }
         }
-
-        Reactions {
-            anchors {
-                bottom: parent.bottom
-                left: parent.left
-            }
-
-            id: reactionRow
-
-            reactions: r.reactions
-            eventId: r.eventId
+    }
+    Reactions {
+        anchors {
+            top: row.bottom
+            left: parent.left
+            leftMargin: Nheko.avatarSize + 16
         }
+
+        id: reactionRow
+
+        reactions: r.reactions
+        eventId: r.eventId
     }
 }
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 0d4ff041..cc7f8d8f 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -56,7 +56,7 @@ Item {
         id: replyContainer
 
         anchors.left: colorLine.right
-        width: parent.width - 8
+        width: parent.width - 4
         spacing: 0
 
         TapHandler {
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index c8ce19a7..80947950 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -809,7 +809,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case Markdown:
             return tr("Send messages as Markdown");
         case Bubbles:
-            return tr("Enable Message bubbles");
+            return tr("Enable message bubbles");
         case AnimateImagesOnHover:
             return tr("Play animated images only on hover");
         case TypingNotifications:
@@ -1059,7 +1059,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
               "Allow using markdown in messages.\nWhen disabled, all messages are sent as a plain "
               "text.");
         case Bubbles:
-            return tr("Messages received a bubble background.");
+            return tr(
+              "Messages get a bubble background. This also triggers some layout changes (WIP).");
         case AnimateImagesOnHover:
             return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.");
         case TypingNotifications:

From 384c9c91e356cf7889860c9886d275ff86900b93 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Mon, 7 Feb 2022 21:00:51 +0100
Subject: [PATCH 06/18] print state events without bubbles, displaynames or
 avatars and in a smaller font

---
 resources/qml/MessageView.qml               | 11 ++++++--
 resources/qml/TimelineRow.qml               |  6 +++-
 resources/qml/delegates/MessageDelegate.qml | 31 +++++++++++++++++++++
 resources/qml/delegates/NoticeMessage.qml   |  3 ++
 resources/qml/delegates/Pill.qml            |  2 ++
 resources/qml/delegates/Reply.qml           |  2 ++
 6 files changed, 51 insertions(+), 4 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 7f2de64d..03ed1a8d 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -252,9 +252,9 @@ ScrollView {
                 topPadding: 4
                 bottomPadding: 4
                 spacing: 8
-                visible: (previousMessageUserId !== userId || previousMessageDay !== day)
+                visible: (previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent) && !isStateEvent
                 width: parentWidth
-                height: ((previousMessageDay !== day) ? dateBubble.height + 8 + userName.height : userName.height) + 8
+                height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent? 0 : userName.height +8 )
 
                 Label {
                     id: dateBubble
@@ -317,7 +317,7 @@ ScrollView {
                         color: TimelineManager.userColor(userId, Nheko.colors.base)
                         textFormat: Text.RichText
                         ToolTip.visible: displayNameHover.hovered
-                    ToolTip.delay: Nheko.tooltipDelay
+                        ToolTip.delay: Nheko.tooltipDelay
                         ToolTip.text: userId
 
                         TapHandler {
@@ -379,6 +379,8 @@ ScrollView {
             required property bool isEncrypted
             required property bool isEditable
             required property bool isEdited
+            required property bool isStateEvent
+            required property bool previousMessageIsStateEvent
             required property string replyTo
             required property string userId
             required property string roomTopic
@@ -455,6 +457,8 @@ ScrollView {
                 property string previousMessageUserId: wrapper.previousMessageUserId
                 property string day: wrapper.day
                 property string previousMessageDay: wrapper.previousMessageDay
+                property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
+                property bool isStateEvent: wrapper.isStateEvent
                 property string userName: wrapper.userName
                 property date timestamp: wrapper.timestamp
 
@@ -487,6 +491,7 @@ ScrollView {
                 isEncrypted: wrapper.isEncrypted
                 isEditable: wrapper.isEditable
                 isEdited: wrapper.isEdited
+                isStateEvent: wrapper.isStateEvent
                 replyTo: wrapper.replyTo
                 userId: wrapper.userId
                 userName: wrapper.userName
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index c221d43f..b91d08d2 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -31,6 +31,7 @@ Item {
     required property bool isEncrypted
     required property bool isEditable
     required property bool isEdited
+    required property bool isStateEvent
     required property string replyTo
     required property string userId
     required property string userName
@@ -89,7 +90,7 @@ Item {
             property color bgColor: Nheko.colors.base
             color: Qt.rgba(userColor.r*0.1+bgColor.r*0.9,userColor.g*0.1+bgColor.g*0.9,userColor.b*0.1+bgColor.b*0.9,1) //TimelineManager.userColor(userId, Nheko.colors.base)
             radius: 4
-            visible: Settings.bubbles
+            visible: Settings.bubbles && !isStateEvent
         }
 
         GridLayout {
@@ -130,6 +131,7 @@ Item {
                 url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? ""
                 originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0
                 isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false
+                isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false
                 userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? ""
                 userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? ""
                 thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? ""
@@ -160,6 +162,7 @@ Item {
                 thumbnailUrl: r.thumbnailUrl
                 originalWidth: r.originalWidth
                 isOnlyEmoji: r.isOnlyEmoji
+                isStateEvent: r.isStateEvent
                 userId: r.userId
                 userName: r.userName
                 roomTopic: r.roomTopic
@@ -176,6 +179,7 @@ Item {
                 Layout.rowSpan: msg.narrowLayout? 1 : 2
                 Layout.bottomMargin: msg.narrowLayout? -4 : 0
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
+                visible: !isStateEvent
 
                 property double scaling: msg.narrowLayout? 0.75 : 1
                 StatusIndicator {
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 8c74bfe8..aef69ba4 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -27,6 +27,7 @@ Item {
     required property string url
     required property string thumbnailUrl
     required property bool isOnlyEmoji
+    required property bool isStateEvent
     required property string userId
     required property string userName
     required property string roomTopic
@@ -77,6 +78,7 @@ Item {
                 body: d.body
                 isOnlyEmoji: d.isOnlyEmoji
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
             }
 
         }
@@ -90,6 +92,7 @@ Item {
                 body: d.body
                 isOnlyEmoji: d.isOnlyEmoji
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
             }
 
         }
@@ -184,6 +187,7 @@ Item {
 
             Pill {
                 text: qsTr("removed")
+                isStateEvent: d.isStateEvent
             }
 
         }
@@ -193,6 +197,7 @@ Item {
 
             Pill {
                 text: qsTr("Encryption enabled")
+                isStateEvent: d.isStateEvent
             }
 
         }
@@ -214,6 +219,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: d.roomName ? qsTr("room name changed to: %1").arg(d.roomName) : qsTr("removed room name")
             }
 
@@ -226,6 +232,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: d.roomTopic ? qsTr("topic changed to: %1").arg(d.roomTopic) : qsTr("removed topic")
             }
 
@@ -238,6 +245,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 changed the room avatar").arg(d.userName)
             }
 
@@ -250,6 +258,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 changed the pinned messages.").arg(d.userName)
             }
 
@@ -262,6 +271,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 changed the stickers and emotes in this room.").arg(d.userName)
             }
 
@@ -274,6 +284,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 changed the addresses for this room.").arg(d.userName)
             }
 
@@ -286,6 +297,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 changed the parent spaces for this room.").arg(d.userName)
             }
 
@@ -298,6 +310,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId)
             }
 
@@ -310,6 +323,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: {
                     switch (d.callType) {
                     case "voice":
@@ -331,6 +345,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 answered the call.").arg(d.userName)
             }
 
@@ -343,6 +358,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("%1 ended the call.").arg(d.userName)
             }
 
@@ -355,6 +371,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: qsTr("Negotiating call...")
             }
 
@@ -368,6 +385,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: d.relatedEventCacheBuster, room.formatPowerLevelEvent(d.eventId)
             }
 
@@ -380,6 +398,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: d.relatedEventCacheBuster, room.formatJoinRuleEvent(d.eventId)
             }
 
@@ -392,6 +411,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: d.relatedEventCacheBuster, room.formatHistoryVisibilityEvent(d.eventId)
             }
 
@@ -404,6 +424,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: d.relatedEventCacheBuster, room.formatGuestAccessEvent(d.eventId)
             }
 
@@ -419,6 +440,7 @@ Item {
                     body: formatted
                     isOnlyEmoji: false
                     isReply: d.isReply
+                    isStateEvent: d.isStateEvent
                     Layout.fillWidth: true
                     formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId)
                 }
@@ -441,6 +463,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationRequest"
             }
 
@@ -453,6 +476,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationStart"
             }
 
@@ -465,6 +489,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationReady"
             }
 
@@ -477,6 +502,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationCancel"
             }
 
@@ -489,6 +515,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationKey"
             }
 
@@ -501,6 +528,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationMac"
             }
 
@@ -513,6 +541,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationDone"
             }
 
@@ -525,6 +554,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationDone"
             }
 
@@ -537,6 +567,7 @@ Item {
                 body: formatted
                 isOnlyEmoji: false
                 isReply: d.isReply
+                isStateEvent: d.isStateEvent
                 formatted: "KeyVerificationAccept"
             }
 
diff --git a/resources/qml/delegates/NoticeMessage.qml b/resources/qml/delegates/NoticeMessage.qml
index fa4bbb35..544af109 100644
--- a/resources/qml/delegates/NoticeMessage.qml
+++ b/resources/qml/delegates/NoticeMessage.qml
@@ -5,7 +5,10 @@
 
 import im.nheko 1.0
 
+
 TextMessage {
+    property bool isStateEvent
     font.italic: true
     color: Nheko.colors.buttonText
+    font.pointSize: isStateEvent? 0.75*fontMetrics.font.pointSize : 1*fontMetrics.font.pointSize
 }
diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml
index 06420586..bfc18b9a 100644
--- a/resources/qml/delegates/Pill.qml
+++ b/resources/qml/delegates/Pill.qml
@@ -8,10 +8,12 @@ import QtQuick.Controls 2.1
 import im.nheko 1.0
 
 Label {
+    property bool isStateEvent
     color: Nheko.colors.text
     horizontalAlignment: Text.AlignHCenter
     height: contentHeight * 1.2
     width: contentWidth * 1.2
+    font.pointSize: isStateEvent? 0.75*fontMetrics.font.pointSize : 1*fontMetrics.font.pointSize
 
     background: Rectangle {
         radius: parent.height / 2
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index cc7f8d8f..f5556c8b 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -26,6 +26,7 @@ Item {
     property string filesize
     property string url
     property bool isOnlyEmoji
+    property bool isStateEvent
     property string userId
     property string userName
     property string thumbnailUrl
@@ -110,6 +111,7 @@ Item {
             thumbnailUrl: r.thumbnailUrl
             originalWidth: r.originalWidth
             isOnlyEmoji: r.isOnlyEmoji
+            isStateEvent: r.isStateEvent
             userId: r.userId
             userName: r.userName
             roomTopic: r.roomTopic

From 12d600db97db7e6e4d9f3acb2755c6dfb5071a83 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Mon, 7 Feb 2022 21:53:37 +0100
Subject: [PATCH 07/18] add translations and actually add changes to
 TimelineModel

---
 resources/qml/MessageView.qml               |  5 +++--
 resources/qml/delegates/MessageDelegate.qml | 10 +++++-----
 src/timeline/TimelineModel.cpp              | 13 +++++++++++--
 src/timeline/TimelineModel.h                |  2 ++
 4 files changed, 21 insertions(+), 9 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 03ed1a8d..aef05a63 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -252,7 +252,7 @@ ScrollView {
                 topPadding: 4
                 bottomPadding: 4
                 spacing: 8
-                visible: (previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent) && !isStateEvent
+                visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
                 width: parentWidth
                 height: ((previousMessageDay !== day) ? dateBubble.height : 0) + (isStateEvent? 0 : userName.height +8 )
 
@@ -278,6 +278,7 @@ ScrollView {
                 Row {
                     height: userName_.height
                     spacing: 8
+                    visible: !isStateEvent
 
                     Avatar {
                         id: messageUserAvatar
@@ -289,7 +290,7 @@ ScrollView {
                         userid: userId
                         onClicked: room.openUserProfile(userId)
                         ToolTip.visible: avatarHover.hovered
-                    ToolTip.delay: Nheko.tooltipDelay
+                        ToolTip.delay: Nheko.tooltipDelay
                         ToolTip.text: userid
 
                         HoverHandler {
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index aef69ba4..58f12c91 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -186,7 +186,7 @@ Item {
             roleValue: MtxEvent.Redaction
 
             Pill {
-                text: qsTr("removed")
+                text: qsTr("%1 removed a message").arg(d.userName)
                 isStateEvent: d.isStateEvent
             }
 
@@ -196,7 +196,7 @@ Item {
             roleValue: MtxEvent.Encryption
 
             Pill {
-                text: qsTr("Encryption enabled")
+                text: qsTr("%1 enabled encryption").arg(d.userName)
                 isStateEvent: d.isStateEvent
             }
 
@@ -220,7 +220,7 @@ Item {
                 isOnlyEmoji: false
                 isReply: d.isReply
                 isStateEvent: d.isStateEvent
-                formatted: d.roomName ? qsTr("room name changed to: %1").arg(d.roomName) : qsTr("removed room name")
+                formatted: d.roomName ? qsTr("%2 changed the room name to: %1").arg(d.roomName).arg(d.userName) : qsTr("%1 removed the room name").arg(d.userName)
             }
 
         }
@@ -233,7 +233,7 @@ Item {
                 isOnlyEmoji: false
                 isReply: d.isReply
                 isStateEvent: d.isStateEvent
-                formatted: d.roomTopic ? qsTr("topic changed to: %1").arg(d.roomTopic) : qsTr("removed topic")
+                formatted: d.roomTopic ? qsTr("%2 changed the topic to: %1").arg(d.roomTopic).arg(d.userName): qsTr("%1 removed the topic").arg(d.userName)
             }
 
         }
@@ -372,7 +372,7 @@ Item {
                 isOnlyEmoji: false
                 isReply: d.isReply
                 isStateEvent: d.isStateEvent
-                formatted: qsTr("Negotiating call...")
+                formatted: qsTr("% is negotiating the call...").arg(d.userName)
             }
 
         }
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 6b380f79..dfd06f0b 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -453,6 +453,7 @@ TimelineModel::roleNames() const
       {UserId, "userId"},
       {UserName, "userName"},
       {PreviousMessageDay, "previousMessageDay"},
+      {PreviousMessageIsStateEvent, "previousMessageIsStateEvent"},
       {Day, "day"},
       {Timestamp, "timestamp"},
       {Url, "url"},
@@ -469,6 +470,7 @@ TimelineModel::roleNames() const
       {IsEdited, "isEdited"},
       {IsEditable, "isEditable"},
       {IsEncrypted, "isEncrypted"},
+      {IsStateEvent, "isStateEvent"},
       {Trustlevel, "trustlevel"},
       {EncryptionError, "encryptionError"},
       {ReplyTo, "replyTo"},
@@ -666,6 +668,9 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
                std::holds_alternative>(
                  *encrypted_event);
     }
+    case IsStateEvent: {
+        return is_state_event(event);
+    }
 
     case Trustlevel: {
         auto encrypted_event = events.get(event_id(event), "", false);
@@ -730,6 +735,7 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r
         m.insert(names[IsEdited], data(event, static_cast(IsEdited)));
         m.insert(names[IsEditable], data(event, static_cast(IsEditable)));
         m.insert(names[IsEncrypted], data(event, static_cast(IsEncrypted)));
+        m.insert(names[IsStateEvent], data(event, static_cast(IsStateEvent)));
         m.insert(names[ReplyTo], data(event, static_cast(ReplyTo)));
         m.insert(names[RoomName], data(event, static_cast(RoomName)));
         m.insert(names[RoomTopic], data(event, static_cast(RoomTopic)));
@@ -762,7 +768,8 @@ TimelineModel::data(const QModelIndex &index, int role) const
     if (!event)
         return "";
 
-    if (role == PreviousMessageDay || role == PreviousMessageUserId) {
+    if (role == PreviousMessageDay || role == PreviousMessageUserId ||
+        role == PreviousMessageIsStateEvent) {
         int prevIdx = rowCount() - index.row() - 2;
         if (prevIdx < 0)
             return {};
@@ -771,8 +778,10 @@ TimelineModel::data(const QModelIndex &index, int role) const
             return {};
         if (role == PreviousMessageUserId)
             return data(*tempEv, UserId);
-        else
+        else if (role == PreviousMessageDay)
             return data(*tempEv, Day);
+        else
+            return data(*tempEv, IsStateEvent);
     }
 
     return data(*event, role);
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index 556f9f54..c50a2c06 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -205,6 +205,7 @@ public:
         UserId,
         UserName,
         PreviousMessageDay,
+        PreviousMessageIsStateEvent,
         Day,
         Timestamp,
         Url,
@@ -221,6 +222,7 @@ public:
         IsEdited,
         IsEditable,
         IsEncrypted,
+        IsStateEvent,
         Trustlevel,
         EncryptionError,
         ReplyTo,

From e290f9938a3ab5b31c168a44d59b9064cce18763 Mon Sep 17 00:00:00 2001
From: Malte E <97891689+maltee1@users.noreply.github.com>
Date: Mon, 7 Feb 2022 22:04:49 +0100
Subject: [PATCH 08/18] Update resources/qml/delegates/Reply.qml

Co-authored-by: DeepBlueV7.X 
---
 resources/qml/delegates/Reply.qml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index f5556c8b..96fa410e 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -134,7 +134,7 @@ Item {
         anchors.fill: replyContainer
         property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
         property color bgColor: Nheko.colors.base
-        color: Qt.rgba(userColor.r*0.1+bgColor.r*0.9,userColor.g*0.1+bgColor.g*0.9,userColor.b*0.1+bgColor.b*0.9,1) // alpha makes this mix with the bubble color
+        color: Qt.tint(bgColor, Qt.rgba(userColor.r, userColor.g, userColor.b, 0.1))
     }
 
 }

From 5db7ec7f037338e45de3dc02767e42484ca292bc Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Wed, 9 Feb 2022 17:53:02 +0100
Subject: [PATCH 09/18] Variable width bubbles (still has binding loop)

---
 resources/qml/TimelineRow.qml               | 17 +++++++++++------
 resources/qml/delegates/Encrypted.qml       |  2 ++
 resources/qml/delegates/MessageDelegate.qml |  2 +-
 resources/qml/delegates/Reply.qml           |  1 +
 4 files changed, 15 insertions(+), 7 deletions(-)

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index b91d08d2..51275f4e 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -78,7 +78,9 @@ Item {
         anchors.rightMargin: 1
         anchors.leftMargin: Nheko.avatarSize + 12 // align bubble with section header
         anchors.left: parent.left
-        anchors.right: parent.right
+        //anchors.right: parent.right
+        property int maxWidth: parent.width-anchors.leftMargin-anchors.rightMargin
+        width: Math.min(maxWidth,msg.implicitWidth)
         height: msg.height
         topInset: -4
         bottomInset: -4
@@ -100,7 +102,7 @@ Item {
                 left: parent.left
                 top: parent.top
             }
-            property bool narrowLayout: (row.width < 350) && Settings.bubbles
+            property bool narrowLayout: (r.width < 500) && Settings.bubbles
             rowSpacing: 0
             columnSpacing: 2
             columns: narrowLayout? 1 : 2
@@ -174,18 +176,21 @@ Item {
             }
 
             RowLayout {
+                id: metadata
                 Layout.column: msg.narrowLayout? 0 : 1
                 Layout.row: msg.narrowLayout? 2 : 0
                 Layout.rowSpan: msg.narrowLayout? 1 : 2
                 Layout.bottomMargin: msg.narrowLayout? -4 : 0
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
+                Layout.preferredWidth: implicitWidth
                 visible: !isStateEvent
 
                 property double scaling: msg.narrowLayout? 0.75 : 1
+
                 StatusIndicator {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
                     Layout.preferredHeight: 16*parent.scaling
-                    width: 16*parent.scaling
+                    Layout.preferredWidth: 16*parent.scaling
                     status: r.status
                     eventId: r.eventId
                 }
@@ -195,8 +200,6 @@ Item {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
                     Layout.preferredHeight: 16*parent.scaling
                     Layout.preferredWidth: 16*parent.scaling
-                    height: 16*parent.scaling
-                    width: 16*parent.scaling
                     sourceSize.width: 16 * Screen.devicePixelRatio*parent.scaling
                     sourceSize.height: 16 * Screen.devicePixelRatio*parent.scaling
                     source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == chat.model.edit) ? Nheko.colors.highlight : Nheko.colors.buttonText)
@@ -217,12 +220,14 @@ Item {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
                     Layout.preferredHeight: 16*parent.scaling
                     Layout.preferredWidth: 16*parent.scaling
+                    sourceSize.width: 16 * Screen.devicePixelRatio*parent.scaling
+                    sourceSize.height: 16 * Screen.devicePixelRatio*parent.scaling
                 }
 
                 Label {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
+                    Layout.preferredWidth: implicitWidth
                     text: timestamp.toLocaleTimeString(Locale.ShortFormat)
-                    width: Math.max(implicitWidth, text.length * fontMetrics.maximumCharacterWidth)
                     color: Nheko.inactiveColors.text
                     ToolTip.visible: ma.hovered
                     ToolTip.delay: Nheko.tooltipDelay
diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml
index 6840c955..d82f027b 100644
--- a/resources/qml/delegates/Encrypted.qml
+++ b/resources/qml/delegates/Encrypted.qml
@@ -17,6 +17,7 @@ Rectangle {
 
     radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium
     width: parent.width
+    implicitWidth: encryptedText.implicitWidth+24+Nheko.paddingMedium*3 // Column doesn't provide a useful implicitWidth, should be replaced by ColumnLayout
     height: contents.implicitHeight + Nheko.paddingMedium * 2
     color: Nheko.colors.alternateBase
 
@@ -39,6 +40,7 @@ Rectangle {
             Layout.fillWidth: true
 
             MatrixText {
+                id: encryptedText
                 text: {
                     switch (encryptionError) {
                     case Olm.MissingSession:
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 58f12c91..0cec51bc 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -13,7 +13,7 @@ Item {
 
     required property bool isReply
     property alias child: chooser.child
-    property real implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
+    implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
     required property double proportionalHeight
     required property int type
     required property string typeString
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 96fa410e..7c8bccce 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -38,6 +38,7 @@ Item {
 
     Layout.preferredHeight: replyContainer.height
     height: replyContainer.height
+    implicitWidth: visible? colorLine.width+replyContainer.implicitWidth : 0
 
     CursorShape {
         anchors.fill: parent

From 0e548b7d390cfbc3c37689ed9dbff0f1e2eab5cc Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Wed, 9 Feb 2022 21:36:04 +0100
Subject: [PATCH 10/18] fixed most of the binding loops

---
 resources/qml/TimelineRow.qml                    |  4 +++-
 resources/qml/delegates/FileMessage.qml          |  4 +++-
 resources/qml/delegates/ImageMessage.qml         |  5 ++++-
 resources/qml/delegates/MessageDelegate.qml      | 10 ++++++++--
 resources/qml/delegates/PlayableMediaMessage.qml |  5 ++++-
 resources/qml/delegates/Redacted.qml             |  1 +
 resources/qml/delegates/Reply.qml                |  2 ++
 7 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 51275f4e..b40ce068 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -102,7 +102,7 @@ Item {
                 left: parent.left
                 top: parent.top
             }
-            property bool narrowLayout: (r.width < 500) && Settings.bubbles
+            property bool narrowLayout: Settings.bubbles //&& (timelineView.width < 500) // timelineView causes fewew binding loops than r. But maybe it shouldn't depend on width anyway
             rowSpacing: 0
             columnSpacing: 2
             columns: narrowLayout? 1 : 2
@@ -142,6 +142,7 @@ Item {
                 callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
                 encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? ""
                 relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
+                maxWidth: row.maxWidth
             }
 
             // actual message content
@@ -173,6 +174,7 @@ Item {
                 encryptionError: r.encryptionError
                 relatedEventCacheBuster: r.relatedEventCacheBuster
                 isReply: false
+                maxWidth: row.maxWidth
             }
 
             RowLayout {
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index f1fd07b9..2ac2a80a 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -11,9 +11,11 @@ Item {
     required property string eventId
     required property string filename
     required property string filesize
+    property int maxWidth
 
     height: row.height + 24
-    width: parent.width
+    width: maxWidth
+    implicitWidth: maxWidth
 
     RowLayout {
         id: row
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index da15bdfe..3bbcd077 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -17,13 +17,16 @@ Item {
     required property string filename
     required property bool isReply
     required property string eventId
-    property double tempWidth: Math.min(parent.width, originalWidth < 1 ? 200 : originalWidth)
+    property int maxWidth
+    property double tempWidth: Math.min(maxWidth, originalWidth < 1 ? 200 : originalWidth)
     property double tempHeight: tempWidth * proportionalHeight
     property double divisor: isReply ? 5 : 3
     property bool tooHigh: tempHeight > timelineView.height / divisor
 
     height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight)
     width: Math.round(tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth)
+    implicitHeight: height
+    implicitWidth: width
 
     Image {
         id: blurhash_
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 0cec51bc..5d987841 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -13,7 +13,7 @@ Item {
 
     required property bool isReply
     property alias child: chooser.child
-    implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : width
+    implicitWidth: (chooser.child && chooser.child.implicitWidth) ? chooser.child.implicitWidth : 0
     required property double proportionalHeight
     required property int type
     required property string typeString
@@ -35,6 +35,7 @@ Item {
     required property string callType
     required property int encryptionError
     required property int relatedEventCacheBuster
+    property int maxWidth
 
     Layout.preferredHeight: chooser.child ? chooser.child.height : Nheko.paddingLarge
 
@@ -110,6 +111,7 @@ Item {
                 filename: d.filename
                 isReply: d.isReply
                 eventId: d.eventId
+                maxWidth: d.maxWidth
             }
 
         }
@@ -127,6 +129,7 @@ Item {
                 filename: d.filename
                 isReply: d.isReply
                 eventId: d.eventId
+                maxWidth: d.maxWidth
             }
 
         }
@@ -138,6 +141,7 @@ Item {
                 eventId: d.eventId
                 filename: d.filename
                 filesize: d.filesize
+                maxWidth: d.maxWidth
             }
 
         }
@@ -154,6 +158,7 @@ Item {
                 url: d.url
                 body: d.body
                 filesize: d.filesize
+                maxWidth: d.maxWidth
             }
 
         }
@@ -170,6 +175,7 @@ Item {
                 url: d.url
                 body: d.body
                 filesize: d.filesize
+                maxWidth: d.maxWidth
             }
 
         }
@@ -372,7 +378,7 @@ Item {
                 isOnlyEmoji: false
                 isReply: d.isReply
                 isStateEvent: d.isStateEvent
-                formatted: qsTr("% is negotiating the call...").arg(d.userName)
+                formatted: qsTr("%1 is negotiating the call...").arg(d.userName)
             }
 
         }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 389d1814..805eafbf 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -22,13 +22,16 @@ Item {
     required property string url
     required property string body
     required property string filesize
-    property double tempWidth: Math.min(parent.width, originalWidth < 1 ? 400 : originalWidth)
+    property int maxWidth
+    property double tempWidth: Math.min(maxWidth, originalWidth < 1 ? 400 : originalWidth)
     property double tempHeight: tempWidth * proportionalHeight
     property double divisor: isReply ? 4 : 2
     property bool tooHigh: tempHeight > timelineRoot.height / divisor
 
     height: (type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 80) + fileInfoLabel.height
     width: type == MtxEvent.VideoMessage ? tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth : 250
+    implicitHeight: height
+    implicitWidth: width
 
     MxcMedia {
         id: mxcmedia
diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml
index 10b92173..b0c9abfe 100644
--- a/resources/qml/delegates/Redacted.qml
+++ b/resources/qml/delegates/Redacted.qml
@@ -13,6 +13,7 @@ Rectangle{
     required property real delegateWidth
     height: redactedLayout.implicitHeight + Nheko.paddingSmall
     width: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium
+    implicitWidth: width
     radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall
     color: Nheko.colors.alternateBase
 
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 7c8bccce..28f0dd21 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -35,6 +35,7 @@ Item {
     property string callType
     property int encryptionError
     property int relatedEventCacheBuster
+    property int maxWidth
 
     Layout.preferredHeight: replyContainer.height
     height: replyContainer.height
@@ -124,6 +125,7 @@ Item {
             enabled: false
             Layout.fillWidth: true
             isReply: true
+            maxWidth: r.maxWidth-4
         }
 
     }

From d0ac110cb7013124c511bbe3ea2dc05db9e695b2 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Fri, 11 Feb 2022 22:02:30 +0100
Subject: [PATCH 11/18] Fix layout - no more binding loops (hopefully)

---
 resources/qml/MessageView.qml                 |  4 +--
 resources/qml/ReplyPopup.qml                  |  1 +
 resources/qml/TimelineRow.qml                 | 31 +++++++------------
 resources/qml/delegates/FileMessage.qml       |  5 ++-
 resources/qml/delegates/ImageMessage.qml      | 11 ++-----
 resources/qml/delegates/MessageDelegate.qml   | 13 ++------
 resources/qml/delegates/Pill.qml              |  4 +--
 .../qml/delegates/PlayableMediaMessage.qml    | 11 ++-----
 resources/qml/delegates/Redacted.qml          | 11 ++++---
 resources/qml/delegates/Reply.qml             |  4 +--
 10 files changed, 35 insertions(+), 60 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index aef05a63..a00ada3e 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -33,7 +33,7 @@ ScrollView {
         //reuseItems: true
         boundsBehavior: Flickable.StopAtBounds
         pixelAligned: true
-        spacing: 4
+        spacing: 2
         verticalLayoutDirection: ListView.BottomToTop
         onCountChanged: {
             // Mark timeline as read
@@ -464,7 +464,7 @@ ScrollView {
                 property date timestamp: wrapper.timestamp
 
                 z: 4
-                active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day
+                active: previousMessageUserId !== undefined && previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
                 //asynchronous: true
                 sourceComponent: sectionHeader
                 visible: status == Loader.Ready
diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml
index e6c83835..ef0d7c60 100644
--- a/resources/qml/ReplyPopup.qml
+++ b/resources/qml/ReplyPopup.qml
@@ -47,6 +47,7 @@ Rectangle {
         userId: modelData.userId ?? ""
         userName: modelData.userName ?? ""
         encryptionError: modelData.encryptionError ?? ""
+        width: parent.width
     }
 
     ImageButton {
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index b40ce068..85e6fa4b 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -47,7 +47,7 @@ Item {
 
     anchors.left: parent.left
     anchors.right: parent.right
-    height: row.height+reactionRow.height+(Settings.bubbles? 8 : 4)
+    height: row.height+reactionRow.height
 
     Rectangle {
         color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? Nheko.colors.alternateBase : "transparent"
@@ -78,30 +78,19 @@ Item {
         anchors.rightMargin: 1
         anchors.leftMargin: Nheko.avatarSize + 12 // align bubble with section header
         anchors.left: parent.left
-        //anchors.right: parent.right
         property int maxWidth: parent.width-anchors.leftMargin-anchors.rightMargin
-        width: Math.min(maxWidth,msg.implicitWidth)
-        height: msg.height
-        topInset: -4
-        bottomInset: -4
-        leftInset: -4
-        rightInset: -4
+        width: Settings.bubbles? Math.min(maxWidth,implicitWidth+metadata.width) : maxWidth
+        padding: isStateEvent? 0 : 3
         background: Rectangle {
-            //anchors.fill: msg
             property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
             property color bgColor: Nheko.colors.base
-            color: Qt.rgba(userColor.r*0.1+bgColor.r*0.9,userColor.g*0.1+bgColor.g*0.9,userColor.b*0.1+bgColor.b*0.9,1) //TimelineManager.userColor(userId, Nheko.colors.base)
-            radius: 4
+            color: Qt.tint(bgColor, Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2))
+            radius: parent.padding*2
             visible: Settings.bubbles && !isStateEvent
         }
 
-        GridLayout {
+        contentItem: GridLayout {
             id: msg
-            anchors {
-                right: parent.right
-                left: parent.left
-                top: parent.top
-            }
             property bool narrowLayout: Settings.bubbles //&& (timelineView.width < 500) // timelineView causes fewew binding loops than r. But maybe it shouldn't depend on width anyway
             rowSpacing: 0
             columnSpacing: 2
@@ -114,6 +103,8 @@ Item {
                 Layout.column: 0
                 Layout.fillWidth: true
                 Layout.bottomMargin: visible? 2 : 0
+                Layout.preferredHeight: height
+                Layout.maximumWidth: implicitWidth
                 id: reply
 
                 function fromModel(role) {
@@ -142,7 +133,6 @@ Item {
                 callType: r.relatedEventCacheBuster, fromModel(Room.CallType) ?? ""
                 encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? ""
                 relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0
-                maxWidth: row.maxWidth
             }
 
             // actual message content
@@ -150,6 +140,8 @@ Item {
                 Layout.row: 1
                 Layout.column: 0
                 Layout.fillWidth: true
+                Layout.preferredHeight: height
+                Layout.maximumWidth: implicitWidth
                 id: contentItem
 
                 blurhash: r.blurhash
@@ -174,7 +166,6 @@ Item {
                 encryptionError: r.encryptionError
                 relatedEventCacheBuster: r.relatedEventCacheBuster
                 isReply: false
-                maxWidth: row.maxWidth
             }
 
             RowLayout {
@@ -182,7 +173,7 @@ Item {
                 Layout.column: msg.narrowLayout? 0 : 1
                 Layout.row: msg.narrowLayout? 2 : 0
                 Layout.rowSpan: msg.narrowLayout? 1 : 2
-                Layout.bottomMargin: msg.narrowLayout? -4 : 0
+                Layout.bottomMargin: -4
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
                 Layout.preferredWidth: implicitWidth
                 visible: !isStateEvent
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 2ac2a80a..1e50fe3a 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -11,11 +11,10 @@ Item {
     required property string eventId
     required property string filename
     required property string filesize
-    property int maxWidth
 
     height: row.height + 24
-    width: maxWidth
-    implicitWidth: maxWidth
+    width: parent.width
+    implicitWidth: row.implicitWidth
 
     RowLayout {
         id: row
diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml
index 3bbcd077..a13bb4f6 100644
--- a/resources/qml/delegates/ImageMessage.qml
+++ b/resources/qml/delegates/ImageMessage.qml
@@ -17,16 +17,11 @@ Item {
     required property string filename
     required property bool isReply
     required property string eventId
-    property int maxWidth
-    property double tempWidth: Math.min(maxWidth, originalWidth < 1 ? 200 : originalWidth)
-    property double tempHeight: tempWidth * proportionalHeight
     property double divisor: isReply ? 5 : 3
-    property bool tooHigh: tempHeight > timelineView.height / divisor
 
-    height: Math.round(tooHigh ? timelineView.height / divisor : tempHeight)
-    width: Math.round(tooHigh ? (timelineView.height / divisor) / proportionalHeight : tempWidth)
-    implicitHeight: height
-    implicitWidth: width
+    implicitWidth: Math.round(originalWidth*Math.min((timelineView.height/divisor)/(originalWidth*proportionalHeight), 1))
+    width: parent.width
+    height: width*proportionalHeight
 
     Image {
         id: blurhash_
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index 5d987841..3210128a 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -35,9 +35,8 @@ Item {
     required property string callType
     required property int encryptionError
     required property int relatedEventCacheBuster
-    property int maxWidth
 
-    Layout.preferredHeight: chooser.child ? chooser.child.height : Nheko.paddingLarge
+    height: chooser.child ? chooser.child.height : Nheko.paddingLarge
 
     DelegateChooser {
         id: chooser
@@ -46,8 +45,7 @@ Item {
         roleValue: type
         //anchors.fill: parent
 
-        anchors.left: parent.left
-        anchors.right: parent.right
+        width: parent.width? parent.width: 0 // this should get rid of "cannot read property 'width' of null"
 
         DelegateChoice {
             roleValue: MtxEvent.UnknownMessage
@@ -111,7 +109,6 @@ Item {
                 filename: d.filename
                 isReply: d.isReply
                 eventId: d.eventId
-                maxWidth: d.maxWidth
             }
 
         }
@@ -129,7 +126,6 @@ Item {
                 filename: d.filename
                 isReply: d.isReply
                 eventId: d.eventId
-                maxWidth: d.maxWidth
             }
 
         }
@@ -141,7 +137,6 @@ Item {
                 eventId: d.eventId
                 filename: d.filename
                 filesize: d.filesize
-                maxWidth: d.maxWidth
             }
 
         }
@@ -158,7 +153,6 @@ Item {
                 url: d.url
                 body: d.body
                 filesize: d.filesize
-                maxWidth: d.maxWidth
             }
 
         }
@@ -175,7 +169,6 @@ Item {
                 url: d.url
                 body: d.body
                 filesize: d.filesize
-                maxWidth: d.maxWidth
             }
 
         }
@@ -184,7 +177,7 @@ Item {
             roleValue: MtxEvent.Redacted
 
             Redacted {
-                delegateWidth: d.width
+                //delegateWidth: d.width
             }
         }
 
diff --git a/resources/qml/delegates/Pill.qml b/resources/qml/delegates/Pill.qml
index bfc18b9a..a3964f73 100644
--- a/resources/qml/delegates/Pill.qml
+++ b/resources/qml/delegates/Pill.qml
@@ -11,8 +11,8 @@ Label {
     property bool isStateEvent
     color: Nheko.colors.text
     horizontalAlignment: Text.AlignHCenter
-    height: contentHeight * 1.2
-    width: contentWidth * 1.2
+    //height: contentHeight * 1.2
+    //width: contentWidth * 1.2
     font.pointSize: isStateEvent? 0.75*fontMetrics.font.pointSize : 1*fontMetrics.font.pointSize
 
     background: Rectangle {
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 805eafbf..9324cdaf 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -22,16 +22,11 @@ Item {
     required property string url
     required property string body
     required property string filesize
-    property int maxWidth
-    property double tempWidth: Math.min(maxWidth, originalWidth < 1 ? 400 : originalWidth)
-    property double tempHeight: tempWidth * proportionalHeight
     property double divisor: isReply ? 4 : 2
-    property bool tooHigh: tempHeight > timelineRoot.height / divisor
-
-    height: (type == MtxEvent.VideoMessage ? tooHigh ? timelineRoot.height / divisor : tempHeight : 80) + fileInfoLabel.height
-    width: type == MtxEvent.VideoMessage ? tooHigh ? (timelineRoot.height / divisor) / proportionalHeight : tempWidth : 250
+    implicitWidth: type == MtxEvent.VideoMessage ? Math.round(originalWidth*Math.min((timelineView.height/divisor)/(originalWidth*proportionalHeight), 1)) : 500
+    width: parent.width
+    height: (type == MtxEvent.VideoMessage ? width*proportionalHeight : 80) + fileInfoLabel.height
     implicitHeight: height
-    implicitWidth: width
 
     MxcMedia {
         id: mxcmedia
diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml
index b0c9abfe..cf723988 100644
--- a/resources/qml/delegates/Redacted.qml
+++ b/resources/qml/delegates/Redacted.qml
@@ -10,10 +10,10 @@ import im.nheko 1.0
 
 Rectangle{
 
-    required property real delegateWidth
+    //required property real delegateWidth
     height: redactedLayout.implicitHeight + Nheko.paddingSmall
-    width: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium
-    implicitWidth: width
+    implicitWidth: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium
+    //implicitWidth: width
     radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall
     color: Nheko.colors.alternateBase
 
@@ -33,8 +33,9 @@ Rectangle{
             id: redactedLabel
             Layout.margins: 0
             Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
-            Layout.fillWidth: true
-            Layout.maximumWidth: delegateWidth - 4 * Nheko.paddingSmall - trashImg.width - 2 * Nheko.paddingMedium
+            Layout.preferredWidth: implicitWidth
+            //Layout.fillWidth: true
+            //Layout.maximumWidth: delegateWidth - 4 * Nheko.paddingSmall - trashImg.width - 2 * Nheko.paddingMedium
             property var redactedPair: room.formatRedactedEvent(eventId)
             text: redactedPair["first"]
             wrapMode: Label.WordWrap
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 28f0dd21..c60867de 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -37,8 +37,8 @@ Item {
     property int relatedEventCacheBuster
     property int maxWidth
 
-    Layout.preferredHeight: replyContainer.height
     height: replyContainer.height
+    implicitHeight: replyContainer.height
     implicitWidth: visible? colorLine.width+replyContainer.implicitWidth : 0
 
     CursorShape {
@@ -99,6 +99,7 @@ Item {
 
         MessageDelegate {
             Layout.leftMargin: 4
+            Layout.preferredHeight: height
             id: reply
             blurhash: r.blurhash
             body: r.body
@@ -125,7 +126,6 @@ Item {
             enabled: false
             Layout.fillWidth: true
             isReply: true
-            maxWidth: r.maxWidth-4
         }
 
     }

From 894438c6cebcba7bc3519460bbfd2a7e2290b446 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Fri, 11 Feb 2022 23:12:04 +0100
Subject: [PATCH 12/18] place own bubbles on right and remove user info

---
 resources/qml/MessageView.qml         | 11 ++++++-----
 resources/qml/TimelineRow.qml         | 17 +++++++++--------
 resources/qml/delegates/Encrypted.qml |  2 +-
 3 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index a00ada3e..3fddc782 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -249,8 +249,8 @@ ScrollView {
             id: sectionHeader
 
             Column {
-                topPadding: 4
-                bottomPadding: 4
+                topPadding: 0
+                bottomPadding: 0
                 spacing: 8
                 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
                 width: parentWidth
@@ -278,13 +278,13 @@ ScrollView {
                 Row {
                     height: userName_.height
                     spacing: 8
-                    visible: !isStateEvent
+                    visible: !isStateEvent && (!isSender || !Settings.bubbles)
 
                     Avatar {
                         id: messageUserAvatar
 
-                        width: Nheko.avatarSize
-                        height: Nheko.avatarSize
+                        width: Nheko.avatarSize * (Settings.bubbles? 0.5 : 1)
+                        height: Nheko.avatarSize * (Settings.bubbles? 0.5 : 1)
                         url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
                         displayName: userName
                         userid: userId
@@ -460,6 +460,7 @@ ScrollView {
                 property string previousMessageDay: wrapper.previousMessageDay
                 property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
                 property bool isStateEvent: wrapper.isStateEvent
+                property bool isSender: wrapper.isSender
                 property string userName: wrapper.userName
                 property date timestamp: wrapper.timestamp
 
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index 85e6fa4b..f189f042 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -45,9 +45,8 @@ Item {
     required property int status
     required property int relatedEventCacheBuster
 
-    anchors.left: parent.left
-    anchors.right: parent.right
-    height: row.height+reactionRow.height
+    width: parent.width
+    height: childrenRect.height
 
     Rectangle {
         color: (Settings.messageHoverHighlight && hoverHandler.hovered) ? Nheko.colors.alternateBase : "transparent"
@@ -74,13 +73,14 @@ Item {
 
     Control {
         id: row
-
-        anchors.rightMargin: 1
-        anchors.leftMargin: Nheko.avatarSize + 12 // align bubble with section header
-        anchors.left: parent.left
+        property bool bubbleOnRight : isSender && Settings.bubbles
+        anchors.rightMargin: isSender || !Settings.bubbles? 0 : parent.width/8
+        anchors.leftMargin: (Settings.bubbles? 0 : Nheko.avatarSize) + (bubbleOnRight? parent.width/8 : 8) // align bubble with section header
+        anchors.left: bubbleOnRight? undefined : parent.left
+        anchors.right: bubbleOnRight? parent.right : undefined
         property int maxWidth: parent.width-anchors.leftMargin-anchors.rightMargin
         width: Settings.bubbles? Math.min(maxWidth,implicitWidth+metadata.width) : maxWidth
-        padding: isStateEvent? 0 : 3
+        padding: isStateEvent? 0 : 2
         background: Rectangle {
             property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
             property color bgColor: Nheko.colors.base
@@ -237,6 +237,7 @@ Item {
     Reactions {
         anchors {
             top: row.bottom
+            topMargin: -2
             left: parent.left
             leftMargin: Nheko.avatarSize + 16
         }
diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml
index d82f027b..ecc771f5 100644
--- a/resources/qml/delegates/Encrypted.qml
+++ b/resources/qml/delegates/Encrypted.qml
@@ -16,7 +16,7 @@ Rectangle {
     required property string eventId
 
     radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium
-    width: parent.width
+    width: parent.width? parent.width : 0
     implicitWidth: encryptedText.implicitWidth+24+Nheko.paddingMedium*3 // Column doesn't provide a useful implicitWidth, should be replaced by ColumnLayout
     height: contents.implicitHeight + Nheko.paddingMedium * 2
     color: Nheko.colors.alternateBase

From 987b9bed6bb2c1c75a81b099c007aeaedf00eb71 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Sun, 13 Feb 2022 13:12:51 +0100
Subject: [PATCH 13/18] add small avatars option and tweak colors and spacings

---
 resources/qml/MessageView.qml     |  8 ++++----
 resources/qml/TimelineRow.qml     | 32 ++++++++++++++++---------------
 resources/qml/delegates/Reply.qml |  2 +-
 src/UserSettingsPage.cpp          | 31 +++++++++++++++++++++++++++++-
 src/UserSettingsPage.h            |  7 ++++++-
 5 files changed, 58 insertions(+), 22 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 3fddc782..c465c72e 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -249,8 +249,8 @@ ScrollView {
             id: sectionHeader
 
             Column {
-                topPadding: 0
-                bottomPadding: 0
+                topPadding: userName_.visible? 4: 0
+                bottomPadding: Settings.bubbles? 0 : 3
                 spacing: 8
                 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
                 width: parentWidth
@@ -283,8 +283,8 @@ ScrollView {
                     Avatar {
                         id: messageUserAvatar
 
-                        width: Nheko.avatarSize * (Settings.bubbles? 0.5 : 1)
-                        height: Nheko.avatarSize * (Settings.bubbles? 0.5 : 1)
+                        width: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1)
+                        height: Nheko.avatarSize * (Settings.smallAvatars? 0.5 : 1)
                         url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/")
                         displayName: userName
                         userid: userId
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index f189f042..c7508a1c 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -74,28 +74,31 @@ Item {
     Control {
         id: row
         property bool bubbleOnRight : isSender && Settings.bubbles
-        anchors.rightMargin: isSender || !Settings.bubbles? 0 : parent.width/8
-        anchors.leftMargin: (Settings.bubbles? 0 : Nheko.avatarSize) + (bubbleOnRight? parent.width/8 : 8) // align bubble with section header
+        property int bubblePadding: (parent.width-(Settings.smallAvatars? 0 : Nheko.avatarSize+8))/10
+        anchors.rightMargin: isSender || !Settings.bubbles? 0 : bubblePadding
+        anchors.leftMargin: (Settings.smallAvatars? 0 : Nheko.avatarSize+8) + (bubbleOnRight? bubblePadding : 0) // align bubble with section header
         anchors.left: bubbleOnRight? undefined : parent.left
         anchors.right: bubbleOnRight? parent.right : undefined
         property int maxWidth: parent.width-anchors.leftMargin-anchors.rightMargin
         width: Settings.bubbles? Math.min(maxWidth,implicitWidth+metadata.width) : maxWidth
-        padding: isStateEvent? 0 : 2
+        leftPadding: 4
+        rightPadding: (Settings.bubbles && !isStateEvent)? 4: 2
+        topPadding: (Settings.bubbles && !isStateEvent)? 4: 2
+        bottomPadding: topPadding
         background: Rectangle {
             property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
             property color bgColor: Nheko.colors.base
-            color: Qt.tint(bgColor, Qt.rgba(userColor.r, userColor.g, userColor.b, 0.2))
-            radius: parent.padding*2
+            color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2))
+            radius: 4
             visible: Settings.bubbles && !isStateEvent
         }
 
         contentItem: GridLayout {
             id: msg
-            property bool narrowLayout: Settings.bubbles //&& (timelineView.width < 500) // timelineView causes fewew binding loops than r. But maybe it shouldn't depend on width anyway
             rowSpacing: 0
             columnSpacing: 2
-            columns: narrowLayout? 1 : 2
-            rows: narrowLayout? 3 : 2
+            columns: Settings.bubbles? 1 : 2
+            rows: Settings.bubbles? 3 : 2
 
             // fancy reply, if this is a reply
             Reply {
@@ -170,15 +173,15 @@ Item {
 
             RowLayout {
                 id: metadata
-                Layout.column: msg.narrowLayout? 0 : 1
-                Layout.row: msg.narrowLayout? 2 : 0
-                Layout.rowSpan: msg.narrowLayout? 1 : 2
-                Layout.bottomMargin: -4
+                Layout.column: Settings.bubbles? 0 : 1
+                Layout.row: Settings.bubbles? 2 : 0
+                Layout.rowSpan: Settings.bubbles? 1 : 2
+                Layout.bottomMargin: -2
                 Layout.alignment: Qt.AlignTop | Qt.AlignRight
                 Layout.preferredWidth: implicitWidth
                 visible: !isStateEvent
 
-                property double scaling: msg.narrowLayout? 0.75 : 1
+                property double scaling: Settings.bubbles? 0.75 : 1
 
                 StatusIndicator {
                     Layout.alignment: Qt.AlignRight | Qt.AlignTop
@@ -238,8 +241,7 @@ Item {
         anchors {
             top: row.bottom
             topMargin: -2
-            left: parent.left
-            leftMargin: Nheko.avatarSize + 16
+            left: row.left
         }
 
         id: reactionRow
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index c60867de..a439b2eb 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -137,7 +137,7 @@ Item {
         anchors.fill: replyContainer
         property color userColor: TimelineManager.userColor(userId, Nheko.colors.base)
         property color bgColor: Nheko.colors.base
-        color: Qt.tint(bgColor, Qt.rgba(userColor.r, userColor.g, userColor.b, 0.1))
+        color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1))
     }
 
 }
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 80947950..265e45a6 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -71,6 +71,7 @@ UserSettings::load(std::optional profile)
       settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
     markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
     bubbles_  = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
+    smallAvatars_  = settings.value(QStringLiteral("user/small_avatars_enabled"), true).toBool();
     animateImagesOnHover_ =
       settings.value(QStringLiteral("user/animate_images_on_hover"), false).toBool();
     typingNotifications_ =
@@ -253,6 +254,16 @@ UserSettings::setBubbles(bool state)
     save();
 }
 
+void
+UserSettings::setSmallAvatars(bool state)
+{
+    if (state == smallAvatars_)
+        return;
+    smallAvatars_ = state;
+    emit smallAvatarsChanged(state);
+    save();
+}
+
 void
 UserSettings::setAnimateImagesOnHover(bool state)
 {
@@ -708,6 +719,7 @@ UserSettings::save()
     settings.setValue(QStringLiteral("group_view"), groupView_);
     settings.setValue(QStringLiteral("markdown_enabled"), markdown_);
     settings.setValue(QStringLiteral("bubbles_enabled"), bubbles_);
+    settings.setValue(QStringLiteral("small_avatars_enabled"), smallAvatars_);
     settings.setValue(QStringLiteral("animate_images_on_hover"), animateImagesOnHover_);
     settings.setValue(QStringLiteral("desktop_notifications"), hasDesktopNotifications_);
     settings.setValue(QStringLiteral("alert_on_notification"), hasAlertOnNotification_);
@@ -810,6 +822,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr("Send messages as Markdown");
         case Bubbles:
             return tr("Enable message bubbles");
+        case SmallAvatars:
+            return tr("Enable small Avatars");
         case AnimateImagesOnHover:
             return tr("Play animated images only on hover");
         case TypingNotifications:
@@ -932,6 +946,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return i->markdown();
         case Bubbles:
             return i->bubbles();
+        case SmallAvatars:
+            return i->smallAvatars();
         case AnimateImagesOnHover:
             return i->animateImagesOnHover();
         case TypingNotifications:
@@ -1061,6 +1077,9 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case Bubbles:
             return tr(
               "Messages get a bubble background. This also triggers some layout changes (WIP).");
+        case SmallAvatars:
+            return tr(
+              "Avatars are resized to fit above the message.");
         case AnimateImagesOnHover:
             return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.");
         case TypingNotifications:
@@ -1178,6 +1197,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
         case GroupView:
         case Markdown:
         case Bubbles:
+        case SmallAvatars:
         case AnimateImagesOnHover:
         case TypingNotifications:
         case SortByImportance:
@@ -1402,6 +1422,13 @@ UserSettingsModel::setData(const QModelIndex &index, const QVariant &value, int
             } else
                 return false;
         }
+        case SmallAvatars: {
+            if (value.userType() == QMetaType::Bool) {
+                i->setSmallAvatars(value.toBool());
+                return true;
+            } else
+                return false;
+        }
         case AnimateImagesOnHover: {
             if (value.userType() == QMetaType::Bool) {
                 i->setAnimateImagesOnHover(value.toBool());
@@ -1767,7 +1794,9 @@ UserSettingsModel::UserSettingsModel(QObject *p)
     connect(s.get(), &UserSettings::bubblesChanged, this, [this]() {
         emit dataChanged(index(Bubbles), index(Bubbles), {Value});
     });
-
+    connect(s.get(), &UserSettings::smallAvatarsChanged, this, [this]() {
+        emit dataChanged(index(SmallAvatars), index(SmallAvatars), {Value});
+    });
     connect(s.get(), &UserSettings::groupViewStateChanged, this, [this]() {
         emit dataChanged(index(GroupView), index(GroupView), {Value});
     });
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index e5d22f6a..8f76c58f 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -41,6 +41,7 @@ class UserSettings : public QObject
     Q_PROPERTY(bool groupView READ groupView WRITE setGroupView NOTIFY groupViewStateChanged)
     Q_PROPERTY(bool markdown READ markdown WRITE setMarkdown NOTIFY markdownChanged)
     Q_PROPERTY(bool bubbles READ bubbles WRITE setBubbles NOTIFY bubblesChanged)
+    Q_PROPERTY(bool smallAvatars READ smallAvatars WRITE setSmallAvatars NOTIFY smallAvatarsChanged)
     Q_PROPERTY(bool animateImagesOnHover READ animateImagesOnHover WRITE setAnimateImagesOnHover
                  NOTIFY animateImagesOnHoverChanged)
     Q_PROPERTY(bool typingNotifications READ typingNotifications WRITE setTypingNotifications NOTIFY
@@ -141,6 +142,7 @@ public:
     void setGroupView(bool state);
     void setMarkdown(bool state);
     void setBubbles(bool state);
+    void setSmallAvatars(bool state);
     void setAnimateImagesOnHover(bool state);
     void setReadReceipts(bool state);
     void setTypingNotifications(bool state);
@@ -193,6 +195,7 @@ public:
     int privacyScreenTimeout() const { return privacyScreenTimeout_; }
     bool markdown() const { return markdown_; }
     bool bubbles() const { return bubbles_; }
+    bool smallAvatars() const { return smallAvatars_; }
     bool animateImagesOnHover() const { return animateImagesOnHover_; }
     bool typingNotifications() const { return typingNotifications_; }
     bool sortByImportance() const { return sortByImportance_; }
@@ -251,6 +254,7 @@ signals:
     void startInTrayChanged(bool state);
     void markdownChanged(bool state);
     void bubblesChanged(bool state);
+    void smallAvatarsChanged(bool state);
     void animateImagesOnHoverChanged(bool state);
     void typingNotificationsChanged(bool state);
     void buttonInTimelineChanged(bool state);
@@ -307,6 +311,7 @@ private:
     bool groupView_;
     bool markdown_;
     bool bubbles_;
+    bool smallAvatars_;
     bool animateImagesOnHover_;
     bool typingNotifications_;
     bool sortByImportance_;
@@ -386,7 +391,7 @@ class UserSettingsModel : public QAbstractListModel
         ButtonsInTimeline,
         Markdown,
         Bubbles,
-
+        SmallAvatars,
         SidebarSection,
         GroupView,
         SortByImportance,

From c70fe9c571e18c78f3a53488ffd49942e175582e Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Sun, 13 Feb 2022 13:22:29 +0100
Subject: [PATCH 14/18] let lint fix the code formatting

---
 src/UserSettingsPage.cpp | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 265e45a6..000e2976 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -69,9 +69,9 @@ UserSettings::load(std::optional profile)
       settings.value(QStringLiteral("user/timeline/message_hover_highlight"), false).toBool();
     enlargeEmojiOnlyMessages_ =
       settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
-    markdown_ = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
-    bubbles_  = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
-    smallAvatars_  = settings.value(QStringLiteral("user/small_avatars_enabled"), true).toBool();
+    markdown_     = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
+    bubbles_      = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
+    smallAvatars_ = settings.value(QStringLiteral("user/small_avatars_enabled"), true).toBool();
     animateImagesOnHover_ =
       settings.value(QStringLiteral("user/animate_images_on_hover"), false).toBool();
     typingNotifications_ =
@@ -1078,8 +1078,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
             return tr(
               "Messages get a bubble background. This also triggers some layout changes (WIP).");
         case SmallAvatars:
-            return tr(
-              "Avatars are resized to fit above the message.");
+            return tr("Avatars are resized to fit above the message.");
         case AnimateImagesOnHover:
             return tr("Plays media like GIFs or WEBPs only when explicitly hovering over them.");
         case TypingNotifications:

From 993b0ae7219024aa917ce43a5d9085b1dee6cc73 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Sun, 13 Feb 2022 13:27:31 +0100
Subject: [PATCH 15/18] fix vertical space for some state events

---
 resources/qml/delegates/TextMessage.qml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml
index eea8cd1e..ac681d40 100644
--- a/resources/qml/delegates/TextMessage.qml
+++ b/resources/qml/delegates/TextMessage.qml
@@ -34,7 +34,7 @@ MatrixText {
     
     " + formatted.replace("
", "
").replace("", "").replace("", "").replace("", "").replace("", "")
     width: parent.width
-    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : undefined
+    height: isReply ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight
     clip: isReply
     selectByMouse: !Settings.mobileMode && !isReply
     font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize

From 414257bf0eba7f8415644509ae617883ac266e40 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Mon, 14 Feb 2022 14:03:17 +0100
Subject: [PATCH 16/18] fix bugs found by Nico

---
 resources/qml/MessageView.qml                    | 2 +-
 resources/qml/delegates/FileMessage.qml          | 3 +--
 resources/qml/delegates/PlayableMediaMessage.qml | 3 ++-
 resources/qml/delegates/Redacted.qml             | 3 +--
 src/UserSettingsPage.cpp                         | 4 ++--
 5 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index c465c72e..f32d68a0 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -250,7 +250,7 @@ ScrollView {
 
             Column {
                 topPadding: userName_.visible? 4: 0
-                bottomPadding: Settings.bubbles? 0 : 3
+                bottomPadding: Settings.bubbles? 2 : 3
                 spacing: 8
                 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
                 width: parentWidth
diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml
index 1e50fe3a..fd81b176 100644
--- a/resources/qml/delegates/FileMessage.qml
+++ b/resources/qml/delegates/FileMessage.qml
@@ -87,8 +87,7 @@ Item {
         color: Nheko.colors.alternateBase
         z: -1
         radius: 10
-        height: row.height + 24
-        width: 44 + 24 + 24 + Math.max(Math.min(filesize_.width, filesize_.implicitWidth), Math.min(filename_.width, filename_.implicitWidth))
+        anchors.fill: parent
     }
 
 }
diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml
index 9324cdaf..54813d23 100644
--- a/resources/qml/delegates/PlayableMediaMessage.qml
+++ b/resources/qml/delegates/PlayableMediaMessage.qml
@@ -23,7 +23,8 @@ Item {
     required property string body
     required property string filesize
     property double divisor: isReply ? 4 : 2
-    implicitWidth: type == MtxEvent.VideoMessage ? Math.round(originalWidth*Math.min((timelineView.height/divisor)/(originalWidth*proportionalHeight), 1)) : 500
+    property int tempWidth: originalWidth < 1? 400: originalWidth
+    implicitWidth: type == MtxEvent.VideoMessage ? Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) : 500
     width: parent.width
     height: (type == MtxEvent.VideoMessage ? width*proportionalHeight : 80) + fileInfoLabel.height
     implicitHeight: height
diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml
index cf723988..7e5a10f9 100644
--- a/resources/qml/delegates/Redacted.qml
+++ b/resources/qml/delegates/Redacted.qml
@@ -10,10 +10,9 @@ import im.nheko 1.0
 
 Rectangle{
 
-    //required property real delegateWidth
     height: redactedLayout.implicitHeight + Nheko.paddingSmall
     implicitWidth: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium
-    //implicitWidth: width
+    width: parent.width
     radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall
     color: Nheko.colors.alternateBase
 
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 000e2976..e3f80699 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -70,8 +70,8 @@ UserSettings::load(std::optional profile)
     enlargeEmojiOnlyMessages_ =
       settings.value(QStringLiteral("user/timeline/enlarge_emoji_only_msg"), false).toBool();
     markdown_     = settings.value(QStringLiteral("user/markdown_enabled"), true).toBool();
-    bubbles_      = settings.value(QStringLiteral("user/bubbles_enabled"), true).toBool();
-    smallAvatars_ = settings.value(QStringLiteral("user/small_avatars_enabled"), true).toBool();
+    bubbles_      = settings.value(QStringLiteral("user/bubbles_enabled"), false).toBool();
+    smallAvatars_ = settings.value(QStringLiteral("user/small_avatars_enabled"), false).toBool();
     animateImagesOnHover_ =
       settings.value(QStringLiteral("user/animate_images_on_hover"), false).toBool();
     typingNotifications_ =

From ce13ccd677ec780185d7fb24f5ed6fcaf50e96bc Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Mon, 14 Feb 2022 14:16:11 +0100
Subject: [PATCH 17/18] fix redactions line-wrapping

---
 resources/qml/delegates/Redacted.qml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml
index 7e5a10f9..b3511cfc 100644
--- a/resources/qml/delegates/Redacted.qml
+++ b/resources/qml/delegates/Redacted.qml
@@ -19,6 +19,7 @@ Rectangle{
     RowLayout {
         id: redactedLayout
         anchors.centerIn: parent
+        width: parent.width
         spacing: Nheko.paddingSmall
 
         Image {
@@ -33,8 +34,7 @@ Rectangle{
             Layout.margins: 0
             Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
             Layout.preferredWidth: implicitWidth
-            //Layout.fillWidth: true
-            //Layout.maximumWidth: delegateWidth - 4 * Nheko.paddingSmall - trashImg.width - 2 * Nheko.paddingMedium
+            Layout.fillWidth: true
             property var redactedPair: room.formatRedactedEvent(eventId)
             text: redactedPair["first"]
             wrapMode: Label.WordWrap

From ddf11d9a8cdd52f5daf85e8e753116d50dd11f48 Mon Sep 17 00:00:00 2001
From: Malte E 
Date: Mon, 14 Feb 2022 14:50:37 +0100
Subject: [PATCH 18/18] improve spacings

---
 resources/qml/MessageView.qml | 2 +-
 resources/qml/TimelineRow.qml | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index f32d68a0..b65b9692 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -250,7 +250,7 @@ ScrollView {
 
             Column {
                 topPadding: userName_.visible? 4: 0
-                bottomPadding: Settings.bubbles? 2 : 3
+                bottomPadding: Settings.bubbles? (isSender? 0 : 2) : 3
                 spacing: 8
                 visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent)
                 width: parentWidth
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index c7508a1c..b74fb5c1 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -80,7 +80,7 @@ Item {
         anchors.left: bubbleOnRight? undefined : parent.left
         anchors.right: bubbleOnRight? parent.right : undefined
         property int maxWidth: parent.width-anchors.leftMargin-anchors.rightMargin
-        width: Settings.bubbles? Math.min(maxWidth,implicitWidth+metadata.width) : maxWidth
+        width: Settings.bubbles? Math.min(maxWidth,implicitWidth+4) : maxWidth
         leftPadding: 4
         rightPadding: (Settings.bubbles && !isStateEvent)? 4: 2
         topPadding: (Settings.bubbles && !isStateEvent)? 4: 2