summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--io.github.NhekoReborn.Nheko.json4
-rw-r--r--resources/qml/Reactions.qml20
-rw-r--r--resources/qml/TimelineRow.qml1
-rw-r--r--resources/qml/TimelineView.qml1
-rw-r--r--src/Olm.cpp4
-rw-r--r--src/timeline/ReactionsModel.cpp98
-rw-r--r--src/timeline/ReactionsModel.h39
-rw-r--r--src/timeline/TimelineModel.cpp29
-rw-r--r--src/timeline/TimelineModel.h3
10 files changed, 186 insertions, 17 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 210340af..10a49dce 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -249,6 +249,7 @@ set(SRC_FILES
 	src/emoji/Provider.cpp
 
 	# Timeline
+	src/timeline/ReactionsModel.cpp
 	src/timeline/TimelineViewManager.cpp
 	src/timeline/TimelineModel.cpp
 	src/timeline/DelegateChooser.cpp
@@ -335,7 +336,7 @@ if(USE_BUNDLED_MTXCLIENT)
 	FetchContent_Declare(
 		MatrixClient
 		GIT_REPOSITORY https://github.com/Nheko-Reborn/mtxclient.git
-		GIT_TAG        v0.3.0
+		GIT_TAG        1893cd6171c40c250ca64d388c082789452340a8
 		)
 	FetchContent_MakeAvailable(MatrixClient)
 else()
@@ -451,6 +452,7 @@ qt5_wrap_cpp(MOC_HEADERS
 	src/emoji/PickButton.h
 
 	# Timeline
+	src/timeline/ReactionsModel.h
 	src/timeline/TimelineViewManager.h
 	src/timeline/TimelineModel.h
 	src/timeline/DelegateChooser.h
diff --git a/io.github.NhekoReborn.Nheko.json b/io.github.NhekoReborn.Nheko.json
index 00e9430f..fe3a4a25 100644
--- a/io.github.NhekoReborn.Nheko.json
+++ b/io.github.NhekoReborn.Nheko.json
@@ -146,9 +146,9 @@
       "name": "mtxclient",
       "sources": [
         {
-          "sha256": "0c2930b5861d93bab9a6515adca74ebaa78984119705d9b4372a9deb275dd30c",
+          "sha256": "a8c0239b7157fe8eadae8b06cd6c4e3531dcc61fc5a7f52dbb3c85106f70e3a5",
           "type": "archive",
-          "url": "https://github.com/Nheko-Reborn/mtxclient/archive/v0.3.0.tar.gz"
+          "url": "https://github.com/Nheko-Reborn/mtxclient/archive/1893cd6171c40c250ca64d388c082789452340a8.tar.gz"
         }
       ]
     },
diff --git a/resources/qml/Reactions.qml b/resources/qml/Reactions.qml
index 86f0071b..c15f0b3d 100644
--- a/resources/qml/Reactions.qml
+++ b/resources/qml/Reactions.qml
@@ -5,18 +5,14 @@ Flow {
 	anchors.left: parent.left
 	anchors.right: parent.right
 	spacing: 4
+
+	property alias reactions: repeater.model
+
 	Repeater {
-		model: ListModel {
-			id: nameModel
-			ListElement { key: "😊"; count: 5; reactedBySelf: true; users: "Nico, RedSky, AAA, BBB, CCC" }
-			ListElement { key: "🤠"; count: 6; reactedBySelf: false; users: "Nico, AAA, BBB, CCC" }
-			ListElement { key: "💘"; count: 1; reactedBySelf: true; users: "Nico" }
-			ListElement { key: "🙈"; count: 7; reactedBySelf: false; users: "Nico, RedSky, AAA, BBB, CCC, DDD" }
-			ListElement { key: "👻"; count: 6; reactedBySelf: false; users: "Nico, RedSky, BBB, CCC" }
-		}
+		id: repeater
+
 		Button {
 			id: reaction
-			//border.width: 1
 			text: model.key
 			hoverEnabled: true
 			implicitWidth: contentItem.childrenRect.width + contentItem.padding*2
@@ -33,7 +29,7 @@ Flow {
 				Text {
 					id: reactionText
 					text: reaction.text
-					font: reaction.font
+					font.family: settings.emoji_font_family
 					opacity: enabled ? 1.0 : 0.3
 					color: reaction.hovered ? colors.highlight : colors.buttonText
 					horizontalAlignment: Text.AlignHCenter
@@ -48,7 +44,7 @@ Flow {
 				}
 
 				Text {
-					text: model.count
+					text: model.counter
 					font: reaction.font
 					opacity: enabled ? 1.0 : 0.3
 					color: reaction.hovered ? colors.highlight : colors.buttonText
@@ -63,7 +59,7 @@ Flow {
 				implicitWidth: reaction.implicitWidth
 				implicitHeight: reaction.implicitHeight
 				opacity: enabled ? 1 : 0.3
-				border.color: (reaction.hovered || model.reactedBySelf )? colors.highlight : colors.buttonText
+				border.color: (reaction.hovered || model.selfReacted )? colors.highlight : colors.buttonText
 				color: colors.dark
 				border.width: 1
 				radius: reaction.height / 2.0
diff --git a/resources/qml/TimelineRow.qml b/resources/qml/TimelineRow.qml
index f3262fbd..22222ef3 100644
--- a/resources/qml/TimelineRow.qml
+++ b/resources/qml/TimelineRow.qml
@@ -54,6 +54,7 @@ MouseArea {
 			}
 
 			Reactions {
+				reactions: model.reactions
 			}
 		}
 
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 997f901e..28d282a1 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -25,6 +25,7 @@ Page {
 		id: settings
 		category: "user"
 		property bool avatar_circles: true
+		property string emoji_font_family: "default"
 	}
 
 	Settings {
diff --git a/src/Olm.cpp b/src/Olm.cpp
index c8e4c13c..8ea39566 100644
--- a/src/Olm.cpp
+++ b/src/Olm.cpp
@@ -164,8 +164,8 @@ encrypt_group_message(const std::string &room_id, const std::string &device_id,
         using namespace mtx::events;
 
         // relations shouldn't be encrypted...
-        mtx::common::RelatesTo relation;
-        if (body["content"].count("m.relates_to") != 0) {
+        mtx::common::ReplyRelatesTo relation;
+        if (body["content"]["m.relates_to"].contains("m.in_reply_to")) {
                 relation = body["content"]["m.relates_to"];
                 body["content"].erase("m.relates_to");
         }
diff --git a/src/timeline/ReactionsModel.cpp b/src/timeline/ReactionsModel.cpp
new file mode 100644
index 00000000..fd061b14
--- /dev/null
+++ b/src/timeline/ReactionsModel.cpp
@@ -0,0 +1,98 @@
+#include "ReactionsModel.h"
+
+#include <MatrixClient.h>
+
+#include "Logging.h"
+
+QHash<int, QByteArray>
+ReactionsModel::roleNames() const
+{
+        return {
+          {Key, "key"},
+          {Count, "counter"},
+          {Users, "users"},
+          {SelfReacted, "selfReacted"},
+        };
+}
+
+int
+ReactionsModel::rowCount(const QModelIndex &) const
+{
+        return static_cast<int>(reactions.size());
+}
+
+QVariant
+ReactionsModel::data(const QModelIndex &index, int role) const
+{
+        const int i = index.row();
+        if (i < 0 || i >= static_cast<int>(reactions.size()))
+                return {};
+
+        switch (role) {
+        case Key:
+                return QString::fromStdString(reactions[i].key);
+        case Count:
+                return static_cast<int>(reactions[i].reactions.size());
+        case Users: {
+                QString users;
+                for (size_t r = 0; r < reactions[i].reactions.size(); r++) {
+                        if (r != 0)
+                                users += ", ";
+                        users += QString::fromStdString(reactions[i].reactions[r].sender);
+                }
+                return users;
+        }
+        case SelfReacted:
+                for (const auto &reaction : reactions[i].reactions)
+                        if (reaction.sender == http::client()->user_id().to_string())
+                                return true;
+                return false;
+        default:
+                return {};
+        }
+}
+
+void
+ReactionsModel::addReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction)
+{
+        int idx = 0;
+        for (auto &storedReactions : reactions) {
+                if (storedReactions.key == reaction.content.relates_to.key) {
+                        storedReactions.reactions.push_back(reaction);
+                        emit dataChanged(index(idx, 0), index(idx, 0));
+                        return;
+                }
+                idx++;
+        }
+
+        beginInsertRows(QModelIndex(), idx, idx);
+        reactions.push_back(KeyReaction{reaction.content.relates_to.key, {reaction}});
+        endInsertRows();
+}
+
+void
+ReactionsModel::removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction)
+{
+        int idx = 0;
+        for (auto &storedReactions : reactions) {
+                if (storedReactions.key == reaction.content.relates_to.key) {
+                        for (auto it = begin(storedReactions.reactions);
+                             it != end(storedReactions.reactions);
+                             ++it) {
+                                if (it->event_id == reaction.event_id) {
+                                        storedReactions.reactions.erase(it);
+                                        break;
+                                }
+                        }
+
+                        if (storedReactions.reactions.size() == 0) {
+                                beginRemoveRows(QModelIndex(), idx, idx);
+                                reactions.erase(reactions.begin() + idx);
+                                endRemoveRows();
+                        } else
+                                emit dataChanged(index(idx, 0), index(idx, 0));
+                        return;
+                }
+                idx++;
+        }
+}
diff --git a/src/timeline/ReactionsModel.h b/src/timeline/ReactionsModel.h
new file mode 100644
index 00000000..ba71f2b7
--- /dev/null
+++ b/src/timeline/ReactionsModel.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <QAbstractListModel>
+#include <QHash>
+
+#include <utility>
+#include <vector>
+
+#include <mtx/events/collections.hpp>
+
+class ReactionsModel : public QAbstractListModel
+{
+        Q_OBJECT
+public:
+        explicit ReactionsModel(QObject *parent = nullptr) { Q_UNUSED(parent); }
+        enum Roles
+        {
+                Key,
+                Count,
+                Users,
+                SelfReacted,
+        };
+
+        QHash<int, QByteArray> roleNames() const override;
+        int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+
+public slots:
+        void addReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction);
+        void removeReaction(const mtx::events::RoomEvent<mtx::events::msg::Reaction> &reaction);
+
+private:
+        struct KeyReaction
+        {
+                std::string key;
+                std::vector<mtx::events::RoomEvent<mtx::events::msg::Reaction>> reactions;
+        };
+        std::vector<KeyReaction> reactions;
+};
diff --git a/src/timeline/TimelineModel.cpp b/src/timeline/TimelineModel.cpp
index 340bae39..0555d2ba 100644
--- a/src/timeline/TimelineModel.cpp
+++ b/src/timeline/TimelineModel.cpp
@@ -223,6 +223,7 @@ TimelineModel::roleNames() const
           {State, "state"},
           {IsEncrypted, "isEncrypted"},
           {ReplyTo, "replyTo"},
+          {Reactions, "reactions"},
           {RoomId, "roomId"},
           {RoomName, "roomName"},
           {RoomTopic, "roomTopic"},
@@ -337,6 +338,11 @@ TimelineModel::data(const QString &id, int role) const
         }
         case ReplyTo:
                 return QVariant(QString::fromStdString(in_reply_to_event(event)));
+        case Reactions:
+                if (reactions.count(id))
+                        return QVariant::fromValue((QObject *)&reactions.at(id));
+                else
+                        return {};
         case RoomId:
                 return QVariant(QString::fromStdString(room_id(event)));
         case RoomName:
@@ -574,6 +580,18 @@ TimelineModel::internalAddEvents(
                         QString redacts = QString::fromStdString(redaction->redacts);
                         auto redacted   = std::find(eventOrder.begin(), eventOrder.end(), redacts);
 
+                        auto event = events.value(redacts);
+                        if (auto reaction =
+                              std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(
+                                &event)) {
+                                QString reactedTo =
+                                  QString::fromStdString(reaction->content.relates_to.event_id);
+                                reactions[reactedTo].removeReaction(*reaction);
+                                int idx = idToIndex(reactedTo);
+                                if (idx >= 0)
+                                        emit dataChanged(index(idx, 0), index(idx, 0));
+                        }
+
                         if (redacted != eventOrder.end()) {
                                 auto redactedEvent = std::visit(
                                   [](const auto &ev)
@@ -597,6 +615,17 @@ TimelineModel::internalAddEvents(
                         continue; // don't insert redaction into timeline
                 }
 
+                if (auto reaction =
+                      std::get_if<mtx::events::RoomEvent<mtx::events::msg::Reaction>>(&e)) {
+                        QString reactedTo =
+                          QString::fromStdString(reaction->content.relates_to.event_id);
+                        reactions[reactedTo].addReaction(*reaction);
+                        int idx = idToIndex(reactedTo);
+                        if (idx >= 0)
+                                emit dataChanged(index(idx, 0), index(idx, 0));
+                        continue; // don't insert reaction into timeline
+                }
+
                 if (auto event =
                       std::get_if<mtx::events::EncryptedEvent<mtx::events::msg::Encrypted>>(&e)) {
                         auto e_      = decryptEvent(*event).event;
diff --git a/src/timeline/TimelineModel.h b/src/timeline/TimelineModel.h
index cc63eca2..ecb64693 100644
--- a/src/timeline/TimelineModel.h
+++ b/src/timeline/TimelineModel.h
@@ -9,6 +9,7 @@
 #include <mtxclient/http/errors.hpp>
 
 #include "CacheCryptoStructs.h"
+#include "ReactionsModel.h"
 
 namespace mtx::http {
 using RequestErr = const std::optional<mtx::http::ClientError> &;
@@ -155,6 +156,7 @@ public:
                 State,
                 IsEncrypted,
                 ReplyTo,
+                Reactions,
                 RoomId,
                 RoomName,
                 RoomTopic,
@@ -271,6 +273,7 @@ private:
         QSet<QString> read;
         QList<QString> pending;
         std::vector<QString> eventOrder;
+        std::map<QString, ReactionsModel> reactions;
 
         QString room_id_;
         QString prev_batch_token_;