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
|