From 1edfe9d82ea9d9a50645d419c736db45bf940978 Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Tue, 27 Feb 2018 20:14:47 +0900
Subject: jobs/generated: SetAccountDataJob, SetAccountDataPerRoomJob

---
 jobs/generated/account-data.cpp | 28 ++++++++++++++++++++++++++++
 jobs/generated/account-data.h   | 27 +++++++++++++++++++++++++++
 2 files changed, 55 insertions(+)
 create mode 100644 jobs/generated/account-data.cpp
 create mode 100644 jobs/generated/account-data.h

diff --git a/jobs/generated/account-data.cpp b/jobs/generated/account-data.cpp
new file mode 100644
index 00000000..35ee94c0
--- /dev/null
+++ b/jobs/generated/account-data.cpp
@@ -0,0 +1,28 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#include "account-data.h"
+
+#include "converters.h"
+
+#include <QtCore/QStringBuilder>
+
+using namespace QMatrixClient;
+
+static const auto basePath = QStringLiteral("/_matrix/client/r0");
+
+SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content)
+    : BaseJob(HttpVerb::Put, "SetAccountDataJob",
+        basePath % "/user/" % userId % "/account_data/" % type)
+{
+    setRequestData(Data(content));
+}
+
+SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content)
+    : BaseJob(HttpVerb::Put, "SetAccountDataPerRoomJob",
+        basePath % "/user/" % userId % "/rooms/" % roomId % "/account_data/" % type)
+{
+    setRequestData(Data(content));
+}
+
diff --git a/jobs/generated/account-data.h b/jobs/generated/account-data.h
new file mode 100644
index 00000000..69ad9fb4
--- /dev/null
+++ b/jobs/generated/account-data.h
@@ -0,0 +1,27 @@
+/******************************************************************************
+ * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN
+ */
+
+#pragma once
+
+#include "../basejob.h"
+
+#include <QtCore/QJsonObject>
+
+
+namespace QMatrixClient
+{
+    // Operations
+
+    class SetAccountDataJob : public BaseJob
+    {
+        public:
+            explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {});
+    };
+
+    class SetAccountDataPerRoomJob : public BaseJob
+    {
+        public:
+            explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {});
+    };
+} // namespace QMatrixClient
-- 
cgit v1.2.3


From bc08637eaaf25fb83b685e48e86553d3edacc09a Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Sun, 4 Mar 2018 18:00:05 +0900
Subject: converters.h: pass QJsonValue by reference; add support of
 QHash<QString, T>

---
 converters.h | 56 +++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 39 insertions(+), 17 deletions(-)

diff --git a/converters.h b/converters.h
index 00d1d339..96efe5f8 100644
--- a/converters.h
+++ b/converters.h
@@ -46,17 +46,22 @@ namespace QMatrixClient
 
     inline QJsonValue toJson(const QByteArray& bytes)
     {
-#if QT_VERSION < QT_VERSION_CHECK(5, 3, 0)
-        return QJsonValue(QLatin1String(bytes.constData()));
-#else
         return QJsonValue(bytes.constData());
-#endif
+    }
+
+    template <typename T>
+    inline QJsonValue toJson(const QHash<QString, T>& hashMap)
+    {
+        QJsonObject json;
+        for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
+            json.insert(it.key(), toJson(it.value()));
+        return json;
     }
 
     template <typename T>
     struct FromJson
     {
-        T operator()(QJsonValue jv) const { return static_cast<T>(jv); }
+        T operator()(const QJsonValue& jv) const { return static_cast<T>(jv); }
     };
 
     template <typename T>
