From 4d8b8c3b816528ece6274bac97d30905f77aabfb Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Thu, 22 Jun 2023 19:54:17 +0200 Subject: [PATCH 01/32] Create an EventDelegateChooser --- CMakeLists.txt | 3 + resources/qml/MessageView.qml | 179 +++---------------- resources/qml/TimelineRow.qml | 3 +- resources/qml/delegates/Reply.qml | 32 +--- src/timeline/EventDelegateChooser.cpp | 244 ++++++++++++++++++++++++++ src/timeline/EventDelegateChooser.h | 136 ++++++++++++++ src/timeline/TimelineModel.cpp | 20 +++ src/timeline/TimelineModel.h | 2 + 8 files changed, 430 insertions(+), 189 deletions(-) create mode 100644 src/timeline/EventDelegateChooser.cpp create mode 100644 src/timeline/EventDelegateChooser.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6334565d..01070a82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -359,6 +359,8 @@ set(SRC_FILES src/timeline/CommunitiesModel.h src/timeline/DelegateChooser.cpp src/timeline/DelegateChooser.h + src/timeline/EventDelegateChooser.cpp + src/timeline/EventDelegateChooser.h src/timeline/EventStore.cpp src/timeline/EventStore.h src/timeline/InputBar.cpp @@ -882,6 +884,7 @@ target_link_libraries(nheko PRIVATE Qt::Gui Qt::Multimedia Qt::Qml + Qt::QmlPrivate Qt::QuickControls2 qt6keychain nlohmann_json::nlohmann_json diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index a0ff0ff1..fde7ee57 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -20,6 +20,7 @@ Item { property int availableWidth: width property int padding: Nheko.paddingMedium property string searchString: "" + property Room roommodel: room // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu Connections { @@ -58,173 +59,33 @@ Item { spacing: 2 verticalLayoutDirection: ListView.BottomToTop - delegate: Item { + delegate: EventDelegateChooser { id: wrapper - - required property string blurhash - required property string body - required property string callType - required property var day - required property string duration - required property int encryptionError - required property string eventId - required property string filename - required property string filesize - required property string formattedBody - required property int index - required property bool isEditable - required property bool isEdited - required property bool isEncrypted - required property bool isOnlyEmoji - required property bool isSender - required property bool isStateEvent - required property int notificationlevel - required property int originalWidth - property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) - property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) - property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) - required property double proportionalHeight - required property var reactions - required property int relatedEventCacheBuster - required property string replyTo - required property string roomName - required property string roomTopic - property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) - required property int status - required property string threadId - required property string thumbnailUrl - required property var timestamp - required property int trustlevel - required property int type - required property string typeString - required property string url - required property string userId - required property string userName - required property int userPowerlevel - ListView.delayRemove: true - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - height: (section.item?.height ?? 0) + timelinerow.height width: chat.delegateMaxWidth + height: main?.height ?? 10 + room: chatRoot.roommodel - Loader { - id: section + EventDelegateChoice { + roleValues: [ + MtxEvent.TextMessage, + MtxEvent.NoticeMessage, + ] + TextArea { + required property string body - property var day: wrapper.day - property bool isSender: wrapper.isSender - property bool isStateEvent: wrapper.isStateEvent - property int parentWidth: parent.width - property var previousMessageDay: wrapper.previousMessageDay - property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent - property string previousMessageUserId: wrapper.previousMessageUserId - property date timestamp: wrapper.timestamp - property string userId: wrapper.userId - property string userName: wrapper.userName - property int userPowerlevel: wrapper.userPowerlevel - - active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent - //asynchronous: true - sourceComponent: sectionHeader - visible: status == Loader.Ready - z: 4 - } - TimelineRow { - id: timelinerow - - blurhash: wrapper.blurhash - body: wrapper.body - callType: wrapper.callType - duration: wrapper.duration - encryptionError: wrapper.encryptionError - eventId: chat.model, wrapper.eventId - filename: wrapper.filename - filesize: wrapper.filesize - formattedBody: wrapper.formattedBody - index: wrapper.index - isEditable: wrapper.isEditable - isEdited: wrapper.isEdited - isEncrypted: wrapper.isEncrypted - isOnlyEmoji: wrapper.isOnlyEmoji - isSender: wrapper.isSender - isStateEvent: wrapper.isStateEvent - notificationlevel: wrapper.notificationlevel - originalWidth: wrapper.originalWidth - proportionalHeight: wrapper.proportionalHeight - reactions: wrapper.reactions - relatedEventCacheBuster: wrapper.relatedEventCacheBuster - replyTo: wrapper.replyTo - roomName: wrapper.roomName - roomTopic: wrapper.roomTopic - status: wrapper.status - threadId: wrapper.threadId - thumbnailUrl: wrapper.thumbnailUrl - timestamp: wrapper.timestamp - trustlevel: wrapper.trustlevel - type: chat.model, wrapper.type - typeString: wrapper.typeString - url: wrapper.url - userId: wrapper.userId - userName: wrapper.userName - width: wrapper.width - y: section.visible && section.active ? section.y + section.height : 0 - - background: Rectangle { - id: scrollHighlight - - color: palette.highlight - enabled: false - opacity: 0 - visible: true - z: 1 - - states: State { - name: "revealed" - when: wrapper.scrolledToThis - } - transitions: Transition { - from: "" - to: "revealed" - - SequentialAnimation { - PropertyAnimation { - duration: 500 - easing.type: Easing.InOutQuad - from: 0 - properties: "opacity" - target: scrollHighlight - to: 1 - } - PropertyAnimation { - duration: 500 - easing.type: Easing.InOutQuad - from: 1 - properties: "opacity" - target: scrollHighlight - to: 0 - } - ScriptAction { - script: room.eventShown() - } - } - } - } - - onHoveredChanged: { - if (!Settings.mobileMode && hovered) { - if (!messageActions.hovered) { - messageActions.attached = timelinerow; - messageActions.model = timelinerow; - } - } + width: parent.width + text: body } } - Connections { - function onMovementEnded() { - if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height) - chat.model.currentIndex = index; - } - target: chat + EventDelegateChoice { + roleValues: [ + ] + TextArea { + width: parent.width + text: "Unsupported" + } } } footer: Item { diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 16a31a3c..64fa80b1 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -147,7 +147,7 @@ AbstractButton { columns: Settings.bubbles ? 1 : 2 rowSpacing: 0 rows: Settings.bubbles ? 3 : 2 - +/* anchors { left: parent.left leftMargin: 4 @@ -230,6 +230,7 @@ AbstractButton { userId: r.userId userName: r.userName } + */ Row { id: metadata diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 4d4983ac..64eb65a3 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -95,37 +95,11 @@ AbstractButton { onClicked: room.openUserProfile(userId) } - MessageDelegate { + Rectangle { Layout.leftMargin: 4 - Layout.preferredHeight: height - id: reply - blurhash: r.blurhash - body: r.body - formattedBody: r.formattedBody - eventId: r.eventId - filename: r.filename - filesize: r.filesize - proportionalHeight: r.proportionalHeight - type: r.type - typeString: r.typeString ?? "" - url: r.url - thumbnailUrl: r.thumbnailUrl - duration: r.duration - originalWidth: r.originalWidth - isOnlyEmoji: r.isOnlyEmoji - isStateEvent: r.isStateEvent - userId: r.userId - userName: r.userName - roomTopic: r.roomTopic - roomName: r.roomName - callType: r.callType - relatedEventCacheBuster: r.relatedEventCacheBuster - encryptionError: r.encryptionError - // This is disabled so that left clicking the reply goes to its location - enabled: false + Layout.preferredHeight: 20 Layout.fillWidth: true - isReply: true - keepFullText: r.keepFullText + color: "green" } } diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp new file mode 100644 index 00000000..7618e20b --- /dev/null +++ b/src/timeline/EventDelegateChooser.cpp @@ -0,0 +1,244 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "EventDelegateChooser.h" +#include "TimelineModel.h" + +#include "Logging.h" + +#include +#include + +// privat qt headers to access required properties +#include +#include + +QQmlComponent * +EventDelegateChoice::delegate() const +{ + return delegate_; +} + +void +EventDelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QList +EventDelegateChoice::roleValues() const +{ + return roleValues_; +} + +void +EventDelegateChoice::setRoleValues(const QList &value) +{ + if (value != roleValues_) { + roleValues_ = value; + emit roleValuesChanged(); + emit changed(); + } +} + +QQmlListProperty +EventDelegateChooser::choices() +{ + return QQmlListProperty(this, + this, + &EventDelegateChooser::appendChoice, + &EventDelegateChooser::choiceCount, + &EventDelegateChooser::choice, + &EventDelegateChooser::clearChoices); +} + +void +EventDelegateChooser::appendChoice(QQmlListProperty *p, EventDelegateChoice *c) +{ + EventDelegateChooser *dc = static_cast(p->object); + dc->choices_.append(c); +} + +qsizetype +EventDelegateChooser::choiceCount(QQmlListProperty *p) +{ + return static_cast(p->object)->choices_.count(); +} +EventDelegateChoice * +EventDelegateChooser::choice(QQmlListProperty *p, qsizetype index) +{ + return static_cast(p->object)->choices_.at(index); +} +void +EventDelegateChooser::clearChoices(QQmlListProperty *p) +{ + static_cast(p->object)->choices_.clear(); +} + +void +EventDelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + // eventIncubator.reset(eventIndex); +} + +void +EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) +{ + auto item = qobject_cast(obj); + if (!item) + return; + + item->setParentItem(&chooser); + + auto roleNames = chooser.room_->roleNames(); + QHash nameToRole; + for (const auto &[k, v] : roleNames.asKeyValueRange()) { + nameToRole.insert(v, k); + } + + QHash roleToPropIdx; + std::vector roles; + + // Workaround for https://bugreports.qt.io/browse/QTBUG-98846 + QHash requiredProperties; + for (const auto &[propKey, prop] : + QQmlIncubatorPrivate::get(this)->requiredProperties()->asKeyValueRange()) { + requiredProperties.insert(prop.propertyName, propKey); + } + + // collect required properties + auto mo = obj->metaObject(); + for (int i = 0; i < mo->propertyCount(); i++) { + auto prop = mo->property(i); + // nhlog::ui()->critical("Found prop {}", prop.name()); + // See https://bugreports.qt.io/browse/QTBUG-98846 + if (!prop.isRequired() && !requiredProperties.contains(prop.name())) + continue; + + if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { + roleToPropIdx.insert(*role, i); + roles.emplace_back(*role); + + nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role); + } else { + nhlog::ui()->critical("Required property {} not found in model!", prop.name()); + } + } + + nhlog::ui()->debug("Querying data for id {}", currentId.toStdString()); + chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); + + QVariantMap rolesToSet; + for (const auto &role : roles) { + const auto &roleName = roleNames[role.role()]; + nhlog::ui()->critical("Setting role {}, {}", role.role(), roleName.toStdString()); + + mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); + rolesToSet.insert(roleName, role.data()); + + if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) + QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); + } + + // setInitialProperties(rolesToSet); + + auto update = + [this, obj, roleToPropIdx = std::move(roleToPropIdx)](const QList &changedRoles) { + std::vector rolesToRequest; + + if (changedRoles.empty()) { + for (auto role : roleToPropIdx.keys()) + rolesToRequest.emplace_back(role); + } else { + for (auto role : changedRoles) { + if (roleToPropIdx.contains(role)) { + rolesToRequest.emplace_back(role); + } + } + } + + if (rolesToRequest.empty()) + return; + + auto mo = obj->metaObject(); + chooser.room_->multiData( + currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest); + for (const auto &role : rolesToRequest) { + mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); + } + }; + + if (!forReply) { + auto row = chooser.room_->idToIndex(currentId); + connect(chooser.room_, + &QAbstractItemModel::dataChanged, + obj, + [row, update](const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QList &changedRoles) { + if (row < topLeft.row() || row > bottomRight.row()) + return; + + update(changedRoles); + }); + } +} + +void +EventDelegateChooser::DelegateIncubator::reset(QString id) +{ + if (!chooser.room_ || id.isEmpty()) + return; + + nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply); + + this->currentId = id; + + auto role = + chooser.room_ + ->dataById(id, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString()) + .toInt(); + + for (const auto choice : qAsConst(chooser.choices_)) { + const auto &choiceValue = choice->roleValues(); + if (choiceValue.contains(role) || choiceValue.empty()) { + if (auto child = qobject_cast(object())) { + child->setParentItem(nullptr); + } + + choice->delegate()->create(*this, QQmlEngine::contextForObject(&chooser)); + return; + } + } +} + +void +EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status) +{ + if (status == QQmlIncubator::Ready) { + auto child = qobject_cast(object()); + if (child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + child->setParentItem(&chooser); + QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::JavaScriptOwnership); + if (forReply) + emit chooser.replyChanged(); + else + emit chooser.mainChanged(); + + } else if (status == QQmlIncubator::Error) { + auto errors_ = errors(); + for (const auto &e : qAsConst(errors_)) + nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); + } +} + diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h new file mode 100644 index 00000000..ce22ca3a --- /dev/null +++ b/src/timeline/EventDelegateChooser.h @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt +// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "TimelineModel.h" + +class EventDelegateChoice : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QList roleValues READ roleValues WRITE setRoleValues NOTIFY roleValuesChanged + REQUIRED FINAL) + Q_PROPERTY( + QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged REQUIRED FINAL) + + [[nodiscard]] QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + [[nodiscard]] QList roleValues() const; + void setRoleValues(const QList &value); + +signals: + void delegateChanged(); + void roleValuesChanged(); + void changed(); + +private: + QList roleValues_; + QQmlComponent *delegate_ = nullptr; +}; + +class EventDelegateChooser : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("DefaultProperty", "choices") + +public: + Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT FINAL) + Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL) + Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) + Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL) + Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL) + + QQmlListProperty choices(); + + [[nodiscard]] QQuickItem *main() const + { + return qobject_cast(eventIncubator.object()); + } + + void setRoom(TimelineModel *m) + { + if (m != room_) { + room_ = m; + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); + emit roomChanged(); + } + } + [[nodiscard]] TimelineModel *room() { return room_; } + + void setEventId(QString idx) + { + eventId_ = idx; + emit eventIdChanged(); + } + [[nodiscard]] QString eventId() const { return eventId_; } + void setReplyTo(QString id) + { + replyId = id; + emit replyToChanged(); + } + [[nodiscard]] QString replyTo() const { return replyId; } + + void componentComplete() override; + +signals: + void mainChanged(); + void replyChanged(); + void roomChanged(); + void eventIdChanged(); + void replyToChanged(); + +private: + struct DelegateIncubator final : public QQmlIncubator + { + DelegateIncubator(EventDelegateChooser &parent, bool forReply) + : QQmlIncubator(QQmlIncubator::AsynchronousIfNested) + , chooser(parent) + , forReply(forReply) + { + } + void setInitialState(QObject *object) override; + void statusChanged(QQmlIncubator::Status status) override; + + void reset(QString id); + + EventDelegateChooser &chooser; + bool forReply; + QString currentId; + + QString instantiatedId; + int instantiatedRole = -1; + QAbstractItemModel *instantiatedModel = nullptr; + }; + + QVariant roleValue_; + QList choices_; + DelegateIncubator eventIncubator{*this, false}; + DelegateIncubator replyIncubator{*this, true}; + TimelineModel *room_{nullptr}; + QString eventId_; + QString replyId; + + static void appendChoice(QQmlListProperty *, EventDelegateChoice *); + static qsizetype choiceCount(QQmlListProperty *); + static EventDelegateChoice *choice(QQmlListProperty *, qsizetype index); + static void clearChoices(QQmlListProperty *); +}; diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index b2a036c5..69ab3f5a 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -926,6 +926,26 @@ TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSp } } +void +TimelineModel::multiData(const QString &id, + const QString &relatedTo, + QModelRoleDataSpan roleDataSpan) const +{ + if (id.isEmpty()) + return; + + auto event = events.get(id.toStdString(), relatedTo.toStdString()); + + if (!event) + return; + + for (QModelRoleData &roleData : roleDataSpan) { + int role = roleData.role(); + + roleData.setData(data(*event, role)); + } +} + QVariant TimelineModel::dataById(const QString &id, int role, const QString &relatedTo) { diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index fccc99eb..57caf26d 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -286,6 +286,8 @@ public: int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override; + void + multiData(const QString &id, const QString &relatedTo, QModelRoleDataSpan roleDataSpan) const; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo); Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const From 76b40f452b8313db972e44f5eca30f59b7fdf4d3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 25 Jun 2023 02:40:44 +0200 Subject: [PATCH 02/32] Working text messages in delegate rework --- resources/qml/MatrixText.qml | 32 ++--- resources/qml/MessageView.qml | 148 ++++++++++++++++++++++-- resources/qml/delegates/TextMessage.qml | 15 +-- src/timeline/EventDelegateChooser.cpp | 26 ++++- src/timeline/EventDelegateChooser.h | 5 + 5 files changed, 191 insertions(+), 35 deletions(-) diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index 94b8bb98..de15e078 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -2,24 +2,24 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.5 -import QtQuick.Controls 2.3 -import im.nheko 1.0 +import QtQuick +import QtQuick.Controls +import im.nheko -TextEdit { +TextArea { id: r property alias cursorShape: cs.cursorShape - //leftInset: 0 - //bottomInset: 0 - //rightInset: 0 - //topInset: 0 - //leftPadding: 0 - //bottomPadding: 0 - //rightPadding: 0 - //topPadding: 0 - //background: null + leftInset: 0 + bottomInset: 0 + rightInset: 0 + topInset: 0 + leftPadding: 0 + bottomPadding: 0 + rightPadding: 0 + topPadding: 0 + background: null ToolTip.text: hoveredLink ToolTip.visible: hoveredLink || false @@ -39,9 +39,9 @@ TextEdit { } onLinkActivated: Nheko.openLink(link) - //// propagate events up - //onPressAndHold: (event) => event.accepted = false - //onPressed: (event) => event.accepted = (event.button == Qt.LeftButton) + // propagate events up + onPressAndHold: (event) => event.accepted = false + onPressed: (event) => event.accepted = (event.button == Qt.LeftButton) NhekoCursorShape { id: cs diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index fde7ee57..2852e5f7 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -63,28 +63,160 @@ Item { id: wrapper ListView.delayRemove: true width: chat.delegateMaxWidth - height: main?.height ?? 10 + height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight, 10) + anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter room: chatRoot.roommodel + required property var day + required property bool isSender + required property bool isStateEvent + //required property var previousMessageDay + //required property bool previousMessageIsStateEvent + //required property string previousMessageUserId + required property int index + property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) + property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) + required property date timestamp + required property string userId + required property string userName + required property string threadId + + data: [ + Loader { + id: section + + property var day: wrapper.day + property bool isSender: wrapper.isSender + property bool isStateEvent: wrapper.isStateEvent + property int parentWidth: wrapper.width + property var previousMessageDay: wrapper.previousMessageDay + property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent + property string previousMessageUserId: wrapper.previousMessageUserId + property date timestamp: wrapper.timestamp + property string userId: wrapper.userId + property string userName: wrapper.userName + + active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent + //asynchronous: true + sourceComponent: sectionHeader + visible: status == Loader.Ready + z: 4 + }, + GridLayout { + id: gridContainer + + width: wrapper.width + y: section.visible && section.active ? section.y + section.height : 0 + + ColumnLayout { + id: contentColumn + Layout.fillWidth: true + Layout.leftMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (wrapper.threadId ? 6 : 0) // align bubble with section header + + AbstractButton { + id: replyRow + visible: wrapper.reply + Layout.fillWidth: true + Layout.maximumHeight: timelineView.height / 8 + Layout.preferredWidth: replyRowLay.implicitWidth + Layout.preferredHeight: replyRowLay.implicitHeight + + property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) + + clip: true + + contentItem: RowLayout { + id: replyRowLay + + anchors.fill: parent + + + Rectangle { + id: replyLine + Layout.fillHeight: true + color: replyRow.userColor + Layout.preferredWidth: 4 + } + + ColumnLayout { + AbstractButton { + id: replyUserButton + Layout.fillWidth: true + contentItem: ElidedLabel { + id: userName_ + fullText: wrapper.reply?.userName ?? '' + color: replyRow.userColor + textFormat: Text.RichText + width: parent.width + elideWidth: width + } + onClicked: room.openUserProfile(wrapper.reply?.userId) + } + data: [ + replyUserButton, + wrapper.reply, + ] + } + } + + background: Rectangle { + width: replyRow.implicitContentWidth + color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) + } + } + + data: [ + replyRow, wrapper.main, + ] + } + + Rectangle { + color: 'yellow' + Layout.preferredWidth: 100 + Layout.preferredHeight: 20 + Layout.alignment: Qt.AlignRight | Qt.AlignTop + } + }, + + Rectangle { + width: Math.min(contentColumn.implicitWidth, contentColumn.width) + height: contentColumn.implicitHeight + color: "blue" + opacity: 0.2 + } + ] + EventDelegateChoice { roleValues: [ MtxEvent.TextMessage, MtxEvent.NoticeMessage, ] - TextArea { - required property string body + TextMessage { + id: textMes + + keepFullText: true + required property string userId + required property string userName + + Layout.fillWidth: true + //Layout.maximumWidth: implicitWidth - width: parent.width - text: body } } EventDelegateChoice { roleValues: [ ] - TextArea { - width: parent.width - text: "Unsupported" + MatrixText { + Layout.fillWidth: true + + required property string typeString + + text: "Unsupported: " + typeString + + required property string userId + required property string userName } } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 1eb5e2c0..d4b72965 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -3,17 +3,19 @@ // SPDX-License-Identifier: GPL-3.0-or-later import ".." -import QtQuick.Controls 2.3 -import im.nheko 1.0 +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko MatrixText { required property string body required property bool isOnlyEmoji required property bool isReply required property bool keepFullText - required property string formatted + required property string formattedBody + property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body - property int metadataWidth + property int metadataWidth: 100 property bool fitsMetadata: false //positionAt(width,height-4) == positionAt(width-metadataWidth-10, height-4) // table border-collapse doesn't seem to work @@ -38,9 +40,8 @@ MatrixText { background-color: " + palette.text + "; }" : "") + // TODO(Nico): Figure out how to support mobile " - " + formatted.replace(//g, "").replace(/<\/del>/g, "").replace(//g, "").replace(/<\/strike>/g, "") - width: parent?.width ?? 0 - height: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight + " + formattedBody.replace(//g, "").replace(/<\/del>/g, "").replace(//g, "").replace(/<\/strike>/g, "") + Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight clip: !keepFullText selectByMouse: !Settings.mobileMode && !isReply enabled: !Settings.mobileMode diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index 7618e20b..5e6ee37e 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -85,6 +85,7 @@ EventDelegateChooser::componentComplete() { QQuickItem::componentComplete(); // eventIncubator.reset(eventIndex); + // eventIncubator.forceCompletion(); } void @@ -104,6 +105,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) QHash roleToPropIdx; std::vector roles; + bool isReplyNeeded = false; // Workaround for https://bugreports.qt.io/browse/QTBUG-98846 QHash requiredProperties; @@ -121,7 +123,10 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) if (!prop.isRequired() && !requiredProperties.contains(prop.name())) continue; - if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { + if (prop.name() == std::string_view("isReply")) { + isReplyNeeded = true; + roleToPropIdx.insert(-1, i); + } else if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { roleToPropIdx.insert(*role, i); roles.emplace_back(*role); @@ -134,13 +139,26 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) nhlog::ui()->debug("Querying data for id {}", currentId.toStdString()); chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); - QVariantMap rolesToSet; for (const auto &role : roles) { const auto &roleName = roleNames[role.role()]; - nhlog::ui()->critical("Setting role {}, {}", role.role(), roleName.toStdString()); + nhlog::ui()->critical("Setting role {}, {} to {}", + role.role(), + roleName.toStdString(), + role.data().toString().toStdString()); + nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[role.role()]).name()); mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); - rolesToSet.insert(roleName, role.data()); + + if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) + QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); + } + + if (isReplyNeeded) { + const auto roleName = QByteArray("isReply"); + nhlog::ui()->critical("Setting role {} to {}", roleName.toStdString(), forReply); + + nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[-1]).name()); + mo->property(roleToPropIdx[-1]).write(obj, forReply); if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h index ce22ca3a..b627b383 100644 --- a/src/timeline/EventDelegateChooser.h +++ b/src/timeline/EventDelegateChooser.h @@ -54,6 +54,7 @@ class EventDelegateChooser : public QQuickItem public: Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT FINAL) Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL) + Q_PROPERTY(QQuickItem *reply READ reply NOTIFY replyChanged FINAL) Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL) Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL) @@ -64,6 +65,10 @@ public: { return qobject_cast(eventIncubator.object()); } + [[nodiscard]] QQuickItem *reply() const + { + return qobject_cast(replyIncubator.object()); + } void setRoom(TimelineModel *m) { From eab8731f5bd695342ad6412d42b1065872f12691 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 8 Jul 2023 19:22:50 +0200 Subject: [PATCH 03/32] Port state events and images --- resources/qml/MessageView.qml | 84 +++++++- resources/qml/delegates/ImageMessage.qml | 15 +- resources/qml/delegates/MessageDelegate.qml | 2 + resources/qml/delegates/TextMessage.qml | 4 +- src/timeline/EventStore.cpp | 4 +- src/timeline/TimelineModel.cpp | 210 ++++++++++++-------- src/timeline/TimelineModel.h | 15 +- 7 files changed, 232 insertions(+), 102 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 2852e5f7..2d4f592c 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -191,13 +191,19 @@ Item { roleValues: [ MtxEvent.TextMessage, MtxEvent.NoticeMessage, + MtxEvent.ElementEffectMessage, + MtxEvent.UnknownMessage, ] TextMessage { - id: textMes - keepFullText: true required property string userId required property string userName + required property string formattedBody + required property int type + + color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text + font.italic: type == MtxEvent.NoticeMessage + formatted: formattedBody Layout.fillWidth: true //Layout.maximumWidth: implicitWidth @@ -205,6 +211,80 @@ Item { } } + EventDelegateChoice { + roleValues: [ + MtxEvent.EmoteMessage, + ] + TextMessage { + keepFullText: true + required property string userId + required property string userName + required property string formattedBody + + formatted: TimelineManager.escapeEmoji(userName) + " " + formattedBody + + color: TimelineManager.userColor(userId, palette.base) + font.italic: true + + Layout.fillWidth: true + //Layout.maximumWidth: implicitWidth + + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.CanonicalAlias, + MtxEvent.ServerAcl, + MtxEvent.Name, + MtxEvent.Topic, + MtxEvent.Avatar, + MtxEvent.PinnedEvents, + MtxEvent.ImagePackInRoom, + MtxEvent.SpaceParent, + MtxEvent.RoomCreate, + MtxEvent.PowerLevels, + MtxEvent.PolicyRuleUser, + MtxEvent.PolicyRuleRoom, + MtxEvent.PolicyRuleServer, + MtxEvent.RoomJoinRules, + MtxEvent.RoomHistoryVisibility, + MtxEvent.RoomGuestAccess, + ] + TextMessage { + keepFullText: true + + required property string userId + required property string userName + required property string formattedStateEvent + + isOnlyEmoji: false + text: formattedStateEvent + formatted: '' + body: '' + + color: palette.buttonText + font.italic: true + + Layout.fillWidth: true + //Layout.maximumWidth: implicitWidth + + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.ImageMessage, + MtxEvent.Sticker, + ] + ImageMessage { + Layout.fillWidth: true + + containerHeight: timelineView.height + Layout.maximumWidth: tempWidth + } + } + EventDelegateChoice { roleValues: [ ] diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 20d727c3..48b7c5e4 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -2,10 +2,11 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.15 -import QtQuick.Window 2.15 -import QtQuick.Controls 2.3 -import im.nheko 1.0 +import QtQuick +import QtQuick.Window +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko AbstractButton { required property int type @@ -17,13 +18,13 @@ AbstractButton { required property string filename required property bool isReply required property string eventId + required property int containerHeight property double divisor: isReply ? 5 : 3 property int tempWidth: originalWidth < 1? 400: originalWidth - implicitWidth: Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) - width: Math.min(parent?.width ?? 2000,implicitWidth) - height: width*proportionalHeight + Layout.preferredWidth: Math.round(tempWidth*Math.min((containerHeight/divisor)/(tempWidth*proportionalHeight), 1)) + Layout.preferredHeight: width*proportionalHeight hoverEnabled: true state: (img.status != Image.Ready || timeline.privacyScreen.active) ? "BlurhashVisible" : "ImageVisible" diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 68f65062..44726a63 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -175,6 +175,7 @@ Item { isReply: d.isReply eventId: d.eventId metadataWidth: d.metadataWidth + containerHeight: timelineView.height } } @@ -193,6 +194,7 @@ Item { isReply: d.isReply eventId: d.eventId metadataWidth: d.metadataWidth + containerHeight: timelineView.height } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index d4b72965..dc8caf01 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -12,7 +12,7 @@ MatrixText { required property bool isOnlyEmoji required property bool isReply required property bool keepFullText - required property string formattedBody + required property string formatted property string copyText: selectedText ? getText(selectionStart, selectionEnd) : body property int metadataWidth: 100 @@ -40,7 +40,7 @@ MatrixText { background-color: " + palette.text + "; }" : "") + // TODO(Nico): Figure out how to support mobile " - " + formattedBody.replace(//g, "").replace(/<\/del>/g, "").replace(//g, "").replace(/<\/strike>/g, "") + " + formatted.replace(//g, "").replace(/<\/del>/g, "").replace(//g, "").replace(/<\/strike>/g, "") Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight clip: !keepFullText selectByMouse: !Settings.mobileMode && !isReply diff --git a/src/timeline/EventStore.cpp b/src/timeline/EventStore.cpp index 63b67474..e29dfb4c 100644 --- a/src/timeline/EventStore.cpp +++ b/src/timeline/EventStore.cpp @@ -843,8 +843,8 @@ EventStore::get(const std::string &id, nhlog::net()->error( "Failed to retrieve event with id {}, which was " "requested to show the replyTo for event {}", - relatedTo, - id); + id, + relatedTo); return; } emit eventFetched(id, relatedTo, timeline); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 69ab3f5a..066d8b01 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -532,6 +532,7 @@ TimelineModel::roleNames() const {IsOnlyEmoji, "isOnlyEmoji"}, {Body, "body"}, {FormattedBody, "formattedBody"}, + {FormattedStateEvent, "formattedStateEvent"}, {IsSender, "isSender"}, {UserId, "userId"}, {UserName, "userName"}, @@ -694,6 +695,76 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return QVariant(utils::replaceEmoji(utils::linkifyMessage(formattedBody_))); } + case FormattedStateEvent: { + if (mtx::accessors::is_state_event(event)) { + return std::visit( + [this](const auto &e) { + constexpr auto t = mtx::events::state_content_to_type; + if constexpr (t == mtx::events::EventType::RoomServerAcl) + return tr("%1 changed which servers are allowed in this room.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::RoomName) { + if (e.content.name.empty()) + return tr("%1 removed the room name.") + .arg(displayName(QString::fromStdString(e.sender))); + else + return tr("%1 changed the room name to: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QString::fromStdString(e.content.name).toHtmlEscaped()); + } else if constexpr (t == mtx::events::EventType::RoomTopic) { + if (e.content.topic.empty()) + return tr("%1 removed the topic.") + .arg(displayName(QString::fromStdString(e.sender))); + else + return tr("%1 changed the topic to: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QString::fromStdString(e.content.topic).toHtmlEscaped()); + } else if constexpr (t == mtx::events::EventType::RoomAvatar) { + if (e.content.url.starts_with("mxc://")) + return tr("%1 changed the room avatar to: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QStringLiteral("") + .arg(QUrl::toPercentEncoding( + QString::fromStdString(e.content.url)))); + else + return tr("%1 removed the room avatar.") + .arg(displayName(QString::fromStdString(e.sender))); + } else if constexpr (t == mtx::events::EventType::RoomPinnedEvents) + return tr("%1 changed the pinned messages.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::ImagePackInRoom) + formatImagePackEvent(e); + else if constexpr (t == mtx::events::EventType::RoomCanonicalAlias) + return tr("%1 changed the addresses for this room.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::SpaceParent) + return tr("%1 changed the parent communities for this room.") + .arg(displayName(QString::fromStdString(e.sender))); + else if constexpr (t == mtx::events::EventType::RoomCreate) + return tr("%1 created and configured room: %2") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(room_id_); + else if constexpr (t == mtx::events::EventType::RoomPowerLevels) + return formatPowerLevelEvent(e); + else if constexpr (t == mtx::events::EventType::PolicyRuleRoom) + return formatPolicyRule(QString::fromStdString(e.event_id)); + else if constexpr (t == mtx::events::EventType::PolicyRuleUser) + return formatPolicyRule(QString::fromStdString(e.event_id)); + else if constexpr (t == mtx::events::EventType::PolicyRuleServer) + return formatPolicyRule(QString::fromStdString(e.event_id)); + else if constexpr (t == mtx::events::EventType::RoomHistoryVisibility) + return formatHistoryVisibilityEvent(e); + else if constexpr (t == mtx::events::EventType::RoomGuestAccess) + return formatGuestAccessEvent(e); + + return tr("%1 changed unknown state event %2.") + .arg(displayName(QString::fromStdString(e.sender))) + .arg(QString::fromStdString(to_string(e.type))); + }, + event); + } + return QString(); + } case Url: return QVariant(QString::fromStdString(url(event))); case ThumbnailUrl: @@ -2308,20 +2379,13 @@ TimelineModel::formatJoinRuleEvent(const QString &id) } QString -TimelineModel::formatGuestAccessEvent(const QString &id) +TimelineModel::formatGuestAccessEvent( + const mtx::events::StateEvent &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if>(e); - if (!event) - return {}; - - QString user = QString::fromStdString(event->sender); + QString user = QString::fromStdString(event.sender); QString name = utils::replaceEmoji(displayName(user)); - switch (event->content.guest_access) { + switch (event.content.guest_access) { case mtx::events::state::AccessState::CanJoin: return tr("%1 made the room open to guests.").arg(name); case mtx::events::state::AccessState::Forbidden: @@ -2332,21 +2396,13 @@ TimelineModel::formatGuestAccessEvent(const QString &id) } QString -TimelineModel::formatHistoryVisibilityEvent(const QString &id) +TimelineModel::formatHistoryVisibilityEvent( + const mtx::events::StateEvent &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if>(e); - - if (!event) - return {}; - - QString user = QString::fromStdString(event->sender); + QString user = QString::fromStdString(event.sender); QString name = utils::replaceEmoji(displayName(user)); - switch (event->content.history_visibility) { + switch (event.content.history_visibility) { case mtx::events::state::Visibility::WorldReadable: return tr("%1 made the room history world readable. Events may be now read by " "non-joined people.") @@ -2364,32 +2420,25 @@ TimelineModel::formatHistoryVisibilityEvent(const QString &id) } QString -TimelineModel::formatPowerLevelEvent(const QString &id) +TimelineModel::formatPowerLevelEvent( + const mtx::events::StateEvent &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if>(e); - if (!event) - return QString(); - mtx::events::StateEvent const *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (!event.unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); if (tempPrevEvent) { prevEvent = std::get_if>(tempPrevEvent); } } - QString user = QString::fromStdString(event->sender); + QString user = QString::fromStdString(event.sender); QString sender_name = utils::replaceEmoji(displayName(user)); // Get the rooms levels for redactions and powerlevel changes to determine "Administrator" and // "Moderator" powerlevels. - auto administrator_power_level = event->content.state_level("m.room.power_levels"); - auto moderator_power_level = event->content.redact; - auto default_powerlevel = event->content.users_default; + auto administrator_power_level = event.content.state_level("m.room.power_levels"); + auto moderator_power_level = event.content.redact; + auto default_powerlevel = event.content.users_default; if (!prevEvent) return tr("%1 has changed the room's permissions.").arg(sender_name); @@ -2399,7 +2448,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id) auto numberOfAffected = 0; // We do only compare to people with explicit PL. Usually others are not going to be // affected either way and this is cheaper to iterate over. - for (auto const &[mxid, currentPowerlevel] : event->content.users) { + for (auto const &[mxid, currentPowerlevel] : event.content.users) { if (currentPowerlevel == newPowerlevelSetting && prevEvent->content.user_level(mxid) < newPowerlevelSetting) { numberOfAffected++; @@ -2413,16 +2462,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id) QStringList resultingMessage{}; // These affect only a few people. Therefor we can print who is affected. - if (event->content.kick != prevEvent->content.kick) { + if (event.content.kick != prevEvent->content.kick) { auto default_message = tr("%1 has changed the room's kick powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.kick) - .arg(event->content.kick); + .arg(event.content.kick); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.kick > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.kick); + if (event.content.kick > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.kick); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2444,16 +2493,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } } - if (event->content.redact != prevEvent->content.redact) { + if (event.content.redact != prevEvent->content.redact) { auto default_message = tr("%1 has changed the room's redact powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.redact) - .arg(event->content.redact); + .arg(event.content.redact); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.redact > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.redact); + if (event.content.redact > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.redact); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2476,16 +2525,16 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } } - if (event->content.ban != prevEvent->content.ban) { + if (event.content.ban != prevEvent->content.ban) { auto default_message = tr("%1 has changed the room's ban powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.ban) - .arg(event->content.ban); + .arg(event.content.ban); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.ban > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.ban); + if (event.content.ban > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.ban); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2507,17 +2556,17 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } } - if (event->content.state_default != prevEvent->content.state_default) { + if (event.content.state_default != prevEvent->content.state_default) { auto default_message = tr("%1 has changed the room's state_default powerlevel from %2 to %3.") .arg(sender_name) .arg(prevEvent->content.state_default) - .arg(event->content.state_default); + .arg(event.content.state_default); // We only calculate affected users if we change to a level above the default users PL // to not accidentally have a DoS vector - if (event->content.state_default > default_powerlevel) { - auto [affected, number_of_affected] = calc_affected(event->content.kick); + if (event.content.state_default > default_powerlevel) { + auto [affected, number_of_affected] = calc_affected(event.content.kick); if (number_of_affected != 0) { auto true_affected_rest = number_of_affected - affected.size(); @@ -2541,42 +2590,42 @@ TimelineModel::formatPowerLevelEvent(const QString &id) // These affect potentially the whole room. We there for do not calculate who gets affected // by this to prevent huge lists of people. - if (event->content.invite != prevEvent->content.invite) { + if (event.content.invite != prevEvent->content.invite) { resultingMessage.append(tr("%1 has changed the room's invite powerlevel from %2 to %3.") .arg(sender_name, QString::number(prevEvent->content.invite), - QString::number(event->content.invite))); + QString::number(event.content.invite))); } - if (event->content.events_default != prevEvent->content.events_default) { - if ((event->content.events_default > default_powerlevel) && + if (event.content.events_default != prevEvent->content.events_default) { + if ((event.content.events_default > default_powerlevel) && prevEvent->content.events_default <= default_powerlevel) { resultingMessage.append( tr("%1 has changed the room's events_default powerlevel from %2 to %3. New " "users can now not send any events.") .arg(sender_name, QString::number(prevEvent->content.events_default), - QString::number(event->content.events_default))); - } else if ((event->content.events_default < prevEvent->content.events_default) && - (event->content.events_default < default_powerlevel) && + QString::number(event.content.events_default))); + } else if ((event.content.events_default < prevEvent->content.events_default) && + (event.content.events_default < default_powerlevel) && (prevEvent->content.events_default > default_powerlevel)) { resultingMessage.append( tr("%1 has changed the room's events_default powerlevel from %2 to %3. New " "users can now send events that are not otherwise restricted.") .arg(sender_name, QString::number(prevEvent->content.events_default), - QString::number(event->content.events_default))); + QString::number(event.content.events_default))); } else { resultingMessage.append( tr("%1 has changed the room's events_default powerlevel from %2 to %3.") .arg(sender_name, QString::number(prevEvent->content.events_default), - QString::number(event->content.events_default))); + QString::number(event.content.events_default))); } } // Compare if a Powerlevel of a user changed - for (auto const &[mxid, powerlevel] : event->content.users) { + for (auto const &[mxid, powerlevel] : event.content.users) { auto nameOfChangedUser = utils::replaceEmoji(displayName(QString::fromStdString(mxid))); if (prevEvent->content.user_level(mxid) != powerlevel) { if (powerlevel >= administrator_power_level) { @@ -2601,7 +2650,7 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } // Handle added/removed/changed event type - for (auto const &[event_type, powerlevel] : event->content.events) { + for (auto const &[event_type, powerlevel] : event.content.events) { auto prev_not_present = prevEvent->content.events.find(event_type) == prevEvent->content.events.end(); @@ -2640,26 +2689,19 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } QString -TimelineModel::formatImagePackEvent(const QString &id) +TimelineModel::formatImagePackEvent( + const mtx::events::StateEvent &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if>(e); - if (!event) - return {}; - mtx::events::StateEvent const *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (!event.unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); if (tempPrevEvent) { prevEvent = std::get_if>(tempPrevEvent); } } - const auto &newImages = event->content.images; + const auto &newImages = event.content.images; const auto oldImages = prevEvent ? prevEvent->content.images : decltype(newImages){}; auto ascent = QFontMetrics(UserSettings::instance()->font()).ascent(); @@ -2682,12 +2724,12 @@ TimelineModel::formatImagePackEvent(const QString &id) auto added = calcChange(newImages, oldImages); auto removed = calcChange(oldImages, newImages); - auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event->sender))); + auto sender = utils::replaceEmoji(displayName(QString::fromStdString(event.sender))); const auto packId = [&event]() -> QString { - if (event->content.pack && !event->content.pack->display_name.empty()) { - return event->content.pack->display_name.c_str(); - } else if (!event->state_key.empty()) { - return event->state_key.c_str(); + if (event.content.pack && !event.content.pack->display_name.empty()) { + return event.content.pack->display_name.c_str(); + } else if (!event.state_key.empty()) { + return event.state_key.c_str(); } return tr("(empty)"); }(); @@ -2712,7 +2754,7 @@ TimelineModel::formatImagePackEvent(const QString &id) } QString -TimelineModel::formatPolicyRule(const QString &id) +TimelineModel::formatPolicyRule(const QString &id) const { auto idStr = id.toStdString(); auto e = events.get(idStr, ""); diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 57caf26d..b81fc209 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -238,6 +238,7 @@ public: IsOnlyEmoji, Body, FormattedBody, + FormattedStateEvent, IsSender, UserId, UserName, @@ -310,11 +311,15 @@ public: Q_INVOKABLE void joinReplacementRoom(const QString &id); Q_INVOKABLE QString formatMemberEvent(const QString &id); Q_INVOKABLE QString formatJoinRuleEvent(const QString &id); - Q_INVOKABLE QString formatHistoryVisibilityEvent(const QString &id); - Q_INVOKABLE QString formatGuestAccessEvent(const QString &id); - Q_INVOKABLE QString formatPowerLevelEvent(const QString &id); - Q_INVOKABLE QString formatImagePackEvent(const QString &id); - Q_INVOKABLE QString formatPolicyRule(const QString &id); + QString formatHistoryVisibilityEvent( + const mtx::events::StateEvent &event) const; + QString + formatGuestAccessEvent(const mtx::events::StateEvent &) const; + QString formatPowerLevelEvent( + const mtx::events::StateEvent &event) const; + QString formatImagePackEvent( + const mtx::events::StateEvent &event) const; + Q_INVOKABLE QString formatPolicyRule(const QString &id) const; Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id); Q_INVOKABLE void viewRawMessage(const QString &id); From 466d3cd52c2d7262449921817c829d22f5721160 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 28 Jul 2023 19:30:13 +0200 Subject: [PATCH 04/32] Port redacted messages --- resources/qml/MessageView.qml | 13 ++++++++++++ resources/qml/delegates/Redacted.qml | 31 ++++++++++++++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 2d4f592c..41d996c1 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -285,6 +285,19 @@ Item { } } + EventDelegateChoice { + roleValues: [ + MtxEvent.Redacted + ] + + Redacted { + Layout.fillWidth: true + + required property string userId + required property string userName + } + } + EventDelegateChoice { roleValues: [ ] diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml index 4a9700dc..a09e4c3f 100644 --- a/resources/qml/delegates/Redacted.qml +++ b/resources/qml/delegates/Redacted.qml @@ -7,20 +7,16 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import im.nheko 1.0 -Rectangle{ +Control { + id: msgRoot - height: redactedLayout.implicitHeight + Nheko.paddingSmall - implicitWidth: redactedLayout.implicitWidth + 2 * Nheko.paddingMedium - width: Math.min(parent.width,implicitWidth+1) - radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall - color: palette.alternateBase - property int metadataWidth - property bool fitsMetadata: parent.width - redactedLayout.width > metadataWidth + 4 + property int metadataWidth: 0 + property bool fitsMetadata: false //parent.width - redactedLayout.width > metadataWidth + 4 - RowLayout { + required property string eventId + + contentItem: RowLayout { id: redactedLayout - anchors.centerIn: parent - width: parent.width - 2 * Nheko.paddingMedium spacing: Nheko.paddingSmall Image { @@ -34,9 +30,9 @@ Rectangle{ id: redactedLabel Layout.margins: 0 Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.preferredWidth: implicitWidth + Layout.maximumWidth: implicitWidth + 1 Layout.fillWidth: true - property var redactedPair: room.formatRedactedEvent(eventId) + property var redactedPair: room.formatRedactedEvent(msgRoot.eventId) text: redactedPair["first"] wrapMode: Label.WordWrap color: palette.text @@ -48,4 +44,13 @@ Rectangle{ } } } + + padding: Nheko.paddingSmall + + Layout.maximumWidth: redactedLayout.Layout.maximumWidth + padding * 2 + + background: Rectangle { + color: palette.alternateBase + radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall + } } From 718a58d388abd228c6a08f9fa3365588c06923ba Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 28 Jul 2023 20:05:47 +0200 Subject: [PATCH 05/32] Get rid of redundant constructions and make room implicit --- resources/qml/MessageView.qml | 4 ++-- resources/qml/delegates/Redacted.qml | 9 +++++---- src/timeline/EventDelegateChooser.cpp | 6 +++++- src/timeline/EventDelegateChooser.h | 15 ++++++++++++--- src/timeline/TimelineModel.cpp | 3 +++ src/timeline/TimelineModel.h | 1 + 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 41d996c1..417a4f5a 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -65,7 +65,7 @@ Item { width: chat.delegateMaxWidth height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight, 10) anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter - room: chatRoot.roommodel + //room: chatRoot.roommodel required property var day required property bool isSender @@ -203,7 +203,7 @@ Item { color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text font.italic: type == MtxEvent.NoticeMessage - formatted: formattedBody + formatted: formattedBody + "a" Layout.fillWidth: true //Layout.maximumWidth: implicitWidth diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml index a09e4c3f..1bb3209f 100644 --- a/resources/qml/delegates/Redacted.qml +++ b/resources/qml/delegates/Redacted.qml @@ -2,10 +2,10 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 -import im.nheko 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko Control { id: msgRoot @@ -14,6 +14,7 @@ Control { property bool fitsMetadata: false //parent.width - redactedLayout.width > metadataWidth + 4 required property string eventId + required property Room room contentItem: RowLayout { id: redactedLayout diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index 5e6ee37e..7fec38dd 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -84,7 +84,8 @@ void EventDelegateChooser::componentComplete() { QQuickItem::componentComplete(); - // eventIncubator.reset(eventIndex); + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); // eventIncubator.forceCompletion(); } @@ -226,6 +227,9 @@ EventDelegateChooser::DelegateIncubator::reset(QString id) for (const auto choice : qAsConst(chooser.choices_)) { const auto &choiceValue = choice->roleValues(); if (choiceValue.contains(role) || choiceValue.empty()) { + nhlog::ui()->debug( + "Instantiating type: {}, c {}", (int)role, choiceValue.contains(role)); + if (auto child = qobject_cast(object())) { child->setParentItem(nullptr); } diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h index b627b383..1cd2d65a 100644 --- a/src/timeline/EventDelegateChooser.h +++ b/src/timeline/EventDelegateChooser.h @@ -55,9 +55,9 @@ public: Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT FINAL) Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL) Q_PROPERTY(QQuickItem *reply READ reply NOTIFY replyChanged FINAL) - Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL) Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL) + Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) QQmlListProperty choices(); @@ -74,9 +74,12 @@ public: { if (m != room_) { room_ = m; - eventIncubator.reset(eventId_); - replyIncubator.reset(replyId); emit roomChanged(); + + if (isComponentComplete()) { + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); + } } } [[nodiscard]] TimelineModel *room() { return room_; } @@ -85,12 +88,18 @@ public: { eventId_ = idx; emit eventIdChanged(); + + if (isComponentComplete()) + eventIncubator.reset(eventId_); } [[nodiscard]] QString eventId() const { return eventId_; } void setReplyTo(QString id) { replyId = id; emit replyToChanged(); + + if (isComponentComplete()) + replyIncubator.reset(replyId); } [[nodiscard]] QString replyTo() const { return replyId; } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 066d8b01..66f7d5b8 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -561,6 +561,7 @@ TimelineModel::roleNames() const {ReplyTo, "replyTo"}, {ThreadId, "threadId"}, {Reactions, "reactions"}, + {Room, "room"}, {RoomId, "roomId"}, {RoomName, "roomName"}, {RoomTopic, "roomTopic"}, @@ -899,6 +900,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r auto id = relations(event).replaces().value_or(event_id(event)); return QVariant::fromValue(events.reactions(id)); } + case Room: + return QVariant::fromValue(this); case RoomId: return QVariant(room_id_); case RoomName: diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index b81fc209..2b22ad61 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -267,6 +267,7 @@ public: ReplyTo, ThreadId, Reactions, + Room, RoomId, RoomName, RoomTopic, From 2360dfd80ae8991c557c9c7d9474c528c00fdaa6 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 13 Aug 2023 11:30:41 +0200 Subject: [PATCH 06/32] Remaining events apart from verification --- resources/qml/MessageView.qml | 184 +++++++++++++++++- resources/qml/delegates/Encrypted.qml | 33 ++-- resources/qml/delegates/EncryptionEnabled.qml | 47 +++-- resources/qml/delegates/FileMessage.qml | 36 ++-- resources/qml/delegates/Redacted.qml | 1 - src/timeline/EventDelegateChooser.cpp | 25 ++- src/timeline/TimelineModel.cpp | 42 ++-- src/timeline/TimelineModel.h | 3 +- src/voip/CallManager.cpp | 3 +- 9 files changed, 286 insertions(+), 88 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 417a4f5a..2f50789f 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -203,7 +203,7 @@ Item { color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text font.italic: type == MtxEvent.NoticeMessage - formatted: formattedBody + "a" + formatted: formattedBody Layout.fillWidth: true //Layout.maximumWidth: implicitWidth @@ -262,6 +262,7 @@ Item { text: formattedStateEvent formatted: '' body: '' + horizontalAlignment: Text.AlignHCenter color: palette.buttonText font.italic: true @@ -272,6 +273,79 @@ Item { } } + EventDelegateChoice { + roleValues: [ + MtxEvent.CallInvite, + ] + TextMessage { + keepFullText: true + + required property string userId + required property string userName + required property string callType + + isOnlyEmoji: false + body: formatted + formatted: { + switch (callType) { + case "voice": + return qsTr("%1 placed a voice call.").arg(TimelineManager.escapeEmoji(userName)); + case "video": + return qsTr("%1 placed a video call.").arg(TimelineManager.escapeEmoji(userName)); + default: + return qsTr("%1 placed a call.").arg(TimelineManager.escapeEmoji(userName)); + } + } + + color: palette.buttonText + font.italic: true + + Layout.fillWidth: true + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.CallAnswer, + MtxEvent.CallReject, + MtxEvent.CallSelectAnswer, + MtxEvent.CallHangUp, + MtxEvent.CallCandidates, + MtxEvent.CallNegotiate, + ] + TextMessage { + keepFullText: true + + required property string userId + required property string userName + required property int type + + isOnlyEmoji: false + body: formatted + formatted: { + switch (type) { + case MtxEvent.CallAnswer: + return qsTr("%1 answered the call.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallReject: + return qsTr("%1 rejected the call.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallSelectAnswer: + return qsTr("%1 selected answer.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallHangUp: + return qsTr("%1 ended the call.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallCandidates: + return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallNegotiate: + return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); + } + } + + color: palette.buttonText + font.italic: true + + Layout.fillWidth: true + } + } + EventDelegateChoice { roleValues: [ MtxEvent.ImageMessage, @@ -285,6 +359,44 @@ Item { } } + EventDelegateChoice { + roleValues: [ + MtxEvent.FileMessage, + ] + FileMessage { + Layout.fillWidth: true + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.VideoMessage, + MtxEvent.AudioMessage, + ] + PlayableMediaMessage { + Layout.fillWidth: true + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.Encrypted, + ] + Encrypted { + Layout.fillWidth: true + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.Encryption, + ] + EncryptionEnabled { + Layout.fillWidth: true + } + } + + EventDelegateChoice { roleValues: [ MtxEvent.Redacted @@ -298,6 +410,76 @@ Item { } } + EventDelegateChoice { + roleValues: [ + MtxEvent.Member + ] + + ColumnLayout { + id: member + + required property string userId + required property string userName + + required property bool isReply + required property Room room + required property string formattedStateEvent + + NoticeMessage { + body: formatted + isOnlyEmoji: false + isReply: tombstone.isReply + keepFullText: true + isStateEvent: true + Layout.fillWidth: true + formatted: member.formattedStateEvent + } + + Button { + visible: room.showAcceptKnockButton(eventId) + Layout.alignment: Qt.AlignHCenter + text: qsTr("Allow them in") + onClicked: room.acceptKnock(member.eventId) + } + + } + } + + EventDelegateChoice { + roleValues: [ + MtxEvent.Tombstone + ] + + ColumnLayout { + id: tombstone + + required property string userId + required property string userName + + required property string body + required property bool isReply + required property Room room + required property string eventId + + NoticeMessage { + body: formatted + isOnlyEmoji: false + isReply: tombstone.isReply + keepFullText: true + isStateEvent: true + Layout.fillWidth: true + formatted: qsTr("This room was replaced for the following reason: %1").arg(tombstone.body) + } + + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Go to replacement room") + onClicked: tombstone.room.joinReplacementRoom(tombstone.eventId) + } + + } + } + EventDelegateChoice { roleValues: [ ] diff --git a/resources/qml/delegates/Encrypted.qml b/resources/qml/delegates/Encrypted.qml index fdfe958e..7aeeb28a 100644 --- a/resources/qml/delegates/Encrypted.qml +++ b/resources/qml/delegates/Encrypted.qml @@ -8,37 +8,34 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import im.nheko 1.0 -Rectangle { +Control { id: r required property int encryptionError required property string eventId - radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium - 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: palette.alternateBase + padding: Nheko.paddingMedium + implicitHeight: contents.implicitHeight + Nheko.paddingMedium * 2 + Layout.maximumWidth: contents.Layout.maximumWidth + padding * 2 + Layout.fillWidth: true - RowLayout { + contentItem: RowLayout { id: contents - anchors.fill: parent - anchors.margins: Nheko.paddingMedium spacing: Nheko.paddingMedium Image { source: "image://colorimage/:/icons/icons/ui/shield-filled-cross.svg?" + Nheko.theme.error Layout.alignment: Qt.AlignVCenter - width: 24 - height: width + Layout.preferredWidth: 24 + Layout.preferredHeight: 24 } - Column { + ColumnLayout { spacing: Nheko.paddingSmall Layout.fillWidth: true - MatrixText { + Label { id: encryptedText text: { switch (encryptionError) { @@ -58,8 +55,11 @@ Rectangle { return qsTr("Unknown decryption error"); } } + textFormat: Text.PlainText + wrapMode: Label.WordWrap color: palette.text - width: parent.width + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + 1 } Button { @@ -72,4 +72,9 @@ Rectangle { } + background: Rectangle { + color: palette.alternateBase + radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingMedium + visible: !Settings.bubbles // the bubble in a bubble looks odd + } } diff --git a/resources/qml/delegates/EncryptionEnabled.qml b/resources/qml/delegates/EncryptionEnabled.qml index 0e2b7fc0..40894543 100644 --- a/resources/qml/delegates/EncryptionEnabled.qml +++ b/resources/qml/delegates/EncryptionEnabled.qml @@ -3,27 +3,24 @@ // SPDX-License-Identifier: GPL-3.0-or-later import ".." -import QtQuick 2.15 -import QtQuick.Layouts 1.15 -import im.nheko 1.0 +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import im.nheko -Rectangle { +Control { id: r - required property string username + required property string userName - radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium - width: parent.width ? Math.min(parent.width, 700) : 0 - height: contents.implicitHeight + Nheko.paddingMedium * 2 - color: palette.alternateBase - border.color: Nheko.theme.green - border.width: 2 + padding: Nheko.paddingMedium + //implicitHeight: contents.implicitHeight + padd * 2 + Layout.maximumWidth: contents.Layout.maximumWidth + padding * 2 + Layout.fillWidth: true - RowLayout { + contentItem: RowLayout { id: contents - anchors.fill: parent - anchors.margins: Nheko.paddingMedium spacing: Nheko.paddingMedium Image { @@ -33,26 +30,36 @@ Rectangle { Layout.preferredHeight: 24 } - Column { + ColumnLayout { spacing: Nheko.paddingSmall Layout.fillWidth: true MatrixText { - text: qsTr("%1 enabled end-to-end encryption").arg(r.username) + text: qsTr("%1 enabled end-to-end encryption").arg(r.userName) font.bold: true font.pointSize: 14 color: palette.text - width: parent.width + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + 1 } - MatrixText { + Label { text: qsTr("Encryption keeps your messages safe by only allowing the people you sent the message to to read it. For extra security, if you want to make sure you are talking to the right people, you can verify them in real life.") - color: palette.text - width: parent.width + textFormat: Text.PlainText + wrapMode: Label.WordWrap + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + 1 } } } + background: Rectangle { + radius: fontMetrics.lineSpacing / 2 + Nheko.paddingMedium + height: contents.implicitHeight + Nheko.paddingMedium * 2 + color: palette.alternateBase + border.color: Nheko.theme.green + border.width: 2 + } } diff --git a/resources/qml/delegates/FileMessage.qml b/resources/qml/delegates/FileMessage.qml index 82b82c1b..9f350123 100644 --- a/resources/qml/delegates/FileMessage.qml +++ b/resources/qml/delegates/FileMessage.qml @@ -2,26 +2,30 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -import QtQuick 2.12 -import QtQuick.Layouts 1.2 -import im.nheko 1.0 +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import im.nheko + +Control { + id: evRoot -Item { required property string eventId required property string filename required property string filesize - height: rowa.height + (Settings.bubbles? 16: 24) - implicitWidth: rowa.implicitWidth + metadataWidth - property int metadataWidth - property bool fitsMetadata: true + padding: Settings.bubbles? 8 : 12 + //Layout.preferredHeight: rowa.implicitHeight + padding + //Layout.maximumWidth: rowa.Layout.maximumWidth + metadataWidth + padding + property int metadataWidth: 0 + property bool fitsMetadata: false - RowLayout { + Layout.maximumWidth: rowa.Layout.maximumWidth + padding * 2 + + contentItem: RowLayout { id: rowa - anchors.centerIn: parent - width: parent.width - (Settings.bubbles? 16 : 24) - spacing: 15 + spacing: 16 Rectangle { id: button @@ -63,6 +67,7 @@ Item { id: filename_ Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + 1 text: filename textFormat: Text.PlainText elide: Text.ElideRight @@ -73,6 +78,7 @@ Item { id: filesize_ Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + 1 text: filesize textFormat: Text.PlainText elide: Text.ElideRight @@ -83,11 +89,9 @@ Item { } - Rectangle { + background: Rectangle { color: palette.alternateBase - z: -1 - radius: 10 - anchors.fill: parent + radius: fontMetrics.lineSpacing / 2 + 2 * Nheko.paddingSmall visible: !Settings.bubbles // the bubble in a bubble looks odd } diff --git a/resources/qml/delegates/Redacted.qml b/resources/qml/delegates/Redacted.qml index 1bb3209f..3c496f08 100644 --- a/resources/qml/delegates/Redacted.qml +++ b/resources/qml/delegates/Redacted.qml @@ -36,7 +36,6 @@ Control { property var redactedPair: room.formatRedactedEvent(msgRoot.eventId) text: redactedPair["first"] wrapMode: Label.WordWrap - color: palette.text ToolTip.text: redactedPair["second"] ToolTip.visible: hh.hovered diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index 7fec38dd..2218d9ee 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -195,17 +195,22 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) if (!forReply) { auto row = chooser.room_->idToIndex(currentId); - connect(chooser.room_, - &QAbstractItemModel::dataChanged, - obj, - [row, update](const QModelIndex &topLeft, - const QModelIndex &bottomRight, - const QList &changedRoles) { - if (row < topLeft.row() || row > bottomRight.row()) - return; + auto connection = connect( + chooser.room_, + &QAbstractItemModel::dataChanged, + obj, + [row, update](const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QList &changedRoles) { + if (row < topLeft.row() || row > bottomRight.row()) + return; - update(changedRoles); - }); + update(changedRoles); + }, + Qt::QueuedConnection); + connect(&this->chooser, &EventDelegateChooser::destroyed, obj, [connection]() { + QObject::disconnect(connection); + }); } } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 66f7d5b8..3e0c6688 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -757,6 +757,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r return formatHistoryVisibilityEvent(e); else if constexpr (t == mtx::events::EventType::RoomGuestAccess) return formatGuestAccessEvent(e); + else if constexpr (t == mtx::events::EventType::RoomMember) + return formatMemberEvent(e); return tr("%1 changed unknown state event %2.") .arg(displayName(QString::fromStdString(e.sender))) @@ -2958,34 +2960,27 @@ TimelineModel::joinReplacementRoom(const QString &id) } QString -TimelineModel::formatMemberEvent(const QString &id) +TimelineModel::formatMemberEvent( + const mtx::events::StateEvent &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if>(e); - if (!event) - return {}; - mtx::events::StateEvent const *prevEvent = nullptr; - if (!event->unsigned_data.replaces_state.empty()) { - auto tempPrevEvent = events.get(event->unsigned_data.replaces_state, event->event_id); + if (!event.unsigned_data.replaces_state.empty()) { + auto tempPrevEvent = events.get(event.unsigned_data.replaces_state, event.event_id); if (tempPrevEvent) { prevEvent = std::get_if>(tempPrevEvent); } } - QString user = QString::fromStdString(event->state_key); + QString user = QString::fromStdString(event.state_key); QString name = utils::replaceEmoji(displayName(user)); QString rendered; - QString sender = QString::fromStdString(event->sender); + QString sender = QString::fromStdString(event.sender); QString senderName = utils::replaceEmoji(displayName(sender)); // see table https://matrix.org/docs/spec/client_server/latest#m-room-member using namespace mtx::events::state; - switch (event->content.membership) { + switch (event.content.membership) { case Membership::Invite: rendered = tr("%1 invited %2.").arg(senderName, name); break; @@ -2994,9 +2989,8 @@ TimelineModel::formatMemberEvent(const QString &id) QString oldName = utils::replaceEmoji( QString::fromStdString(prevEvent->content.display_name).toHtmlEscaped()); - bool displayNameChanged = - prevEvent->content.display_name != event->content.display_name; - bool avatarChanged = prevEvent->content.avatar_url != event->content.avatar_url; + bool displayNameChanged = prevEvent->content.display_name != event.content.display_name; + bool avatarChanged = prevEvent->content.avatar_url != event.content.avatar_url; if (displayNameChanged && avatarChanged) rendered = tr("%1 has changed their avatar and changed their " @@ -3011,30 +3005,30 @@ TimelineModel::formatMemberEvent(const QString &id) // the case of nothing changed but join follows join shouldn't happen, so // just show it as join } else { - if (event->content.join_authorised_via_users_server.empty()) + if (event.content.join_authorised_via_users_server.empty()) rendered = tr("%1 joined.").arg(name); else rendered = tr("%1 joined via authorisation from %2's server.") .arg(name, - QString::fromStdString(event->content.join_authorised_via_users_server)); + QString::fromStdString(event.content.join_authorised_via_users_server)); } break; case Membership::Leave: if (!prevEvent || prevEvent->content.membership == Membership::Join) { - if (event->state_key == event->sender) + if (event.state_key == event.sender) rendered = tr("%1 left the room.").arg(name); else rendered = tr("%2 kicked %1.").arg(name, senderName); } else if (prevEvent->content.membership == Membership::Invite) { - if (event->state_key == event->sender) + if (event.state_key == event.sender) rendered = tr("%1 rejected their invite.").arg(name); else rendered = tr("%2 revoked the invite to %1.").arg(name, senderName); } else if (prevEvent->content.membership == Membership::Ban) { rendered = tr("%2 unbanned %1.").arg(name, senderName); } else if (prevEvent->content.membership == Membership::Knock) { - if (event->state_key == event->sender) + if (event.state_key == event.sender) rendered = tr("%1 redacted their knock.").arg(name); else rendered = tr("%2 rejected the knock from %1.").arg(name, senderName); @@ -3053,8 +3047,8 @@ TimelineModel::formatMemberEvent(const QString &id) break; } - if (event->content.reason != "") { - rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event->content.reason)); + if (event.content.reason != "") { + rendered += " " + tr("Reason: %1").arg(QString::fromStdString(event.content.reason)); } return rendered; diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 2b22ad61..8f787f21 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -310,7 +310,8 @@ public: Q_INVOKABLE bool showAcceptKnockButton(const QString &id); Q_INVOKABLE void acceptKnock(const QString &id); Q_INVOKABLE void joinReplacementRoom(const QString &id); - Q_INVOKABLE QString formatMemberEvent(const QString &id); + Q_INVOKABLE QString + formatMemberEvent(const mtx::events::StateEvent &event) const; Q_INVOKABLE QString formatJoinRuleEvent(const QString &id); QString formatHistoryVisibilityEvent( const mtx::events::StateEvent &event) const; diff --git a/src/voip/CallManager.cpp b/src/voip/CallManager.cpp index 5479ba31..46679e71 100644 --- a/src/voip/CallManager.cpp +++ b/src/voip/CallManager.cpp @@ -92,7 +92,8 @@ CallManager::CallManager(QObject *parent) if (QGuiApplication::platformName() != QStringLiteral("wayland")) { // Selected by default screenShareType_ = ScreenShareType::X11; - std::swap(screenShareTypes_[0], screenShareTypes_[1]); + if (screenShareTypes_.size() >= 2) + std::swap(screenShareTypes_[0], screenShareTypes_[1]); } } #endif From aef0cb988438a125802c90ee41032c13ef6f8fec Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 14 Aug 2023 01:11:27 +0200 Subject: [PATCH 07/32] Show reactions again --- resources/qml/MessageView.qml | 40 +++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 2f50789f..976312f2 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -63,7 +63,7 @@ Item { id: wrapper ListView.delayRemove: true width: chat.delegateMaxWidth - height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight, 10) + height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter //room: chatRoot.roommodel @@ -81,6 +81,10 @@ Item { required property string userId required property string userName required property string threadId + required property int userPowerlevel + required property var reactions + + property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (wrapper.threadId ? 6 : 0) // align bubble with section header data: [ Loader { @@ -96,6 +100,7 @@ Item { property date timestamp: wrapper.timestamp property string userId: wrapper.userId property string userName: wrapper.userName + property string userPowerlevel: wrapper.userPowerlevel active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent //asynchronous: true @@ -112,7 +117,7 @@ Item { ColumnLayout { id: contentColumn Layout.fillWidth: true - Layout.leftMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (wrapper.threadId ? 6 : 0) // align bubble with section header + Layout.leftMargin: wrapper.avatarMargin // align bubble with section header AbstractButton { id: replyRow @@ -179,6 +184,37 @@ Item { } }, + Reactions { + id: reactionRow + + eventId: wrapper.eventId + layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight + reactions: wrapper.reactions + width: wrapper.width - wrapper.avatarMargin + x: wrapper.avatarMargin + + anchors { + //left: row.bubbleOnRight ? undefined : row.left + //right: row.bubbleOnRight ? row.right : undefined + top: gridContainer.bottom + topMargin: -4 + } + }, + Rectangle { + id: unreadRow + + color: palette.highlight + height: visible ? 3 : 0 + visible: (wrapper.index > 0 && (room.fullyReadEventId == wrapper.eventId)) + + anchors { + left: parent.left + right: parent.right + top: reactionRow.bottom + topMargin: 5 + } + }, + Rectangle { width: Math.min(contentColumn.implicitWidth, contentColumn.width) height: contentColumn.implicitHeight From b187440e68334b828d7271ac3d51dfd4fd24b18d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Aug 2023 20:43:04 +0200 Subject: [PATCH 08/32] Reimplement reply delegate by moving out the timeline event without layout --- CMakeLists.txt | 1 + resources/qml/Completer.qml | 5 - resources/qml/ForwardCompleter.qml | 18 +- resources/qml/MatrixText.qml | 23 +-- resources/qml/MessageView.qml | 313 +---------------------------- resources/qml/Reactions.qml | 2 +- resources/qml/ReplyPopup.qml | 16 +- resources/qml/RoomList.qml | 2 +- resources/qml/Root.qml | 25 ++- resources/qml/TimelineEvent.qml | 255 +++++++++++++++++++++++ resources/qml/TimelineRow.qml | 3 +- resources/qml/TopBar.qml | 17 +- resources/qml/delegates/Reply.qml | 132 ++++++------ 13 files changed, 352 insertions(+), 460 deletions(-) create mode 100644 resources/qml/TimelineEvent.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 01070a82..548d5303 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -713,6 +713,7 @@ set(QML_SOURCES resources/qml/UploadBox.qml resources/qml/MessageInput.qml resources/qml/MessageView.qml + resources/qml/TimelineEvent.qml resources/qml/PrivacyScreen.qml resources/qml/Reactions.qml resources/qml/ReplyPopup.qml diff --git a/resources/qml/Completer.qml b/resources/qml/Completer.qml index 590d5bb8..9ebe0a40 100644 --- a/resources/qml/Completer.qml +++ b/resources/qml/Completer.qml @@ -145,7 +145,6 @@ Control { roleValue: "user" RowLayout { - anchors.centerIn: centerRowContent ? parent : undefined spacing: rowSpacing @@ -171,7 +170,6 @@ Control { roleValue: "emoji" RowLayout { - anchors.centerIn: parent spacing: rowSpacing @@ -207,7 +205,6 @@ Control { roleValue: "command" RowLayout { - anchors.centerIn: parent spacing: rowSpacing @@ -226,7 +223,6 @@ Control { roleValue: "room" RowLayout { - anchors.centerIn: centerRowContent ? parent : undefined spacing: rowSpacing @@ -251,7 +247,6 @@ Control { roleValue: "roomAliases" RowLayout { - anchors.centerIn: parent spacing: rowSpacing diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 0174e0f6..6f95c663 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -54,24 +54,8 @@ Popup { Reply { id: replyPreview - property var modelData: room ? room.getDump(mid, "") : {} - - blurhash: modelData.blurhash ?? "" - body: modelData.body ?? "" - encryptionError: modelData.encryptionError ?? "" - eventId: modelData.eventId ?? "" - filename: modelData.filename ?? "" - filesize: modelData.filesize ?? "" - formattedBody: modelData.formattedBody ?? "" - isOnlyEmoji: modelData.isOnlyEmoji ?? false - originalWidth: modelData.originalWidth ?? 0 - proportionalHeight: modelData.proportionalHeight ?? 1 - type: modelData.type ?? MtxEvent.UnknownMessage - typeString: modelData.typeString ?? "" - url: modelData.url ?? "" + eventId: mid userColor: TimelineManager.userColor(modelData.userId, palette.window) - userId: modelData.userId ?? "" - userName: modelData.userName ?? "" width: parent.width } MatrixTextField { diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index de15e078..bf953d56 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -11,25 +11,24 @@ TextArea { property alias cursorShape: cs.cursorShape - leftInset: 0 - bottomInset: 0 - rightInset: 0 - topInset: 0 - leftPadding: 0 - bottomPadding: 0 - rightPadding: 0 - topPadding: 0 - background: null - ToolTip.text: hoveredLink ToolTip.visible: hoveredLink || false + background: null + bottomInset: 0 + bottomPadding: 0 // this always has to be enabled, otherwise you can't click links anymore! //enabled: selectByMouse color: palette.text focus: false + leftInset: 0 + leftPadding: 0 readOnly: true + rightInset: 0 + rightPadding: 0 selectByMouse: !Settings.mobileMode textFormat: TextEdit.RichText + topInset: 0 + topPadding: 0 wrapMode: Text.Wrap // Setting a tooltip delay makes the hover text empty .-. @@ -40,8 +39,8 @@ TextArea { onLinkActivated: Nheko.openLink(link) // propagate events up - onPressAndHold: (event) => event.accepted = false - onPressed: (event) => event.accepted = (event.button == Qt.LeftButton) + onPressAndHold: event => event.accepted = false + onPressed: event => event.accepted = (event.button == Qt.LeftButton) NhekoCursorShape { id: cs diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 976312f2..2df9b731 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -59,7 +59,7 @@ Item { spacing: 2 verticalLayoutDirection: ListView.BottomToTop - delegate: EventDelegateChooser { + delegate: TimelineEvent { id: wrapper ListView.delayRemove: true width: chat.delegateMaxWidth @@ -69,7 +69,6 @@ Item { required property var day required property bool isSender - required property bool isStateEvent //required property var previousMessageDay //required property bool previousMessageIsStateEvent //required property string previousMessageUserId @@ -145,6 +144,8 @@ Item { } ColumnLayout { + spacing: 0 + AbstractButton { id: replyUserButton Layout.fillWidth: true @@ -222,314 +223,6 @@ Item { opacity: 0.2 } ] - - EventDelegateChoice { - roleValues: [ - MtxEvent.TextMessage, - MtxEvent.NoticeMessage, - MtxEvent.ElementEffectMessage, - MtxEvent.UnknownMessage, - ] - TextMessage { - keepFullText: true - required property string userId - required property string userName - required property string formattedBody - required property int type - - color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text - font.italic: type == MtxEvent.NoticeMessage - formatted: formattedBody - - Layout.fillWidth: true - //Layout.maximumWidth: implicitWidth - - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.EmoteMessage, - ] - TextMessage { - keepFullText: true - required property string userId - required property string userName - required property string formattedBody - - formatted: TimelineManager.escapeEmoji(userName) + " " + formattedBody - - color: TimelineManager.userColor(userId, palette.base) - font.italic: true - - Layout.fillWidth: true - //Layout.maximumWidth: implicitWidth - - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.CanonicalAlias, - MtxEvent.ServerAcl, - MtxEvent.Name, - MtxEvent.Topic, - MtxEvent.Avatar, - MtxEvent.PinnedEvents, - MtxEvent.ImagePackInRoom, - MtxEvent.SpaceParent, - MtxEvent.RoomCreate, - MtxEvent.PowerLevels, - MtxEvent.PolicyRuleUser, - MtxEvent.PolicyRuleRoom, - MtxEvent.PolicyRuleServer, - MtxEvent.RoomJoinRules, - MtxEvent.RoomHistoryVisibility, - MtxEvent.RoomGuestAccess, - ] - TextMessage { - keepFullText: true - - required property string userId - required property string userName - required property string formattedStateEvent - - isOnlyEmoji: false - text: formattedStateEvent - formatted: '' - body: '' - horizontalAlignment: Text.AlignHCenter - - color: palette.buttonText - font.italic: true - - Layout.fillWidth: true - //Layout.maximumWidth: implicitWidth - - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.CallInvite, - ] - TextMessage { - keepFullText: true - - required property string userId - required property string userName - required property string callType - - isOnlyEmoji: false - body: formatted - formatted: { - switch (callType) { - case "voice": - return qsTr("%1 placed a voice call.").arg(TimelineManager.escapeEmoji(userName)); - case "video": - return qsTr("%1 placed a video call.").arg(TimelineManager.escapeEmoji(userName)); - default: - return qsTr("%1 placed a call.").arg(TimelineManager.escapeEmoji(userName)); - } - } - - color: palette.buttonText - font.italic: true - - Layout.fillWidth: true - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.CallAnswer, - MtxEvent.CallReject, - MtxEvent.CallSelectAnswer, - MtxEvent.CallHangUp, - MtxEvent.CallCandidates, - MtxEvent.CallNegotiate, - ] - TextMessage { - keepFullText: true - - required property string userId - required property string userName - required property int type - - isOnlyEmoji: false - body: formatted - formatted: { - switch (type) { - case MtxEvent.CallAnswer: - return qsTr("%1 answered the call.").arg(TimelineManager.escapeEmoji(userName)); - case MtxEvent.CallReject: - return qsTr("%1 rejected the call.").arg(TimelineManager.escapeEmoji(userName)); - case MtxEvent.CallSelectAnswer: - return qsTr("%1 selected answer.").arg(TimelineManager.escapeEmoji(userName)); - case MtxEvent.CallHangUp: - return qsTr("%1 ended the call.").arg(TimelineManager.escapeEmoji(userName)); - case MtxEvent.CallCandidates: - return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); - case MtxEvent.CallNegotiate: - return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); - } - } - - color: palette.buttonText - font.italic: true - - Layout.fillWidth: true - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.ImageMessage, - MtxEvent.Sticker, - ] - ImageMessage { - Layout.fillWidth: true - - containerHeight: timelineView.height - Layout.maximumWidth: tempWidth - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.FileMessage, - ] - FileMessage { - Layout.fillWidth: true - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.VideoMessage, - MtxEvent.AudioMessage, - ] - PlayableMediaMessage { - Layout.fillWidth: true - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.Encrypted, - ] - Encrypted { - Layout.fillWidth: true - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.Encryption, - ] - EncryptionEnabled { - Layout.fillWidth: true - } - } - - - EventDelegateChoice { - roleValues: [ - MtxEvent.Redacted - ] - - Redacted { - Layout.fillWidth: true - - required property string userId - required property string userName - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.Member - ] - - ColumnLayout { - id: member - - required property string userId - required property string userName - - required property bool isReply - required property Room room - required property string formattedStateEvent - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: tombstone.isReply - keepFullText: true - isStateEvent: true - Layout.fillWidth: true - formatted: member.formattedStateEvent - } - - Button { - visible: room.showAcceptKnockButton(eventId) - Layout.alignment: Qt.AlignHCenter - text: qsTr("Allow them in") - onClicked: room.acceptKnock(member.eventId) - } - - } - } - - EventDelegateChoice { - roleValues: [ - MtxEvent.Tombstone - ] - - ColumnLayout { - id: tombstone - - required property string userId - required property string userName - - required property string body - required property bool isReply - required property Room room - required property string eventId - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: tombstone.isReply - keepFullText: true - isStateEvent: true - Layout.fillWidth: true - formatted: qsTr("This room was replaced for the following reason: %1").arg(tombstone.body) - } - - Button { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Go to replacement room") - onClicked: tombstone.room.joinReplacementRoom(tombstone.eventId) - } - - } - } - - EventDelegateChoice { - roleValues: [ - ] - MatrixText { - Layout.fillWidth: true - - required property string typeString - - text: "Unsupported: " + typeString - - required property string userId - required property string userName - } - } } footer: Item { anchors.horizontalCenter: parent.horizontalCenter diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml index eff62fc1..5b994145 100644 --- a/resources/qml/Reactions.qml +++ b/resources/qml/Reactions.qml @@ -74,10 +74,10 @@ Flow { anchors.verticalCenter: divider.verticalCenter fillMode: Image.PreserveAspectFit height: textMetrics.height + mipmap: true source: modelData.key.startsWith("mxc://") ? (modelData.key.replace("mxc://", "image://MxcImage/") + "?scale") : "" visible: modelData.key.startsWith("mxc://") width: textMetrics.height - mipmap: true } Rectangle { id: divider diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index ce24297c..64c58e56 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -29,22 +29,8 @@ Rectangle { anchors.rightMargin: replyPopup.width < 450 ? 2 * (22 + 16) : 3 * (22 + 16) anchors.top: parent.top anchors.topMargin: Nheko.paddingSmall - blurhash: modelData.blurhash ?? "" - body: modelData.body ?? "" - encryptionError: modelData.encryptionError ?? 0 - eventId: modelData.eventId ?? "" - filename: modelData.filename ?? "" - filesize: modelData.filesize ?? "" - formattedBody: modelData.formattedBody ?? "" - isOnlyEmoji: modelData.isOnlyEmoji ?? false - originalWidth: modelData.originalWidth ?? 0 - proportionalHeight: modelData.proportionalHeight ?? 1 - type: modelData.type ?? MtxEvent.UnknownMessage - typeString: modelData.typeString ?? "" - url: modelData.url ?? "" + eventId: room.reply ?? "" userColor: TimelineManager.userColor(modelData.userId, palette.window) - userId: modelData.userId ?? "" - userName: modelData.userName ?? "" visible: room && room.reply width: parent.width } diff --git a/resources/qml/RoomList.qml b/resources/qml/RoomList.qml index 20e5b95b..0c432189 100644 --- a/resources/qml/RoomList.qml +++ b/resources/qml/RoomList.qml @@ -728,9 +728,9 @@ Page { } Platform.MenuItem { text: qsTr("Mark as read") + onTriggered: Rooms.getRoomById(roomContextMenu.roomid).markRoomAsRead() } - Platform.MenuItem { text: qsTr("Room settings") diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 1e8a6a27..09a8f442 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -355,7 +355,6 @@ Pane { onAccepted: UIA.continue3pidReceived() } - Connections { function onConfirm3pidToken() { uiaConfirmationLinkDialog.open(); @@ -363,6 +362,18 @@ Pane { function onEmail() { uiaEmailPrompt.show(); } + function onFallbackAuth(fallback) { + var component = Qt.createComponent("qrc:/resources/qml/dialogs/FallbackAuthDialog.qml"); + if (component.status == Component.Ready) { + var dialog = component.createObject(timelineRoot, { + "fallback": fallback + }); + dialog.show(); + destroyOnClose(dialog); + } else { + console.error("Failed to create component: " + component.errorString()); + } + } function onPassword() { console.log("UIA: password needed"); uiaPassPrompt.show(); @@ -385,18 +396,6 @@ Pane { console.error("Failed to create component: " + component.errorString()); } } - function onFallbackAuth(fallback) { - var component = Qt.createComponent("qrc:/resources/qml/dialogs/FallbackAuthDialog.qml"); - if (component.status == Component.Ready) { - var dialog = component.createObject(timelineRoot, { - "fallback": fallback - }); - dialog.show(); - destroyOnClose(dialog); - } else { - console.error("Failed to create component: " + component.errorString()); - } - } target: UIA } diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml new file mode 100644 index 00000000..787fb7dc --- /dev/null +++ b/resources/qml/TimelineEvent.qml @@ -0,0 +1,255 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./components" +import "./delegates" +import "./emoji" +import "./ui" +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.13 +import im.nheko 1.0 + +EventDelegateChooser { + id: wrapper + + required property bool isStateEvent + + EventDelegateChoice { + roleValues: [MtxEvent.TextMessage, MtxEvent.NoticeMessage, MtxEvent.ElementEffectMessage, MtxEvent.UnknownMessage,] + + TextMessage { + required property string formattedBody + required property int type + required property string userId + required property string userName + + Layout.fillWidth: true + //Layout.maximumWidth: implicitWidth + + color: type == MtxEvent.NoticeMessage ? palette.buttonText : palette.text + font.italic: type == MtxEvent.NoticeMessage + formatted: formattedBody + keepFullText: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.EmoteMessage,] + + TextMessage { + required property string formattedBody + required property string userId + required property string userName + + Layout.fillWidth: true + //Layout.maximumWidth: implicitWidth + + color: TimelineManager.userColor(userId, palette.base) + font.italic: true + formatted: TimelineManager.escapeEmoji(userName) + " " + formattedBody + keepFullText: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.CanonicalAlias, MtxEvent.ServerAcl, MtxEvent.Name, MtxEvent.Topic, MtxEvent.Avatar, MtxEvent.PinnedEvents, MtxEvent.ImagePackInRoom, MtxEvent.SpaceParent, MtxEvent.RoomCreate, MtxEvent.PowerLevels, MtxEvent.PolicyRuleUser, MtxEvent.PolicyRuleRoom, MtxEvent.PolicyRuleServer, MtxEvent.RoomJoinRules, MtxEvent.RoomHistoryVisibility, MtxEvent.RoomGuestAccess,] + + TextMessage { + required property string formattedStateEvent + required property string userId + required property string userName + + Layout.fillWidth: true + //Layout.maximumWidth: implicitWidth + + body: '' + color: palette.buttonText + font.italic: true + formatted: '' + horizontalAlignment: Text.AlignHCenter + isOnlyEmoji: false + keepFullText: true + text: formattedStateEvent + } + } + EventDelegateChoice { + roleValues: [MtxEvent.CallInvite,] + + TextMessage { + required property string callType + required property string userId + required property string userName + + Layout.fillWidth: true + body: formatted + color: palette.buttonText + font.italic: true + formatted: { + switch (callType) { + case "voice": + return qsTr("%1 placed a voice call.").arg(TimelineManager.escapeEmoji(userName)); + case "video": + return qsTr("%1 placed a video call.").arg(TimelineManager.escapeEmoji(userName)); + default: + return qsTr("%1 placed a call.").arg(TimelineManager.escapeEmoji(userName)); + } + } + isOnlyEmoji: false + keepFullText: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.CallAnswer, MtxEvent.CallReject, MtxEvent.CallSelectAnswer, MtxEvent.CallHangUp, MtxEvent.CallCandidates, MtxEvent.CallNegotiate,] + + TextMessage { + required property int type + required property string userId + required property string userName + + Layout.fillWidth: true + body: formatted + color: palette.buttonText + font.italic: true + formatted: { + switch (type) { + case MtxEvent.CallAnswer: + return qsTr("%1 answered the call.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallReject: + return qsTr("%1 rejected the call.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallSelectAnswer: + return qsTr("%1 selected answer.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallHangUp: + return qsTr("%1 ended the call.").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallCandidates: + return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); + case MtxEvent.CallNegotiate: + return qsTr("%1 is negotiating the call...").arg(TimelineManager.escapeEmoji(userName)); + } + } + isOnlyEmoji: false + keepFullText: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.ImageMessage, MtxEvent.Sticker,] + + ImageMessage { + Layout.fillWidth: true + Layout.maximumWidth: tempWidth + containerHeight: timelineView.height + } + } + EventDelegateChoice { + roleValues: [MtxEvent.FileMessage,] + + FileMessage { + Layout.fillWidth: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.VideoMessage, MtxEvent.AudioMessage,] + + PlayableMediaMessage { + Layout.fillWidth: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.Encrypted,] + + Encrypted { + Layout.fillWidth: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.Encryption,] + + EncryptionEnabled { + Layout.fillWidth: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.Redacted] + + Redacted { + required property string userId + required property string userName + + Layout.fillWidth: true + } + } + EventDelegateChoice { + roleValues: [MtxEvent.Member] + + ColumnLayout { + id: member + + required property string formattedStateEvent + required property bool isReply + required property Room room + required property string userId + required property string userName + + NoticeMessage { + Layout.fillWidth: true + body: formatted + formatted: member.formattedStateEvent + isOnlyEmoji: false + isReply: member.isReply + isStateEvent: true + keepFullText: true + } + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Allow them in") + visible: room.showAcceptKnockButton(eventId) + + onClicked: room.acceptKnock(member.eventId) + } + } + } + EventDelegateChoice { + roleValues: [MtxEvent.Tombstone] + + ColumnLayout { + id: tombstone + + required property string body + required property string eventId + required property bool isReply + required property Room room + required property string userId + required property string userName + + NoticeMessage { + Layout.fillWidth: true + body: formatted + formatted: qsTr("This room was replaced for the following reason: %1").arg(tombstone.body) + isOnlyEmoji: false + isReply: tombstone.isReply + isStateEvent: true + keepFullText: true + } + Button { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Go to replacement room") + + onClicked: tombstone.room.joinReplacementRoom(tombstone.eventId) + } + } + } + EventDelegateChoice { + roleValues: [] + + MatrixText { + required property string typeString + required property string userId + required property string userName + + Layout.fillWidth: true + text: "Unsupported: " + typeString + } + } +} diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 64fa80b1..539882ed 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -147,7 +147,8 @@ AbstractButton { columns: Settings.bubbles ? 1 : 2 rowSpacing: 0 rows: Settings.bubbles ? 3 : 2 -/* + + /* anchors { left: parent.left leftMargin: 4 diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 699595e6..4c70348b 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -286,24 +286,9 @@ Pane { property var e: room ? room.getDump(modelData, "pins") : {} Layout.fillWidth: true - Layout.preferredHeight: height - blurhash: e.blurhash ?? "" - body: e.body ?? "" - encryptionError: e.encryptionError ?? 0 + //Layout.preferredHeight: height eventId: e.eventId ?? "" - filename: e.filename ?? "" - filesize: e.filesize ?? "" - formattedBody: e.formattedBody ?? "" - isOnlyEmoji: e.isOnlyEmoji ?? false - keepFullText: true - originalWidth: e.originalWidth ?? 0 - proportionalHeight: e.proportionalHeight ?? 1 - type: e.type ?? MtxEvent.UnknownMessage - typeString: e.typeString ?? "" - url: e.url ?? "" userColor: TimelineManager.userColor(e.userId, palette.window) - userId: e.userId ?? "" - userName: e.userName ?? "" Connections { function onPinnedMessagesChanged() { diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 64eb65a3..55a376f7 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -14,102 +14,96 @@ AbstractButton { id: r property color userColor: "red" - property double proportionalHeight - property int type - property string typeString - property int originalWidth - property string blurhash - property string body - property string formattedBody - property string eventId - property string filename - property string filesize - property string url - property bool isOnlyEmoji - property bool isStateEvent - property string userId - property string userName - property string thumbnailUrl - property string roomTopic - property string roomName - property string callType - property int duration - property int encryptionError - property int relatedEventCacheBuster - property int maxWidth property bool keepFullText: false - height: replyContainer.height - implicitHeight: replyContainer.height - implicitWidth: visible? colorLine.width+Math.max(replyContainer.implicitWidth,userName_.fullTextWidth) : 0 // visible? seems to be causing issues + required property string eventId + + property var room_: room + + property string userId: eventId ? room.dataById(eventId, Room.UserId, "") : "" + property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : "" + implicitHeight: replyContainer.implicitHeight + implicitWidth: replyContainer.implicitWidth NhekoCursorShape { anchors.fill: parent cursorShape: Qt.PointingHandCursor } - Rectangle { - id: colorLine - - anchors.top: replyContainer.top - anchors.bottom: replyContainer.bottom - width: 4 - color: TimelineManager.userColor(userId, palette.base) - } - onClicked: { - let link = reply.child.linkAt != undefined && reply.child.linkAt(pressX-colorLine.width, pressY - userName_.implicitHeight); + let link = reply.child.linkAt != undefined && reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight); if (link) { Nheko.openLink(link) } else { room.showEvent(r.eventId) } } - onPressAndHold: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(pressX-colorLine.width, pressY - userName_.implicitHeight), r.eventId) + onPressAndHold: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(pressX-colorline.width, pressY - userName_.implicitHeight), r.eventId) - ColumnLayout { - id: replyContainer + contentItem: TimelineEvent { + id: timelineEvent - anchors.left: colorLine.right - width: parent.width - 4 - spacing: 0 + isStateEvent: false + room: room_ + eventId: r.eventId + replyTo: "" - TapHandler { - acceptedButtons: Qt.RightButton - onSingleTapped: replyContextMenu.show(reply.child.copyText, reply.child.linkAt(eventPoint.position.x, eventPoint.position.y - userName_.implicitHeight), r.eventId) - gesturePolicy: TapHandler.ReleaseWithinBounds - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - } + width: parent.width + height: replyContainer.implicitHeight - AbstractButton { - Layout.leftMargin: 4 - Layout.fillWidth: true - contentItem: ElidedLabel { - id: userName_ - fullText: userName - color: r.userColor - textFormat: Text.RichText - width: parent.width - elideWidth: width + //height: replyContainer.implicitHeight + data: GridLayout { + id: replyContainer + + width: parent.width + columns: 2 + rows: 2 + columnSpacing: Nheko.paddingMedium + rowSpacing: Nheko.paddingSmall + + Rectangle { + id: colorline + + Layout.preferredWidth: 4 + Layout.rowSpan: 2 + Layout.fillHeight: true + + Layout.row: 0 + Layout.column: 0 + + color: TimelineManager.userColor(r.userId, palette.base) } - onClicked: room.openUserProfile(userId) - } - Rectangle { - Layout.leftMargin: 4 - Layout.preferredHeight: 20 - Layout.fillWidth: true - color: "green" - } + AbstractButton { + id: usernameBtn + Layout.fillWidth: true + Layout.row: 0 + Layout.column: 1 + + contentItem: ElidedLabel { + id: userName_ + fullText: r.userName + color: r.userColor + textFormat: Text.RichText + width: parent.width + elideWidth: width + } + onClicked: room.openUserProfile(r.userId) + } + + data: [ + colorline, usernameBtn, timelineEvent.main, + ] + + } } - Rectangle { + background: Rectangle { id: backgroundItem z: -1 - anchors.fill: replyContainer - property color userColor: TimelineManager.userColor(userId, palette.base) + property color userColor: TimelineManager.userColor(r.userId, palette.base) property color bgColor: palette.base color: Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.1)) } From 55107ed0ce6d457b82fa2e64878712198a6c0f1f Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 25 Aug 2023 21:03:05 +0200 Subject: [PATCH 09/32] Fix some reply layouting loops --- resources/qml/ForwardCompleter.qml | 2 +- resources/qml/ReplyPopup.qml | 2 +- resources/qml/TopBar.qml | 2 +- resources/qml/delegates/Reply.qml | 6 ++---- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/resources/qml/ForwardCompleter.qml b/resources/qml/ForwardCompleter.qml index 6f95c663..caaea440 100644 --- a/resources/qml/ForwardCompleter.qml +++ b/resources/qml/ForwardCompleter.qml @@ -56,7 +56,7 @@ Popup { eventId: mid userColor: TimelineManager.userColor(modelData.userId, palette.window) - width: parent.width + maxWidth: parent.width } MatrixTextField { id: roomTextInput diff --git a/resources/qml/ReplyPopup.qml b/resources/qml/ReplyPopup.qml index 64c58e56..5657c08a 100644 --- a/resources/qml/ReplyPopup.qml +++ b/resources/qml/ReplyPopup.qml @@ -32,7 +32,7 @@ Rectangle { eventId: room.reply ?? "" userColor: TimelineManager.userColor(modelData.userId, palette.window) visible: room && room.reply - width: parent.width + maxWidth: parent.width - anchors.leftMargin - anchors.rightMargin } ImageButton { id: closeReplyButton diff --git a/resources/qml/TopBar.qml b/resources/qml/TopBar.qml index 4c70348b..aba91763 100644 --- a/resources/qml/TopBar.qml +++ b/resources/qml/TopBar.qml @@ -285,7 +285,7 @@ Pane { property var e: room ? room.getDump(modelData, "pins") : {} - Layout.fillWidth: true + maxWidth: pinnedMessages.width //Layout.preferredHeight: height eventId: e.eventId ?? "" userColor: TimelineManager.userColor(e.userId, palette.window) diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 55a376f7..52cc982d 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -24,6 +24,7 @@ AbstractButton { property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : "" implicitHeight: replyContainer.implicitHeight implicitWidth: replyContainer.implicitWidth + property int maxWidth NhekoCursorShape { anchors.fill: parent @@ -48,14 +49,11 @@ AbstractButton { eventId: r.eventId replyTo: "" - width: parent.width - height: replyContainer.implicitHeight - //height: replyContainer.implicitHeight data: GridLayout { id: replyContainer - width: parent.width + width: r.maxWidth columns: 2 rows: 2 columnSpacing: Nheko.paddingMedium From 9ed0e887bd1552f4fcf61be7c265c748932bdca5 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 26 Aug 2023 11:10:41 +0200 Subject: [PATCH 10/32] Fix reply backgrounds in images --- resources/qml/TimelineEvent.qml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml index 787fb7dc..30ec27b2 100644 --- a/resources/qml/TimelineEvent.qml +++ b/resources/qml/TimelineEvent.qml @@ -137,6 +137,9 @@ EventDelegateChooser { roleValues: [MtxEvent.ImageMessage, MtxEvent.Sticker,] ImageMessage { + required property string userId + required property string userName + Layout.fillWidth: true Layout.maximumWidth: tempWidth containerHeight: timelineView.height @@ -146,6 +149,9 @@ EventDelegateChooser { roleValues: [MtxEvent.FileMessage,] FileMessage { + required property string userId + required property string userName + Layout.fillWidth: true } } @@ -153,6 +159,9 @@ EventDelegateChooser { roleValues: [MtxEvent.VideoMessage, MtxEvent.AudioMessage,] PlayableMediaMessage { + required property string userId + required property string userName + Layout.fillWidth: true } } @@ -160,6 +169,9 @@ EventDelegateChooser { roleValues: [MtxEvent.Encrypted,] Encrypted { + required property string userId + required property string userName + Layout.fillWidth: true } } @@ -167,6 +179,9 @@ EventDelegateChooser { roleValues: [MtxEvent.Encryption,] EncryptionEnabled { + required property string userId + required property string userName + Layout.fillWidth: true } } From aff58272c6518281bf535f604dcd3c5addd6c652 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 27 Aug 2023 12:38:48 +0200 Subject: [PATCH 11/32] Improve image size limiting --- resources/qml/TimelineEvent.qml | 1 + resources/qml/delegates/ImageMessage.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml index 30ec27b2..3e98a2cf 100644 --- a/resources/qml/TimelineEvent.qml +++ b/resources/qml/TimelineEvent.qml @@ -142,6 +142,7 @@ EventDelegateChooser { Layout.fillWidth: true Layout.maximumWidth: tempWidth + Layout.maximumHeight: timelineView.height / 8 containerHeight: timelineView.height } } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 48b7c5e4..504aba0d 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -117,6 +117,7 @@ AbstractButton { source: url != "" ? (url.replace("mxc://", "image://MxcImage/") + "?scale") : "" asynchronous: true fillMode: Image.PreserveAspectFit + horizontalAlignment: Image.AlignLeft smooth: true mipmap: true From 1744f38e0ff4db526ad2713f9b7e050a10839b35 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 28 Aug 2023 02:15:20 +0200 Subject: [PATCH 12/32] Reenable status part of timeline messages --- resources/qml/MessageView.qml | 121 +++++++++++++++++++++++++++++----- resources/qml/TimelineRow.qml | 14 ++-- 2 files changed, 108 insertions(+), 27 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 2df9b731..a013c78b 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -69,21 +69,23 @@ Item { required property var day required property bool isSender - //required property var previousMessageDay - //required property bool previousMessageIsStateEvent - //required property string previousMessageUserId required property int index property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) + required property date timestamp required property string userId required property string userName required property string threadId required property int userPowerlevel + required property bool isEdited + required property bool isEncrypted required property var reactions + required property int status + required property int trustlevel - property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (wrapper.threadId ? 6 : 0) // align bubble with section header + property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header data: [ Loader { @@ -107,16 +109,36 @@ Item { visible: status == Loader.Ready z: 4 }, - GridLayout { + RowLayout { id: gridContainer width: wrapper.width y: section.visible && section.active ? section.y + section.height : 0 + Item { + Layout.preferredWidth: wrapper.avatarMargin + } + + AbstractButton { + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered + Layout.fillHeight: true + visible: wrapper.threadId + Layout.preferredWidth: 4 + + onClicked: room.thread = wrapper.threadId + + Rectangle { + id: threadLine + + anchors.fill: parent + color: TimelineManager.userColor(wrapper.threadId, palette.base) + } + } ColumnLayout { id: contentColumn Layout.fillWidth: true - Layout.leftMargin: wrapper.avatarMargin // align bubble with section header AbstractButton { id: replyRow @@ -177,11 +199,81 @@ Item { ] } - Rectangle { - color: 'yellow' - Layout.preferredWidth: 100 - Layout.preferredHeight: 20 - Layout.alignment: Qt.AlignRight | Qt.AlignTop + RowLayout { + id: metadata + + property int iconSize: Math.floor(fontMetrics.ascent * scaling) + property double scaling: Settings.bubbles ? 0.75 : 1 + + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.preferredWidth: implicitWidth + spacing: 2 + visible: !isStateEvent + + StatusIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + eventId: wrapper.eventId + height: parent.iconSize + status: wrapper.status + width: parent.iconSize + } + Image { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Edited") + ToolTip.visible: editHovered.hovered + height: parent.iconSize + source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((wrapper.eventId == room.edit) ? palette.highlight : palette.buttonText) + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + visible: wrapper.isEdited || wrapper.eventId == room.edit + width: parent.iconSize + + HoverHandler { + id: editHovered + + } + } + ImageButton { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered + buttonTextColor: TimelineManager.userColor(wrapper.threadId, palette.base) + height: parent.iconSize + image: ":/icons/icons/ui/thread.svg" + visible: wrapper.threadId + width: parent.iconSize + + onClicked: room.thread = threadId + } + EncryptionIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + encrypted: wrapper.isEncrypted + height: parent.iconSize + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + trust: wrapper.trustlevel + visible: room.isEncrypted + width: parent.iconSize + } + Label { + id: ts + + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredWidth: implicitWidth + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: Qt.formatDateTime(wrapper.timestamp, Qt.DefaultLocaleLongDate) + ToolTip.visible: ma.hovered + color: palette.inactive.text + font.pointSize: fontMetrics.font.pointSize * parent.scaling + text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat) + + HoverHandler { + id: ma + + } + } } }, @@ -214,13 +306,6 @@ Item { top: reactionRow.bottom topMargin: 5 } - }, - - Rectangle { - width: Math.min(contentColumn.implicitWidth, contentColumn.width) - height: contentColumn.implicitHeight - color: "blue" - opacity: 0.2 } ] } diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml index 539882ed..fc5c6a76 100644 --- a/resources/qml/TimelineRow.qml +++ b/resources/qml/TimelineRow.qml @@ -232,7 +232,7 @@ AbstractButton { userName: r.userName } */ - Row { + RowLayout { id: metadata property int iconSize: Math.floor(fontMetrics.ascent * scaling) @@ -249,19 +249,17 @@ AbstractButton { visible: !isStateEvent StatusIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - anchors.verticalCenter: ts.verticalCenter + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter eventId: r.eventId height: parent.iconSize status: r.status width: parent.iconSize } Image { - Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Edited") ToolTip.visible: editHovered.hovered - anchors.verticalCenter: ts.verticalCenter height: parent.iconSize source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) sourceSize.height: parent.iconSize * Screen.devicePixelRatio @@ -275,11 +273,10 @@ AbstractButton { } } ImageButton { - Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Part of a thread") ToolTip.visible: hovered - anchors.verticalCenter: ts.verticalCenter buttonTextColor: TimelineManager.userColor(threadId, palette.base) height: parent.iconSize image: ":/icons/icons/ui/thread.svg" @@ -289,8 +286,7 @@ AbstractButton { onClicked: room.thread = threadId } EncryptionIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignTop - anchors.verticalCenter: ts.verticalCenter + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter encrypted: isEncrypted height: parent.iconSize sourceSize.height: parent.iconSize * Screen.devicePixelRatio From 0a3aeb0a1ab6638152a4d70f8319ad4587d32159 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Fri, 8 Sep 2023 01:41:57 +0200 Subject: [PATCH 13/32] Fix timeline menus --- resources/qml/MessageView.qml | 41 +++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index a013c78b..411254d8 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -84,9 +84,13 @@ Item { required property var reactions required property int status required property int trustlevel + required property int type + required property bool isEditable property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header + property alias hovered: messageHover.hovered + data: [ Loader { id: section @@ -109,6 +113,19 @@ Item { visible: status == Loader.Ready z: 4 }, + Rectangle { + anchors.fill: gridContainer + color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" + + // this looks better without margins + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) + } + }, RowLayout { id: gridContainer @@ -276,7 +293,23 @@ Item { } } }, - + Item { + id: messageActionsAnchor + anchors.fill: gridContainer + property alias hovered: messageHover.hovered + HoverHandler { + id: messageHover + onHoveredChanged: () => { + if (!Settings.mobileMode && hovered) { + if (!messageActions.hovered) { + messageActions.model = wrapper; + messageActions.attached = wrapper; + messageActions.anchors.bottomMargin = -gridContainer.y + } + } + } + } + }, Reactions { id: reactionRow @@ -346,15 +379,15 @@ Item { property Item attached: null // use comma to update on scroll - property var attachedPos: chat.contentY, attached ? chat.mapFromItem(attached, attached ? attached.width - width : 0, -height) : null property alias model: row.model hoverEnabled: true padding: Nheko.paddingSmall visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered) - x: attached ? attachedPos.x : 0 - y: attached ? attachedPos.y + Nheko.paddingSmall : 0 z: 10 + parent: chat.contentItem + anchors.bottom: attached?.top + anchors.right: attached?.right background: Rectangle { border.color: palette.buttonText From e96b552959afbc878247d1a5cca44ab991851ae8 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 12 Sep 2023 00:43:07 +0200 Subject: [PATCH 14/32] Fix power level indicator size --- resources/qml/MessageView.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 411254d8..eccd0496 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -689,6 +689,12 @@ Item { //anchors.horizontalCenter: parent.horizontalCenter powerlevel: userPowerlevel + height: fontMetrics.lineSpacing + width: fontMetrics.lineSpacing + + sourceSize.width: fontMetrics.lineSpacing + sourceSize.height: fontMetrics.lineSpacing + permissions: room ? room.permissions : null visible: isAdmin || isModerator } From 43c8e64ed35419e0a257b7b194aa5a9fa6b3c527 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 17 Sep 2023 23:11:24 +0200 Subject: [PATCH 15/32] Fix alignment of file messages and redactions --- resources/qml/MessageView.qml | 5 +++++ resources/qml/TimelineEvent.qml | 5 ++--- resources/qml/delegates/ImageMessage.qml | 9 +++++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index eccd0496..1add0ce7 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -216,6 +216,11 @@ Item { ] } + Item { + // spacer to fill width if needed + Layout.fillWidth: true + } + RowLayout { id: metadata diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml index 3e98a2cf..f1b6bd2a 100644 --- a/resources/qml/TimelineEvent.qml +++ b/resources/qml/TimelineEvent.qml @@ -141,8 +141,8 @@ EventDelegateChooser { required property string userName Layout.fillWidth: true - Layout.maximumWidth: tempWidth - Layout.maximumHeight: timelineView.height / 8 + //Layout.maximumWidth: tempWidth + //Layout.maximumHeight: timelineView.height / 8 containerHeight: timelineView.height } } @@ -181,7 +181,6 @@ EventDelegateChooser { EncryptionEnabled { required property string userId - required property string userName Layout.fillWidth: true } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 504aba0d..466041bd 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -11,6 +11,7 @@ import im.nheko AbstractButton { required property int type required property int originalWidth + required property int originalHeight required property double proportionalHeight required property string url required property string blurhash @@ -21,10 +22,10 @@ AbstractButton { required property int containerHeight property double divisor: isReply ? 5 : 3 - property int tempWidth: originalWidth < 1? 400: originalWidth - - Layout.preferredWidth: Math.round(tempWidth*Math.min((containerHeight/divisor)/(tempWidth*proportionalHeight), 1)) - Layout.preferredHeight: width*proportionalHeight + //Layout.maximumWidth: originalWidth + Layout.maximumHeight: Math.min(originalHeight, containerHeight / divisor) + implicitWidth: height/proportionalHeight + implicitHeight: Math.min(Layout.maximumHeight, width*proportionalHeight) hoverEnabled: true state: (img.status != Image.Ready || timeline.privacyScreen.active) ? "BlurhashVisible" : "ImageVisible" From 184806bf71faa9f4bcb1745a654dcb8dccc2e864 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 20 Sep 2023 02:17:20 +0200 Subject: [PATCH 16/32] Move message styles to their own files and work around hover not propagating to siblings --- CMakeLists.txt | 2 + resources/qml/MessageView.qml | 495 ++---------------- resources/qml/TimelineDefaultMessageStyle.qml | 320 +++++++++++ resources/qml/TimelineSectionHeader.qml | 164 ++++++ resources/qml/delegates/ImageMessage.qml | 13 +- resources/qml/delegates/TextMessage.qml | 3 +- 6 files changed, 530 insertions(+), 467 deletions(-) create mode 100644 resources/qml/TimelineDefaultMessageStyle.qml create mode 100644 resources/qml/TimelineSectionHeader.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 548d5303..7e02bf34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -701,6 +701,8 @@ set(QML_SOURCES resources/qml/ChatPage.qml resources/qml/CommunitiesList.qml resources/qml/RoomList.qml + resources/qml/TimelineSectionHeader.qml + resources/qml/TimelineDefaultMessageStyle.qml resources/qml/TimelineView.qml resources/qml/Avatar.qml resources/qml/Completer.qml diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 1add0ce7..907ef63c 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -25,7 +25,7 @@ Item { // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu Connections { function onHideMenu() { - messageContextMenu.close(); + messageContextMenuC.close(); replyContextMenu.close(); } @@ -59,293 +59,9 @@ Item { spacing: 2 verticalLayoutDirection: ListView.BottomToTop - delegate: TimelineEvent { - id: wrapper - ListView.delayRemove: true - width: chat.delegateMaxWidth - height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) - anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter - //room: chatRoot.roommodel - - required property var day - required property bool isSender - required property int index - property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) - property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) - property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) - - required property date timestamp - required property string userId - required property string userName - required property string threadId - required property int userPowerlevel - required property bool isEdited - required property bool isEncrypted - required property var reactions - required property int status - required property int trustlevel - required property int type - required property bool isEditable - - property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header - - property alias hovered: messageHover.hovered - - data: [ - Loader { - id: section - - property var day: wrapper.day - property bool isSender: wrapper.isSender - property bool isStateEvent: wrapper.isStateEvent - property int parentWidth: wrapper.width - property var previousMessageDay: wrapper.previousMessageDay - property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent - property string previousMessageUserId: wrapper.previousMessageUserId - property date timestamp: wrapper.timestamp - property string userId: wrapper.userId - property string userName: wrapper.userName - property string userPowerlevel: wrapper.userPowerlevel - - active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent - //asynchronous: true - sourceComponent: sectionHeader - visible: status == Loader.Ready - z: 4 - }, - Rectangle { - anchors.fill: gridContainer - color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" - - // this looks better without margins - TapHandler { - acceptedButtons: Qt.RightButton - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - gesturePolicy: TapHandler.ReleaseWithinBounds - - onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) - } - }, - RowLayout { - id: gridContainer - - width: wrapper.width - y: section.visible && section.active ? section.y + section.height : 0 - - Item { - Layout.preferredWidth: wrapper.avatarMargin - } - - AbstractButton { - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - ToolTip.visible: hovered - Layout.fillHeight: true - visible: wrapper.threadId - Layout.preferredWidth: 4 - - onClicked: room.thread = wrapper.threadId - - Rectangle { - id: threadLine - - anchors.fill: parent - color: TimelineManager.userColor(wrapper.threadId, palette.base) - } - } - ColumnLayout { - id: contentColumn - Layout.fillWidth: true - - AbstractButton { - id: replyRow - visible: wrapper.reply - Layout.fillWidth: true - Layout.maximumHeight: timelineView.height / 8 - Layout.preferredWidth: replyRowLay.implicitWidth - Layout.preferredHeight: replyRowLay.implicitHeight - - property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) - - clip: true - - contentItem: RowLayout { - id: replyRowLay - - anchors.fill: parent - - - Rectangle { - id: replyLine - Layout.fillHeight: true - color: replyRow.userColor - Layout.preferredWidth: 4 - } - - ColumnLayout { - spacing: 0 - - AbstractButton { - id: replyUserButton - Layout.fillWidth: true - contentItem: ElidedLabel { - id: userName_ - fullText: wrapper.reply?.userName ?? '' - color: replyRow.userColor - textFormat: Text.RichText - width: parent.width - elideWidth: width - } - onClicked: room.openUserProfile(wrapper.reply?.userId) - } - data: [ - replyUserButton, - wrapper.reply, - ] - } - } - - background: Rectangle { - width: replyRow.implicitContentWidth - color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) - } - } - - data: [ - replyRow, wrapper.main, - ] - } - - Item { - // spacer to fill width if needed - Layout.fillWidth: true - } - - RowLayout { - id: metadata - - property int iconSize: Math.floor(fontMetrics.ascent * scaling) - property double scaling: Settings.bubbles ? 0.75 : 1 - - Layout.alignment: Qt.AlignTop | Qt.AlignRight - Layout.preferredWidth: implicitWidth - spacing: 2 - visible: !isStateEvent - - StatusIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - eventId: wrapper.eventId - height: parent.iconSize - status: wrapper.status - width: parent.iconSize - } - Image { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Edited") - ToolTip.visible: editHovered.hovered - height: parent.iconSize - source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((wrapper.eventId == room.edit) ? palette.highlight : palette.buttonText) - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - visible: wrapper.isEdited || wrapper.eventId == room.edit - width: parent.iconSize - - HoverHandler { - id: editHovered - - } - } - ImageButton { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - ToolTip.visible: hovered - buttonTextColor: TimelineManager.userColor(wrapper.threadId, palette.base) - height: parent.iconSize - image: ":/icons/icons/ui/thread.svg" - visible: wrapper.threadId - width: parent.iconSize - - onClicked: room.thread = threadId - } - EncryptionIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - encrypted: wrapper.isEncrypted - height: parent.iconSize - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - trust: wrapper.trustlevel - visible: room.isEncrypted - width: parent.iconSize - } - Label { - id: ts - - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredWidth: implicitWidth - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: Qt.formatDateTime(wrapper.timestamp, Qt.DefaultLocaleLongDate) - ToolTip.visible: ma.hovered - color: palette.inactive.text - font.pointSize: fontMetrics.font.pointSize * parent.scaling - text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat) - - HoverHandler { - id: ma - - } - } - } - }, - Item { - id: messageActionsAnchor - anchors.fill: gridContainer - property alias hovered: messageHover.hovered - HoverHandler { - id: messageHover - onHoveredChanged: () => { - if (!Settings.mobileMode && hovered) { - if (!messageActions.hovered) { - messageActions.model = wrapper; - messageActions.attached = wrapper; - messageActions.anchors.bottomMargin = -gridContainer.y - } - } - } - } - }, - Reactions { - id: reactionRow - - eventId: wrapper.eventId - layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight - reactions: wrapper.reactions - width: wrapper.width - wrapper.avatarMargin - x: wrapper.avatarMargin - - anchors { - //left: row.bubbleOnRight ? undefined : row.left - //right: row.bubbleOnRight ? row.right : undefined - top: gridContainer.bottom - topMargin: -4 - } - }, - Rectangle { - id: unreadRow - - color: palette.highlight - height: visible ? 3 : 0 - visible: (wrapper.index > 0 && (room.fullyReadEventId == wrapper.eventId)) - - anchors { - left: parent.left - right: parent.right - top: reactionRow.bottom - topMargin: 5 - } - } - ] + delegate: TimelineDefaultMessageStyle { + messageActions: messageActionsC + messageContextMenu: messageContextMenuC } footer: Item { anchors.horizontalCenter: parent.horizontalCenter @@ -380,7 +96,7 @@ Item { source: room } Control { - id: messageActions + id: messageActionsC property Item attached: null // use comma to update on scroll @@ -405,7 +121,7 @@ Item { property var model - spacing: messageActions.padding + spacing: messageActionsC.padding Repeater { model: Settings.recentReactions @@ -542,7 +258,7 @@ Item { image: ":/icons/icons/ui/options.svg" width: 16 - onClicked: messageContextMenu.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) + onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton) } } } @@ -624,152 +340,9 @@ Item { room.setCurrentIndex(room.currentIndex); } } - Component { - id: sectionHeader - - Column { - bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3 - spacing: 8 - topPadding: userName_.visible ? 4 : 0 - visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent) - width: parentWidth - - Label { - id: dateBubble - - anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined - color: palette.text - height: Math.round(fontMetrics.height * 1.4) - horizontalAlignment: Text.AlignHCenter - text: room ? room.formatDateSeparator(timestamp) : "" - verticalAlignment: Text.AlignVCenter - visible: room && previousMessageDay !== day - width: contentWidth * 1.2 - - background: Rectangle { - color: palette.window - radius: parent.height / 2 - } - } - Row { - id: userInfo - - property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width - - height: userName_.height - spacing: 8 - visible: !isStateEvent && (!isSender || !Settings.bubbles) - - Avatar { - id: messageUserAvatar - - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: userid - ToolTip.visible: messageUserAvatar.hovered - displayName: userName - height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) - url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") - userid: userId - width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) - - onClicked: room.openUserProfile(userId) - } - Connections { - function onRoomAvatarUrlChanged() { - messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); - } - function onScrollToIndex(index) { - chat.positionViewAtIndex(index, ListView.Center); - } - - target: room - } - - AbstractButton { - id: userNameButton - - PowerlevelIndicator { - id: powerlevelIndicator - anchors.left: parent.left - //anchors.horizontalCenter: parent.horizontalCenter - - powerlevel: userPowerlevel - height: fontMetrics.lineSpacing - width: fontMetrics.lineSpacing - - sourceSize.width: fontMetrics.lineSpacing - sourceSize.height: fontMetrics.lineSpacing - - permissions: room ? room.permissions : null - visible: isAdmin || isModerator - } - - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: userId - ToolTip.visible: hovered - leftPadding: powerlevelIndicator.visible ? 16 : 0 - leftInset: 0 - rightInset: 0 - rightPadding: 0 - - contentItem: Label { - id: userName_ - - color: TimelineManager.userColor(userId, palette.base) - text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText) - textFormat: Text.RichText - } - - onClicked: room.openUserProfile(userId) - - TextMetrics { - id: userNameTextMetrics - - elide: Text.ElideRight - elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3) - text: userName - } - NhekoCursorShape { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - } - } - Label { - id: statusMsg - - property string userStatus: Presence.userStatus(userId) - - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("%1's status message").arg(userName) - ToolTip.visible: statusMsgHoverHandler.hovered - anchors.baseline: userNameButton.baseline - color: palette.buttonText - elide: Text.ElideRight - font.italic: true - font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) - text: userStatus.replace(/\n/g, " ") - textFormat: Text.PlainText - width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) - - HoverHandler { - id: statusMsgHoverHandler - - } - Connections { - function onPresenceChanged(id) { - if (id == userId) - statusMsg.userStatus = Presence.userStatus(userId); - } - - target: Presence - } - } - } - } - } } Platform.Menu { - id: messageContextMenu + id: messageContextMenuC property string eventId property int eventType @@ -824,22 +397,22 @@ Item { onTriggered: function () { topBar.searchString = ""; - room.showEvent(messageContextMenu.eventId); + room.showEvent(messageContextMenuC.eventId); } } Platform.MenuItem { enabled: visible text: qsTr("&Copy") - visible: messageContextMenu.text + visible: messageContextMenuC.text - onTriggered: Clipboard.text = messageContextMenu.text + onTriggered: Clipboard.text = messageContextMenuC.text } Platform.MenuItem { enabled: visible text: qsTr("Copy &link location") - visible: messageContextMenu.link + visible: messageContextMenuC.link - onTriggered: Clipboard.text = messageContextMenu.link + onTriggered: Clipboard.text = messageContextMenuC.link } Platform.MenuItem { id: reactionOption @@ -848,7 +421,7 @@ Item { visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) { - room.input.reaction(messageContextMenu.eventId, plaintext); + room.input.reaction(messageContextMenuC.eventId, plaintext); TimelineManager.focusMessageInput(); }) } @@ -856,41 +429,41 @@ Item { text: qsTr("Repl&y") visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false - onTriggered: room.reply = (messageContextMenu.eventId) + onTriggered: room.reply = (messageContextMenuC.eventId) } Platform.MenuItem { enabled: visible text: qsTr("&Edit") - visible: messageContextMenu.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) - onTriggered: room.edit = (messageContextMenu.eventId) + onTriggered: room.edit = (messageContextMenuC.eventId) } Platform.MenuItem { enabled: visible text: qsTr("&Thread") visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) - onTriggered: room.thread = (messageContextMenu.threadId || messageContextMenu.eventId) + onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId) } Platform.MenuItem { enabled: visible - text: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? qsTr("Un&pin") : qsTr("&Pin") + text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin") visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) - onTriggered: visible && room.pinnedMessages.includes(messageContextMenu.eventId) ? room.unpin(messageContextMenu.eventId) : room.pin(messageContextMenu.eventId) + onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId) } Platform.MenuItem { text: qsTr("&Read receipts") - onTriggered: room.showReadReceipts(messageContextMenu.eventId) + onTriggered: room.showReadReceipts(messageContextMenuC.eventId) } Platform.MenuItem { text: qsTr("&Forward") - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker || messageContextMenu.eventType == MtxEvent.TextMessage || messageContextMenu.eventType == MtxEvent.LocationMessage || messageContextMenu.eventType == MtxEvent.EmoteMessage || messageContextMenu.eventType == MtxEvent.NoticeMessage + visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage onTriggered: { var forwardMess = forwardCompleterComponent.createObject(timelineRoot); - forwardMess.setMessageEventId(messageContextMenu.eventId); + forwardMess.setMessageEventId(messageContextMenuC.eventId); forwardMess.open(); timelineRoot.destroyOnClose(forwardMess); } @@ -901,23 +474,23 @@ Item { Platform.MenuItem { text: qsTr("View raw message") - onTriggered: room.viewRawMessage(messageContextMenu.eventId) + onTriggered: room.viewRawMessage(messageContextMenuC.eventId) } Platform.MenuItem { enabled: visible text: qsTr("View decrypted raw message") // TODO(Nico): Fix this still being iterated over, when using keyboard to select options - visible: messageContextMenu.isEncrypted + visible: messageContextMenuC.isEncrypted - onTriggered: room.viewDecryptedRawMessage(messageContextMenu.eventId) + onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId) } Platform.MenuItem { text: qsTr("Remo&ve message") - visible: (room ? room.permissions.canRedact() : false) || messageContextMenu.isSender + visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender onTriggered: function () { var dialog = removeReason.createObject(timelineRoot); - dialog.eventId = messageContextMenu.eventId; + dialog.eventId = messageContextMenuC.eventId; dialog.show(); dialog.forceActiveFocus(); timelineRoot.destroyOnClose(dialog); @@ -926,23 +499,23 @@ Item { Platform.MenuItem { enabled: visible text: qsTr("&Save as") - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker - onTriggered: room.saveMedia(messageContextMenu.eventId) + onTriggered: room.saveMedia(messageContextMenuC.eventId) } Platform.MenuItem { enabled: visible text: qsTr("&Open in external program") - visible: messageContextMenu.eventType == MtxEvent.ImageMessage || messageContextMenu.eventType == MtxEvent.VideoMessage || messageContextMenu.eventType == MtxEvent.AudioMessage || messageContextMenu.eventType == MtxEvent.FileMessage || messageContextMenu.eventType == MtxEvent.Sticker + visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker - onTriggered: room.openMedia(messageContextMenu.eventId) + onTriggered: room.openMedia(messageContextMenuC.eventId) } Platform.MenuItem { enabled: visible text: qsTr("Copy link to eve&nt") - visible: messageContextMenu.eventId + visible: messageContextMenuC.eventId - onTriggered: room.copyLinkToEvent(messageContextMenu.eventId) + onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId) } } Component { diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml new file mode 100644 index 00000000..2dcf153c --- /dev/null +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -0,0 +1,320 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./components" +import "./delegates" +import "./emoji" +import "./ui" +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import im.nheko + +TimelineEvent { + id: wrapper + ListView.delayRemove: true + width: chat.delegateMaxWidth + height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) + anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter + //room: chatRoot.roommodel + + required property var day + required property bool isSender + required property int index + property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) + property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) + + required property date timestamp + required property string userId + required property string userName + required property string threadId + required property int userPowerlevel + required property bool isEdited + required property bool isEncrypted + required property var reactions + required property int status + required property int trustlevel + required property int type + required property bool isEditable + + required property QtObject messageContextMenu + required property Item messageActions + + property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header + + property alias hovered: messageHover.hovered + + data: [ + Loader { + id: section + + active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent + //asynchronous: true + sourceComponent: TimelineSectionHeader { + day: wrapper.day + isSender: wrapper.isSender + isStateEvent: wrapper.isStateEvent + parentWidth: wrapper.width + previousMessageDay: wrapper.previousMessageDay + previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent + previousMessageUserId: wrapper.previousMessageUserId + timestamp: wrapper.timestamp + userId: wrapper.userId + userName: wrapper.userName + userPowerlevel: wrapper.userPowerlevel + } + visible: status == Loader.Ready + z: 4 + }, + Rectangle { + anchors.fill: gridContainer + color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" + + // this looks better without margins + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) + } + }, + RowLayout { + id: gridContainer + + width: wrapper.width + y: section.visible && section.active ? section.y + section.height : 0 + + HoverHandler { + id: messageHover + blocking: false + onHoveredChanged: () => { + if (!Settings.mobileMode && hovered) { + if (!messageActions.hovered) { + messageActions.model = wrapper; + messageActions.attached = wrapper; + messageActions.anchors.bottomMargin = -gridContainer.y + } + } + } + + } + + Item { + Layout.preferredWidth: wrapper.avatarMargin + } + + AbstractButton { + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered + Layout.fillHeight: true + visible: wrapper.threadId + Layout.preferredWidth: 4 + + onClicked: wrapper.room.thread = wrapper.threadId + + Rectangle { + id: threadLine + + anchors.fill: parent + color: TimelineManager.userColor(wrapper.threadId, palette.base) + } + } + ColumnLayout { + id: contentColumn + Layout.fillWidth: true + + AbstractButton { + id: replyRow + visible: wrapper.reply + Layout.fillWidth: true + Layout.maximumHeight: timelineView.height / 8 + Layout.preferredWidth: replyRowLay.implicitWidth + Layout.preferredHeight: replyRowLay.implicitHeight + + property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) + + clip: true + + NhekoCursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + + contentItem: RowLayout { + id: replyRowLay + + anchors.fill: parent + + + Rectangle { + id: replyLine + Layout.fillHeight: true + color: replyRow.userColor + Layout.preferredWidth: 4 + } + + ColumnLayout { + spacing: 0 + + AbstractButton { + id: replyUserButton + Layout.fillWidth: true + contentItem: ElidedLabel { + id: userName_ + fullText: wrapper.reply?.userName ?? '' + color: replyRow.userColor + textFormat: Text.RichText + width: parent.width + elideWidth: width + } + onClicked: wrapper.room.openUserProfile(wrapper.reply?.userId) + } + data: [ + replyUserButton, + wrapper.reply, + ] + } + } + + background: Rectangle { + width: replyRow.implicitContentWidth + color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) + } + + onClicked: { + let link = wrapper.reply.hoveredLink + if (link) { + Nheko.openLink(link) + } else { + console.log("Scrolling to "+wrapper.replyTo); + wrapper.room.showEvent(wrapper.replyTo) + } + } + } + + data: [ + replyRow, wrapper.main, + ] + } + + Item { + // spacer to fill width if needed + Layout.fillWidth: true + } + + RowLayout { + id: metadata + + property int iconSize: Math.floor(fontMetrics.ascent * scaling) + property double scaling: Settings.bubbles ? 0.75 : 1 + + Layout.alignment: Qt.AlignTop | Qt.AlignRight + Layout.preferredWidth: implicitWidth + spacing: 2 + visible: !isStateEvent + + StatusIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + eventId: wrapper.eventId + height: parent.iconSize + status: wrapper.status + width: parent.iconSize + } + Image { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Edited") + ToolTip.visible: editHovered.hovered + height: parent.iconSize + source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((wrapper.eventId == wrapper.room.edit) ? palette.highlight : palette.buttonText) + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + visible: wrapper.isEdited || wrapper.eventId == wrapper.room.edit + width: parent.iconSize + + HoverHandler { + id: editHovered + + } + } + ImageButton { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered + buttonTextColor: TimelineManager.userColor(wrapper.threadId, palette.base) + height: parent.iconSize + image: ":/icons/icons/ui/thread.svg" + visible: wrapper.threadId + width: parent.iconSize + + onClicked: wrapper.room.thread = threadId + } + EncryptionIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + encrypted: wrapper.isEncrypted + height: parent.iconSize + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + trust: wrapper.trustlevel + visible: wrapper.room.isEncrypted + width: parent.iconSize + } + Label { + id: ts + + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredWidth: implicitWidth + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: Qt.formatDateTime(wrapper.timestamp, Qt.DefaultLocaleLongDate) + ToolTip.visible: ma.hovered + color: palette.inactive.text + font.pointSize: fontMetrics.font.pointSize * parent.scaling + text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat) + + HoverHandler { + id: ma + + } + } + } + }, + Reactions { + id: reactionRow + + eventId: wrapper.eventId + layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight + reactions: wrapper.reactions + width: wrapper.width - wrapper.avatarMargin + x: wrapper.avatarMargin + + anchors { + //left: row.bubbleOnRight ? undefined : row.left + //right: row.bubbleOnRight ? row.right : undefined + top: gridContainer.bottom + topMargin: -4 + } + }, + Rectangle { + id: unreadRow + + color: palette.highlight + height: visible ? 3 : 0 + visible: (wrapper.index > 0 && (wrapper.room.fullyReadEventId == wrapper.eventId)) + + anchors { + left: parent.left + right: parent.right + top: reactionRow.bottom + topMargin: 5 + } + } + ] +} diff --git a/resources/qml/TimelineSectionHeader.qml b/resources/qml/TimelineSectionHeader.qml new file mode 100644 index 00000000..61c5ef28 --- /dev/null +++ b/resources/qml/TimelineSectionHeader.qml @@ -0,0 +1,164 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import Qt.labs.platform 1.1 as Platform +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.2 +import QtQuick.Window 2.13 +import im.nheko 1.0 + +Column { + + required property var day + required property bool isSender + required property bool isStateEvent + required property int parentWidth + required property var previousMessageDay + required property bool previousMessageIsStateEvent + required property string previousMessageUserId + required property date timestamp + required property string userId + required property string userName + required property string userPowerlevel + + bottomPadding: Settings.bubbles ? (isSender && previousMessageDay == day ? 0 : 2) : 3 + spacing: 8 + topPadding: userName_.visible ? 4 : 0 + visible: (previousMessageUserId !== userId || previousMessageDay !== day || isStateEvent !== previousMessageIsStateEvent) + width: parentWidth + + Label { + id: dateBubble + + anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined + color: palette.text + height: Math.round(fontMetrics.height * 1.4) + horizontalAlignment: Text.AlignHCenter + text: room ? room.formatDateSeparator(timestamp) : "" + verticalAlignment: Text.AlignVCenter + visible: room && previousMessageDay !== day + width: contentWidth * 1.2 + + background: Rectangle { + color: palette.window + radius: parent.height / 2 + } + } + Row { + id: userInfo + + property int remainingWidth: chat.delegateMaxWidth - spacing - messageUserAvatar.width + + height: userName_.height + spacing: 8 + visible: !isStateEvent && (!isSender || !Settings.bubbles) + + Avatar { + id: messageUserAvatar + + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: userid + ToolTip.visible: messageUserAvatar.hovered + displayName: userName + height: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) + url: !room ? "" : room.avatarUrl(userId).replace("mxc://", "image://MxcImage/") + userid: userId + width: Nheko.avatarSize * (Settings.smallAvatars ? 0.5 : 1) + + onClicked: room.openUserProfile(userId) + } + Connections { + function onRoomAvatarUrlChanged() { + messageUserAvatar.url = room.avatarUrl(userId).replace("mxc://", "image://MxcImage/"); + } + function onScrollToIndex(index) { + chat.positionViewAtIndex(index, ListView.Center); + } + + target: room + } + + AbstractButton { + id: userNameButton + + PowerlevelIndicator { + id: powerlevelIndicator + anchors.left: parent.left + //anchors.horizontalCenter: parent.horizontalCenter + + powerlevel: userPowerlevel + height: fontMetrics.lineSpacing + width: fontMetrics.lineSpacing + + sourceSize.width: fontMetrics.lineSpacing + sourceSize.height: fontMetrics.lineSpacing + + permissions: room ? room.permissions : null + visible: isAdmin || isModerator + } + + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: userId + ToolTip.visible: hovered + leftPadding: powerlevelIndicator.visible ? 16 : 0 + leftInset: 0 + rightInset: 0 + rightPadding: 0 + + contentItem: Label { + id: userName_ + + color: TimelineManager.userColor(userId, palette.base) + text: TimelineManager.escapeEmoji(userNameTextMetrics.elidedText) + textFormat: Text.RichText + } + + onClicked: room.openUserProfile(userId) + + TextMetrics { + id: userNameTextMetrics + + elide: Text.ElideRight + elideWidth: userInfo.remainingWidth - Math.min(statusMsg.implicitWidth, userInfo.remainingWidth / 3) + text: userName + } + NhekoCursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + } + Label { + id: statusMsg + + property string userStatus: Presence.userStatus(userId) + + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("%1's status message").arg(userName) + ToolTip.visible: statusMsgHoverHandler.hovered + anchors.baseline: userNameButton.baseline + color: palette.buttonText + elide: Text.ElideRight + font.italic: true + font.pointSize: Math.floor(fontMetrics.font.pointSize * 0.8) + text: userStatus.replace(/\n/g, " ") + textFormat: Text.PlainText + width: Math.min(implicitWidth, userInfo.remainingWidth - userName_.width - parent.spacing) + + HoverHandler { + id: statusMsgHoverHandler + + } + Connections { + function onPresenceChanged(id) { + if (id == userId) + statusMsg.userStatus = Presence.userStatus(userId); + } + + target: Presence + } + } + } +} + diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 466041bd..0369d5a1 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -130,21 +130,23 @@ AbstractButton { id: mxcimage visible: loaded - anchors.fill: parent roomm: room play: !Settings.animateImagesOnHover || parent.hovered eventId: parent.eventId + width: parent.implicitWidth + height: parent.implicitHeight } Image { id: blurhash_ - anchors.fill: parent source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText) asynchronous: true fillMode: Image.PreserveAspectFit - sourceSize.width: parent.width * Screen.devicePixelRatio - sourceSize.height: parent.height * Screen.devicePixelRatio + sourceSize.width: parent.implicitWidth * Screen.devicePixelRatio + sourceSize.height: parent.implicitHeight * Screen.devicePixelRatio + width: parent.implicitWidth + height: parent.implicitHeight } onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight); @@ -152,7 +154,8 @@ AbstractButton { Item { id: overlay - anchors.fill: parent + width: parent.implicitWidth + height: parent.implicitHeight visible: parent.hovered Rectangle { diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index dc8caf01..9ef2e6cc 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -44,7 +44,8 @@ MatrixText { Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight clip: !keepFullText selectByMouse: !Settings.mobileMode && !isReply - enabled: !Settings.mobileMode + enabled: !Settings.mobileMode && !isReply + hoverEnabled: !Settings.mobileMode font.pointSize: (Settings.enlargeEmojiOnlyMessages && isOnlyEmoji > 0 && isOnlyEmoji < 4) ? Settings.fontSize * 3 : Settings.fontSize NhekoCursorShape { From 205a42dade6cb7f54ca1b037593a2050d5366937 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Wed, 20 Sep 2023 02:38:06 +0200 Subject: [PATCH 17/32] Reimplement mention highlight and scroll to highlight --- resources/qml/MessageView.qml | 1 + resources/qml/TimelineDefaultMessageStyle.qml | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 907ef63c..3cdfda22 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -62,6 +62,7 @@ Item { delegate: TimelineDefaultMessageStyle { messageActions: messageActionsC messageContextMenu: messageContextMenuC + scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) } footer: Item { anchors.horizontalCenter: parent.horizontalCenter diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index 2dcf153c..1d12daef 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -39,6 +39,7 @@ TimelineEvent { required property var reactions required property int status required property int trustlevel + required property int notificationlevel required property int type required property bool isEditable @@ -48,6 +49,7 @@ TimelineEvent { property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header property alias hovered: messageHover.hovered + property bool scrolledToThis: false data: [ Loader { @@ -84,6 +86,59 @@ TimelineEvent { onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) } }, + Rectangle { + id: scrollHighlight + anchors.fill: gridContainer + + color: palette.highlight + enabled: false + opacity: 0 + visible: true + z: 1 + + states: State { + name: "revealed" + when: wrapper.scrolledToThis + } + transitions: Transition { + from: "" + to: "revealed" + + SequentialAnimation { + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 0 + properties: "opacity" + target: scrollHighlight + to: 1 + } + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 1 + properties: "opacity" + target: scrollHighlight + to: 0 + } + ScriptAction { + script: wrapper.room.eventShown() + } + } + } + }, + Rectangle { + anchors.top: gridContainer.top + anchors.left: gridContainer.left + anchors.topMargin: -2 + anchors.leftMargin: wrapper.avatarMargin + 2 + color: "transparent" + border.color: Nheko.theme.red + border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 + radius: 4 + height: contentColumn.implicitHeight + 4 + width: contentColumn.implicitWidth + 4 + }, RowLayout { id: gridContainer From 6c6370c83fab2140a81f431b8909746a0b6833dc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Oct 2023 20:14:13 +0200 Subject: [PATCH 18/32] Switch to manual polishing of event delegates --- resources/qml/MessageView.qml | 4 +- resources/qml/TimelineDefaultMessageStyle.qml | 71 ++++++++--------- resources/qml/delegates/MessageDelegate.qml | 1 - resources/qml/delegates/TextMessage.qml | 7 +- src/timeline/EventDelegateChooser.cpp | 78 +++++++++++++++++-- src/timeline/EventDelegateChooser.h | 60 +++++++++++++- src/timeline/TimelineModel.cpp | 8 +- 7 files changed, 169 insertions(+), 60 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 3cdfda22..3ffe7d9a 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -52,8 +52,8 @@ Item { //onModelChanged: if (room) room.sendReset() //reuseItems: true boundsBehavior: Flickable.StopAtBounds - displayMarginBeginning: height / 2 - displayMarginEnd: height / 2 + displayMarginBeginning: height / 4 + displayMarginEnd: height / 4 model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room //pixelAligned: true spacing: 2 diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index 1d12daef..0866fab6 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -18,7 +18,7 @@ TimelineEvent { id: wrapper ListView.delayRemove: true width: chat.delegateMaxWidth - height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) + height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 20) anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter //room: chatRoot.roommodel @@ -51,6 +51,8 @@ TimelineEvent { property alias hovered: messageHover.hovered property bool scrolledToThis: false + maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width + data: [ Loader { id: section @@ -131,7 +133,7 @@ TimelineEvent { anchors.top: gridContainer.top anchors.left: gridContainer.left anchors.topMargin: -2 - anchors.leftMargin: wrapper.avatarMargin + 2 + anchors.leftMargin: -2 color: "transparent" border.color: Nheko.theme.red border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 @@ -139,11 +141,13 @@ TimelineEvent { height: contentColumn.implicitHeight + 4 width: contentColumn.implicitWidth + 4 }, - RowLayout { + Row { id: gridContainer - width: wrapper.width + width: wrapper.width - wrapper.avatarMargin + x: wrapper.avatarMargin y: section.visible && section.active ? section.y + section.height : 0 + spacing: Nheko.paddingSmall HoverHandler { id: messageHover @@ -154,23 +158,20 @@ TimelineEvent { messageActions.model = wrapper; messageActions.attached = wrapper; messageActions.anchors.bottomMargin = -gridContainer.y + messageActions.anchors.rightMargin = metadata.width } } } } - Item { - Layout.preferredWidth: wrapper.avatarMargin - } - AbstractButton { ToolTip.delay: Nheko.tooltipDelay ToolTip.text: qsTr("Part of a thread") ToolTip.visible: hovered - Layout.fillHeight: true + height: contentColumn.height visible: wrapper.threadId - Layout.preferredWidth: 4 + width: 4 onClicked: wrapper.room.thread = wrapper.threadId @@ -181,17 +182,16 @@ TimelineEvent { color: TimelineManager.userColor(wrapper.threadId, palette.base) } } - ColumnLayout { + Column { id: contentColumn - Layout.fillWidth: true AbstractButton { id: replyRow visible: wrapper.reply - Layout.fillWidth: true - Layout.maximumHeight: timelineView.height / 8 - Layout.preferredWidth: replyRowLay.implicitWidth - Layout.preferredHeight: replyRowLay.implicitHeight + //Layout.fillWidth: true + //Layout.maximumHeight: timelineView.height / 8 + //Layout.preferredWidth: replyRowLay.implicitWidth + //Layout.preferredHeight: replyRowLay.implicitHeight property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) @@ -202,32 +202,33 @@ TimelineEvent { cursorShape: Qt.PointingHandCursor } - contentItem: RowLayout { + contentItem: Row { id: replyRowLay - anchors.fill: parent - + spacing: Nheko.paddingSmall Rectangle { id: replyLine - Layout.fillHeight: true + height: replyCol.height color: replyRow.userColor - Layout.preferredWidth: 4 + width: 4 } - ColumnLayout { + Column { spacing: 0 + id: replyCol + AbstractButton { id: replyUserButton - Layout.fillWidth: true - contentItem: ElidedLabel { + + contentItem: Label { id: userName_ - fullText: wrapper.reply?.userName ?? '' + text: wrapper.reply?.userName ?? '' color: replyRow.userColor textFormat: Text.RichText - width: parent.width - elideWidth: width + width: wrapper.maxWidth + //elideWidth: wrapper.maxWidth } onClicked: wrapper.room.openUserProfile(wrapper.reply?.userId) } @@ -239,7 +240,7 @@ TimelineEvent { } background: Rectangle { - width: replyRow.implicitContentWidth + //width: replyRow.implicitContentWidth color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) } @@ -259,19 +260,16 @@ TimelineEvent { ] } - Item { - // spacer to fill width if needed - Layout.fillWidth: true - } - + }, RowLayout { id: metadata property int iconSize: Math.floor(fontMetrics.ascent * scaling) property double scaling: Settings.bubbles ? 0.75 : 1 - Layout.alignment: Qt.AlignTop | Qt.AlignRight - Layout.preferredWidth: implicitWidth + anchors.right: parent.right + y: section.visible && section.active ? section.y + section.height : 0 + spacing: 2 visible: !isStateEvent @@ -339,8 +337,7 @@ TimelineEvent { } } - } - }, + }, Reactions { id: reactionRow diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml index 44726a63..cd1bdcd4 100644 --- a/resources/qml/delegates/MessageDelegate.qml +++ b/resources/qml/delegates/MessageDelegate.qml @@ -620,7 +620,6 @@ Item { roleValue: MtxEvent.Member ColumnLayout { - width: parent?.width ?? 100 NoticeMessage { body: formatted diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 9ef2e6cc..03623924 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -4,7 +4,7 @@ import ".." import QtQuick.Controls -import QtQuick.Layouts +//import QtQuick.Layouts import im.nheko MatrixText { @@ -41,7 +41,10 @@ MatrixText { }" : "") + // TODO(Nico): Figure out how to support mobile " " + formatted.replace(//g, "").replace(/<\/del>/g, "").replace(//g, "").replace(/<\/strike>/g, "") - Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight + //Layout.maximumHeight: !keepFullText ? Math.round(Math.min(timelineView.height / 8, implicitHeight)) : implicitHeight + + //EventDelegateChooser.fillWidth: true + clip: !keepFullText selectByMouse: !Settings.mobileMode && !isReply enabled: !Settings.mobileMode && !isReply diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index 2218d9ee..0060907d 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -131,7 +131,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) roleToPropIdx.insert(*role, i); roles.emplace_back(*role); - nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role); + // nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role); } else { nhlog::ui()->critical("Required property {} not found in model!", prop.name()); } @@ -140,14 +140,15 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) nhlog::ui()->debug("Querying data for id {}", currentId.toStdString()); chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); + Qt::beginPropertyUpdateGroup(); for (const auto &role : roles) { const auto &roleName = roleNames[role.role()]; - nhlog::ui()->critical("Setting role {}, {} to {}", - role.role(), - roleName.toStdString(), - role.data().toString().toStdString()); + // nhlog::ui()->critical("Setting role {}, {} to {}", + // role.role(), + // roleName.toStdString(), + // role.data().toString().toStdString()); - nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[role.role()]).name()); + // nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[role.role()]).name()); mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) @@ -156,14 +157,15 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) if (isReplyNeeded) { const auto roleName = QByteArray("isReply"); - nhlog::ui()->critical("Setting role {} to {}", roleName.toStdString(), forReply); + // nhlog::ui()->critical("Setting role {} to {}", roleName.toStdString(), forReply); - nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[-1]).name()); + // nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[-1]).name()); mo->property(roleToPropIdx[-1]).write(obj, forReply); if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); } + Qt::endPropertyUpdateGroup(); // setInitialProperties(rolesToSet); @@ -188,9 +190,12 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) auto mo = obj->metaObject(); chooser.room_->multiData( currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest); + + Qt::beginPropertyUpdateGroup(); for (const auto &role : rolesToRequest) { mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); } + Qt::endPropertyUpdateGroup(); }; if (!forReply) { @@ -257,11 +262,22 @@ EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status sta child->setParentItem(&chooser); QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::JavaScriptOwnership); + + // connect(child, &QQuickItem::parentChanged, child, [child](QQuickItem *) { + // // QTBUG-115687 + // if (child->flags().testFlag(QQuickItem::ItemObservesViewport)) { + // nhlog::ui()->critical("SETTING OBSERVES VIEWPORT"); + // // Re-trigger the parent traversal to get subtreeTransformChangedEnabled turned + // on child->setFlag(QQuickItem::ItemObservesViewport); + // } + // }); + if (forReply) emit chooser.replyChanged(); else emit chooser.mainChanged(); + chooser.polish(); } else if (status == QQmlIncubator::Error) { auto errors_ = errors(); for (const auto &e : qAsConst(errors_)) @@ -269,3 +285,49 @@ EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status sta } } +void +EventDelegateChooser::updatePolish() +{ + auto mainChild = qobject_cast(eventIncubator.object()); + auto replyChild = qobject_cast(replyIncubator.object()); + + nhlog::ui()->critical("POLISHING {}", (void *)this); + + if (mainChild) { + auto attached = qobject_cast( + qmlAttachedPropertiesObject(mainChild)); + Q_ASSERT(attached != nullptr); + + // in theory we could also reset the width, but that doesn't seem to work nicely for text + // areas because of how they cache it. + mainChild->setWidth(maxWidth_); + mainChild->ensurePolished(); + auto width = mainChild->implicitWidth(); + + if (width > maxWidth_ || attached->fillWidth()) + width = maxWidth_; + + nhlog::ui()->debug( + "Made event delegate width: {}, {}", width, mainChild->metaObject()->className()); + mainChild->setWidth(width); + mainChild->ensurePolished(); + } + + if (replyChild) { + auto attached = qobject_cast( + qmlAttachedPropertiesObject(replyChild)); + Q_ASSERT(attached != nullptr); + + // in theory we could also reset the width, but that doesn't seem to work nicely for text + // areas because of how they cache it. + replyChild->setWidth(maxWidth_); + replyChild->ensurePolished(); + auto width = replyChild->implicitWidth(); + + if (width > maxWidth_ || attached->fillWidth()) + width = maxWidth_; + + replyChild->setWidth(width); + replyChild->ensurePolished(); + } +} diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h index 1cd2d65a..ff67ccd8 100644 --- a/src/timeline/EventDelegateChooser.h +++ b/src/timeline/EventDelegateChooser.h @@ -2,9 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-or-later -// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt -// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent - #pragma once #include @@ -17,6 +14,32 @@ #include "TimelineModel.h" +class EventDelegateChooserAttachedType : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged) + QML_ANONYMOUS +public: + EventDelegateChooserAttachedType(QObject *parent) + : QObject(parent) + { + } + bool fillWidth() const { return fillWidth_; } + void setFillWidth(bool fill) + { + fillWidth_ = fill; + emit fillWidthChanged(); + } +signals: + void fillWidthChanged(); + +private: + bool fillWidth_ = false, keepAspectRatio = false; + double aspectRatio = 1.; + int maxWidth = -1; + int maxHeight = -1; +}; + class EventDelegateChoice : public QObject { Q_OBJECT @@ -51,14 +74,18 @@ class EventDelegateChooser : public QQuickItem QML_ELEMENT Q_CLASSINFO("DefaultProperty", "choices") -public: + QML_ATTACHED(EventDelegateChooserAttachedType) + Q_PROPERTY(QQmlListProperty choices READ choices CONSTANT FINAL) Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL) Q_PROPERTY(QQuickItem *reply READ reply NOTIFY replyChanged FINAL) Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL) Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL) Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) + Q_PROPERTY(bool sameWidth READ sameWidth WRITE setSameWidth NOTIFY sameWidthChanged) + Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) +public: QQmlListProperty choices(); [[nodiscard]] QQuickItem *main() const @@ -70,6 +97,20 @@ public: return qobject_cast(replyIncubator.object()); } + bool sameWidth() const { return sameWidth_; } + void setSameWidth(bool width) + { + sameWidth_ = width; + emit sameWidthChanged(); + } + bool maxWidth() const { return maxWidth_; } + void setMaxWidth(int width) + { + maxWidth_ = width; + emit maxWidthChanged(); + polish(); + } + void setRoom(TimelineModel *m) { if (m != room_) { @@ -105,12 +146,21 @@ public: void componentComplete() override; + static EventDelegateChooserAttachedType *qmlAttachedProperties(QObject *object) + { + return new EventDelegateChooserAttachedType(object); + } + + void updatePolish() override; + signals: void mainChanged(); void replyChanged(); void roomChanged(); void eventIdChanged(); void replyToChanged(); + void sameWidthChanged(); + void maxWidthChanged(); private: struct DelegateIncubator final : public QQmlIncubator @@ -142,6 +192,8 @@ private: TimelineModel *room_{nullptr}; QString eventId_; QString replyId; + bool sameWidth_ = false; + int maxWidth_ = 400; static void appendChoice(QQmlListProperty *, EventDelegateChoice *); static qsizetype choiceCount(QQmlListProperty *); diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index 3e0c6688..f5b9e142 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -601,12 +601,8 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r case UserName: return QVariant(displayName(QString::fromStdString(acc::sender(event)))); case UserPowerlevel: { - return static_cast(mtx::events::state::PowerLevels{ - cache::client() - ->getStateEvent(room_id_.toStdString()) - .value_or(mtx::events::StateEvent{}) - .content} - .user_level(acc::sender(event))); + return static_cast( + permissions_.powerlevelEvent().user_level(acc::sender(event))); } case Day: { From c4d2ec875dfce9c29adab0ec85e4317277b5509d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sun, 8 Oct 2023 23:52:23 +0200 Subject: [PATCH 19/32] Fixup reply and state event rendering --- CMakeLists.txt | 1 - resources/qml/TimelineDefaultMessageStyle.qml | 20 +- resources/qml/TimelineEvent.qml | 10 +- resources/qml/delegates/ImageMessage.qml | 30 +- resources/qml/delegates/MessageDelegate.qml | 780 ------------------ .../qml/delegates/PlayableMediaMessage.qml | 2 +- resources/qml/delegates/Reply.qml | 50 +- resources/qml/delegates/TextMessage.qml | 3 +- src/timeline/EventDelegateChooser.cpp | 112 ++- src/timeline/EventDelegateChooser.h | 96 ++- 10 files changed, 207 insertions(+), 897 deletions(-) delete mode 100644 resources/qml/delegates/MessageDelegate.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e02bf34..cbe2b20e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -742,7 +742,6 @@ set(QML_SOURCES resources/qml/delegates/Encrypted.qml resources/qml/delegates/FileMessage.qml resources/qml/delegates/ImageMessage.qml - resources/qml/delegates/MessageDelegate.qml resources/qml/delegates/NoticeMessage.qml resources/qml/delegates/Pill.qml resources/qml/delegates/Placeholder.qml diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index 0866fab6..8beaa8f0 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -18,7 +18,7 @@ TimelineEvent { id: wrapper ListView.delayRemove: true width: chat.delegateMaxWidth - height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 20) + height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter //room: chatRoot.roommodel @@ -51,6 +51,9 @@ TimelineEvent { property alias hovered: messageHover.hovered property bool scrolledToThis: false + mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4 + replyInset: mainInset + 4 + Nheko.paddingSmall + maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width data: [ @@ -182,16 +185,21 @@ TimelineEvent { color: TimelineManager.userColor(wrapper.threadId, palette.base) } } + + Item { + visible: wrapper.isStateEvent + width: (wrapper.maxWidth - (wrapper.main?.width ?? 0)) / 2 + height: 1 + } + Column { id: contentColumn AbstractButton { id: replyRow visible: wrapper.reply - //Layout.fillWidth: true - //Layout.maximumHeight: timelineView.height / 8 - //Layout.preferredWidth: replyRowLay.implicitWidth - //Layout.preferredHeight: replyRowLay.implicitHeight + + height: replyLine.height property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) @@ -209,7 +217,7 @@ TimelineEvent { Rectangle { id: replyLine - height: replyCol.height + height: Math.min( wrapper.reply?.height, timelineView.height / 5) + Nheko.paddingSmall + replyUserButton.height color: replyRow.userColor width: 4 } diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml index f1b6bd2a..5120fd12 100644 --- a/resources/qml/TimelineEvent.qml +++ b/resources/qml/TimelineEvent.qml @@ -62,12 +62,10 @@ EventDelegateChooser { required property string userId required property string userName - Layout.fillWidth: true - //Layout.maximumWidth: implicitWidth - body: '' color: palette.buttonText font.italic: true + font.pointSize: Settings.fontSize * 0.8 formatted: '' horizontalAlignment: Text.AlignHCenter isOnlyEmoji: false @@ -202,7 +200,6 @@ EventDelegateChooser { id: member required property string formattedStateEvent - required property bool isReply required property Room room required property string userId required property string userName @@ -212,7 +209,7 @@ EventDelegateChooser { body: formatted formatted: member.formattedStateEvent isOnlyEmoji: false - isReply: member.isReply + isReply: EventDelegateChooser.isReply isStateEvent: true keepFullText: true } @@ -233,7 +230,6 @@ EventDelegateChooser { required property string body required property string eventId - required property bool isReply required property Room room required property string userId required property string userName @@ -243,7 +239,7 @@ EventDelegateChooser { body: formatted formatted: qsTr("This room was replaced for the following reason: %1").arg(tombstone.body) isOnlyEmoji: false - isReply: tombstone.isReply + isReply: EventDelegateChooser.isReply isStateEvent: true keepFullText: true } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 0369d5a1..06a1a9e7 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -5,7 +5,6 @@ import QtQuick import QtQuick.Window import QtQuick.Controls -import QtQuick.Layouts import im.nheko AbstractButton { @@ -17,16 +16,17 @@ AbstractButton { required property string blurhash required property string body required property string filename - required property bool isReply required property string eventId required property int containerHeight - property double divisor: isReply ? 5 : 3 + property double divisor: EventDelegateChooser.isReply ? 5 : 3 + + EventDelegateChooser.keepAspectRatio: true + EventDelegateChooser.maxWidth: originalWidth + EventDelegateChooser.maxHeight: containerHeight / divisor + EventDelegateChooser.aspectRatio: proportionalHeight - //Layout.maximumWidth: originalWidth - Layout.maximumHeight: Math.min(originalHeight, containerHeight / divisor) - implicitWidth: height/proportionalHeight - implicitHeight: Math.min(Layout.maximumHeight, width*proportionalHeight) hoverEnabled: true + enabled: !EventDelegateChooser.isReply state: (img.status != Image.Ready || timeline.privacyScreen.active) ? "BlurhashVisible" : "ImageVisible" states: [ @@ -133,8 +133,8 @@ AbstractButton { roomm: room play: !Settings.animateImagesOnHover || parent.hovered eventId: parent.eventId - width: parent.implicitWidth - height: parent.implicitHeight + + anchors.fill: parent } Image { @@ -143,10 +143,10 @@ AbstractButton { source: blurhash ? ("image://blurhash/" + blurhash) : ("image://colorimage/:/icons/icons/ui/image-failed.svg?" + palette.buttonText) asynchronous: true fillMode: Image.PreserveAspectFit - sourceSize.width: parent.implicitWidth * Screen.devicePixelRatio - sourceSize.height: parent.implicitHeight * Screen.devicePixelRatio - width: parent.implicitWidth - height: parent.implicitHeight + sourceSize.width: parent.width * Screen.devicePixelRatio + sourceSize.height: parent.height * Screen.devicePixelRatio + + anchors.fill: parent } onClicked: Settings.openImageExternal ? room.openMedia(eventId) : TimelineManager.openImageOverlay(room, url, eventId, originalWidth, proportionalHeight); @@ -154,8 +154,8 @@ AbstractButton { Item { id: overlay - width: parent.implicitWidth - height: parent.implicitHeight + anchors.fill: parent + visible: parent.hovered Rectangle { diff --git a/resources/qml/delegates/MessageDelegate.qml b/resources/qml/delegates/MessageDelegate.qml deleted file mode 100644 index cd1bdcd4..00000000 --- a/resources/qml/delegates/MessageDelegate.qml +++ /dev/null @@ -1,780 +0,0 @@ -// SPDX-FileCopyrightText: Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -import QtQuick 2.6 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.2 -import im.nheko 1.0 - -Item { - id: d - - required property bool isReply - property bool keepFullText: !isReply - property alias child: chooser.child - //implicitWidth: chooser.child?.implicitWidth ?? 0 - required property double proportionalHeight - required property int type - required property string typeString - required property int originalWidth - required property int duration - required property string blurhash - required property string body - required property string formattedBody - required property string eventId - required property string filename - required property string filesize - 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 - required property string roomName - required property string callType - required property int encryptionError - required property int relatedEventCacheBuster - property bool fitsMetadata: (chooser.child && chooser.child.fitsMetadata) ? chooser.child.fitsMetadata : false - property int metadataWidth - - implicitWidth: chooser.child?.implicitWidth - - height: 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 - - width: parent?.width ?? 0 // this should get rid of "cannot read property 'width' of null" - - DelegateChoice { - roleValue: MtxEvent.UnknownEvent - - Placeholder { - typeString: d.typeString - text: "Unretrieved event" - } - - } - - DelegateChoice { - roleValue: MtxEvent.Tombstone - - - ColumnLayout { - width: parent.width - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - Layout.fillWidth: true - formatted: qsTr("This room was replaced for the following reason: %1").arg(d.body) - } - - Button { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Go to replacement room") - onClicked: room.joinReplacementRoom(eventId) - } - - } - } - - DelegateChoice { - roleValue: MtxEvent.TextMessage - - TextMessage { - formatted: d.formattedBody - body: d.body - isOnlyEmoji: d.isOnlyEmoji - isReply: d.isReply - keepFullText: d.keepFullText - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.UnknownMessage - - TextMessage { - formatted: d.formattedBody - body: d.body - isOnlyEmoji: d.isOnlyEmoji - isReply: d.isReply - keepFullText: d.keepFullText - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.ElementEffectMessage - - TextMessage { - formatted: d.formattedBody - body: d.body - isOnlyEmoji: d.isOnlyEmoji - isReply: d.isReply - keepFullText: d.keepFullText - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.NoticeMessage - - NoticeMessage { - formatted: d.formattedBody - body: d.body - isOnlyEmoji: d.isOnlyEmoji - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.EmoteMessage - - NoticeMessage { - formatted: TimelineManager.escapeEmoji(d.userName) + " " + d.formattedBody - color: TimelineManager.userColor(d.userId, palette.base) - body: d.body - isOnlyEmoji: d.isOnlyEmoji - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.ImageMessage - - ImageMessage { - type: d.type - originalWidth: d.originalWidth - proportionalHeight: d.proportionalHeight - url: d.url - blurhash: d.blurhash - body: d.body - filename: d.filename - isReply: d.isReply - eventId: d.eventId - metadataWidth: d.metadataWidth - containerHeight: timelineView.height - } - - } - - DelegateChoice { - roleValue: MtxEvent.Sticker - - ImageMessage { - type: d.type - originalWidth: d.originalWidth - proportionalHeight: d.proportionalHeight - url: d.url - blurhash: d.blurhash - body: d.body - filename: d.filename - isReply: d.isReply - eventId: d.eventId - metadataWidth: d.metadataWidth - containerHeight: timelineView.height - } - - } - - DelegateChoice { - roleValue: MtxEvent.FileMessage - - FileMessage { - eventId: d.eventId - filename: d.filename - filesize: d.filesize - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.VideoMessage - - PlayableMediaMessage { - proportionalHeight: d.proportionalHeight - type: d.type - originalWidth: d.originalWidth - thumbnailUrl: d.thumbnailUrl - eventId: d.eventId - url: d.url - body: d.body - filesize: d.filesize - duration: d.duration - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.AudioMessage - - PlayableMediaMessage { - proportionalHeight: d.proportionalHeight - type: d.type - originalWidth: d.originalWidth - thumbnailUrl: d.thumbnailUrl - eventId: d.eventId - url: d.url - body: d.body - filesize: d.filesize - duration: d.duration - metadataWidth: d.metadataWidth - } - - } - - DelegateChoice { - roleValue: MtxEvent.Redacted - - Redacted { - metadataWidth: d.metadataWidth - } - } - - DelegateChoice { - roleValue: MtxEvent.Redaction - - Pill { - text: qsTr("%1 removed a message").arg(d.userName) - isStateEvent: d.isStateEvent - } - - } - - DelegateChoice { - roleValue: MtxEvent.Encryption - - EncryptionEnabled { - username: d.userName - } - - } - - DelegateChoice { - roleValue: MtxEvent.Encrypted - - Encrypted { - encryptionError: d.encryptionError - eventId: d.eventId - } - - } - - DelegateChoice { - roleValue: MtxEvent.ServerAcl - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 changed which servers are allowed in this room.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.Name - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - 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) - } - - } - - DelegateChoice { - roleValue: MtxEvent.Topic - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.roomTopic ? qsTr("%2 changed the topic to: %1").arg(d.roomTopic).arg(d.userName): qsTr("%1 removed the topic").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.Avatar - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 changed the room avatar").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.PinnedEvents - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 changed the pinned messages.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.ImagePackInRoom - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatImagePackEvent(d.eventId) - } - - } - - - DelegateChoice { - roleValue: MtxEvent.CanonicalAlias - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 changed the addresses for this room.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.SpaceParent - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 changed the parent communities for this room.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.RoomCreate - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 created and configured room: %2").arg(d.userName).arg(room.roomId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.CallInvite - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: { - switch (d.callType) { - case "voice": - return qsTr("%1 placed a voice call.").arg(d.userName); - case "video": - return qsTr("%1 placed a video call.").arg(d.userName); - default: - return qsTr("%1 placed a call.").arg(d.userName); - } - } - } - - } - - DelegateChoice { - roleValue: MtxEvent.CallAnswer - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 answered the call.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.CallReject - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 rejected the call.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.CallSelectAnswer - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 select answer").arg(d.userName) - // formatted: qsTr("Call answered elsewhere") - } - } - - DelegateChoice { - roleValue: MtxEvent.CallHangUp - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 ended the call.").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.CallCandidates - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 is negotiating the call...").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.CallNegotiate - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: qsTr("%1 is negotiating the call...").arg(d.userName) - } - - } - - DelegateChoice { - roleValue: MtxEvent.PowerLevels - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatPowerLevelEvent(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.PolicyRuleUser - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatPolicyRule(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.PolicyRuleRoom - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatPolicyRule(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.PolicyRuleServer - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatPolicyRule(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.RoomJoinRules - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatJoinRuleEvent(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.RoomHistoryVisibility - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatHistoryVisibilityEvent(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.RoomGuestAccess - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: d.relatedEventCacheBuster, room.formatGuestAccessEvent(d.eventId) - } - - } - - DelegateChoice { - roleValue: MtxEvent.Member - - ColumnLayout { - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - Layout.fillWidth: true - formatted: d.relatedEventCacheBuster, room.formatMemberEvent(d.eventId) - } - - Button { - visible: d.relatedEventCacheBuster, room.showAcceptKnockButton(d.eventId) - Layout.alignment: Qt.AlignHCenter - text: qsTr("Allow them in") - onClicked: room.acceptKnock(eventId) - } - - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationRequest - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationRequest" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationStart - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationStart" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationReady - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationReady" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationCancel - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationCancel" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationKey - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationKey" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationMac - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationMac" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationDone - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationDone" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationDone - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationDone" - } - - } - - DelegateChoice { - roleValue: MtxEvent.KeyVerificationAccept - - NoticeMessage { - body: formatted - isOnlyEmoji: false - isReply: d.isReply - keepFullText: d.keepFullText - isStateEvent: d.isStateEvent - formatted: "KeyVerificationAccept" - } - - } - - DelegateChoice { - Placeholder { - typeString: d.typeString - } - - } - - } - -} diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index fb7bf0cc..ac4a82b0 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -22,7 +22,7 @@ Item { required property string url required property string body required property string filesize - property double divisor: isReply ? 4 : 2 + property double divisor: EventDelegateChooser.isReply ? 5 : 3 property int tempWidth: originalWidth < 1? 400: originalWidth implicitWidth: type == MtxEvent.VideoMessage ? Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) : 500 width: Math.min(parent?.width ?? implicitWidth, implicitWidth) diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 52cc982d..1598e8c0 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -48,51 +48,43 @@ AbstractButton { room: room_ eventId: r.eventId replyTo: "" + mainInset: 4 + Nheko.paddingMedium + maxWidth: r.maxWidth //height: replyContainer.implicitHeight - data: GridLayout { + data: Row { id: replyContainer - width: r.maxWidth - columns: 2 - rows: 2 - columnSpacing: Nheko.paddingMedium - rowSpacing: Nheko.paddingSmall + spacing: Nheko.paddingSmall Rectangle { id: colorline - Layout.preferredWidth: 4 - Layout.rowSpan: 2 - Layout.fillHeight: true - - Layout.row: 0 - Layout.column: 0 + width: 4 color: TimelineManager.userColor(r.userId, palette.base) } - AbstractButton { - id: usernameBtn - Layout.fillWidth: true + Column { + spacing: 0 - Layout.row: 0 - Layout.column: 1 + AbstractButton { + id: usernameBtn - contentItem: ElidedLabel { - id: userName_ - fullText: r.userName - color: r.userColor - textFormat: Text.RichText - width: parent.width - elideWidth: width + contentItem: Label { + id: userName_ + text: r.userName + color: r.userColor + textFormat: Text.RichText + width: timelineEvent.main?.width + } + onClicked: room.openUserProfile(r.userId) } - onClicked: room.openUserProfile(r.userId) - } - data: [ - colorline, usernameBtn, timelineEvent.main, - ] + data: [ + usernameBtn, timelineEvent.main, + ] + } } } diff --git a/resources/qml/delegates/TextMessage.qml b/resources/qml/delegates/TextMessage.qml index 03623924..e9254eed 100644 --- a/resources/qml/delegates/TextMessage.qml +++ b/resources/qml/delegates/TextMessage.qml @@ -10,7 +10,7 @@ import im.nheko MatrixText { required property string body required property bool isOnlyEmoji - required property bool isReply + property bool isReply: EventDelegateChooser.isReply required property bool keepFullText required property string formatted @@ -45,7 +45,6 @@ MatrixText { //EventDelegateChooser.fillWidth: true - clip: !keepFullText selectByMouse: !Settings.mobileMode && !isReply enabled: !Settings.mobileMode && !isReply hoverEnabled: !Settings.mobileMode diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index 0060907d..a8629b3e 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -97,6 +97,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) return; item->setParentItem(&chooser); + item->setParent(&chooser); auto roleNames = chooser.room_->roleNames(); QHash nameToRole; @@ -106,8 +107,6 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) QHash roleToPropIdx; std::vector roles; - bool isReplyNeeded = false; - // Workaround for https://bugreports.qt.io/browse/QTBUG-98846 QHash requiredProperties; for (const auto &[propKey, prop] : @@ -124,10 +123,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) if (!prop.isRequired() && !requiredProperties.contains(prop.name())) continue; - if (prop.name() == std::string_view("isReply")) { - isReplyNeeded = true; - roleToPropIdx.insert(-1, i); - } else if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { + if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) { roleToPropIdx.insert(*role, i); roles.emplace_back(*role); @@ -141,6 +137,11 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); Qt::beginPropertyUpdateGroup(); + auto attached = qobject_cast( + qmlAttachedPropertiesObject(obj)); + Q_ASSERT(attached != nullptr); + attached->setIsReply(this->forReply); + for (const auto &role : roles) { const auto &roleName = roleNames[role.role()]; // nhlog::ui()->critical("Setting role {}, {} to {}", @@ -155,22 +156,25 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); } - if (isReplyNeeded) { - const auto roleName = QByteArray("isReply"); - // nhlog::ui()->critical("Setting role {} to {}", roleName.toStdString(), forReply); - - // nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[-1]).name()); - mo->property(roleToPropIdx[-1]).write(obj, forReply); - - if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) - QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); - } Qt::endPropertyUpdateGroup(); // setInitialProperties(rolesToSet); auto update = [this, obj, roleToPropIdx = std::move(roleToPropIdx)](const QList &changedRoles) { + if (changedRoles.empty() || changedRoles.contains(TimelineModel::Roles::Type)) { + int type = chooser.room_ + ->dataById(currentId, + TimelineModel::Roles::Type, + forReply ? chooser.eventId_ : QString()) + .toInt(); + if (type != oldType) { + nhlog::ui()->debug("Type changed!"); + reset(currentId); + return; + } + } + std::vector rolesToRequest; if (changedRoles.empty()) { @@ -233,6 +237,7 @@ EventDelegateChooser::DelegateIncubator::reset(QString id) chooser.room_ ->dataById(id, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString()) .toInt(); + this->oldType = role; for (const auto choice : qAsConst(chooser.choices_)) { const auto &choiceValue = choice->roleValues(); @@ -293,41 +298,58 @@ EventDelegateChooser::updatePolish() nhlog::ui()->critical("POLISHING {}", (void *)this); - if (mainChild) { - auto attached = qobject_cast( - qmlAttachedPropertiesObject(mainChild)); - Q_ASSERT(attached != nullptr); + auto layoutItem = [this](QQuickItem *item, int inset) { + if (item) { + auto attached = qobject_cast( + qmlAttachedPropertiesObject(item)); + Q_ASSERT(attached != nullptr); - // in theory we could also reset the width, but that doesn't seem to work nicely for text - // areas because of how they cache it. - mainChild->setWidth(maxWidth_); - mainChild->ensurePolished(); - auto width = mainChild->implicitWidth(); + int maxWidth = maxWidth_ - inset; - if (width > maxWidth_ || attached->fillWidth()) - width = maxWidth_; + // in theory we could also reset the width, but that doesn't seem to work nicely for + // text areas because of how they cache it. + if (attached->maxWidth() > 0) + item->setWidth(attached->maxWidth()); + else + item->setWidth(maxWidth); + item->ensurePolished(); + auto width = item->implicitWidth(); - nhlog::ui()->debug( - "Made event delegate width: {}, {}", width, mainChild->metaObject()->className()); - mainChild->setWidth(width); - mainChild->ensurePolished(); - } + if (width < 1 || width > maxWidth) + width = maxWidth; - if (replyChild) { - auto attached = qobject_cast( - qmlAttachedPropertiesObject(replyChild)); - Q_ASSERT(attached != nullptr); + if (attached->maxWidth() > 0 && width > attached->maxWidth()) + width = attached->maxWidth(); - // in theory we could also reset the width, but that doesn't seem to work nicely for text - // areas because of how they cache it. - replyChild->setWidth(maxWidth_); - replyChild->ensurePolished(); - auto width = replyChild->implicitWidth(); + if (attached->keepAspectRatio()) { + auto height = width * attached->aspectRatio(); + if (attached->maxHeight() && height > attached->maxHeight()) { + height = attached->maxHeight(); + width = height / attached->aspectRatio(); + } - if (width > maxWidth_ || attached->fillWidth()) - width = maxWidth_; + item->setHeight(height); + } - replyChild->setWidth(width); - replyChild->ensurePolished(); + nhlog::ui()->debug( + "Made event delegate width: {}, {}", width, item->metaObject()->className()); + item->setWidth(width); + item->ensurePolished(); + } + }; + + layoutItem(mainChild, mainInset_); + layoutItem(replyChild, replyInset_); +} + +void +EventDelegateChooserAttachedType::polishChooser() +{ + auto p = parent(); + if (p) { + auto chooser = qobject_cast(p->parent()); + if (chooser) { + chooser->polish(); + } } } diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h index ff67ccd8..ce79444a 100644 --- a/src/timeline/EventDelegateChooser.h +++ b/src/timeline/EventDelegateChooser.h @@ -17,27 +17,78 @@ class EventDelegateChooserAttachedType : public QObject { Q_OBJECT - Q_PROPERTY(bool fillWidth READ fillWidth WRITE setFillWidth NOTIFY fillWidthChanged) + Q_PROPERTY(bool keepAspectRatio READ keepAspectRatio WRITE setKeepAspectRatio NOTIFY + keepAspectRatioChanged) + Q_PROPERTY(double aspectRatio READ aspectRatio WRITE setAspectRatio NOTIFY aspectRatioChanged) + Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) + Q_PROPERTY(int maxHeight READ maxHeight WRITE setMaxHeight NOTIFY maxHeightChanged) + Q_PROPERTY(bool isReply READ isReply WRITE setIsReply NOTIFY isReplyChanged) + QML_ANONYMOUS public: EventDelegateChooserAttachedType(QObject *parent) : QObject(parent) { } - bool fillWidth() const { return fillWidth_; } - void setFillWidth(bool fill) + + bool keepAspectRatio() const { return keepAspectRatio_; } + void setKeepAspectRatio(bool fill) { - fillWidth_ = fill; - emit fillWidthChanged(); + if (fill != keepAspectRatio_) { + keepAspectRatio_ = fill; + emit keepAspectRatioChanged(); + polishChooser(); + } } + + double aspectRatio() const { return aspectRatio_; } + void setAspectRatio(double fill) + { + aspectRatio_ = fill; + emit aspectRatioChanged(); + polishChooser(); + } + + int maxWidth() const { return maxWidth_; } + void setMaxWidth(int fill) + { + maxWidth_ = fill; + emit maxWidthChanged(); + polishChooser(); + } + + int maxHeight() const { return maxHeight_; } + void setMaxHeight(int fill) + { + maxHeight_ = fill; + emit maxHeightChanged(); + } + + bool isReply() const { return isReply_; } + void setIsReply(bool fill) + { + if (fill != isReply_) { + isReply_ = fill; + emit isReplyChanged(); + polishChooser(); + } + } + signals: - void fillWidthChanged(); + void keepAspectRatioChanged(); + void aspectRatioChanged(); + void maxWidthChanged(); + void maxHeightChanged(); + void isReplyChanged(); private: - bool fillWidth_ = false, keepAspectRatio = false; - double aspectRatio = 1.; - int maxWidth = -1; - int maxHeight = -1; + void polishChooser(); + + double aspectRatio_ = 1.; + int maxWidth_ = -1; + int maxHeight_ = -1; + bool keepAspectRatio_ = false; + bool isReply_ = false; }; class EventDelegateChoice : public QObject @@ -84,6 +135,8 @@ class EventDelegateChooser : public QQuickItem Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL) Q_PROPERTY(bool sameWidth READ sameWidth WRITE setSameWidth NOTIFY sameWidthChanged) Q_PROPERTY(int maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged) + Q_PROPERTY(int replyInset READ replyInset WRITE setReplyInset NOTIFY replyInsetChanged) + Q_PROPERTY(int mainInset READ mainInset WRITE setMainInset NOTIFY mainInsetChanged) public: QQmlListProperty choices(); @@ -103,7 +156,7 @@ public: sameWidth_ = width; emit sameWidthChanged(); } - bool maxWidth() const { return maxWidth_; } + int maxWidth() const { return maxWidth_; } void setMaxWidth(int width) { maxWidth_ = width; @@ -111,6 +164,22 @@ public: polish(); } + int replyInset() const { return replyInset_; } + void setReplyInset(int width) + { + replyInset_ = width; + emit replyInsetChanged(); + polish(); + } + + int mainInset() const { return mainInset_; } + void setMainInset(int width) + { + mainInset_ = width; + emit mainInsetChanged(); + polish(); + } + void setRoom(TimelineModel *m) { if (m != room_) { @@ -161,6 +230,8 @@ signals: void replyToChanged(); void sameWidthChanged(); void maxWidthChanged(); + void replyInsetChanged(); + void mainInsetChanged(); private: struct DelegateIncubator final : public QQmlIncubator @@ -183,6 +254,7 @@ private: QString instantiatedId; int instantiatedRole = -1; QAbstractItemModel *instantiatedModel = nullptr; + int oldType = -1; }; QVariant roleValue_; @@ -194,6 +266,8 @@ private: QString replyId; bool sameWidth_ = false; int maxWidth_ = 400; + int replyInset_ = 0; + int mainInset_ = 0; static void appendChoice(QQmlListProperty *, EventDelegateChoice *); static qsizetype choiceCount(QQmlListProperty *); From 2a687a202af605763ce49880cf11379ce4c95d44 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 00:20:30 +0200 Subject: [PATCH 20/32] Fix typing notifications --- src/timeline/EventDelegateChooser.cpp | 4 ++-- src/timeline/EventDelegateChooser.h | 6 +++--- src/timeline/RoomlistModel.cpp | 2 +- src/timeline/TimelineModel.cpp | 5 +++-- src/timeline/TimelineModel.h | 14 +++++++------- 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index a8629b3e..e2319460 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -203,7 +203,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) }; if (!forReply) { - auto row = chooser.room_->idToIndex(currentId); + auto row = chooser.room_->idToIndex(currentId); auto connection = connect( chooser.room_, &QAbstractItemModel::dataChanged, @@ -293,7 +293,7 @@ EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status sta void EventDelegateChooser::updatePolish() { - auto mainChild = qobject_cast(eventIncubator.object()); + auto mainChild = qobject_cast(eventIncubator.object()); auto replyChild = qobject_cast(replyIncubator.object()); nhlog::ui()->critical("POLISHING {}", (void *)this); diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h index ce79444a..df1953ab 100644 --- a/src/timeline/EventDelegateChooser.h +++ b/src/timeline/EventDelegateChooser.h @@ -84,8 +84,8 @@ signals: private: void polishChooser(); - double aspectRatio_ = 1.; - int maxWidth_ = -1; + double aspectRatio_ = 1.; + int maxWidth_ = -1; int maxHeight_ = -1; bool keepAspectRatio_ = false; bool isReply_ = false; @@ -252,7 +252,7 @@ private: QString currentId; QString instantiatedId; - int instantiatedRole = -1; + int instantiatedRole = -1; QAbstractItemModel *instantiatedModel = nullptr; int oldType = -1; }; diff --git a/src/timeline/RoomlistModel.cpp b/src/timeline/RoomlistModel.cpp index 8d8d2977..2bffc9be 100644 --- a/src/timeline/RoomlistModel.cpp +++ b/src/timeline/RoomlistModel.cpp @@ -541,7 +541,7 @@ RoomlistModel::sync(const mtx::responses::Sync &sync_) if (auto t = std::get_if>( &ev)) { - std::vector typing; + QStringList typing; typing.reserve(t->content.user_ids.size()); for (const auto &user : t->content.user_ids) { if (user != http::client()->user_id().to_string()) diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index f5b9e142..c2bcfeb5 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -2288,8 +2288,9 @@ TimelineModel::markSpecialEffectsDone() } QString -TimelineModel::formatTypingUsers(const std::vector &users, const QColor &bg) +TimelineModel::formatTypingUsers(const QStringList &users, const QColor &bg) { + nhlog::db()->critical("TYPING USERS!"); QString temp = tr("%1 and %2 are typing.", "Multiple users are typing. First argument is a comma separated list of potentially " @@ -2335,7 +2336,7 @@ TimelineModel::formatTypingUsers(const std::vector &users, const QColor }; uidWithoutLast.reserve(static_cast(users.size())); - for (size_t i = 0; i + 1 < users.size(); i++) { + for (qsizetype i = 0; i + 1 < users.size(); i++) { uidWithoutLast.append(formatUser(users[i])); } diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h index 8f787f21..23c3c802 100644 --- a/src/timeline/TimelineModel.h +++ b/src/timeline/TimelineModel.h @@ -199,8 +199,8 @@ class TimelineModel final : public QAbstractListModel QML_UNCREATABLE("") Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) - Q_PROPERTY(std::vector typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY - typingUsersChanged) + Q_PROPERTY( + QStringList typingUsers READ typingUsers WRITE updateTypingUsers NOTIFY typingUsersChanged) Q_PROPERTY(QString scrollTarget READ scrollTarget NOTIFY scrollTargetChanged) Q_PROPERTY(QString reply READ reply WRITE setReply NOTIFY replyChanged RESET resetReply) Q_PROPERTY(QString edit READ edit WRITE setEdit NOTIFY editChanged RESET resetEdit) @@ -306,7 +306,7 @@ public: Q_INVOKABLE QString displayName(const QString &id) const; Q_INVOKABLE QString avatarUrl(const QString &id) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const; - Q_INVOKABLE QString formatTypingUsers(const std::vector &users, const QColor &bg); + Q_INVOKABLE QString formatTypingUsers(const QStringList &users, const QColor &bg); Q_INVOKABLE bool showAcceptKnockButton(const QString &id); Q_INVOKABLE void acceptKnock(const QString &id); Q_INVOKABLE void joinReplacementRoom(const QString &id); @@ -405,14 +405,14 @@ public slots: void lastReadIdOnWindowFocus(); void checkAfterFetch(); QVariantMap getDump(const QString &eventId, const QString &relatedTo) const; - void updateTypingUsers(const std::vector &users) + void updateTypingUsers(const QStringList &users) { if (this->typingUsers_ != users) { this->typingUsers_ = users; emit typingUsersChanged(typingUsers_); } } - std::vector typingUsers() const { return typingUsers_; } + QStringList typingUsers() const { return typingUsers_; } bool paginationInProgress() const { return m_paginationInProgress; } QString reply() const { return reply_; } void setReply(const QString &newReply); @@ -470,7 +470,7 @@ signals: void redactionFailed(QString id); void mediaCached(QString mxcUrl, QString cacheUrl); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); - void typingUsersChanged(std::vector users); + void typingUsersChanged(QStringList users); void replyChanged(QString reply); void editChanged(QString reply); void threadChanged(QString id); @@ -528,7 +528,7 @@ private: QString currentId, currentReadId; QString reply_, edit_, thread_; QString textBeforeEdit, replyBeforeEdit; - std::vector typingUsers_; + QStringList typingUsers_; TimelineViewManager *manager_; From 25f19c11b1ab583002e0ba02acd0b4da67fbb164 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 03:48:58 +0200 Subject: [PATCH 21/32] Fix high CPU use caused by ItemParticle --- resources/qml/ui/TimelineEffects.qml | 46 +++++++++++++++++++++------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/resources/qml/ui/TimelineEffects.qml b/resources/qml/ui/TimelineEffects.qml index 72237e31..4e2acea4 100644 --- a/resources/qml/ui/TimelineEffects.qml +++ b/resources/qml/ui/TimelineEffects.qml @@ -9,6 +9,7 @@ Item { id: effectRoot readonly property int maxLifespan: Math.max(confettiEmitter.lifeSpan, rainfallEmitter.lifeSpan) required property bool shouldEffectsRun + visible: effectRoot.shouldEffectsRun function pulseConfetti() { @@ -25,11 +26,13 @@ Item { Component.onCompleted: pause(); paused: !effectRoot.shouldEffectsRun + running: effectRoot.shouldEffectsRun } Emitter { id: confettiEmitter + Component.onCompleted: stop(); group: "confetti" width: effectRoot.width * 3/4 enabled: false @@ -89,26 +92,47 @@ Item { enabled: false anchors.horizontalCenter: effectRoot.horizontalCenter y: -60 - emitRate: effectRoot.width / 50 + emitRate: effectRoot.width / 30 lifeSpan: 10000 system: particleSystem velocity: PointDirection { x: 0 - y: 300 + y: 400 xVariation: 0 yVariation: 75 } - ItemParticle { - system: particleSystem - groups: ["rain"] - fade: false - delegate: Rectangle { - width: 2 - height: 30 + 30 * Math.random() - radius: 2 + // causes high CPU load, see: https://bugreports.qt.io/browse/QTBUG-117923 + //ItemParticle { + // system: particleSystem + // groups: ["rain"] + // fade: false + // visible: effectRoot.shouldEffectsRun + // delegate: Rectangle { + // width: 2 + // height: 30 + 30 * Math.random() + // radius: 2 + // color: "#0099ff" + // } + //} + + ImageParticle { + system: particleSystem + groups: ["rain"] + source: "qrc:/confettiparticle.svg" + rotationVelocity: 0 + rotationVelocityVariation: 0 + colorVariation: 0 color: "#0099ff" + entryEffect: ImageParticle.None + xVector: PointDirection { + x: 0.01 + y: 0 + } + yVector: PointDirection { + x: 0 + y: 5 + } } } } -} From 15b5712f9a7d2f92bc3925848fedc621b6ba0471 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 03:49:45 +0200 Subject: [PATCH 22/32] Start working on bubble delegate --- resources/qml/MessageView.qml | 14 ++++++++++---- resources/qml/delegates/Reply.qml | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 3ffe7d9a..5ea73fb5 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -59,11 +59,17 @@ Item { spacing: 2 verticalLayoutDirection: ListView.BottomToTop - delegate: TimelineDefaultMessageStyle { - messageActions: messageActionsC - messageContextMenu: messageContextMenuC - scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + Component { + id: defaultMessageStyle + + TimelineDefaultMessageStyle { + messageActions: messageActionsC + messageContextMenu: messageContextMenuC + scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + } } + + delegate: defaultMessageStyle footer: Item { anchors.horizontalCenter: parent.horizontalCenter anchors.margins: Nheko.paddingLarge diff --git a/resources/qml/delegates/Reply.qml b/resources/qml/delegates/Reply.qml index 1598e8c0..ece838b7 100644 --- a/resources/qml/delegates/Reply.qml +++ b/resources/qml/delegates/Reply.qml @@ -24,7 +24,7 @@ AbstractButton { property string userName: eventId ? room.dataById(eventId, Room.UserName, "") : "" implicitHeight: replyContainer.implicitHeight implicitWidth: replyContainer.implicitWidth - property int maxWidth + required property int maxWidth NhekoCursorShape { anchors.fill: parent @@ -61,11 +61,13 @@ AbstractButton { id: colorline width: 4 + height: content.height color: TimelineManager.userColor(r.userId, palette.base) } Column { + id: content spacing: 0 AbstractButton { From b03bfa53e46215d49e658f226a8eb6acd7993aab Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 04:18:16 +0200 Subject: [PATCH 23/32] Fix CPU usage from out of frame animated images --- src/ui/MxcAnimatedImage.cpp | 9 +++++++-- src/ui/MxcAnimatedImage.h | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp index 14f5dbd8..ffe54c71 100644 --- a/src/ui/MxcAnimatedImage.cpp +++ b/src/ui/MxcAnimatedImage.cpp @@ -102,10 +102,12 @@ MxcAnimatedImage::startDownload() if (buffer.bytesAvailable() < 4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM movie.setCacheMode(QMovie::CacheAll); - if (play_) + if (play_ && movie.frameCount() > 1) movie.start(); - else + else { movie.jumpToFrame(0); + movie.setPaused(true); + } emit loadedChanged(); update(); }); @@ -173,6 +175,9 @@ MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeD if (!imageDirty) return oldNode; + if (clipRect().isEmpty()) + return oldNode; + imageDirty = false; QSGImageNode *n = static_cast(oldNode); if (!n) { diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h index c9f89764..1f2c0b74 100644 --- a/src/ui/MxcAnimatedImage.h +++ b/src/ui/MxcAnimatedImage.h @@ -29,6 +29,7 @@ public: connect(this, &MxcAnimatedImage::roomChanged, &MxcAnimatedImage::startDownload); connect(&movie, &QMovie::frameChanged, this, &MxcAnimatedImage::newFrame); setFlag(QQuickItem::ItemHasContents); + setFlag(QQuickItem::ItemObservesViewport); // setAcceptHoverEvents(true); } @@ -55,7 +56,12 @@ public: { if (play_ != newPlay) { play_ = newPlay; - movie.setPaused(!play_); + if (movie.frameCount() > 1) + movie.setPaused(!play_); + else { + movie.jumpToFrame(0); + movie.setPaused(true); + } emit playChanged(); } } @@ -77,7 +83,8 @@ private slots: { currentFrame = frame; imageDirty = true; - update(); + if (!clipRect().isEmpty()) + update(); } private: From fc7a1bdfba50253624b446a077fc55aa2b2eeee9 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 19:32:06 +0200 Subject: [PATCH 24/32] Fix missing property showAcceptKnockButton --- resources/qml/TimelineEvent.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/TimelineEvent.qml b/resources/qml/TimelineEvent.qml index 5120fd12..7e60328d 100644 --- a/resources/qml/TimelineEvent.qml +++ b/resources/qml/TimelineEvent.qml @@ -216,9 +216,9 @@ EventDelegateChooser { Button { Layout.alignment: Qt.AlignHCenter text: qsTr("Allow them in") - visible: room.showAcceptKnockButton(eventId) + visible: member.room.showAcceptKnockButton(member.eventId) - onClicked: room.acceptKnock(member.eventId) + onClicked: member.room.acceptKnock(member.eventId) } } } From a86e364d1ab5aecc8fcc3c74fbecb7d9876cdbfc Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 21:28:39 +0200 Subject: [PATCH 25/32] Basic bubble style --- CMakeLists.txt | 2 + resources/qml/MessageView.qml | 11 +- resources/qml/TimelineBubbleMessageStyle.qml | 323 ++++++++++++++++++ resources/qml/TimelineDefaultMessageStyle.qml | 82 +---- resources/qml/TimelineMetadata.qml | 98 ++++++ 5 files changed, 445 insertions(+), 71 deletions(-) create mode 100644 resources/qml/TimelineBubbleMessageStyle.qml create mode 100644 resources/qml/TimelineMetadata.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index cbe2b20e..a426f5d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -703,6 +703,8 @@ set(QML_SOURCES resources/qml/RoomList.qml resources/qml/TimelineSectionHeader.qml resources/qml/TimelineDefaultMessageStyle.qml + resources/qml/TimelineBubbleMessageStyle.qml + resources/qml/TimelineMetadata.qml resources/qml/TimelineView.qml resources/qml/Avatar.qml resources/qml/Completer.qml diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index 5ea73fb5..ab8a3ee8 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -68,8 +68,17 @@ Item { scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) } } + Component { + id: bubbleMessageStyle - delegate: defaultMessageStyle + TimelineBubbleMessageStyle { + messageActions: messageActionsC + messageContextMenu: messageContextMenuC + scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) + } + } + + delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle footer: Item { anchors.horizontalCenter: parent.horizontalCenter anchors.margins: Nheko.paddingLarge diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml new file mode 100644 index 00000000..c6c1aede --- /dev/null +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -0,0 +1,323 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./components" +import "./delegates" +import "./emoji" +import "./ui" +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import im.nheko + +TimelineEvent { + id: wrapper + ListView.delayRemove: true + width: chat.delegateMaxWidth + height: Math.max((section.item?.height ?? 0) + gridContainer.implicitHeight + reactionRow.implicitHeight + unreadRow.height, 10) + anchors.horizontalCenter: ListView.view.contentItem.horizontalCenter + //room: chatRoot.roommodel + + required property var day + required property bool isSender + required property int index + property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day) + property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent) + property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId) + + required property date timestamp + required property string userId + required property string userName + required property string threadId + required property int userPowerlevel + required property bool isEdited + required property bool isEncrypted + required property var reactions + required property int status + required property int trustlevel + required property int notificationlevel + required property int type + required property bool isEditable + + required property QtObject messageContextMenu + required property Item messageActions + + property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header + + property alias hovered: messageHover.hovered + property bool scrolledToThis: false + + mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4 + replyInset: mainInset + 4 + Nheko.paddingSmall + + property int bubbleMargin: 40 + + maxWidth: chat.delegateMaxWidth - avatarMargin - bubbleMargin + + data: [ + Loader { + id: section + + active: wrapper.previousMessageUserId !== wrapper.userId || wrapper.previousMessageDay !== wrapper.day || wrapper.previousMessageIsStateEvent !== wrapper.isStateEvent + //asynchronous: true + sourceComponent: TimelineSectionHeader { + day: wrapper.day + isSender: wrapper.isSender + isStateEvent: wrapper.isStateEvent + parentWidth: wrapper.width + previousMessageDay: wrapper.previousMessageDay + previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent + previousMessageUserId: wrapper.previousMessageUserId + timestamp: wrapper.timestamp + userId: wrapper.userId + userName: wrapper.userName + userPowerlevel: wrapper.userPowerlevel + } + visible: status == Loader.Ready + z: 4 + }, + Rectangle { + anchors.fill: gridContainer + color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" + + // this looks better without margins + TapHandler { + acceptedButtons: Qt.RightButton + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + gesturePolicy: TapHandler.ReleaseWithinBounds + + onSingleTapped: messageContextMenu.show(wrapper.eventId, wrapper.threadId, wrapper.type, wrapper.isSender, wrapper.isEncrypted, wrapper.isEditable, wrapper.main.hoveredLink, wrapper.main.copyText) + } + }, + Rectangle { + id: scrollHighlight + anchors.fill: gridContainer + + color: palette.highlight + enabled: false + opacity: 0 + visible: true + z: 1 + + states: State { + name: "revealed" + when: wrapper.scrolledToThis + } + transitions: Transition { + from: "" + to: "revealed" + + SequentialAnimation { + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 0 + properties: "opacity" + target: scrollHighlight + to: 1 + } + PropertyAnimation { + duration: 500 + easing.type: Easing.InOutQuad + from: 1 + properties: "opacity" + target: scrollHighlight + to: 0 + } + ScriptAction { + script: wrapper.room.eventShown() + } + } + } + }, + Item { + id: gridContainer + + width: wrapper.width - wrapper.avatarMargin + implicitHeight: messageBubble.implicitHeight + x: wrapper.avatarMargin + y: section.visible && section.active ? section.y + section.height : 0 + + HoverHandler { + id: messageHover + blocking: false + onHoveredChanged: () => { + if (!Settings.mobileMode && hovered) { + if (!messageActions.hovered) { + messageActions.model = wrapper; + messageActions.attached = wrapper; + messageActions.anchors.bottomMargin = -gridContainer.y + //messageActions.anchors.rightMargin = metadata.width + } + } + } + + } + + + AbstractButton { + id: messageBubble + + anchors.left: (wrapper.isStateEvent || wrapper.isSender) ? undefined : parent.left + anchors.right: (wrapper.isStateEvent || !wrapper.isSender) ? undefined : parent.right + anchors.horizontalCenter: wrapper.isStateEvent ? parent.horizontalCenter : undefined + + property color userColor: TimelineManager.userColor(wrapper.main?.userId ?? '', palette.base) + + contentItem: Item { + id: contentPlacementContainer + + property int metadataWidth: 100 + property int metadataHeight: 20 + + property bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth + + implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + (fitsMetadata ? metadata.width : 0)) + implicitHeight: contentColumn.implicitHeight + (fitsMetadata ? 0 : metadata.height) + + TimelineMetadata { + id: metadata + + scaling: 0.75 + + anchors.right: parent.right + anchors.bottom: parent.bottom + + visible: !wrapper.isStateEvent + + eventId: wrapper.eventId + status: wrapper.status + trustlevel: wrapper.trustlevel + isEdited: wrapper.isEdited + isEncrypted: wrapper.isEncrypted + threadId: wrapper.threadId + timestamp: wrapper.timestamp + room: wrapper.room + } + + Column { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + + AbstractButton { + id: replyRow + visible: wrapper.reply + + height: replyLine.height + anchors.left: parent.left + anchors.right: parent.right + + property color userColor: TimelineManager.userColor(wrapper.reply?.userId ?? '', palette.base) + + clip: true + + NhekoCursorShape { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + } + + contentItem: Row { + id: replyRowLay + + spacing: Nheko.paddingSmall + + Rectangle { + id: replyLine + height: Math.min( wrapper.reply?.height, timelineView.height / 5) + Nheko.paddingSmall + replyUserButton.height + color: replyRow.userColor + width: 4 + } + + Column { + spacing: 0 + + id: replyCol + + AbstractButton { + id: replyUserButton + + contentItem: Label { + id: userName_ + text: wrapper.reply?.userName ?? '' + color: replyRow.userColor + textFormat: Text.RichText + width: wrapper.maxWidth + //elideWidth: wrapper.maxWidth + } + onClicked: wrapper.room.openUserProfile(wrapper.reply?.userId) + } + data: [ + replyUserButton, + wrapper.reply, + ] + } + } + + background: Rectangle { + //width: replyRow.implicitContentWidth + color: Qt.tint(palette.base, Qt.hsla(replyRow.userColor.hslHue, 0.5, replyRow.userColor.hslLightness, 0.1)) + } + + onClicked: { + let link = wrapper.reply.hoveredLink + if (link) { + Nheko.openLink(link) + } else { + console.log("Scrolling to "+wrapper.replyTo); + wrapper.room.showEvent(wrapper.replyTo) + } + } + } + + data: [replyRow, wrapper.main] + } + } + + padding: 4 + background: Rectangle { + color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent" + radius: 4 + border.color: Nheko.theme.red + border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 + } + } + }, + Reactions { + id: reactionRow + + eventId: wrapper.eventId + layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight + reactions: wrapper.reactions + width: wrapper.width - wrapper.avatarMargin + x: wrapper.avatarMargin + + anchors { + //left: row.bubbleOnRight ? undefined : row.left + //right: row.bubbleOnRight ? row.right : undefined + top: gridContainer.bottom + topMargin: -4 + } + }, + Rectangle { + id: unreadRow + + color: palette.highlight + height: visible ? 3 : 0 + visible: (wrapper.index > 0 && (wrapper.room.fullyReadEventId == wrapper.eventId)) + + anchors { + left: parent.left + right: parent.right + top: reactionRow.bottom + topMargin: 5 + } + } + ] +} + diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index 8beaa8f0..f4906208 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -51,7 +51,7 @@ TimelineEvent { property alias hovered: messageHover.hovered property bool scrolledToThis: false - mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) + 4 + mainInset: (threadId ? (4 + Nheko.paddingSmall) : 0) replyInset: mainInset + 4 + Nheko.paddingSmall maxWidth: chat.delegateMaxWidth - avatarMargin - metadata.width @@ -269,82 +269,24 @@ TimelineEvent { } }, - RowLayout { + TimelineMetadata { id: metadata - property int iconSize: Math.floor(fontMetrics.ascent * scaling) - property double scaling: Settings.bubbles ? 0.75 : 1 + scaling: 1 anchors.right: parent.right y: section.visible && section.active ? section.y + section.height : 0 - spacing: 2 - visible: !isStateEvent + visible: !wrapper.isStateEvent - StatusIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - eventId: wrapper.eventId - height: parent.iconSize - status: wrapper.status - width: parent.iconSize - } - Image { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Edited") - ToolTip.visible: editHovered.hovered - height: parent.iconSize - source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((wrapper.eventId == wrapper.room.edit) ? palette.highlight : palette.buttonText) - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - visible: wrapper.isEdited || wrapper.eventId == wrapper.room.edit - width: parent.iconSize - - HoverHandler { - id: editHovered - - } - } - ImageButton { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - ToolTip.visible: hovered - buttonTextColor: TimelineManager.userColor(wrapper.threadId, palette.base) - height: parent.iconSize - image: ":/icons/icons/ui/thread.svg" - visible: wrapper.threadId - width: parent.iconSize - - onClicked: wrapper.room.thread = threadId - } - EncryptionIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - encrypted: wrapper.isEncrypted - height: parent.iconSize - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - trust: wrapper.trustlevel - visible: wrapper.room.isEncrypted - width: parent.iconSize - } - Label { - id: ts - - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredWidth: implicitWidth - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: Qt.formatDateTime(wrapper.timestamp, Qt.DefaultLocaleLongDate) - ToolTip.visible: ma.hovered - color: palette.inactive.text - font.pointSize: fontMetrics.font.pointSize * parent.scaling - text: wrapper.timestamp.toLocaleTimeString(Locale.ShortFormat) - - HoverHandler { - id: ma - - } - } + eventId: wrapper.eventId + status: wrapper.status + trustlevel: wrapper.trustlevel + isEdited: wrapper.isEdited + isEncrypted: wrapper.isEncrypted + threadId: wrapper.threadId + timestamp: wrapper.timestamp + room: wrapper.room }, Reactions { id: reactionRow diff --git a/resources/qml/TimelineMetadata.qml b/resources/qml/TimelineMetadata.qml new file mode 100644 index 00000000..53282fc5 --- /dev/null +++ b/resources/qml/TimelineMetadata.qml @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import "./components" +import "./delegates" +import "./emoji" +import "./ui" +import "./dialogs" +import Qt.labs.platform 1.1 as Platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import im.nheko + +RowLayout { + id: metadata + + property int iconSize: Math.floor(fontMetrics.ascent * scaling) + required property double scaling + + required property string eventId + required property int status + required property int trustlevel + required property bool isEdited + required property bool isEncrypted + required property string threadId + required property date timestamp + required property Room room + + spacing: 2 + + StatusIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + eventId: metadata.eventId + height: parent.iconSize + status: metadata.status + width: parent.iconSize + } + Image { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Edited") + ToolTip.visible: editHovered.hovered + height: parent.iconSize + source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((metadata.eventId == metadata.room.edit) ? palette.highlight : palette.buttonText) + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + visible: metadata.isEdited || metadata.eventId == metadata.room.edit + width: parent.iconSize + + HoverHandler { + id: editHovered + + } + } + ImageButton { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: qsTr("Part of a thread") + ToolTip.visible: hovered + buttonTextColor: TimelineManager.userColor(metadata.threadId, palette.base) + height: parent.iconSize + image: ":/icons/icons/ui/thread.svg" + visible: metadata.threadId + width: parent.iconSize + + onClicked: metadata.room.thread = threadId + } + EncryptionIndicator { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + encrypted: metadata.isEncrypted + height: parent.iconSize + sourceSize.height: parent.iconSize * Screen.devicePixelRatio + sourceSize.width: parent.iconSize * Screen.devicePixelRatio + trust: metadata.trustlevel + visible: metadata.room.isEncrypted + width: parent.iconSize + } + Label { + id: ts + + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.preferredWidth: implicitWidth + ToolTip.delay: Nheko.tooltipDelay + ToolTip.text: Qt.formatDateTime(metadata.timestamp, Qt.DefaultLocaleLongDate) + ToolTip.visible: ma.hovered + color: palette.inactive.text + font.pointSize: fontMetrics.font.pointSize * parent.scaling + text: metadata.timestamp.toLocaleTimeString(Locale.ShortFormat) + + HoverHandler { + id: ma + + } + } +} From b0df40496d6c9b53eeb29e9d67b944f8e1fb8dd5 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 21:41:00 +0200 Subject: [PATCH 26/32] Shrink replies --- resources/qml/TimelineBubbleMessageStyle.qml | 2 +- resources/qml/TimelineDefaultMessageStyle.qml | 2 +- resources/qml/delegates/ImageMessage.qml | 2 +- resources/qml/delegates/PlayableMediaMessage.qml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml index c6c1aede..cf6f1dec 100644 --- a/resources/qml/TimelineBubbleMessageStyle.qml +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -229,7 +229,7 @@ TimelineEvent { Rectangle { id: replyLine - height: Math.min( wrapper.reply?.height, timelineView.height / 5) + Nheko.paddingSmall + replyUserButton.height + height: Math.min( wrapper.reply?.height, timelineView.height / 10) + Nheko.paddingSmall + replyUserButton.height color: replyRow.userColor width: 4 } diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index f4906208..9685727d 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -217,7 +217,7 @@ TimelineEvent { Rectangle { id: replyLine - height: Math.min( wrapper.reply?.height, timelineView.height / 5) + Nheko.paddingSmall + replyUserButton.height + height: Math.min( wrapper.reply?.height, timelineView.height / 10) + Nheko.paddingSmall + replyUserButton.height color: replyRow.userColor width: 4 } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 06a1a9e7..9c93c25b 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -18,7 +18,7 @@ AbstractButton { required property string filename required property string eventId required property int containerHeight - property double divisor: EventDelegateChooser.isReply ? 5 : 3 + property double divisor: EventDelegateChooser.isReply ? 10 : 4 EventDelegateChooser.keepAspectRatio: true EventDelegateChooser.maxWidth: originalWidth diff --git a/resources/qml/delegates/PlayableMediaMessage.qml b/resources/qml/delegates/PlayableMediaMessage.qml index ac4a82b0..99928369 100644 --- a/resources/qml/delegates/PlayableMediaMessage.qml +++ b/resources/qml/delegates/PlayableMediaMessage.qml @@ -22,7 +22,7 @@ Item { required property string url required property string body required property string filesize - property double divisor: EventDelegateChooser.isReply ? 5 : 3 + property double divisor: EventDelegateChooser.isReply ? 10 : 4 property int tempWidth: originalWidth < 1? 400: originalWidth implicitWidth: type == MtxEvent.VideoMessage ? Math.round(tempWidth*Math.min((timelineView.height/divisor)/(tempWidth*proportionalHeight), 1)) : 500 width: Math.min(parent?.width ?? implicitWidth, implicitWidth) From b29ce3ca440760c9681e56a6c8e0067de3dd9e61 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 21:58:21 +0200 Subject: [PATCH 27/32] cleanups --- .gitlab-ci.yml | 2 +- resources/qml/TimelineBubbleMessageStyle.qml | 2 +- src/timeline/EventDelegateChooser.cpp | 19 ++++++++++--------- src/timeline/TimelineModel.cpp | 1 - 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0dab93df..c345883d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -106,7 +106,6 @@ build-tw: "pkgconfig" "spdlog-devel" "zlib-devel" - "libQt5PlatformHeaders-devel" "cmake(re2)" "cmake(Qt6Core)" "cmake(Qt6DBus)" @@ -117,6 +116,7 @@ build-tw: "cmake(Qt6Svg)" "cmake(Qt6Widgets)" "cmake(Qt6Gui)" + "qt6-qml-private-devel" "pkgconfig(libcurl)" "pkgconfig(libevent)" "pkgconfig(gstreamer-webrtc-1.0)" diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml index cf6f1dec..7635fba9 100644 --- a/resources/qml/TimelineBubbleMessageStyle.qml +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -279,7 +279,7 @@ TimelineEvent { } } - padding: 4 + padding: wrapper.isStateEvent ? 0 : 4 background: Rectangle { color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent" radius: 4 diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp index e2319460..4367bb9c 100644 --- a/src/timeline/EventDelegateChooser.cpp +++ b/src/timeline/EventDelegateChooser.cpp @@ -10,6 +10,8 @@ #include #include +#include + // privat qt headers to access required properties #include #include @@ -133,7 +135,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) } } - nhlog::ui()->debug("Querying data for id {}", currentId.toStdString()); + // nhlog::ui()->debug("Querying data for id {}", currentId.toStdString()); chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles); Qt::beginPropertyUpdateGroup(); @@ -169,7 +171,7 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) forReply ? chooser.eventId_ : QString()) .toInt(); if (type != oldType) { - nhlog::ui()->debug("Type changed!"); + // nhlog::ui()->debug("Type changed!"); reset(currentId); return; } @@ -178,7 +180,8 @@ EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) std::vector rolesToRequest; if (changedRoles.empty()) { - for (auto role : roleToPropIdx.keys()) + for (const auto role : + std::ranges::subrange(roleToPropIdx.keyBegin(), roleToPropIdx.keyEnd())) rolesToRequest.emplace_back(role); } else { for (auto role : changedRoles) { @@ -229,7 +232,7 @@ EventDelegateChooser::DelegateIncubator::reset(QString id) if (!chooser.room_ || id.isEmpty()) return; - nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply); + // nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply); this->currentId = id; @@ -242,8 +245,8 @@ EventDelegateChooser::DelegateIncubator::reset(QString id) for (const auto choice : qAsConst(chooser.choices_)) { const auto &choiceValue = choice->roleValues(); if (choiceValue.contains(role) || choiceValue.empty()) { - nhlog::ui()->debug( - "Instantiating type: {}, c {}", (int)role, choiceValue.contains(role)); + // nhlog::ui()->debug( + // "Instantiating type: {}, c {}", (int)role, choiceValue.contains(role)); if (auto child = qobject_cast(object())) { child->setParentItem(nullptr); @@ -296,7 +299,7 @@ EventDelegateChooser::updatePolish() auto mainChild = qobject_cast(eventIncubator.object()); auto replyChild = qobject_cast(replyIncubator.object()); - nhlog::ui()->critical("POLISHING {}", (void *)this); + // nhlog::ui()->trace("POLISHING {}", (void *)this); auto layoutItem = [this](QQuickItem *item, int inset) { if (item) { @@ -331,8 +334,6 @@ EventDelegateChooser::updatePolish() item->setHeight(height); } - nhlog::ui()->debug( - "Made event delegate width: {}, {}", width, item->metaObject()->className()); item->setWidth(width); item->ensurePolished(); } diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index c2bcfeb5..aa7a5e6a 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -2290,7 +2290,6 @@ TimelineModel::markSpecialEffectsDone() QString TimelineModel::formatTypingUsers(const QStringList &users, const QColor &bg) { - nhlog::db()->critical("TYPING USERS!"); QString temp = tr("%1 and %2 are typing.", "Multiple users are typing. First argument is a comma separated list of potentially " From 57a27396ad139eccf31d17824b932dba4c19c827 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 22:14:01 +0200 Subject: [PATCH 28/32] Remove old TimelineRow --- CMakeLists.txt | 1 - resources/qml/TimelineRow.qml | 347 ---------------------------------- 2 files changed, 348 deletions(-) delete mode 100644 resources/qml/TimelineRow.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index a426f5d2..98116829 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -722,7 +722,6 @@ set(QML_SOURCES resources/qml/Reactions.qml resources/qml/ReplyPopup.qml resources/qml/StatusIndicator.qml - resources/qml/TimelineRow.qml resources/qml/TopBar.qml resources/qml/QuickSwitcher.qml resources/qml/ForwardCompleter.qml diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml deleted file mode 100644 index fc5c6a76..00000000 --- a/resources/qml/TimelineRow.qml +++ /dev/null @@ -1,347 +0,0 @@ -// SPDX-FileCopyrightText: Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -import "./delegates" -import "./emoji" -import QtQuick 2.15 -import QtQuick.Controls 2.3 -import QtQuick.Layouts 1.2 -import QtQuick.Window 2.13 -import im.nheko 1.0 - -AbstractButton { - id: r - - required property string blurhash - required property string body - required property string callType - required property int duration - required property int encryptionError - required property string eventId - required property string filename - required property string filesize - required property string formattedBody - required property int index - required property bool isEditable - required property bool isEdited - required property bool isEncrypted - required property bool isOnlyEmoji - required property bool isSender - required property bool isStateEvent - required property int notificationlevel - required property int originalWidth - required property double proportionalHeight - required property var reactions - required property int relatedEventCacheBuster - required property string replyTo - required property string roomName - required property string roomTopic - required property int status - required property string threadId - required property string thumbnailUrl - required property var timestamp - required property int trustlevel - required property int type - required property string typeString - required property string url - required property string userId - required property string userName - - height: row.height + (reactionRow.height > 0 ? reactionRow.height - 2 : 0) + unreadRow.height - hoverEnabled: true - - states: State { - name: "dragging" - when: draghandler.active - } - transitions: Transition { - from: "dragging" - to: "" - - PropertyAnimation { - duration: 100 - easing.type: Easing.InOutQuad - properties: "x" - target: r - to: 0 - } - } - - onClicked: { - let link = contentItem.child.linkAt != undefined && contentItem.child.linkAt(pressX - row.x - msg.x, pressY - row.y - msg.y - contentItem.y); - if (link) { - Nheko.openLink(link); - } - } - onDoubleClicked: room.reply = eventId - onPressAndHold: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - - Rectangle { - anchors.fill: parent - color: (Settings.messageHoverHighlight && hovered) ? palette.alternateBase : "transparent" - - // this looks better without margins - TapHandler { - acceptedButtons: Qt.RightButton - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad - gesturePolicy: TapHandler.ReleaseWithinBounds - - onSingleTapped: messageContextMenu.show(eventId, threadId, type, isSender, isEncrypted, isEditable, contentItem.child.hoveredLink, contentItem.child.copyText) - } - } - DragHandler { - id: draghandler - - xAxis.maximum: 100 - xAxis.minimum: -100 - yAxis.enabled: false - - onActiveChanged: { - if (!active && (x < -70 || x > 70)) - room.reply = eventId; - } - } - AbstractButton { - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - ToolTip.visible: hovered - anchors.left: parent.left - anchors.leftMargin: Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8) // align bubble with section header - height: parent.height - visible: threadId - width: 4 - - onClicked: room.thread = threadId - - Rectangle { - id: threadLine - - anchors.fill: parent - color: TimelineManager.userColor(threadId, palette.base) - } - } - Rectangle { - id: row - - property color bgColor: palette.base - property bool bubbleOnRight: isSender && Settings.bubbles - property int maxWidth: (parent.width - (Settings.smallAvatars || isStateEvent ? 0 : Nheko.avatarSize + 8)) * (Settings.bubbles && !isStateEvent ? 0.9 : 1) - property color userColor: TimelineManager.userColor(userId, palette.base) - - anchors.horizontalCenter: isStateEvent ? parent.horizontalCenter : undefined - anchors.left: (isStateEvent || bubbleOnRight) ? undefined : parent.left - anchors.leftMargin: (isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) + (threadId ? 6 : 0) // align bubble with section header - anchors.right: (isStateEvent || !bubbleOnRight) ? undefined : parent.right - border.color: Nheko.theme.red - border.width: r.notificationlevel == MtxEvent.Highlight ? 1 : 0 - color: (Settings.bubbles && !isStateEvent) ? Qt.tint(bgColor, Qt.hsla(userColor.hslHue, 0.5, userColor.hslLightness, 0.2)) : "#00000000" - height: msg.height + msg.anchors.margins * 2 - radius: 4 - width: Settings.bubbles ? Math.min(maxWidth, Math.max(reply.implicitWidth + 8, contentItem.implicitWidth + metadata.width + 20)) : maxWidth - - GridLayout { - id: msg - - columnSpacing: 2 - columns: Settings.bubbles ? 1 : 2 - rowSpacing: 0 - rows: Settings.bubbles ? 3 : 2 - - /* - anchors { - left: parent.left - leftMargin: 4 - margins: (Settings.bubbles && !isStateEvent) ? 4 : 2 - right: parent.right - rightMargin: 4 - top: parent.top - } - - // fancy reply, if this is a reply - Reply { - id: reply - - function fromModel(role) { - return replyTo != "" ? room.dataById(replyTo, role, r.eventId) : null; - } - - Layout.bottomMargin: visible ? 2 : 0 - Layout.column: 0 - Layout.fillWidth: true - Layout.maximumWidth: Settings.bubbles ? Number.MAX_VALUE : implicitWidth - Layout.preferredHeight: height - Layout.row: 0 - blurhash: r.relatedEventCacheBuster, fromModel(Room.Blurhash) ?? "" - body: r.relatedEventCacheBuster, fromModel(Room.Body) ?? "" - callType: r.relatedEventCacheBuster, fromModel(Room.Voip) ?? "" - duration: r.relatedEventCacheBuster, fromModel(Room.Duration) ?? 0 - encryptionError: r.relatedEventCacheBuster, fromModel(Room.EncryptionError) ?? 0 - eventId: fromModel(Room.EventId) ?? "" - filename: r.relatedEventCacheBuster, fromModel(Room.Filename) ?? "" - filesize: r.relatedEventCacheBuster, fromModel(Room.Filesize) ?? "" - formattedBody: r.relatedEventCacheBuster, fromModel(Room.FormattedBody) ?? "" - isOnlyEmoji: r.relatedEventCacheBuster, fromModel(Room.IsOnlyEmoji) ?? false - isStateEvent: r.relatedEventCacheBuster, fromModel(Room.IsStateEvent) ?? false - originalWidth: r.relatedEventCacheBuster, fromModel(Room.OriginalWidth) ?? 0 - proportionalHeight: r.relatedEventCacheBuster, fromModel(Room.ProportionalHeight) ?? 1 - relatedEventCacheBuster: r.relatedEventCacheBuster, fromModel(Room.RelatedEventCacheBuster) ?? 0 - roomName: r.relatedEventCacheBuster, fromModel(Room.RoomName) ?? "" - roomTopic: r.relatedEventCacheBuster, fromModel(Room.RoomTopic) ?? "" - thumbnailUrl: r.relatedEventCacheBuster, fromModel(Room.ThumbnailUrl) ?? "" - type: r.relatedEventCacheBuster, fromModel(Room.Type) ?? MtxEvent.UnknownMessage - typeString: r.relatedEventCacheBuster, fromModel(Room.TypeString) ?? "" - url: r.relatedEventCacheBuster, fromModel(Room.Url) ?? "" - userColor: r.relatedEventCacheBuster, TimelineManager.userColor(userId, palette.base) - userId: r.relatedEventCacheBuster, fromModel(Room.UserId) ?? "" - userName: r.relatedEventCacheBuster, fromModel(Room.UserName) ?? "" - visible: replyTo - } - - // actual message content - MessageDelegate { - id: contentItem - - Layout.column: 0 - Layout.fillWidth: true - Layout.preferredHeight: height - Layout.row: 1 - blurhash: r.blurhash - body: r.body - callType: r.callType - duration: r.duration - encryptionError: r.encryptionError - eventId: r.eventId - filename: r.filename - filesize: r.filesize - formattedBody: r.formattedBody - isOnlyEmoji: r.isOnlyEmoji - isReply: false - isStateEvent: r.isStateEvent - metadataWidth: metadata.width - originalWidth: r.originalWidth - proportionalHeight: r.proportionalHeight - relatedEventCacheBuster: r.relatedEventCacheBuster - roomName: r.roomName - roomTopic: r.roomTopic - thumbnailUrl: r.thumbnailUrl - type: r.type - typeString: r.typeString ?? "" - url: r.url - userId: r.userId - userName: r.userName - } - */ - RowLayout { - id: metadata - - property int iconSize: Math.floor(fontMetrics.ascent * scaling) - property double scaling: Settings.bubbles ? 0.75 : 1 - - Layout.alignment: Qt.AlignTop | Qt.AlignRight - Layout.bottomMargin: -2 - Layout.column: Settings.bubbles ? 0 : 1 - Layout.preferredWidth: implicitWidth - Layout.row: Settings.bubbles ? 2 : 0 - Layout.rowSpan: Settings.bubbles ? 1 : 2 - Layout.topMargin: (contentItem.fitsMetadata && Settings.bubbles) ? -height - Layout.bottomMargin : 0 - spacing: 2 - visible: !isStateEvent - - StatusIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - eventId: r.eventId - height: parent.iconSize - status: r.status - width: parent.iconSize - } - Image { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Edited") - ToolTip.visible: editHovered.hovered - height: parent.iconSize - source: "image://colorimage/:/icons/icons/ui/edit.svg?" + ((eventId == room.edit) ? palette.highlight : palette.buttonText) - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - visible: isEdited || eventId == room.edit - width: parent.iconSize - - HoverHandler { - id: editHovered - - } - } - ImageButton { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: qsTr("Part of a thread") - ToolTip.visible: hovered - buttonTextColor: TimelineManager.userColor(threadId, palette.base) - height: parent.iconSize - image: ":/icons/icons/ui/thread.svg" - visible: threadId - width: parent.iconSize - - onClicked: room.thread = threadId - } - EncryptionIndicator { - Layout.alignment: Qt.AlignRight | Qt.AlignVCenter - encrypted: isEncrypted - height: parent.iconSize - sourceSize.height: parent.iconSize * Screen.devicePixelRatio - sourceSize.width: parent.iconSize * Screen.devicePixelRatio - trust: trustlevel - visible: room.isEncrypted - width: parent.iconSize - } - Label { - id: ts - - Layout.alignment: Qt.AlignRight | Qt.AlignTop - Layout.preferredWidth: implicitWidth - ToolTip.delay: Nheko.tooltipDelay - ToolTip.text: Qt.formatDateTime(timestamp, Qt.DefaultLocaleLongDate) - ToolTip.visible: ma.hovered - color: palette.inactive.text - font.pointSize: fontMetrics.font.pointSize * parent.scaling - text: timestamp.toLocaleTimeString(Locale.ShortFormat) - - HoverHandler { - id: ma - - } - } - } - } - } - Reactions { - id: reactionRow - - eventId: r.eventId - layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight - reactions: r.reactions - width: row.maxWidth - - anchors { - left: row.bubbleOnRight ? undefined : row.left - right: row.bubbleOnRight ? row.right : undefined - top: row.bottom - topMargin: -4 - } - } - Rectangle { - id: unreadRow - - color: palette.highlight - height: visible ? 3 : 0 - visible: (r.index > 0 && (room.fullyReadEventId == r.eventId)) - - anchors { - left: parent.left - right: parent.right - top: reactionRow.bottom - topMargin: 5 - } - } -} From f47f66f748cb2e40d498fccd7496c98ba6af1e8d Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 22:29:53 +0200 Subject: [PATCH 29/32] Add threads display and prettier highlight to bubbles style --- resources/qml/TimelineBubbleMessageStyle.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml index 7635fba9..d1a80976 100644 --- a/resources/qml/TimelineBubbleMessageStyle.qml +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -82,7 +82,9 @@ TimelineEvent { }, Rectangle { anchors.fill: gridContainer - color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : "transparent" + property color threadColor: TimelineManager.userColor(wrapper.threadId, palette.base) + property color threadBackgroundColor: wrapper.threadId ? Qt.tint(palette.base, Qt.hsla(threadColor.hslHue, 0.7, threadColor.hslLightness, 0.1)) : "transparent" + color: (Settings.messageHoverHighlight && messageHover.hovered) ? palette.alternateBase : threadBackgroundColor // this looks better without margins TapHandler { @@ -281,7 +283,7 @@ TimelineEvent { padding: wrapper.isStateEvent ? 0 : 4 background: Rectangle { - color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent" + color: !wrapper.isStateEvent ? Qt.tint(palette.base, Qt.hsla(messageBubble.userColor.hslHue, wrapper.hovered ? 0.8 : 0.5, messageBubble.userColor.hslLightness, 0.2)) : "transparent" radius: 4 border.color: Nheko.theme.red border.width: wrapper.notificationlevel == MtxEvent.Highlight ? 1 : 0 From 149535efbe5ced135ae16821f97332fb8972c3c5 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 22:50:41 +0200 Subject: [PATCH 30/32] Make effect messages stand out more --- resources/qml/ui/TimelineEffects.qml | 3 +-- src/timeline/TimelineModel.cpp | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/resources/qml/ui/TimelineEffects.qml b/resources/qml/ui/TimelineEffects.qml index 4e2acea4..35c54c04 100644 --- a/resources/qml/ui/TimelineEffects.qml +++ b/resources/qml/ui/TimelineEffects.qml @@ -24,7 +24,7 @@ Item { ParticleSystem { id: particleSystem - Component.onCompleted: pause(); + Component.onCompleted: stop(); paused: !effectRoot.shouldEffectsRun running: effectRoot.shouldEffectsRun } @@ -32,7 +32,6 @@ Item { Emitter { id: confettiEmitter - Component.onCompleted: stop(); group: "confetti" width: effectRoot.width * 3/4 enabled: false diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp index aa7a5e6a..e8b5d40e 100644 --- a/src/timeline/TimelineModel.cpp +++ b/src/timeline/TimelineModel.cpp @@ -690,6 +690,16 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r formattedBody_.replace(curImg, imgReplacement); } + if (auto effectMessage = + std::get_if>(&event)) { + if (effectMessage->content.msgtype == std::string_view("nic.custom.confetti")) { + formattedBody_.append(QUtf8StringView(u8"🎊")); + } else if (effectMessage->content.msgtype == + std::string_view("io.element.effect.rainfall")) { + formattedBody_.append(QUtf8StringView(u8"🌧️")); + } + } + return QVariant(utils::replaceEmoji(utils::linkifyMessage(formattedBody_))); } case FormattedStateEvent: { From 4d2a5d1e9b1fded11591fbefa6950baf51792959 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 9 Oct 2023 22:53:53 +0200 Subject: [PATCH 31/32] Attach reactions on the right for our own bubbled messages --- resources/qml/TimelineBubbleMessageStyle.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml index d1a80976..ef6aba6e 100644 --- a/resources/qml/TimelineBubbleMessageStyle.qml +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -294,7 +294,7 @@ TimelineEvent { id: reactionRow eventId: wrapper.eventId - layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight + layoutDirection: (!wrapper.isStateEvent && wrapper.isSender) ? Qt.RightToLeft : Qt.LeftToRight reactions: wrapper.reactions width: wrapper.width - wrapper.avatarMargin x: wrapper.avatarMargin From a7af3bb96c60386074ed6220955ada05b27a84b3 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Tue, 10 Oct 2023 00:00:17 +0200 Subject: [PATCH 32/32] Restore reply context menu --- resources/qml/MessageView.qml | 16 +++++++++------- resources/qml/TimelineBubbleMessageStyle.qml | 19 ++++++++++++++----- resources/qml/TimelineDefaultMessageStyle.qml | 11 ++++++++--- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index ab8a3ee8..98b9748a 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -26,7 +26,7 @@ Item { Connections { function onHideMenu() { messageContextMenuC.close(); - replyContextMenu.close(); + replyContextMenuC.close(); } target: MainWindow @@ -65,6 +65,7 @@ Item { TimelineDefaultMessageStyle { messageActions: messageActionsC messageContextMenu: messageContextMenuC + replyContextMenu: replyContextMenuC scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) } } @@ -74,6 +75,7 @@ Item { TimelineBubbleMessageStyle { messageActions: messageActionsC messageContextMenu: messageContextMenuC + replyContextMenu: replyContextMenuC scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY) } } @@ -541,7 +543,7 @@ Item { } } Platform.Menu { - id: replyContextMenu + id: replyContextMenuC property string eventId property string link @@ -557,23 +559,23 @@ Item { Platform.MenuItem { enabled: visible text: qsTr("&Copy") - visible: replyContextMenu.text + visible: replyContextMenuC.text - onTriggered: Clipboard.text = replyContextMenu.text + onTriggered: Clipboard.text = replyContextMenuC.text } Platform.MenuItem { enabled: visible text: qsTr("Copy &link location") - visible: replyContextMenu.link + visible: replyContextMenuC.link - onTriggered: Clipboard.text = replyContextMenu.link + onTriggered: Clipboard.text = replyContextMenuC.link } Platform.MenuItem { enabled: visible text: qsTr("&Go to quoted message") visible: true - onTriggered: room.showEvent(replyContextMenu.eventId) + onTriggered: room.showEvent(replyContextMenuC.eventId) } } RoundButton { diff --git a/resources/qml/TimelineBubbleMessageStyle.qml b/resources/qml/TimelineBubbleMessageStyle.qml index ef6aba6e..2df3d917 100644 --- a/resources/qml/TimelineBubbleMessageStyle.qml +++ b/resources/qml/TimelineBubbleMessageStyle.qml @@ -44,6 +44,7 @@ TimelineEvent { required property bool isEditable required property QtObject messageContextMenu + required property QtObject replyContextMenu required property Item messageActions property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header @@ -173,13 +174,14 @@ TimelineEvent { contentItem: Item { id: contentPlacementContainer - property int metadataWidth: 100 - property int metadataHeight: 20 - property bool fitsMetadata: ((wrapper.main?.width ?? 0) + wrapper.mainInset + metadata.width) < wrapper.maxWidth - implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + (fitsMetadata ? metadata.width : 0)) - implicitHeight: contentColumn.implicitHeight + (fitsMetadata ? 0 : metadata.height) + // This doesnt work because of tables. They might have content in the top of the cell, while the background reaches to the bottom. Maybe using the textDocument we could do more? + // property bool fitsMetadataInside: wrapper.main?.positionAt ? (wrapper.main.positionAt(wrapper.main.width, wrapper.main.height - 4) == wrapper.main.positionAt(wrapper.main.width - metadata.width, wrapper.main.height - 4)) : false + property bool fitsMetadataInside: false + + implicitWidth: Math.max((wrapper.reply?.width ?? 0) + wrapper.replyInset, (wrapper.main?.width ?? 0) + wrapper.mainInset + ((fitsMetadata && !fitsMetadataInside) ? metadata.width : 0)) + implicitHeight: contentColumn.implicitHeight + ((fitsMetadata || fitsMetadataInside) ? 0 : metadata.height) TimelineMetadata { id: metadata @@ -275,6 +277,13 @@ TimelineEvent { wrapper.room.showEvent(wrapper.replyTo) } } + onPressAndHold: wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(pressX-replyLine.width - Nheko.paddingSmall, pressY - replyUserButton.implicitHeight) : "", wrapper.replyTo) + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: (eventPoint) => wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(eventPoint.position.x-replyLine.width - Nheko.paddingSmall, eventPoint.position.y - replyUserButton.implicitHeight) : "", wrapper.replyTo) + gesturePolicy: TapHandler.ReleaseWithinBounds + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + } } data: [replyRow, wrapper.main] diff --git a/resources/qml/TimelineDefaultMessageStyle.qml b/resources/qml/TimelineDefaultMessageStyle.qml index 9685727d..e9a0712d 100644 --- a/resources/qml/TimelineDefaultMessageStyle.qml +++ b/resources/qml/TimelineDefaultMessageStyle.qml @@ -44,6 +44,7 @@ TimelineEvent { required property bool isEditable required property QtObject messageContextMenu + required property QtObject replyContextMenu required property Item messageActions property int avatarMargin: (wrapper.isStateEvent || Settings.smallAvatars ? 0 : (Nheko.avatarSize + 8)) // align bubble with section header @@ -261,6 +262,13 @@ TimelineEvent { wrapper.room.showEvent(wrapper.replyTo) } } + onPressAndHold: wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(pressX-replyLine.width - Nheko.paddingSmall, pressY - replyUserButton.implicitHeight) : "", wrapper.replyTo) + TapHandler { + acceptedButtons: Qt.RightButton + onSingleTapped: (eventPoint) => wrapper.replyContextMenu.show(wrapper.reply.copyText ?? "", wrapper.reply.linkAt ? wrapper.reply.linkAt(eventPoint.position.x-replyLine.width - Nheko.paddingSmall, eventPoint.position.y - replyUserButton.implicitHeight) : "", wrapper.replyTo) + gesturePolicy: TapHandler.ReleaseWithinBounds + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus | PointerDevice.TouchPad + } } data: [ @@ -292,14 +300,11 @@ TimelineEvent { id: reactionRow eventId: wrapper.eventId - layoutDirection: row.bubbleOnRight ? Qt.RightToLeft : Qt.LeftToRight reactions: wrapper.reactions width: wrapper.width - wrapper.avatarMargin x: wrapper.avatarMargin anchors { - //left: row.bubbleOnRight ? undefined : row.left - //right: row.bubbleOnRight ? row.right : undefined top: gridContainer.bottom topMargin: -4 }