summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--resources/icons/ui/pin-off.svg1
-rw-r--r--resources/icons/ui/pin.svg1
-rw-r--r--resources/qml/MessageView.qml7
-rw-r--r--resources/qml/TopBar.qml137
-rw-r--r--resources/qml/delegates/MessageDelegate.qml48
-rw-r--r--resources/qml/delegates/Reply.qml4
-rw-r--r--resources/res.qrc2
-rw-r--r--src/UserSettingsPage.cpp10
-rw-r--r--src/UserSettingsPage.h6
-rw-r--r--src/timeline/TimelineModel.cpp87
-rw-r--r--src/timeline/TimelineModel.h9
11 files changed, 298 insertions, 14 deletions
diff --git a/resources/icons/ui/pin-off.svg b/resources/icons/ui/pin-off.svg
new file mode 100644
index 00000000..598610ad
--- /dev/null
+++ b/resources/icons/ui/pin-off.svg
@@ -0,0 +1 @@
+<svg width="32" height="32" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l5.905 5.905L4.81 10.33a1.25 1.25 0 0 0-.476 2.065L7.439 15.5 3 19.94V21h1.06l4.44-4.44 3.105 3.105a1.25 1.25 0 0 0 2.065-.476l1.145-3.313 5.905 5.904a.75.75 0 0 0 1.06-1.06L3.28 2.22Zm10.355 12.476-1.252 3.626-6.705-6.705 3.626-1.252 4.331 4.331Zm6.048-3.876-3.787 1.894 1.118 1.118 3.34-1.67a2.75 2.75 0 0 0 .714-4.404l-4.825-4.826a2.75 2.75 0 0 0-4.405.715l-1.67 3.34 1.118 1.117 1.894-3.787a1.25 1.25 0 0 1 2.002-.325l4.826 4.826a1.25 1.25 0 0 1-.325 2.002Z" fill="#212121"/></svg>
diff --git a/resources/icons/ui/pin.svg b/resources/icons/ui/pin.svg
new file mode 100644
index 00000000..76d1124d
--- /dev/null
+++ b/resources/icons/ui/pin.svg
@@ -0,0 +1 @@
+<svg width="32" height="32" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m16.242 2.932 4.826 4.826a2.75 2.75 0 0 1-.715 4.404l-4.87 2.435a.75.75 0 0 0-.374.426l-1.44 4.166a1.25 1.25 0 0 1-2.065.476L8.5 16.561 4.06 21H3v-1.06l4.44-4.44-3.105-3.104a1.25 1.25 0 0 1 .476-2.066l4.166-1.44a.75.75 0 0 0 .426-.373l2.435-4.87a2.75 2.75 0 0 1 4.405-.715Zm3.766 5.886-4.826-4.826a1.25 1.25 0 0 0-2.002.325l-2.435 4.871a2.25 2.25 0 0 1-1.278 1.12l-3.789 1.31 6.705 6.704 1.308-3.789a2.25 2.25 0 0 1 1.12-1.277l4.872-2.436a1.25 1.25 0 0 0 .325-2.002Z" fill="#212121"/></svg>
diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml
index 2acdf839..375b4017 100644
--- a/resources/qml/MessageView.qml
+++ b/resources/qml/MessageView.qml
@@ -583,6 +583,13 @@ ScrollView {
         }
 
         Platform.MenuItem {
+            visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
+            enabled: visible
+            text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
+            onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId)
+        }
+
+        Platform.MenuItem {
             text: qsTr("Read receip&ts")
             onTriggered: room.showReadReceipts(messageContextMenu.eventId)
         }
diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml
index 53acdc39..ef12eaf7 100644
--- a/resources/qml/TopBar.qml
+++ b/resources/qml/TopBar.qml
@@ -8,6 +8,8 @@ import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.2
 import im.nheko 1.0
 