@@ -67,32 +72,32 @@ namespace QMatrixClient
 
     template <> struct FromJson<bool>
     {
-        bool operator()(QJsonValue jv) const { return jv.toBool(); }
+        bool operator()(const QJsonValue& jv) const { return jv.toBool(); }
     };
 
     template <> struct FromJson<int>
     {
-        int operator()(QJsonValue jv) const { return jv.toInt(); }
+        int operator()(const QJsonValue& jv) const { return jv.toInt(); }
     };
 
     template <> struct FromJson<double>
     {
-        double operator()(QJsonValue jv) const { return jv.toDouble(); }
+        double operator()(const QJsonValue& jv) const { return jv.toDouble(); }
     };
 
     template <> struct FromJson<qint64>
     {
-        qint64 operator()(QJsonValue jv) const { return qint64(jv.toDouble()); }
+        qint64 operator()(const QJsonValue& jv) const { return qint64(jv.toDouble()); }
     };
 
     template <> struct FromJson<QString>
     {
-        QString operator()(QJsonValue jv) const { return jv.toString(); }
+        QString operator()(const QJsonValue& jv) const { return jv.toString(); }
     };
 
     template <> struct FromJson<QDateTime>
     {
-        QDateTime operator()(QJsonValue jv) const
+        QDateTime operator()(const QJsonValue& jv) const
         {
             return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
         }
@@ -100,7 +105,7 @@ namespace QMatrixClient
 
     template <> struct FromJson<QDate>
     {
-        QDate operator()(QJsonValue jv) const
+        QDate operator()(const QJsonValue& jv) const
         {
             return fromJson<QDateTime>(jv).date();
         }
@@ -108,17 +113,23 @@ namespace QMatrixClient
 
     template <> struct FromJson<QJsonObject>
     {
-        QJsonObject operator()(QJsonValue jv) const { return jv.toObject(); }
+        QJsonObject operator()(const QJsonValue& jv) const
+        {
+            return jv.toObject();
+        }
     };
 
     template <> struct FromJson<QJsonArray>
     {
-        QJsonArray operator()(QJsonValue jv) const { return jv.toArray(); }
+        QJsonArray operator()(const QJsonValue& jv) const
+        {
+            return jv.toArray();
+        }
     };
 
     template <typename T> struct FromJson<QVector<T>>
     {
-        QVector<T> operator()(QJsonValue jv) const
+        QVector<T> operator()(const QJsonValue& jv) const
         {
             const auto jsonArray = jv.toArray();
             QVector<T> vect; vect.resize(jsonArray.size());
@@ -130,7 +141,7 @@ namespace QMatrixClient
 
     template <typename T> struct FromJson<QList<T>>
     {
-        QList<T> operator()(QJsonValue jv) const
+        QList<T> operator()(const QJsonValue& jv) const
         {
             const auto jsonArray = jv.toArray();
             QList<T> sl; sl.reserve(jsonArray.size());
@@ -144,10 +155,21 @@ namespace QMatrixClient
 
     template <> struct FromJson<QByteArray>
     {
-        QByteArray operator()(QJsonValue jv) const
+        inline QByteArray operator()(const QJsonValue& jv) const
         {
             return fromJson<QString>(jv).toLatin1();
         }
     };
 
+    template <typename T> struct FromJson<QHash<QString, T>>
+    {
+        QHash<QString, T> operator()(const QJsonValue& jv) const
+        {
+            const auto json = jv.toObject();
+            QHash<QString, T> h; h.reserve(json.size());
+            for (auto it = json.begin(); it != json.end(); ++it)
+                h.insert(it.key(), fromJson<T>(it.value()));
+            return h;
+        }
+    };
 }  // namespace QMatrixClient
-- 
cgit v1.2.3


From 0be3571d7c96e3df7ec523217e02d58850c7fe73 Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 5 Mar 2018 10:08:30 +0900
Subject: Support saving account data on the server

Closes #152. Saving of specific event types should be added separately.
---
 room.cpp | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/room.cpp b/room.cpp
index cfa705bb..e03a2b5b 100644
--- a/room.cpp
+++ b/room.cpp
@@ -24,6 +24,7 @@
 #include "jobs/generated/leaving.h"
 #include "jobs/generated/receipts.h"
 #include "jobs/generated/redaction.h"
+#include "jobs/generated/account-data.h"
 #include "jobs/setroomstatejob.h"
 #include "events/simplestateevents.h"
 #include "events/roomavatarevent.h"
@@ -202,6 +203,13 @@ class Room::Private
          */
         void processRedaction(RoomEventPtr redactionEvent);
 
+        template <typename EvT>
+        SetAccountDataPerRoomJob* setAccountData(const EvT& event)
+        {
+            return connection->callApi<SetAccountDataPerRoomJob>(
+                    connection->userId(), id, EvT::typeId(), event.toJson());
+        }
+
         QJsonObject toJson() const;
 
     private:
-- 
cgit v1.2.3


From 9057fc02b06bdd3e38e9cf39e68287e02d58596b Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 5 Mar 2018 13:17:27 +0900
Subject: Use constants instead of hardcoded strings

---
 connection.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/connection.cpp b/connection.cpp
index a57bd8b4..80685dd1 100644
--- a/connection.cpp
+++ b/connection.cpp
@@ -539,12 +539,12 @@ QHash<QString, QVector<Room*>> Connection::tagsToRooms() const
 
 QStringList Connection::tagNames() const
 {
-    QStringList tags ({"m.favourite"});
+    QStringList tags ({FavouriteTag});
     for (auto* r: d->roomMap)
         for (const auto& tag: r->tagNames())
-            if (tag != "m.lowpriority" && !tags.contains(tag))
+            if (tag != LowPriorityTag && !tags.contains(tag))
                 tags.push_back(tag);
-    tags.push_back("m.lowpriority");
+    tags.push_back(LowPriorityTag);
     return tags;
 }
 
-- 
cgit v1.2.3


From 6ea1fb621488910de055bd3af4d00343a763541a Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 5 Mar 2018 10:16:20 +0900
Subject: ReadMarkerEvent; TagEvent remade with less boilerplate code

tagevent.h -> accountdataevents.h now has a macro to define more
simplistic events along the lines of simplestateevents.h but inheriting
from Event instead. TagEvent and ReadMarkerEvent(m.fully_read) are
defined using this macro. ReadMarkerEvent is also wired through event.*
(but not further yet).
---
 CMakeLists.txt             |  1 -
 events/accountdataevents.h | 78 ++++++++++++++++++++++++++++++++++++++++++++++
 events/event.cpp           |  5 +--
 events/event.h             |  2 +-
 events/tagevent.cpp        | 71 -----------------------------------------
 events/tagevent.h          | 74 -------------------------------------------
 libqmatrixclient.pri       |  3 +-
 room.cpp                   | 36 ++++++++++++++-------
 room.h                     | 10 +++---
 9 files changed, 112 insertions(+), 168 deletions(-)
 create mode 100644 events/accountdataevents.h
 delete mode 100644 events/tagevent.cpp
 delete mode 100644 events/tagevent.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index d7762e17..e95c72d0 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -60,7 +60,6 @@ set(libqmatrixclient_SRCS
     events/roomavatarevent.cpp
     events/typingevent.cpp
     events/receiptevent.cpp
-    events/tagevent.cpp
     jobs/requestdata.cpp
     jobs/basejob.cpp
     jobs/checkauthmethods.cpp
diff --git a/events/accountdataevents.h b/events/accountdataevents.h
new file mode 100644
index 00000000..78cf9c46
--- /dev/null
+++ b/events/accountdataevents.h
@@ -0,0 +1,78 @@
+#include <utility>
+
+/******************************************************************************
+ * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#pragma once
+
+#include "event.h"
+#include "eventcontent.h"
+
+namespace QMatrixClient
+{
+    static constexpr const char* FavouriteTag = "m.favourite";
+    static constexpr const char* LowPriorityTag = "m.lowpriority";
+
+    struct TagRecord
+    {
+        TagRecord (QString order) : order(std::move(order)) { }
+        explicit TagRecord(const QJsonValue& jv = {})
+            : order(jv.toObject().value("order").toString())
+        { }
+
+        QString order;
+
+        bool operator==(const TagRecord& other) const
+            { return order == other.order; }
+        bool operator!=(const TagRecord& other) const
+            { return !operator==(other); }
+    };
+
+    inline QJsonValue toJson(const TagRecord& rec)
+    {
+        return QJsonObject {{ QStringLiteral("order"), rec.order }};
+    }
+
+    using TagsMap = QHash<QString, TagRecord>;
+
+#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _EnumType, _ContentType, _ContentKey) \
+    class _Name : public Event \
+    { \
+        public: \
+            static constexpr const char* TypeId = _TypeId; \
+            static const char* typeId() { return TypeId; } \
+            explicit _Name(const QJsonObject& obj) \
+                : Event((_EnumType), obj) \
+                , _content(contentJson(), QStringLiteral(#_ContentKey)) \
+            { } \
+            template <typename... Ts> \
+            explicit _Name(Ts&&... contentArgs) \
+                : Event(_EnumType) \
+                , _content(QStringLiteral(#_ContentKey), \
+                           std::forward<Ts>(contentArgs)...) \
+            { } \
+            const _ContentType& _ContentKey() const { return _content.value; } \
+            QJsonObject toJson() const { return _content.toJson(); } \
+        protected: \
+            EventContent::SimpleContent<_ContentType> _content; \
+    };
+
+    DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", EventType::Tag, TagsMap, tags)
+    DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", EventType::ReadMarker,
+                        QString, event_id)
+}
diff --git a/events/event.cpp b/events/event.cpp
index 74a2c3d7..f3e965e2 100644
--- a/events/event.cpp
+++ b/events/event.cpp
@@ -24,7 +24,7 @@
 #include "roomavatarevent.h"
 #include "typingevent.h"
 #include "receiptevent.h"
-#include "tagevent.h"
+#include "accountdataevents.h"
 #include "redactionevent.h"
 #include "logging.h"
 
@@ -88,7 +88,8 @@ EventPtr _impl::doMakeEvent<Event>(const QJsonObject& obj)
         return EventPtr(move(e));
 
     return EventPtr { makeIfMatches<Event,
-        TypingEvent, ReceiptEvent, TagEvent>(obj, obj["type"].toString()) };
+        TypingEvent, ReceiptEvent, TagEvent, ReadMarkerEvent>(
+                    obj, obj["type"].toString()) };
 }
 
 RoomEvent::RoomEvent(Event::Type type) : Event(type) { }
diff --git a/events/event.h b/events/event.h
index f0ca2d15..eccfec41 100644
--- a/events/event.h
+++ b/events/event.h
@@ -45,7 +45,7 @@ namespace QMatrixClient
             enum class Type : quint16
             {
                 Unknown = 0,
-                Typing, Receipt, Tag, DirectChat,
+                Typing, Receipt, Tag, DirectChat, ReadMarker,
                 RoomEventBase = 0x1000,
                 RoomMessage = RoomEventBase + 1,
                 RoomEncryptedMessage, Redaction,
diff --git a/events/tagevent.cpp b/events/tagevent.cpp
deleted file mode 100644
index c643ac62..00000000
--- a/events/tagevent.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-
-#include "tagevent.h"
-
-using namespace QMatrixClient;
-
-TagRecord::TagRecord(const QJsonObject& json)
-    : order(json.value("order").toString())
-{ }
-
-TagEvent::TagEvent()
-    : Event(Type::Tag)
-{
-    // TODO: Support getting a list of tags and saving it
-}
-
-TagEvent::TagEvent(const QJsonObject& obj)
-    : Event(Type::Tag, obj)
-{
-    Q_ASSERT(obj["type"].toString() == TypeId);
-}
-
-QStringList TagEvent::tagNames() const
-{
-    return tagsObject().keys();
-}
-
-QHash<QString, TagRecord> TagEvent::tags() const
-{
-    QHash<QString, TagRecord> result;
-    auto allTags = tagsObject();
-    for (auto it = allTags.begin(); it != allTags.end(); ++ it)
-        result.insert(it.key(), TagRecord(it.value().toObject()));
-    return result;
-}
-
-bool TagEvent::empty() const
-{
-    return tagsObject().empty();
-}
-
-bool TagEvent::contains(const QString& name) const
-{
-    return tagsObject().contains(name);
-}
-
-TagRecord TagEvent::recordForTag(const QString& name) const
-{
-    return TagRecord(tagsObject().value(name).toObject());
-}
-
-QJsonObject TagEvent::tagsObject() const
-{
-    return contentJson().value("tags").toObject();
-}
diff --git a/events/tagevent.h b/events/tagevent.h
deleted file mode 100644
index 26fe8788..00000000
--- a/events/tagevent.h
+++ /dev/null
@@ -1,74 +0,0 @@
-/******************************************************************************
- * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-
-#pragma once
-
-#include "event.h"
-
-namespace QMatrixClient
-{
-    static constexpr const char* FavouriteTag = "m.favourite";
-    static constexpr const char* LowPriorityTag = "m.lowpriority";
-
-    struct TagRecord
-    {
-        explicit TagRecord(const QJsonObject& json = {});
-
-        QString order;
-    };
-
-    class TagEvent : public Event
-    {
-        public:
-            TagEvent();
-            explicit TagEvent(const QJsonObject& obj);
-
-            /** Get the list of tag names */
-            QStringList tagNames() const;
-
-            /** Get the list of tags along with information on each */
-            QHash<QString, TagRecord> tags() const;
-
-            /** Check if the event lists no tags */
-            bool empty() const;
-
-            /** Check whether the tags list contains the specified name */
-            bool contains(const QString& name) const;
-
-            /** Get the record for the given tag name */
-            TagRecord recordForTag(const QString& name) const;
-
-            /** Get the whole tags content as a JSON object
-             * It's NOT recommended to use this method directly from client code.
-             * Use other convenience methods provided by the class.
-             */
-            QJsonObject tagsObject() const;
-
-            static constexpr const char * TypeId = "m.tag";
-    };
-
-    using TagEventPtr = event_ptr_tt<TagEvent>;
-
-    inline QJsonValue toJson(const TagEventPtr& tagEvent)
-    {
-        return QJsonObject {{ "type", "m.tag" },
-            // TODO: Replace tagsObject() with a genuine list of tags
-            // (or make the needed JSON upon TagEvent creation)
-            { "content", QJsonObject {{ "tags", tagEvent->tagsObject() }} }};
-    }
-}
diff --git a/libqmatrixclient.pri b/libqmatrixclient.pri
index 7cfa94a1..c7b95617 100644
--- a/libqmatrixclient.pri
+++ b/libqmatrixclient.pri
@@ -24,7 +24,7 @@ HEADERS += \
     $$PWD/events/roomavatarevent.h \
     $$PWD/events/typingevent.h \
     $$PWD/events/receiptevent.h \
-    $$PWD/events/tagevent.h \
+    $$PWD/events/accountdataevents.h \
     $$PWD/events/redactionevent.h \
     $$PWD/jobs/requestdata.h \
     $$PWD/jobs/basejob.h \
@@ -56,7 +56,6 @@ SOURCES += \
     $$PWD/events/roommemberevent.cpp \
     $$PWD/events/typingevent.cpp \
     $$PWD/events/receiptevent.cpp \
-    $$PWD/events/tagevent.cpp \
     $$PWD/jobs/requestdata.cpp \
     $$PWD/jobs/basejob.cpp \
     $$PWD/jobs/checkauthmethods.cpp \
diff --git a/room.cpp b/room.cpp
index e03a2b5b..971e4121 100644
--- a/room.cpp
+++ b/room.cpp
@@ -65,7 +65,6 @@ enum EventsPlacement : int { Older = -1, Newer = 1 };
 #  define WORKAROUND_EXTENDED_INITIALIZER_LIST
 #endif
 
-
 class Room::Private
 {
     public:
@@ -106,7 +105,7 @@ class Room::Private
         QString firstDisplayedEventId;
         QString lastDisplayedEventId;
         QHash<const User*, QString> lastReadEventIds;
-        TagEventPtr tags = std::make_unique<TagEvent>();
+        TagsMap tags;
         QHash<QString, QVariantHash> accountData;
         QString prevBatch;
         QPointer<RoomMessagesJob> roomMessagesJob;
@@ -574,27 +573,36 @@ void Room::resetHighlightCount()
 
 QStringList Room::tagNames() const
 {
-    return d->tags->tagNames();
+    return d->tags.keys();
 }
 
-QHash<QString, TagRecord> Room::tags() const
+TagsMap Room::tags() const
 {
-    return d->tags->tags();
+    return d->tags;
 }
 
 TagRecord Room::tag(const QString& name) const
 {
-    return d->tags->recordForTag(name);
+    return d->tags.value(name);
+}
+
+void Room::setTags(const TagsMap& newTags)
+{
+    if (newTags == d->tags)
+        return;
+    d->tags = newTags;
+    d->setAccountData(TagEvent(d->tags));
+    emit tagsChanged();
 }
 
 bool Room::isFavourite() const
 {
-    return d->tags->contains(FavouriteTag);
+    return d->tags.contains(FavouriteTag);
 }
 
 bool Room::isLowPriority() const
 {
-    return d->tags->contains(LowPriorityTag);
+    return d->tags.contains(LowPriorityTag);
 }
 
 const RoomMessageEvent*
@@ -1485,13 +1493,19 @@ void Room::processAccountDataEvent(EventPtr event)
     switch (event->type())
     {
         case EventType::Tag:
-            d->tags.reset(static_cast<TagEvent*>(event.release()));
+        {
+            auto newTags = static_cast<TagEvent*>(event.get())->tags();
+            if (newTags == d->tags)
+                break;
+            d->tags = newTags;
             qCDebug(MAIN) << "Room" << id() << "is tagged with: "
                           << tagNames().join(", ");
             emit tagsChanged();
             break;
+        }
         default:
-            d->accountData[event->jsonType()] = event->contentJson().toVariantHash();
+            d->accountData[event->jsonType()] =
+                    event->contentJson().toVariantHash();
     }
 }
 
@@ -1644,7 +1658,7 @@ QJsonObject Room::Private::toJson() const
     }
 
     QJsonArray accountDataEvents;
-    if (!tags->empty())
+    if (!tags.empty())
         accountDataEvents.append(QMatrixClient::toJson(tags));
 
     if (!accountData.empty())
diff --git a/room.h b/room.h
index 71d5c433..bdd11452 100644
--- a/room.h
+++ b/room.h
@@ -20,13 +20,9 @@
 
 #include "jobs/syncjob.h"
 #include "events/roommessageevent.h"
-#include "events/tagevent.h"
+#include "events/accountdataevents.h"
 #include "joinstate.h"
 
-#include <QtCore/QList>
-#include <QtCore/QStringList>
-#include <QtCore/QObject>
-#include <QtCore/QJsonObject>
 #include <QtGui/QPixmap>
 
 #include <memory>
@@ -241,9 +237,11 @@ namespace QMatrixClient
             Q_INVOKABLE void resetHighlightCount();
 
             QStringList tagNames() const;
-            QHash<QString, TagRecord> tags() const;
+            TagsMap tags() const;
             TagRecord tag(const QString& name) const;
 
+            void setTags(const TagsMap& newTags);
+
             /** Check whether the list of tags has m.favourite */
             bool isFavourite() const;
             /** Check whether the list of tags has m.lowpriority */
-- 
cgit v1.2.3


From fcf335e202a49c62be29566daf233866cd2f3584 Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 5 Mar 2018 13:18:37 +0900
Subject: Room::toJson(): Fix caching of tags

---
 room.cpp | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/room.cpp b/room.cpp
index 971e4121..86d1e6cd 100644
--- a/room.cpp
+++ b/room.cpp
@@ -1659,7 +1659,10 @@ QJsonObject Room::Private::toJson() const
 
     QJsonArray accountDataEvents;
     if (!tags.empty())
-        accountDataEvents.append(QMatrixClient::toJson(tags));
+        accountDataEvents.append(QJsonObject(
+            { { QStringLiteral("type"), QStringLiteral("m.tag") }
+            , { QStringLiteral("content"), TagEvent(tags).toJson() }
+            }));
 
     if (!accountData.empty())
     {
-- 
cgit v1.2.3


From f9cd6410623b7ddebf97e248584d1a8e838b4da8 Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 5 Mar 2018 11:44:00 +0900
Subject: Room: addTag() and removeTag()

Slightly changed TagRecord constructors to match.
---
 events/accountdataevents.h |  4 ++--
 room.cpp                   | 20 ++++++++++++++++++++
 room.h                     | 19 +++++++++++++++++++
 3 files changed, 41 insertions(+), 2 deletions(-)

diff --git a/events/accountdataevents.h b/events/accountdataevents.h
index 78cf9c46..f3ba27bb 100644
--- a/events/accountdataevents.h
+++ b/events/accountdataevents.h
@@ -30,8 +30,8 @@ namespace QMatrixClient
 
     struct TagRecord
     {
-        TagRecord (QString order) : order(std::move(order)) { }
-        explicit TagRecord(const QJsonValue& jv = {})
+        TagRecord (QString order = {}) : order(std::move(order)) { }
+        explicit TagRecord(const QJsonValue& jv)
             : order(jv.toObject().value("order").toString())
         { }
 
diff --git a/room.cpp b/room.cpp
index 86d1e6cd..cb94ddb6 100644
--- a/room.cpp
+++ b/room.cpp
@@ -586,6 +586,26 @@ TagRecord Room::tag(const QString& name) const
     return d->tags.value(name);
 }
 
+void Room::addTag(const QString& name, const TagRecord& record)
+{
+    if (d->tags.contains(name))
+        return;
+
+    d->tags.insert(name, record);
+    d->setAccountData(TagEvent(d->tags));
+    emit tagsChanged();
+}
+
+void Room::removeTag(const QString& name)
+{
+    if (!d->tags.contains(name))
+        return;
+
+    d->tags.remove(name);
+    d->setAccountData(TagEvent(d->tags));
+    emit tagsChanged();
+}
+
 void Room::setTags(const TagsMap& newTags)
 {
     if (newTags == d->tags)
diff --git a/room.h b/room.h
index bdd11452..0eb5ecc3 100644
--- a/room.h
+++ b/room.h
@@ -240,6 +240,25 @@ namespace QMatrixClient
             TagsMap tags() const;
             TagRecord tag(const QString& name) const;
 
+            /** Add a new tag to this room
+             * If this room already has this tag, nothing happens. If it's a new
+             * tag for the room, the respective tag record is added to the set
+             * of tags and the new set is sent to the server to update other
+             * clients.
+             */
+            void addTag(const QString& name, const TagRecord& record = {});
+
+            /** Remove a tag from the room */
+            void removeTag(const QString& name);
+
+            /** Overwrite the room's tags
+             * This completely replaces the existing room's tags with a set
+             * of new ones and updates the new set on the server. Unlike
+             * most other methods in Room, this one sends a signal about changes
+             * immediately, not waiting for confirmation from the server
+             * (because tags are saved in account data rather than in shared
+             * room state).
+             */
             void setTags(const TagsMap& newTags);
 
             /** Check whether the list of tags has m.favourite */
-- 
cgit v1.2.3


From 2e7627528308da7629f1293757de2fb4bb22a7ad Mon Sep 17 00:00:00 2001
From: Kitsune Ral <Kitsune-Ral@users.sf.net>
Date: Mon, 5 Mar 2018 16:12:47 +0900
Subject: qmc-example: tests for redaction and tagging; send origin in test
 messages

---
 .travis.yml              |   2 +-
 examples/qmc-example.cpp | 108 +++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 91 insertions(+), 19 deletions(-)

diff --git a/.travis.yml b/.travis.yml
index 001ba11f..c9002c13 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -31,7 +31,7 @@ script:
 - cd ..
 - qmake qmc-example.pro "CONFIG += debug" "CONFIG -= app_bundle" "QMAKE_CC = $CC" "QMAKE_CXX = $CXX"
 - make all
-- $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" '#qmc-test:matrix.org'
+- $VALGRIND ./qmc-example "$QMC_TEST_USER" "$QMC_TEST_PWD" '#qmc-test:matrix.org' "Travis CI job $TRAVIS_JOB_NUMBER"
 
 notifications:
   webhooks:
diff --git a/examples/qmc-example.cpp b/examples/qmc-example.cpp
index e0aabca9..f63b32a2 100644
--- a/examples/qmc-example.cpp
+++ b/examples/qmc-example.cpp
@@ -2,18 +2,68 @@
 #include "connection.h"
 #include "room.h"
 #include "user.h"
+#include "jobs/sendeventjob.h"
 
 #include <QtCore/QCoreApplication>
 #include <QtCore/QStringBuilder>
+#include <QtCore/QTimer>
 #include <iostream>
 
 using namespace QMatrixClient;
 using std::cout;
 using std::endl;
-using std::bind;
 using namespace std::placeholders;
 
-void onNewRoom(Room* r, const char* targetRoomName)
+static int semaphor = 0;
+static Room* targetRoom = nullptr;
+
+#define QMC_CHECK(origin, description, condition) \
+    cout << (description) \
+         << (!!(condition) ? " successul" : " FAILED") << endl; \
+    targetRoom->postMessage(QString(origin) % ": " % QStringLiteral(description) % \
+        (!!(condition) ? QStringLiteral(" successful") : \
+                         QStringLiteral(" FAILED")), MessageEventType::Notice)
+
+void addAndRemoveTag(const char* origin)
+{
+    ++semaphor;
+    static const auto TestTag = QStringLiteral("org.qmatrixclient.test");
+    QObject::connect(targetRoom, &Room::tagsChanged, targetRoom, [=] {
+        cout << "Room " << targetRoom->id().toStdString()
+             << ", tag(s) changed:" << endl
+             << "  " << targetRoom->tagNames().join(", ").toStdString() << endl;
+        if (targetRoom->tags().contains(TestTag))
+        {
+            targetRoom->removeTag(TestTag);
+            QMC_CHECK(origin, "Tagging test",
+                      !targetRoom->tags().contains(TestTag));
+            --semaphor;
+            QObject::disconnect(targetRoom, &Room::tagsChanged, nullptr, nullptr);
+        }
+    });
+    // The reverse order because tagsChanged is emitted synchronously.
+    targetRoom->addTag(TestTag);
+}
+
+void sendAndRedact(const char* origin)
+{
+    ++semaphor;
+    auto* job = targetRoom->connection()->callApi<SendEventJob>(targetRoom->id(),
+            RoomMessageEvent("Message to redact"));
+    QObject::connect(job, &BaseJob::success, targetRoom, [job] {
+        targetRoom->redactEvent(job->eventId(), "qmc-example");
+    });
+    QObject::connect(targetRoom, &Room::replacedEvent, targetRoom,
+        [=] (const RoomEvent* newEvent) {
+            QMC_CHECK(origin, "Redaction", newEvent->isRedacted() &&
+                        newEvent->redactionReason() == "qmc-example");
+            --semaphor;
+            QObject::disconnect(targetRoom, &Room::replacedEvent,
+                                nullptr, nullptr);
+        });
+}
+
+void onNewRoom(Room* r, const char* targetRoomName, const char* origin)
 {
     cout << "New room: " << r->id().toStdString() << endl;
     QObject::connect(r, &Room::namesChanged, [=] {
@@ -24,11 +74,15 @@ void onNewRoom(Room* r, const char* targetRoomName)
         if (targetRoomName && (r->name() == targetRoomName ||
                 r->canonicalAlias() == targetRoomName))
         {
-            r->postMessage(
-                "This is a test message from an example application\n"
-                "The current user is " % r->localUser()->fullName(r) % "\n" %
-                QStringLiteral("This room has %1 member(s)")
-                    .arg(r->memberCount()) % "\n" %
+            cout << "Found the target room, proceeding for tests" << endl;
+            targetRoom = r;
+            addAndRemoveTag(origin);
+            sendAndRedact(origin);
+            targetRoom->postMessage(
+                "This is a test notice from an example application\n"
+                "Origin: " % QString(origin) % "\n"
+                "The current user is " %
+                    targetRoom->localUser()->fullName(targetRoom) % "\n" %
 //                "The room is " %
 //                    (r->isDirectChat() ? "" : "not ") % "a direct chat\n" %
                 "Have a good day",
@@ -36,11 +90,7 @@ void onNewRoom(Room* r, const char* targetRoomName)
             );
         }
     });
-    QObject::connect(r, &Room::tagsChanged, [=] {
-        cout << "Room " << r->id().toStdString() << ", tag(s) changed:" << endl
-             << "  " << r->tagNames().join(", ").toStdString() << endl << endl;
-    });
-    QObject::connect(r, &Room::aboutToAddNewMessages, [=] (RoomEventsRange timeline) {
+    QObject::connect(r, &Room::aboutToAddNewMessages, [r] (RoomEventsRange timeline) {
         cout << timeline.size() << " new event(s) in room "
              << r->id().toStdString() << endl;
 //        for (const auto& item: timeline)
@@ -56,13 +106,15 @@ void onNewRoom(Room* r, const char* targetRoomName)
 
 void finalize(Connection* conn)
 {
+    if (semaphor)
+        cout << "One or more tests FAILED" << endl;
     cout << "Logging out" << endl;
     conn->logout();
     QObject::connect(conn, &Connection::loggedOut, QCoreApplication::instance(),
         [conn] {
             conn->deleteLater();
-            QCoreApplication::instance()->processEvents();
-            QCoreApplication::instance()->quit();
+            QCoreApplication::processEvents();
+            QCoreApplication::exit(semaphor);
         });
 }
 
@@ -83,10 +135,30 @@ int main(int argc, char* argv[])
     });
     const char* targetRoomName = argc >= 4 ? argv[3] : nullptr;
     if (targetRoomName)
-        cout << "Target room name: " << targetRoomName;
+        cout << "Target room name: " << targetRoomName << endl;
+    const char* origin = argc >= 5 ? argv[4] : nullptr;
+    if (origin)
+        cout << "Origin for the test message: " << origin << endl;
     QObject::connect(conn, &Connection::newRoom,
-                     bind(onNewRoom, _1, targetRoomName));
-    QObject::connect(conn, &Connection::syncDone,
-                     bind(finalize, conn));
+        [=](Room* room) { onNewRoom(room, targetRoomName, origin); });
+    QObject::connect(conn, &Connection::syncDone, conn, [conn] {
+        cout << "Sync complete, " << semaphor << " tests in the air" << endl;
+        if (semaphor)
+            conn->sync(10000);
+        else
+        {
+            if (targetRoom)
+            {
+                auto j = conn->callApi<SendEventJob>(targetRoom->id(),
+                            RoomMessageEvent("All tests finished"));
+                QObject::connect(j, &BaseJob::finished,
+                                 conn, [conn] { finalize(conn); });
+            }
+            else
+                finalize(conn);
+        }
+    });
+    // Big red countdown
+    QTimer::singleShot(180000, conn, [conn] { finalize(conn); });
     return app.exec();
 }
-- 
cgit v1.2.3