diff --git a/src/timeline/ReactionsModel.cpp b/src/timeline/ReactionsModel.cpp
index 2e249819..1200e2ba 100644
--- a/src/timeline/ReactionsModel.cpp
+++ b/src/timeline/ReactionsModel.cpp
@@ -10,7 +10,7 @@ ReactionsModel::roleNames() const
{Key, "key"},
{Count, "counter"},
{Users, "users"},
- {SelfReacted, "selfReacted"},
+ {SelfReactedEvent, "selfReactedEvent"},
};
}
@@ -45,11 +45,11 @@ ReactionsModel::data(const QModelIndex &index, int role) const
}
return users;
}
- case SelfReacted:
+ case SelfReactedEvent:
for (const auto &reaction : reactions[i].reactions)
if (reaction.second.sender == http::client()->user_id().to_string())
- return true;
- return false;
+ return QString::fromStdString(reaction.second.event_id);
+ return QStringLiteral("");
default:
return {};
}
diff --git a/src/timeline/ReactionsModel.h b/src/timeline/ReactionsModel.h
index 5f61cd42..c839afc8 100644
--- a/src/timeline/ReactionsModel.h
+++ b/src/timeline/ReactionsModel.h
@@ -18,7 +18,7 @@ public:
Key,
Count,
Users,
- SelfReacted,
+ SelfReactedEvent,
};
QHash<int, QByteArray> roleNames() const override;
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index c454c6b2..16e4f207 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -154,13 +154,25 @@ TimelineModel::TimelineModel(TimelineViewManager *manager, QString room_id, QObj
connect(this, &TimelineModel::messageSent, this, [this](QString txn_id, QString event_id) {
pending.removeOne(txn_id);
+ auto ev = events.value(txn_id);
+
+ if (auto reaction =
+ std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&ev)) {
+ QString reactedTo =
+ QString::fromStdString(reaction->content.relates_to.event_id);
+ auto &rModel = reactions[reactedTo];
+ rModel.removeReaction(*reaction);
+ auto rCopy = *reaction;
+ rCopy.event_id = event_id.toStdString();
+ rModel.addReaction(room_id_.toStdString(), rCopy);
+ }
+
int idx = idToIndex(txn_id);
if (idx < 0) {
// transaction already received via sync
return;
}
eventOrder[idx] = event_id;
- auto ev = events.value(txn_id);
ev = std::visit(
[event_id](const auto &e) -> mtx::events::collections::TimelineEvents {
auto eventCopy = e;
@@ -379,7 +391,7 @@ TimelineModel::data(const QString &id, int role) const
else
return {};
case RoomId:
- return QVariant(QString::fromStdString(room_id(event)));
+ return QVariant(room_id_);
case RoomName:
return QVariant(QString::fromStdString(room_name(event)));
case RoomTopic:
@@ -683,6 +695,14 @@ TimelineModel::internalAddEvents(
QString reactedTo =
QString::fromStdString(reaction->content.relates_to.event_id);
events.insert(id, e);
+
+ // remove local echo
+ if (!txid.isEmpty()) {
+ auto rCopy = *reaction;
+ rCopy.event_id = txid.toStdString();
+ reactions[reactedTo].removeReaction(rCopy);
+ }
+
reactions[reactedTo].addReaction(room_id_.toStdString(), *reaction);
int idx = idToIndex(reactedTo);
if (idx >= 0)
@@ -1355,10 +1375,11 @@ struct SendMessageVisitor
, model_(model)
{}
+ // Do-nothing operator for all unhandled events
template<typename T>
void operator()(const mtx::events::Event<T> &)
{}
-
+ // Operator for m.room.message events that contain a msgtype in their content
template<typename T,
std::enable_if_t<std::is_same<decltype(T::msgtype), std::string>::value, int> = 0>
void operator()(const mtx::events::RoomEvent<T> &msg)
@@ -1395,6 +1416,36 @@ struct SendMessageVisitor
}
}
+ // Special operator for reactions, which are a type of m.room.message, but need to be
+ // handled distinctly for their differences from normal room messages. Specifically,
+ // reactions need to have the relation outside of ciphertext, or synapse / the homeserver
+ // cannot handle it correctly. See the MSC for more details:
+ // https://github.com/matrix-org/matrix-doc/blob/matthew/msc1849/proposals/1849-aggregations.md#end-to-end-encryption
+ void operator()(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &msg)
+
+ {
+ QString txn_id_qstr = txn_id_qstr_;
+ TimelineModel *model = model_;
+ http::client()
+ ->send_room_message<mtx::events::msg::Reaction, mtx::events::EventType::Reaction>(
+ model->room_id_.toStdString(),
+ txn_id_qstr.toStdString(),
+ msg.content,
+ [txn_id_qstr, model](const mtx::responses::EventId &res,
+ mtx::http::RequestErr err) {
+ if (err) {
+ const int status_code = static_cast<int>(err->status_code);
+ nhlog::net()->warn("[{}] failed to send message: {} {}",
+ txn_id_qstr.toStdString(),
+ err->matrix_error.error,
+ status_code);
+ emit model->messageFailed(txn_id_qstr);
+ }
+ emit model->messageSent(
+ txn_id_qstr, QString::fromStdString(res.event_id.to_string()));
+ });
+ }
+
QString txn_id_qstr_;
TimelineModel *model_;
};
@@ -1426,10 +1477,12 @@ TimelineModel::addPendingMessage(mtx::events::collections::TimelineEvents event)
internalAddEvents({event});
QString txn_id_qstr = QString::fromStdString(mtx::accessors::event_id(event));
- beginInsertRows(QModelIndex(), 0, 0);
pending.push_back(txn_id_qstr);
- this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr);
- endInsertRows();
+ if (!std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&event)) {
+ beginInsertRows(QModelIndex(), 0, 0);
+ this->eventOrder.insert(this->eventOrder.begin(), txn_id_qstr);
+ endInsertRows();
+ }
updateLastMessage();
emit nextPendingMessage();
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index dc7b4985..a3b92f83 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -217,7 +217,6 @@ public slots:
}
std::vector<QString> typingUsers() const { return typingUsers_; }
bool paginationInProgress() const { return m_paginationInProgress; }
-
QString reply() const { return reply_; }
void setReply(QString newReply)
{
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index 30abe506..d6f9fde1 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -13,6 +13,8 @@
#include "MxcImageProvider.h"
#include "UserSettingsPage.h"
#include "dialogs/ImageOverlay.h"
+#include "emoji/EmojiModel.h"
+#include "emoji/Provider.h"
Q_DECLARE_METATYPE(mtx::events::collections::TimelineEvents)
@@ -72,6 +74,18 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
qmlRegisterType<DelegateChoice>("im.nheko", 1, 0, "DelegateChoice");
qmlRegisterType<DelegateChooser>("im.nheko", 1, 0, "DelegateChooser");
qRegisterMetaType<mtx::events::collections::TimelineEvents>();
+ qmlRegisterType<emoji::EmojiModel>("im.nheko.EmojiModel", 1, 0, "EmojiModel");
+ qmlRegisterType<emoji::EmojiProxyModel>("im.nheko.EmojiModel", 1, 0, "EmojiProxyModel");
+ qmlRegisterUncreatableType<QAbstractItemModel>(
+ "im.nheko.EmojiModel", 1, 0, "QAbstractItemModel", "Used by proxy models");
+ qmlRegisterUncreatableType<emoji::Emoji>(
+ "im.nheko.EmojiModel", 1, 0, "Emoji", "Used by emoji models");
+ qmlRegisterUncreatableMetaObject(emoji::staticMetaObject,
+ "im.nheko.EmojiModel",
+ 1,
+ 0,
+ "EmojiCategory",
+ "Error: Only enums");
#ifdef USE_QUICK_VIEW
view = new QQuickView();
@@ -284,6 +298,36 @@ TimelineViewManager::queueEmoteMessage(const QString &msg)
}
void
+TimelineViewManager::reactToMessage(const QString &roomId,
+ const QString &reactedEvent,
+ const QString &reactionKey,
+ const QString &selfReactedEvent)
+{
+ // If selfReactedEvent is empty, that means we haven't previously reacted
+ if (selfReactedEvent.isEmpty()) {
+ queueReactionMessage(roomId, reactedEvent, reactionKey);
+ // Otherwise, we have previously reacted and the reaction should be redacted
+ } else {
+ auto model = models.value(roomId);
+ model->redactEvent(selfReactedEvent);
+ }
+}
+
+void
+TimelineViewManager::queueReactionMessage(const QString &roomId,
+ const QString &reactedEvent,
+ const QString &reactionKey)
+{
+ mtx::events::msg::Reaction reaction;
+ reaction.relates_to.rel_type = mtx::common::RelationType::Annotation;
+ reaction.relates_to.event_id = reactedEvent.toStdString();
+ reaction.relates_to.key = reactionKey.toStdString();
+
+ auto model = models.value(roomId);
+ model->sendMessage(reaction);
+}
+
+void
TimelineViewManager::queueImageMessage(const QString &roomid,
const QString &filename,
const std::optional<mtx::crypto::EncryptedFile> &file,
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 45a603af..48505bc0 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -1,5 +1,6 @@
#pragma once
+#include <QHash>
#include <QQuickView>
#include <QQuickWidget>
#include <QSharedPointer>
@@ -12,6 +13,8 @@
#include "Logging.h"
#include "TimelineModel.h"
#include "Utils.h"
+#include "emoji/EmojiModel.h"
+#include "emoji/Provider.h"
class MxcImageProvider;
class BlurhashProvider;
@@ -55,7 +58,13 @@ public slots:
void setHistoryView(const QString &room_id);
void updateColorPalette();
-
+ void queueReactionMessage(const QString &roomId,
+ const QString &reactedEvent,
+ const QString &reaction);
+ void reactToMessage(const QString &roomId,
+ const QString &reactedEvent,
+ const QString &reactionKey,
+ const QString &selfReactedEvent);
void queueTextMessage(const QString &msg);
void queueEmoteMessage(const QString &msg);
void queueImageMessage(const QString &roomid,
@@ -100,7 +109,8 @@ private:
QHash<QString, QSharedPointer<TimelineModel>> models;
TimelineModel *timeline_ = nullptr;
- bool isInitialSync_ = true;
+
+ bool isInitialSync_ = true;
QSharedPointer<UserSettings> settings;
QHash<QString, QColor> userColors;
|