diff options
author | Nicolas Werner <nicolas.werner@hotmail.de> | 2023-10-10 00:33:39 +0200 |
---|---|---|
committer | Nicolas Werner <nicolas.werner@hotmail.de> | 2023-10-10 00:33:39 +0200 |
commit | 3a0d5788e1fc49816742ddd4799ad0d658925ba2 (patch) | |
tree | 55686a2e0bd354260ebd8c17fedb755b5d0d131c /src | |
parent | Translated using Weblate (French) (diff) | |
parent | Restore reply context menu (diff) | |
download | nheko-3a0d5788e1fc49816742ddd4799ad0d658925ba2.tar.xz |
Merge branch 'delegate-rework'
Diffstat (limited to 'src')
-rw-r--r-- | src/timeline/EventDelegateChooser.cpp | 356 | ||||
-rw-r--r-- | src/timeline/EventDelegateChooser.h | 276 | ||||
-rw-r--r-- | src/timeline/EventStore.cpp | 4 | ||||
-rw-r--r-- | src/timeline/RoomlistModel.cpp | 2 | ||||
-rw-r--r-- | src/timeline/TimelineModel.cpp | 297 | ||||
-rw-r--r-- | src/timeline/TimelineModel.h | 35 | ||||
-rw-r--r-- | src/ui/MxcAnimatedImage.cpp | 9 | ||||
-rw-r--r-- | src/ui/MxcAnimatedImage.h | 11 | ||||
-rw-r--r-- | src/voip/CallManager.cpp | 3 |
9 files changed, 856 insertions, 137 deletions
diff --git a/src/timeline/EventDelegateChooser.cpp b/src/timeline/EventDelegateChooser.cpp new file mode 100644 index 00000000..4367bb9c --- /dev/null +++ b/src/timeline/EventDelegateChooser.cpp @@ -0,0 +1,356 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "EventDelegateChooser.h" +#include "TimelineModel.h" + +#include "Logging.h" + +#include <QQmlEngine> +#include <QtGlobal> + +#include <ranges> + +// privat qt headers to access required properties +#include <QtQml/private/qqmlincubator_p.h> +#include <QtQml/private/qqmlobjectcreator_p.h> + +QQmlComponent * +EventDelegateChoice::delegate() const +{ + return delegate_; +} + +void +EventDelegateChoice::setDelegate(QQmlComponent *delegate) +{ + if (delegate != delegate_) { + delegate_ = delegate; + emit delegateChanged(); + emit changed(); + } +} + +QList<int> +EventDelegateChoice::roleValues() const +{ + return roleValues_; +} + +void +EventDelegateChoice::setRoleValues(const QList<int> &value) +{ + if (value != roleValues_) { + roleValues_ = value; + emit roleValuesChanged(); + emit changed(); + } +} + +QQmlListProperty<EventDelegateChoice> +EventDelegateChooser::choices() +{ + return QQmlListProperty<EventDelegateChoice>(this, + this, + &EventDelegateChooser::appendChoice, + &EventDelegateChooser::choiceCount, + &EventDelegateChooser::choice, + &EventDelegateChooser::clearChoices); +} + +void +EventDelegateChooser::appendChoice(QQmlListProperty<EventDelegateChoice> *p, EventDelegateChoice *c) +{ + EventDelegateChooser *dc = static_cast<EventDelegateChooser *>(p->object); + dc->choices_.append(c); +} + +qsizetype +EventDelegateChooser::choiceCount(QQmlListProperty<EventDelegateChoice> *p) +{ + return static_cast<EventDelegateChooser *>(p->object)->choices_.count(); +} +EventDelegateChoice * +EventDelegateChooser::choice(QQmlListProperty<EventDelegateChoice> *p, qsizetype index) +{ + return static_cast<EventDelegateChooser *>(p->object)->choices_.at(index); +} +void +EventDelegateChooser::clearChoices(QQmlListProperty<EventDelegateChoice> *p) +{ + static_cast<EventDelegateChooser *>(p->object)->choices_.clear(); +} + +void +EventDelegateChooser::componentComplete() +{ + QQuickItem::componentComplete(); + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); + // eventIncubator.forceCompletion(); +} + +void +EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) +{ + auto item = qobject_cast<QQuickItem *>(obj); + if (!item) + return; + + item->setParentItem(&chooser); + item->setParent(&chooser); + + auto roleNames = chooser.room_->roleNames(); + QHash<QByteArray, int> nameToRole; + for (const auto &[k, v] : roleNames.asKeyValueRange()) { + nameToRole.insert(v, k); + } + + QHash<int, int> roleToPropIdx; + std::vector<QModelRoleData> roles; + // Workaround for https://bugreports.qt.io/browse/QTBUG-98846 + QHash<QString, RequiredPropertyKey> 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); + + Qt::beginPropertyUpdateGroup(); + auto attached = qobject_cast<EventDelegateChooserAttachedType *>( + qmlAttachedPropertiesObject<EventDelegateChooser>(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 {}", + // 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()); + + 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<int> &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<QModelRoleData> rolesToRequest; + + if (changedRoles.empty()) { + for (const auto role : + std::ranges::subrange(roleToPropIdx.keyBegin(), roleToPropIdx.keyEnd())) + 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); + + Qt::beginPropertyUpdateGroup(); + for (const auto &role : rolesToRequest) { + mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); + } + Qt::endPropertyUpdateGroup(); + }; + + if (!forReply) { + auto row = chooser.room_->idToIndex(currentId); + auto connection = connect( + chooser.room_, + &QAbstractItemModel::dataChanged, + obj, + [row, update](const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QList<int> &changedRoles) { + if (row < topLeft.row() || row > bottomRight.row()) + return; + + update(changedRoles); + }, + Qt::QueuedConnection); + connect(&this->chooser, &EventDelegateChooser::destroyed, obj, [connection]() { + QObject::disconnect(connection); + }); + } +} + +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(); + this->oldType = role; + + 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<QQuickItem *>(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<QQuickItem *>(object()); + if (child == nullptr) { + nhlog::ui()->error("Delegate has to be derived of Item!"); + return; + } + + 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_)) + nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); + } +} + +void +EventDelegateChooser::updatePolish() +{ + auto mainChild = qobject_cast<QQuickItem *>(eventIncubator.object()); + auto replyChild = qobject_cast<QQuickItem *>(replyIncubator.object()); + + // nhlog::ui()->trace("POLISHING {}", (void *)this); + + auto layoutItem = [this](QQuickItem *item, int inset) { + if (item) { + auto attached = qobject_cast<EventDelegateChooserAttachedType *>( + qmlAttachedPropertiesObject<EventDelegateChooser>(item)); + Q_ASSERT(attached != nullptr); + + int maxWidth = maxWidth_ - inset; + + // 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(); + + if (width < 1 || width > maxWidth) + width = maxWidth; + + if (attached->maxWidth() > 0 && width > attached->maxWidth()) + width = attached->maxWidth(); + + if (attached->keepAspectRatio()) { + auto height = width * attached->aspectRatio(); + if (attached->maxHeight() && height > attached->maxHeight()) { + height = attached->maxHeight(); + width = height / attached->aspectRatio(); + } + + item->setHeight(height); + } + + item->setWidth(width); + item->ensurePolished(); + } + }; + + layoutItem(mainChild, mainInset_); + layoutItem(replyChild, replyInset_); +} + +void +EventDelegateChooserAttachedType::polishChooser() +{ + auto p = parent(); + if (p) { + auto chooser = qobject_cast<EventDelegateChooser *>(p->parent()); + if (chooser) { + chooser->polish(); + } + } +} diff --git a/src/timeline/EventDelegateChooser.h b/src/timeline/EventDelegateChooser.h new file mode 100644 index 00000000..df1953ab --- /dev/null +++ b/src/timeline/EventDelegateChooser.h @@ -0,0 +1,276 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <QAbstractItemModel> +#include <QQmlComponent> +#include <QQmlIncubator> +#include <QQmlListProperty> +#include <QQuickItem> +#include <QtCore/QObject> +#include <QtCore/QVariant> + +#include "TimelineModel.h" + +class EventDelegateChooserAttachedType : public QObject +{ + Q_OBJECT + 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 keepAspectRatio() const { return keepAspectRatio_; } + void setKeepAspectRatio(bool fill) + { + 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 keepAspectRatioChanged(); + void aspectRatioChanged(); + void maxWidthChanged(); + void maxHeightChanged(); + void isReplyChanged(); + +private: + void polishChooser(); + + double aspectRatio_ = 1.; + int maxWidth_ = -1; + int maxHeight_ = -1; + bool keepAspectRatio_ = false; + bool isReply_ = false; +}; + +class EventDelegateChoice : public QObject +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("DefaultProperty", "delegate") + +public: + Q_PROPERTY(QList<int> 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<int> roleValues() const; + void setRoleValues(const QList<int> &value); + +signals: + void delegateChanged(); + void roleValuesChanged(); + void changed(); + +private: + QList<int> roleValues_; + QQmlComponent *delegate_ = nullptr; +}; + +class EventDelegateChooser : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + Q_CLASSINFO("DefaultProperty", "choices") + + QML_ATTACHED(EventDelegateChooserAttachedType) + + Q_PROPERTY(QQmlListProperty<EventDelegateChoice> 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) + Q_PROPERTY(int replyInset READ replyInset WRITE setReplyInset NOTIFY replyInsetChanged) + Q_PROPERTY(int mainInset READ mainInset WRITE setMainInset NOTIFY mainInsetChanged) + +public: + QQmlListProperty<EventDelegateChoice> choices(); + + [[nodiscard]] QQuickItem *main() const + { + return qobject_cast<QQuickItem *>(eventIncubator.object()); + } + [[nodiscard]] QQuickItem *reply() const + { + return qobject_cast<QQuickItem *>(replyIncubator.object()); + } + + bool sameWidth() const { return sameWidth_; } + void setSameWidth(bool width) + { + sameWidth_ = width; + emit sameWidthChanged(); + } + int maxWidth() const { return maxWidth_; } + void setMaxWidth(int width) + { + maxWidth_ = width; + emit maxWidthChanged(); + 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_) { + room_ = m; + emit roomChanged(); + + if (isComponentComplete()) { + eventIncubator.reset(eventId_); + replyIncubator.reset(replyId); + } + } + } + [[nodiscard]] TimelineModel *room() { return room_; } + + void setEventId(QString idx) + { + 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; } + + 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(); + void replyInsetChanged(); + void mainInsetChanged(); + +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; + int oldType = -1; + }; + + QVariant roleValue_; + QList<EventDelegateChoice *> choices_; + DelegateIncubator eventIncubator{*this, false}; + DelegateIncubator replyIncubator{*this, true}; + TimelineModel *room_{nullptr}; + QString eventId_; + QString replyId; + bool sameWidth_ = false; + int maxWidth_ = 400; + int replyInset_ = 0; + int mainInset_ = 0; + + static void appendChoice(QQmlListProperty<EventDelegateChoice> *, EventDelegateChoice *); + static qsizetype choiceCount(QQmlListProperty<EventDelegateChoice> *); + static EventDelegateChoice *choice(QQmlListProperty<EventDelegateChoice> *, qsizetype index); + static void clearChoices(QQmlListProperty<EventDelegateChoice> *); +}; 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/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<mtx::events::EphemeralEvent<mtx::events::ephemeral::Typing>>( &ev)) { - std::vector<QString> 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 b2a036c5..e8b5d40e 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"}, @@ -560,6 +561,7 @@ TimelineModel::roleNames() const {ReplyTo, "replyTo"}, {ThreadId, "threadId"}, {Reactions, "reactions"}, + {Room, "room"}, {RoomId, "roomId"}, {RoomName, "roomName"}, {RoomTopic, "roomTopic"}, @@ -599,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<qlonglong>(mtx::events::state::PowerLevels{ - cache::client() - ->getStateEvent<mtx::events::state::PowerLevels>(room_id_.toStdString()) - .value_or(mtx::events::StateEvent<mtx::events::state::PowerLevels>{}) - .content} - .user_level(acc::sender(event))); + return static_cast<qlonglong>( + permissions_.powerlevelEvent().user_level(acc::sender(event))); } case Day: { @@ -692,8 +690,90 @@ TimelineModel::data(const mtx::events::collections::TimelineEvents &event, int r formattedBody_.replace(curImg, imgReplacement); } + if (auto effectMessage = + std::get_if<mtx::events::RoomEvent<mtx::events::msg::ElementEffect>>(&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: { + if (mtx::accessors::is_state_event(event)) { + return std::visit( + [this](const auto &e) { + constexpr auto t = mtx::events::state_content_to_type<decltype(e.content)>; + 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("<img height=\"32\" src=\"%1\">") + .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); + 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))) + .arg(QString::fromStdString(to_string(e.type))); + }, + event); + } + return QString(); + } case Url: return QVariant(QString::fromStdString(url(event))); case ThumbnailUrl: @@ -828,6 +908,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: @@ -926,6 +1008,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) { @@ -2196,7 +2298,7 @@ TimelineModel::markSpecialEffectsDone() } QString -TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor &bg) +TimelineModel::formatTypingUsers(const QStringList &users, const QColor &bg) { QString temp = tr("%1 and %2 are typing.", @@ -2243,7 +2345,7 @@ TimelineModel::formatTypingUsers(const std::vector<QString> &users, const QColor }; uidWithoutLast.reserve(static_cast<int>(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])); } @@ -2288,20 +2390,13 @@ TimelineModel::formatJoinRuleEvent(const QString &id) } QString -TimelineModel::formatGuestAccessEvent(const QString &id) +TimelineModel::formatGuestAccessEvent( + const mtx::events::StateEvent<mtx::events::state::GuestAccess> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::GuestAccess>>(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: @@ -2312,21 +2407,13 @@ TimelineModel::formatGuestAccessEvent(const QString &id) } QString -TimelineModel::formatHistoryVisibilityEvent(const QString &id) +TimelineModel::formatHistoryVisibilityEvent( + const mtx::events::StateEvent<mtx::events::state::HistoryVisibility> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::HistoryVisibility>>(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.") @@ -2344,32 +2431,25 @@ TimelineModel::formatHistoryVisibilityEvent(const QString &id) } QString -TimelineModel::formatPowerLevelEvent(const QString &id) +TimelineModel::formatPowerLevelEvent( + const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(e); - if (!event) - return QString(); - mtx::events::StateEvent<mtx::events::state::PowerLevels> 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<mtx::events::StateEvent<mtx::events::state::PowerLevels>>(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); @@ -2379,7 +2459,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++; @@ -2393,16 +2473,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(); @@ -2424,16 +2504,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(); @@ -2456,16 +2536,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(); @@ -2487,17 +2567,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(); @@ -2521,42 +2601,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) { @@ -2581,7 +2661,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(); @@ -2620,26 +2700,19 @@ TimelineModel::formatPowerLevelEvent(const QString &id) } QString -TimelineModel::formatImagePackEvent(const QString &id) +TimelineModel::formatImagePackEvent( + const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(e); - if (!event) - return {}; - mtx::events::StateEvent<mtx::events::msc2545::ImagePack> 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<mtx::events::StateEvent<mtx::events::msc2545::ImagePack>>(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(); @@ -2662,12 +2735,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)"); }(); @@ -2692,7 +2765,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, ""); @@ -2893,34 +2966,27 @@ TimelineModel::joinReplacementRoom(const QString &id) } QString -TimelineModel::formatMemberEvent(const QString &id) +TimelineModel::formatMemberEvent( + const mtx::events::StateEvent<mtx::events::state::Member> &event) const { - auto e = events.get(id.toStdString(), ""); - if (!e) - return {}; - - auto event = std::get_if<mtx::events::StateEvent<mtx::events::state::Member>>(e); - if (!event) - return {}; - mtx::events::StateEvent<mtx::events::state::Member> 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<mtx::events::StateEvent<mtx::events::state::Member>>(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; @@ -2929,9 +2995,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 " @@ -2946,30 +3011,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); @@ -2988,8 +3053,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 fccc99eb..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<QString> 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) @@ -238,6 +238,7 @@ public: IsOnlyEmoji, Body, FormattedBody, + FormattedStateEvent, IsSender, UserId, UserName, @@ -266,6 +267,7 @@ public: ReplyTo, ThreadId, Reactions, + Room, RoomId, RoomName, RoomTopic, @@ -286,6 +288,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 @@ -302,17 +306,22 @@ 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<QString> &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); - Q_INVOKABLE QString formatMemberEvent(const QString &id); + Q_INVOKABLE QString + formatMemberEvent(const mtx::events::StateEvent<mtx::events::state::Member> &event) const; 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<mtx::events::state::HistoryVisibility> &event) const; + QString + formatGuestAccessEvent(const mtx::events::StateEvent<mtx::events::state::GuestAccess> &) const; + QString formatPowerLevelEvent( + const mtx::events::StateEvent<mtx::events::state::PowerLevels> &event) const; + QString formatImagePackEvent( + const mtx::events::StateEvent<mtx::events::msc2545::ImagePack> &event) const; + Q_INVOKABLE QString formatPolicyRule(const QString &id) const; Q_INVOKABLE QVariantMap formatRedactedEvent(const QString &id); Q_INVOKABLE void viewRawMessage(const QString &id); @@ -396,14 +405,14 @@ public slots: void lastReadIdOnWindowFocus(); void checkAfterFetch(); QVariantMap getDump(const QString &eventId, const QString &relatedTo) const; - void updateTypingUsers(const std::vector<QString> &users) + void updateTypingUsers(const QStringList &users) { if (this->typingUsers_ != users) { this->typingUsers_ = users; emit typingUsersChanged(typingUsers_); } } - std::vector<QString> 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); @@ -461,7 +470,7 @@ signals: void redactionFailed(QString id); void mediaCached(QString mxcUrl, QString cacheUrl); void newEncryptedImage(mtx::crypto::EncryptedFile encryptionInfo); - void typingUsersChanged(std::vector<QString> users); + void typingUsersChanged(QStringList users); void replyChanged(QString reply); void editChanged(QString reply); void threadChanged(QString id); @@ -519,7 +528,7 @@ private: QString currentId, currentReadId; QString reply_, edit_, thread_; QString textBeforeEdit, replyBeforeEdit; - std::vector<QString> typingUsers_; + QStringList typingUsers_; TimelineViewManager *manager_; 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<QSGImageNode *>(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: 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 |