+import "./delegates"
+
 Rectangle {
     id: topBar
 
@@ -28,6 +30,19 @@ Rectangle {
 
     TapHandler {
         onSingleTapped: {
+            if (eventPoint.position.y > topBar.height - pinnedMessages.height) {
+                eventPoint.accepted = true
+                return;
+            }
+            if (showBackButton && eventPoint.position.x < Nheko.paddingMedium + backToRoomsButton.width) {
+                eventPoint.accepted = true
+                return;
+            }
+            if (eventPoint.position.x > topBar.width - Nheko.paddingMedium - roomOptionsButton.width) {
+                eventPoint.accepted = true
+                return;
+            }
+
             if (room) {
                 let p = topBar.mapToItem(roomTopicC, eventPoint.position.x, eventPoint.position.y);
                 let link = roomTopicC.linkAt(p.x, p.y);
@@ -46,11 +61,11 @@ Rectangle {
 
     HoverHandler {
         grabPermissions: PointerHandler.TakeOverForbidden | PointerHandler.CanTakeOverFromAnything
-        //cursorShape: Qt.PointingHandCursor
     }
 
     CursorShape {
         anchors.fill: parent
+        anchors.bottomMargin: pinnedMessages.height
         cursorShape: Qt.PointingHandCursor
     }
 
@@ -61,6 +76,8 @@ Rectangle {
         anchors.right: parent.right
         anchors.margins: Nheko.paddingMedium
         anchors.verticalCenter: parent.verticalCenter
+        columnSpacing: Nheko.paddingSmall
+        rowSpacing: Nheko.paddingSmall
 
         ImageButton {
             id: backToRoomsButton
@@ -129,24 +146,54 @@ Rectangle {
             trust: trustlevel
             ToolTip.text: {
                 if (!encrypted)
-                    return qsTr("This room is not encrypted!");
+                return qsTr("This room is not encrypted!");
 
                 switch (trust) {
-                case Crypto.Verified:
+                    case Crypto.Verified:
                     return qsTr("This room contains only verified devices.");
-                case Crypto.TOFU:
+                    case Crypto.TOFU:
                     return qsTr("This room contains verified devices and devices which have never changed their master key.");
-                default:
+                    default:
                     return qsTr("This room contains unverified devices!");
                 }
             }
         }
 
         ImageButton {
+            id: pinButton
+
+            property bool pinsShown: !Settings.hiddenPins.includes(roomId)
+
+            visible: !!room && room.pinnedMessages.length > 0
+            Layout.column: 4
+            Layout.row: 0
+            Layout.rowSpan: 2
+            Layout.alignment: Qt.AlignVCenter
+            Layout.preferredHeight: Nheko.avatarSize - Nheko.paddingMedium
+            Layout.preferredWidth: Nheko.avatarSize - Nheko.paddingMedium
+            image: pinsShown ? ":/icons/icons/ui/pin.svg" : ":/icons/icons/ui/pin-off.svg"
+            ToolTip.visible: hovered
+            ToolTip.text: qsTr("Show or hide pinned messages")
+            onClicked: {
+                var ps = Settings.hiddenPins;
+                if (pinsShown) {
+                    ps.push(roomId);
+                } else {
+                    const index = ps.indexOf(roomId);
+                    if (index > -1) {
+                        ps.splice(index, 1);
+                    }
+                }
+                Settings.hiddenPins = ps;
+            }
+
+        }
+
+        ImageButton {
             id: roomOptionsButton
 
             visible: !!room
-            Layout.column: 4
+            Layout.column: 5
             Layout.row: 0
             Layout.rowSpan: 2
             Layout.alignment: Qt.AlignVCenter
@@ -185,11 +232,79 @@ Rectangle {
 
         }
 
-    }
+        ScrollView {
+            id: pinnedMessages
 
-    CursorShape {
-        anchors.fill: parent
-        cursorShape: Qt.PointingHandCursor
-    }
+            Layout.row: 2
+            Layout.column: 2
+            Layout.columnSpan: 1
+
+            Layout.fillWidth: true
+            Layout.preferredHeight: Math.min(contentHeight, Nheko.avatarSize * 4)
+
+            visible: !!room && room.pinnedMessages.length > 0 && !Settings.hiddenPins.includes(roomId)
+            clip: true
+
+            palette: Nheko.colors
+            ScrollBar.horizontal.visible: false
+
+            ListView {
+
+                spacing: Nheko.paddingSmall
+                model: room ? room.pinnedMessages : undefined
+                delegate: RowLayout {
+                    required property string modelData
+
+                    width: ListView.view.width
+                    height: implicitHeight
 
+                    Reply {
+                        property var e: room ? room.getDump(modelData, "") : {}
+                        Layout.fillWidth: true
+                        Layout.preferredHeight: height
+
+                        userColor: TimelineManager.userColor(e.userId, Nheko.colors.window)
+                        blurhash: e.blurhash ?? ""
+                        body: e.body ?? ""
+                        formattedBody: e.formattedBody ?? ""
+                        eventId: e.eventId ?? ""
+                        filename: e.filename ?? ""
+                        filesize: e.filesize ?? ""
+                        proportionalHeight: e.proportionalHeight ?? 1
+                        type: e.type ?? MtxEvent.UnknownMessage
+                        typeString: e.typeString ?? ""
+                        url: e.url ?? ""
+                        originalWidth: e.originalWidth ?? 0
+                        isOnlyEmoji: e.isOnlyEmoji ?? false
+                        userId: e.userId ?? ""
+                        userName: e.userName ?? ""
+                        encryptionError: e.encryptionError ?? ""
+                    }
+
+                    ImageButton {
+                        id: deletePinButton
+
+                        Layout.preferredHeight: 16
+                        Layout.preferredWidth: 16
+                        Layout.alignment: Qt.AlignTop | Qt.AlignLeft
+                        visible: room.permissions.canChange(MtxEvent.PinnedEvents)
+
+                        hoverEnabled: true
+                        image: ":/icons/icons/ui/dismiss.svg"
+                        ToolTip.visible: hovered
+                        ToolTip.text: qsTr("Unpin")
+
+                        onClicked: room.unpin(modelData)
+                    }
+                }
+
+
+                ScrollHelper {
+                    flickable: parent
+                    anchors.fill: parent
+                    enabled: !Settings.mobileMode
+                }
+            }
+        }
+    }
 }
diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml
index dc88cf24..74f7d011 100644
--- a/resources/qml/delegates/MessageDelegate.qml
+++ b/resources/qml/delegates/MessageDelegate.qml
@@ -240,6 +240,54 @@ Item {
         }
 
         DelegateChoice {
+            roleValue: MtxEvent.PinnedEvents
+
+            NoticeMessage {
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 changed the pinned messages.").arg(d.userName)
+            }
+
+        }
+
+        DelegateChoice {
+            roleValue: MtxEvent.ImagePackInRoom
+
+            NoticeMessage {
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 changed the stickers and emotes in this room.").arg(d.userName)
+            }
+
+        }
+
+        DelegateChoice {
+            roleValue: MtxEvent.CanonicalAlias
+
+            NoticeMessage {
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 changed the addresses for this room.").arg(d.userName)
+            }
+
+        }
+
+        DelegateChoice {
+            roleValue: MtxEvent.SpaceParent
+
+            NoticeMessage {
+                body: formatted
+                isOnlyEmoji: false
+                isReply: d.isReply
+                formatted: qsTr("%1 changed the parent spaces for this room.").arg(d.userName)
+            }
+
+        }
+
+        DelegateChoice {
             roleValue: MtxEvent.RoomCreate
 
             NoticeMessage {
diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml
index 4e973c3d..547044f3 100644
--- a/resources/qml/delegates/Reply.qml
+++ b/resources/qml/delegates/Reply.qml
@@ -60,7 +60,7 @@ Item {
 
         TapHandler {
             acceptedButtons: Qt.LeftButton
-            onSingleTapped: chat.model.showEvent(r.eventId)
+            onSingleTapped: room.showEvent(r.eventId)
             gesturePolicy: TapHandler.ReleaseWithinBounds
         }
 
@@ -79,7 +79,7 @@ Item {
             textFormat: Text.RichText
 
             TapHandler {
-                onSingleTapped: chat.model.openUserProfile(userId)
+                onSingleTapped: room.openUserProfile(userId)
                 gesturePolicy: TapHandler.ReleaseWithinBounds
             }
 
diff --git a/resources/res.qrc b/resources/res.qrc
index 2ab60e3a..67c35351 100644
--- a/resources/res.qrc
+++ b/resources/res.qrc
@@ -23,6 +23,8 @@
         <file>icons/ui/pause-symbol.svg</file>
         <file>icons/ui/people.svg</file>
         <file>icons/ui/picture-in-picture.svg</file>
+        <file>icons/ui/pin-off.svg</file>
+        <file>icons/ui/pin.svg</file>
         <file>icons/ui/place-call.svg</file>
         <file>icons/ui/play-sign.svg</file>
         <file>icons/ui/power-off.svg</file>
diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp
index 5ba1dcdc..eae31b71 100644
--- a/src/UserSettingsPage.cpp
+++ b/src/UserSettingsPage.cpp
@@ -117,6 +117,7 @@ UserSettings::load(std::optional<QString> profile)
     userId_        = settings.value(prefix + "auth/user_id", "").toString();
     deviceId_      = settings.value(prefix + "auth/device_id", "").toString();
     hiddenTags_    = settings.value(prefix + "user/hidden_tags", QStringList{}).toStringList();
+    hiddenPins_    = settings.value(prefix + "user/hidden_pins", QStringList{}).toStringList();
 
     collapsedSpaces_.clear();
     for (const auto &e :
@@ -201,6 +202,14 @@ UserSettings::setHiddenTags(QStringList hiddenTags)
 }
 
 void
+UserSettings::setHiddenPins(QStringList hiddenTags)
+{
+    hiddenPins_ = hiddenTags;
+    save();
+    emit hiddenPinsChanged();
+}
+
+void
 UserSettings::setCollapsedSpaces(QList<QStringList> spaces)
 {
     collapsedSpaces_ = spaces;
@@ -707,6 +716,7 @@ UserSettings::save()
                       onlyShareKeysWithVerifiedUsers_);
     settings.setValue(prefix + "user/online_key_backup", useOnlineKeyBackup_);
     settings.setValue(prefix + "user/hidden_tags", hiddenTags_);
+    settings.setValue(prefix + "user/hidden_pins", hiddenPins_);
 
     QVariantList v;
     for (const auto &e : collapsedSpaces_)
diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h
index c47844cb..ab73414e 100644
--- a/src/UserSettingsPage.h
+++ b/src/UserSettingsPage.h
@@ -105,6 +105,8 @@ class UserSettings : public QObject
                  setDisableCertificateValidation NOTIFY disableCertificateValidationChanged)
     Q_PROPERTY(bool useIdenticon READ useIdenticon WRITE setUseIdenticon NOTIFY useIdenticonChanged)
 
+    Q_PROPERTY(QStringList hiddenPins READ hiddenPins WRITE setHiddenPins NOTIFY hiddenPinsChanged)
+
     UserSettings();
 
 public:
@@ -171,6 +173,7 @@ public:
     void setHomeserver(QString homeserver);
     void setDisableCertificateValidation(bool disabled);
     void setHiddenTags(QStringList hiddenTags);
+    void setHiddenPins(QStringList hiddenTags);
     void setUseIdenticon(bool state);
     void setCollapsedSpaces(QList<QStringList> spaces);
 
@@ -228,6 +231,7 @@ public:
     QString homeserver() const { return homeserver_; }
     bool disableCertificateValidation() const { return disableCertificateValidation_; }
     QStringList hiddenTags() const { return hiddenTags_; }
+    QStringList hiddenPins() const { return hiddenPins_; }
     bool useIdenticon() const { return useIdenticon_ && JdenticonProvider::isAvailable(); }
     QList<QStringList> collapsedSpaces() const { return collapsedSpaces_; }
 
@@ -278,6 +282,7 @@ signals:
     void homeserverChanged(QString homeserver);
     void disableCertificateValidationChanged(bool disabled);
     void useIdenticonChanged(bool state);
+    void hiddenPinsChanged();
 
 private:
     // Default to system theme if QT_QPA_PLATFORMTHEME var is set.
@@ -331,6 +336,7 @@ private:
     QString deviceId_;
     QString homeserver_;
     QStringList hiddenTags_;
+    QStringList hiddenPins_;
     QList<QStringList> collapsedSpaces_;
     bool useIdenticon_;
 
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 5a5f4850..b9941dfa 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -93,6 +93,10 @@ struct RoomEventType
             return qml_mtx_events::EventType::Sticker;
         case EventType::Tag:
             return qml_mtx_events::EventType::Tag;
+        case EventType::SpaceParent:
+            return qml_mtx_events::EventType::SpaceParent;
+        case EventType::SpaceChild:
+            return qml_mtx_events::EventType::SpaceChild;
         case EventType::Unsupported:
             return qml_mtx_events::EventType::Unsupported;
         default:
@@ -286,6 +290,12 @@ qml_mtx_events::fromRoomEventType(qml_mtx_events::EventType t)
     // m.tag
     case qml_mtx_events::Tag:
         return mtx::events::EventType::Tag;
+    // m.space.parent
+    case qml_mtx_events::SpaceParent:
+        return mtx::events::EventType::SpaceParent;
+    // m.space.child
+    case qml_mtx_events::SpaceChild:
+        return mtx::events::EventType::SpaceChild;
     /// m.room.message
     case qml_mtx_events::AudioMessage:
     case qml_mtx_events::EmoteMessage:
@@ -808,7 +818,9 @@ TimelineModel::syncState(const mtx::responses::State &s)
             emit roomNameChanged();
         else if (std::holds_alternative<StateEvent<state::Topic>>(e))
             emit roomTopicChanged();
-        else if (std::holds_alternative<StateEvent<state::Topic>>(e)) {
+        else if (std::holds_alternative<StateEvent<state::PinnedEvents>>(e))
+            emit pinnedMessagesChanged();
+        else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
             permissions_.invalidate();
             emit permissionsChanged();
         } else if (std::holds_alternative<StateEvent<state::Member>>(e)) {
@@ -870,6 +882,8 @@ TimelineModel::addEvents(const mtx::responses::Timeline &timeline)
             emit roomNameChanged();
         else if (std::holds_alternative<StateEvent<state::Topic>>(e))
             emit roomTopicChanged();
+        else if (std::holds_alternative<StateEvent<state::PinnedEvents>>(e))
+            emit pinnedMessagesChanged();
         else if (std::holds_alternative<StateEvent<state::PowerLevels>>(e)) {
             permissions_.invalidate();
             emit permissionsChanged();
@@ -1085,6 +1099,60 @@ TimelineModel::replyAction(QString id)
 }
 
 void
+TimelineModel::unpin(QString id)
+{
+    auto pinned =
+      cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());
+
+    mtx::events::state::PinnedEvents content{};
+    if (pinned)
+        content = pinned->content;
+
+    auto idStr = id.toStdString();
+
+    for (auto it = content.pinned.begin(); it != content.pinned.end(); ++it) {
+        if (*it == idStr) {
+            content.pinned.erase(it);
+            break;
+        }
+    }
+
+    http::client()->send_state_event(
+      room_id_.toStdString(),
+      content,
+      [idStr](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+          if (err)
+              nhlog::net()->error("Failed to unpin {}: {}", idStr, *err);
+          else
+              nhlog::net()->debug("Unpinned {}", idStr);
+      });
+}
+
+void
+TimelineModel::pin(QString id)
+{
+    auto pinned =
+      cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());
+
+    mtx::events::state::PinnedEvents content{};
+    if (pinned)
+        content = pinned->content;
+
+    auto idStr = id.toStdString();
+    content.pinned.push_back(idStr);
+
+    http::client()->send_state_event(
+      room_id_.toStdString(),
+      content,
+      [idStr](const mtx::responses::EventId &, mtx::http::RequestErr err) {
+          if (err)
+              nhlog::net()->error("Failed to pin {}: {}", idStr, *err);
+          else
+              nhlog::net()->debug("Pinned {}", idStr);
+      });
+}
+
+void
 TimelineModel::editAction(QString id)
 {
     setEdit(id);
@@ -2108,6 +2176,23 @@ TimelineModel::roomTopic() const
           utils::linkifyMessage(QString::fromStdString(info[room_id_].topic).toHtmlEscaped()));
 }
 
+QStringList
+TimelineModel::pinnedMessages() const
+{
+    auto pinned =
+      cache::client()->getStateEvent<mtx::events::state::PinnedEvents>(room_id_.toStdString());
+
+    if (!pinned || pinned->content.pinned.empty())
+        return {};
+
+    QStringList list;
+    list.reserve(pinned->content.pinned.size());
+    for (const auto &p : pinned->content.pinned)
+        list.push_back(QString::fromStdString(p));
+
+    return list;
+}
+
 crypto::Trust
 TimelineModel::trustlevel() const
 {
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index fe09af75..a06d4063 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -115,6 +115,10 @@ enum EventType
     ImagePackInAccountData,
     //! m.image_pack.rooms, currently im.ponies.emote_rooms
     ImagePackRooms,
+    // m.space.parent
+    SpaceParent,
+    // m.space.child
+    SpaceChild,
 };
 Q_ENUM_NS(EventType)
 mtx::events::EventType fromRoomEventType(qml_mtx_events::EventType);
@@ -172,6 +176,7 @@ class TimelineModel : public QAbstractListModel
     Q_PROPERTY(QString plainRoomName READ plainRoomName NOTIFY plainRoomNameChanged)
     Q_PROPERTY(QString roomAvatarUrl READ roomAvatarUrl NOTIFY roomAvatarUrlChanged)
     Q_PROPERTY(QString roomTopic READ roomTopic NOTIFY roomTopicChanged)
+    Q_PROPERTY(QStringList pinnedMessages READ pinnedMessages NOTIFY pinnedMessagesChanged)
     Q_PROPERTY(int roomMemberCount READ roomMemberCount NOTIFY roomMemberCountChanged)
     Q_PROPERTY(bool isEncrypted READ isEncrypted NOTIFY encryptionChanged)
     Q_PROPERTY(bool isSpace READ isSpace CONSTANT)
@@ -256,6 +261,8 @@ public:
     Q_INVOKABLE void openUserProfile(QString userid);
     Q_INVOKABLE void editAction(QString id);
     Q_INVOKABLE void replyAction(QString id);
+    Q_INVOKABLE void unpin(QString id);
+    Q_INVOKABLE void pin(QString id);
     Q_INVOKABLE void showReadReceipts(QString id);
     Q_INVOKABLE void redactEvent(QString id);
     Q_INVOKABLE int idToIndex(QString id) const;
@@ -354,6 +361,7 @@ public slots:
     QString roomName() const;
     QString plainRoomName() const;
     QString roomTopic() const;
+    QStringList pinnedMessages() const;
     InputBar *input() { return &input_; }
     Permissions *permissions() { return &permissions_; }
     QString roomAvatarUrl() const;
@@ -395,6 +403,7 @@ signals:
     void roomNameChanged();
     void plainRoomNameChanged();
     void roomTopicChanged();
+    void pinnedMessagesChanged();
     void roomAvatarUrlChanged();
     void roomMemberCountChanged();
     void isDirectChanged();