From 6b79b8b2b45b85f3ec54dae2933601e841ab8309 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 13 Sep 2024 14:28:29 +0200 Subject: [PATCH 1/5] AbstractEventMetaType: expose derivedTypes; cleanup/reformatting --- Quotient/events/event.cpp | 45 +++++++++++++-------------------------- Quotient/events/event.h | 5 +++-- 2 files changed, 18 insertions(+), 32 deletions(-) diff --git a/Quotient/events/event.cpp b/Quotient/events/event.cpp index 0c7341c95..9da7ae1ff 100644 --- a/Quotient/events/event.cpp +++ b/Quotient/events/event.cpp @@ -4,7 +4,7 @@ #include "event.h" #include "../logging_categories_p.h" -#include "stateevent.h" +#include "../ranges_extras.h" #include @@ -13,46 +13,31 @@ using namespace Quotient; void AbstractEventMetaType::addDerived(const AbstractEventMetaType* newType) { if (const auto existing = - std::find_if(derivedTypes.cbegin(), derivedTypes.cend(), - [&newType](const AbstractEventMetaType* t) { - return t->matrixId == newType->matrixId; - }); - existing != derivedTypes.cend()) - { + findIndirect(_derivedTypes, newType->matrixId, &AbstractEventMetaType::matrixId); + existing != _derivedTypes.cend()) { if (*existing == newType) return; // Two different metatype objects claim the same Matrix type id; this // is not normal, so give as much information as possible to diagnose if ((*existing)->className == newType->className) { - qCritical(EVENTS) - << newType->className << "claims" << newType->matrixId - << "repeatedly; check that it's exported across translation " - "units or shared objects"; + qCritical(EVENTS) << newType->className << "claims" << newType->matrixId + << "repeatedly; check that it's exported across translation " + "units or shared objects"; Q_ASSERT(false); // That situation is plain wrong return; // So maybe std::terminate() even? } - qWarning(EVENTS).nospace() - << newType->matrixId << " is already mapped to " - << (*existing)->className << " before " << newType->className - << "; unless the two have different isValid() conditions, the " - "latter class will never be used"; + qWarning(EVENTS).nospace() << newType->matrixId << " is already mapped to " + << (*existing)->className << " before " << newType->className + << "; unless the two have different isValid() conditions, the " + "latter class will never be used"; } - derivedTypes.emplace_back(newType); - qDebug(EVENTS).nospace() - << newType->matrixId << " -> " << newType->className << "; " - << derivedTypes.size() << " derived type(s) registered for " - << className; + _derivedTypes.emplace_back(newType); + qDebug(EVENTS).nospace() << newType->matrixId << " -> " << newType->className << "; " + << _derivedTypes.size() << " derived type(s) registered for " + << className; } -Event::Event(const QJsonObject& json) - : _json(json) -{ - if (!json.contains(ContentKey) - && !json.value(UnsignedKey).toObject().contains(RedactedCauseKey)) { - qCWarning(EVENTS) << "Event without 'content' node"; - qCWarning(EVENTS) << formatJson << json; - } -} +Event::Event(const QJsonObject& json) : _json(json) {} Event::~Event() = default; diff --git a/Quotient/events/event.h b/Quotient/events/event.h index 2d067b7f5..3d7144101 100644 --- a/Quotient/events/event.h +++ b/Quotient/events/event.h @@ -55,6 +55,7 @@ class QUOTIENT_API AbstractEventMetaType { } void addDerived(const AbstractEventMetaType* newType); + const auto& derivedTypes() const { return _derivedTypes; } virtual ~AbstractEventMetaType() = default; @@ -69,7 +70,7 @@ class QUOTIENT_API AbstractEventMetaType { Event*& event) const = 0; private: - std::vector derivedTypes{}; + std::vector _derivedTypes{}; Q_DISABLE_COPY_MOVE(AbstractEventMetaType) }; @@ -146,7 +147,7 @@ class QUOTIENT_API EventMetaType : public AbstractEventMetaType { if (EventT::TypeId != type) return false; } else { - for (const auto& p : derivedTypes) { + for (const auto& p : _derivedTypes) { p->doLoadFrom(fullJson, type, event); if (event) { Q_ASSERT(is(*event)); From 0a08b9435426c3efb7413519f803f90a786978e7 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 13 Sep 2024 14:40:34 +0200 Subject: [PATCH 2/5] Quotient::isStateEvent(); move Event::isStateEvent() to RoomEvent Checking whether an event is a state event only makes sense in room context, hence the moving. As for Quotient::isStateEvent(), this checks an event type id against known event types that are derived from StateEvent; the function will be used to simplify the API to check power levels in a later commit. --- Quotient/events/event.cpp | 6 ++++++ Quotient/events/event.h | 10 +++++----- Quotient/events/roomevent.cpp | 21 +++++++++++++++++++-- Quotient/events/roomevent.h | 10 +++++++++- Quotient/room.cpp | 3 +-- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Quotient/events/event.cpp b/Quotient/events/event.cpp index 9da7ae1ff..528058cbc 100644 --- a/Quotient/events/event.cpp +++ b/Quotient/events/event.cpp @@ -8,6 +8,10 @@ #include +#if Quotient_VERSION_MAJOR == 0 && Quotient_VERSION_MINOR <= 9 +#include "stateevent.h" // For deprecated isStateEvent(); remove, once Event::isStateEvent() is gone +#endif + using namespace Quotient; void AbstractEventMetaType::addDerived(const AbstractEventMetaType* newType) @@ -53,7 +57,9 @@ const QJsonObject Event::unsignedJson() const return fullJson()[UnsignedKey].toObject(); } +#if Quotient_VERSION_MAJOR == 0 && Quotient_VERSION_MINOR <= 9 bool Event::isStateEvent() const { return is(); } +#endif void Event::dumpTo(QDebug dbg) const { diff --git a/Quotient/events/event.h b/Quotient/events/event.h index 3d7144101..1eb5eaecc 100644 --- a/Quotient/events/event.h +++ b/Quotient/events/event.h @@ -19,6 +19,7 @@ constexpr inline auto TypeKey = "type"_L1; constexpr inline auto BodyKey = "body"_L1; constexpr inline auto ContentKey = "content"_L1; constexpr inline auto SenderKey = "sender"_L1; +constexpr inline auto UnsignedKey = "unsigned"_L1; using event_type_t = QLatin1String; @@ -55,7 +56,7 @@ class QUOTIENT_API AbstractEventMetaType { } void addDerived(const AbstractEventMetaType* newType); - const auto& derivedTypes() const { return _derivedTypes; } + auto derivedTypes() const { return std::span(_derivedTypes); } virtual ~AbstractEventMetaType() = default; @@ -329,10 +330,9 @@ class QUOTIENT_API Event { return dbg; } - // State events are quite special in Matrix; so isStateEvent() is here, - // as an exception. For other base events, Event::is<>() and - // Quotient::is<>() should be used; don't add is* methods here - bool isStateEvent() const; +#if Quotient_VERSION_MAJOR == 0 && Quotient_VERSION_MINOR <= 9 + [[deprecated("isStateEvent() has moved to RoomEvent")]] bool isStateEvent() const; +#endif protected: friend class EventMetaType; // To access the below constructor diff --git a/Quotient/events/roomevent.cpp b/Quotient/events/roomevent.cpp index 051e4d88a..fb392b1a4 100644 --- a/Quotient/events/roomevent.cpp +++ b/Quotient/events/roomevent.cpp @@ -3,10 +3,11 @@ #include "roomevent.h" -#include "../logging_categories_p.h" +#include "encryptedevent.h" #include "redactionevent.h" +#include "stateevent.h" -#include "encryptedevent.h" +#include "../logging_categories_p.h" using namespace Quotient; @@ -48,6 +49,8 @@ QString RoomEvent::transactionId() const return unsignedPart("transaction_id"_L1); } +bool RoomEvent::isStateEvent() const { return is(); } + QString RoomEvent::stateKey() const { return fullJson()[StateKeyKey].toString(); @@ -98,3 +101,17 @@ const QJsonObject RoomEvent::encryptedJson() const } return _originalEvent->fullJson(); } + +namespace { +bool containsEventType(const auto& haystack, const auto& needle) +{ + return std::ranges::any_of(haystack, [needle](const AbstractEventMetaType* candidate) { + return candidate->matrixId == needle || containsEventType(candidate->derivedTypes(), needle); + }); +} +} + +bool Quotient::isStateEvent(const QString& eventTypeId) +{ + return containsEventType(StateEvent::BaseMetaType.derivedTypes(), eventTypeId); +} diff --git a/Quotient/events/roomevent.h b/Quotient/events/roomevent.h index fe71e3d8b..4f013e54e 100644 --- a/Quotient/events/roomevent.h +++ b/Quotient/events/roomevent.h @@ -14,7 +14,6 @@ constexpr inline auto RoomIdKey = "room_id"_L1; constexpr inline auto StateKeyKey = "state_key"_L1; constexpr inline auto RedactedCauseKey = "redacted_because"_L1; constexpr inline auto RelatesToKey = "m.relates_to"_L1; -constexpr inline auto UnsignedKey = "unsigned"_L1; class RedactionEvent; class EncryptedEvent; @@ -49,6 +48,12 @@ class QUOTIENT_API RoomEvent : public Event { //! The transaction_id JSON value for the event. QString transactionId() const; + // State events are special in Matrix; so isStateEvent() and stateKey() are here, + // as an exception. For other event types (including base types), Event::is<>() and + // Quotient::is<>() should be used + + bool isStateEvent() const; + QString stateKey() const; //! \brief Fill the pending event object with the room id @@ -89,6 +94,9 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = std::ranges::subrange; +//! \brief Determine whether a given event type is that of a state event +QUOTIENT_API bool isStateEvent(const QString& eventTypeId); + } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) Q_DECLARE_METATYPE(const Quotient::RoomEvent*) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 8a461b6e9..d3be909e0 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -3078,8 +3078,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) Q_ASSERT(result != Change::None); // Whatever the outcome, the relevant piece of state should stay valid // (the absense of event is a valid state, too) - Q_ASSERT(currentState().queryOr(e.matrixType(), e.stateKey(), - &Event::isStateEvent, true)); + Q_ASSERT(currentState().queryOr(e.matrixType(), e.stateKey(), &RoomEvent::isStateEvent, true)); return result; } From 1ae0a01d532f9052ef96862b759280642de8c84c Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Fri, 13 Sep 2024 14:42:54 +0200 Subject: [PATCH 3/5] Rewrite PowerLevelsEventContent to make it an aggregate Allows to use designated initialisers, see the next commit. --- Quotient/events/roompowerlevelsevent.cpp | 55 ++++++++++---------- Quotient/events/roompowerlevelsevent.h | 64 ++++++++++++++++++------ 2 files changed, 78 insertions(+), 41 deletions(-) diff --git a/Quotient/events/roompowerlevelsevent.cpp b/Quotient/events/roompowerlevelsevent.cpp index 0a68b6044..63b42d26a 100644 --- a/Quotient/events/roompowerlevelsevent.cpp +++ b/Quotient/events/roompowerlevelsevent.cpp @@ -5,33 +5,36 @@ using namespace Quotient; -// The default values used below are defined in -// https://spec.matrix.org/v1.3/client-server-api/#mroompower_levels -PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : - invite(json["invite"_L1].toInt(50)), - kick(json["kick"_L1].toInt(50)), - ban(json["ban"_L1].toInt(50)), - redact(json["redact"_L1].toInt(50)), - events(fromJson>(json["events"_L1])), - eventsDefault(json["events_default"_L1].toInt(0)), - stateDefault(json["state_default"_L1].toInt(50)), - users(fromJson>(json["users"_L1])), - usersDefault(json["users_default"_L1].toInt(0)), - notifications(Notifications{json["notifications"_L1].toObject()["room"_L1].toInt(50)}) -{} - -QJsonObject PowerLevelsEventContent::toJson() const +PowerLevelsEventContent JsonConverter::load(const QJsonValue& jv) { - return QJsonObject{ { u"invite"_s, invite }, - { u"kick"_s, kick }, - { u"ban"_s, ban }, - { u"redact"_s, redact }, - { u"events"_s, Quotient::toJson(events) }, - { u"events_default"_s, eventsDefault }, - { u"state_default"_s, stateDefault }, - { u"users"_s, Quotient::toJson(users) }, - { u"users_default"_s, usersDefault }, - { u"notifications"_s, QJsonObject{ { u"room"_s, notifications.room } } } }; + const auto& jo = jv.toObject(); + PowerLevelsEventContent c; +#define FROM_JSON(member) fromJson(jo[toSnakeCase(#member##_L1)], c.member) + FROM_JSON(invite); + FROM_JSON(kick); + FROM_JSON(ban); + FROM_JSON(redact); + FROM_JSON(events); + FROM_JSON(eventsDefault); + FROM_JSON(stateDefault); + FROM_JSON(users); + FROM_JSON(usersDefault); + fromJson(jo["notifications"_L1]["room"_L1], c.notifications.room); +#undef FROM_JSON + return c; +} + +QJsonObject JsonConverter::dump(const PowerLevelsEventContent& c) { + return QJsonObject{ { u"invite"_s, c.invite }, + { u"kick"_s, c.kick }, + { u"ban"_s, c.ban }, + { u"redact"_s, c.redact }, + { u"events"_s, Quotient::toJson(c.events) }, + { u"events_default"_s, c.eventsDefault }, + { u"state_default"_s, c.stateDefault }, + { u"users"_s, Quotient::toJson(c.users) }, + { u"users_default"_s, c.usersDefault }, + { u"notifications"_s, QJsonObject{ { u"room"_s, c.notifications.room } } } }; } int RoomPowerLevelsEvent::powerLevelForEvent(const QString& eventTypeId) const diff --git a/Quotient/events/roompowerlevelsevent.h b/Quotient/events/roompowerlevelsevent.h index aedc7a725..56191f683 100644 --- a/Quotient/events/roompowerlevelsevent.h +++ b/Quotient/events/roompowerlevelsevent.h @@ -6,28 +6,33 @@ #include "stateevent.h" namespace Quotient { + struct QUOTIENT_API PowerLevelsEventContent { - struct Notifications { - int room; - }; + // See https://spec.matrix.org/v1.11/client-server-api/#mroompower_levels for the defaults - explicit PowerLevelsEventContent(const QJsonObject& json); - QJsonObject toJson() const; + int invite = 0; + int kick = 50; + int ban = 50; - int invite; - int kick; - int ban; + int redact = 50; - int redact; + QHash events{}; + int eventsDefault = 0; + int stateDefault = 50; - QHash events; - int eventsDefault; - int stateDefault; + QHash users{}; + int usersDefault = 0; - QHash users; - int usersDefault; + struct Notifications { + int room = 50; + }; + Notifications notifications{}; +}; - Notifications notifications; +template <> +struct QUOTIENT_API JsonConverter { + static PowerLevelsEventContent load(const QJsonValue& jv); + static QJsonObject dump(const PowerLevelsEventContent& c); }; class QUOTIENT_API RoomPowerLevelsEvent @@ -52,8 +57,37 @@ class QUOTIENT_API RoomPowerLevelsEvent int roomNotification() const { return content().notifications.room; } + //! \brief Get the power level for message events of a given type + //! + //! \note You normally should not compare power levels returned from this + //! and other powerLevelFor*() functions directly; use + //! Room::canSendEvents() instead int powerLevelForEvent(const QString& eventTypeId) const; + + //! \brief Get the power level for state events of a given type + //! + //! \note You normally should not compare power levels returned from this + //! and other powerLevelFor*() functions directly; use + //! Room::canSetState() instead int powerLevelForState(const QString& eventTypeId) const; + + //! \brief Get the power level for a given user + //! + //! \note You normally should not directly use power levels returned by this + //! and other powerLevelFor*() functions; use Room API instead + //! \sa Room::canSend, Room::canSendEvents, Room::canSetState, + //! Room::effectivePowerLevel int powerLevelForUser(const QString& userId) const; + + template + int powerLevelForEventType() const + { + if constexpr (std::is_base_of_v) { + return powerLevelForState(EvT::TypeId); + } else { + return powerLevelForEvent(EvT::TypeId); + } + } }; + } // namespace Quotient From 0a2543f8bd0beea5a9b1b4484c63493e40930da5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 15 Sep 2024 09:10:07 +0200 Subject: [PATCH 4/5] Refactor power level event management; Room::powerLevelFor() This commit makes RoomPowerLevelsEvent _always_ available via currentState() - no need to query()/queryOr(), just get() and safely access all the members. This is possible because the spec defines default power levels for all kinds of cases; we can just inject a synthetic RoomPowerLevelsEvent object with those defaults into currentState() and once the real event comes (either from the homeserver or from the cache) it replaces the synthetic one by normal means of state processing. Thanks to that, Room::memberEffectivePowerLevel() implementation becomes much simpler; and to match it on the other side of power level checks, Room::powerLevelFor() is introduced as a unified call returning the power level necessary to send any specific event type, state or not. --- Quotient/room.cpp | 55 ++++++++++++++++++++++++++-------------- Quotient/room.h | 47 ++++++++++++++++++++++++---------- Quotient/roomstateview.h | 3 ++- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index d3be909e0..20772c9d7 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -94,13 +94,18 @@ class Q_DECL_HIDDEN Room::Private { QString id; JoinState joinState; RoomSummary summary = { {}, 0, {} }; - /// The state of the room at timeline position before-0 + // TODO: remove the below when Room becomes constructed from the first sync batch; a synthetic + // default power levels event would be constructed in baseState then, if needed + //! Fallback when/while the real event is not available + std::unique_ptr defaultPowerLevels = + std::make_unique(); + //! The state of the room at timeline position before-0 std::unordered_map baseState; - /// The state of the room at syncEdge() - /// \sa syncEdge - RoomStateView currentState; - /// Servers with aliases for this room except the one of the local user - /// \sa Room::remoteAliases + //! The state of the room at syncEdge() + //! \sa syncEdge + RoomStateView currentState{ { { RoomPowerLevelsEvent::TypeId, {} }, defaultPowerLevels.get() } }; + //! Servers with aliases for this room except the one of the local user + //! \sa Room::remoteAliases QSet aliasServers; Timeline timeline; @@ -1522,20 +1527,17 @@ RoomStateView Room::currentState() const return d->currentState; } -int Room::memberEffectivePowerLevel(const QString& memberId) const +int Room::memberEffectivePowerLevel(const UserId& memberId) const { - if (!successorId().isEmpty()) { - return 0; // No one can upgrade a room that's already upgraded - } + return currentState().get()->powerLevelForUser( + memberId.isEmpty() ? connection()->userId() : memberId); +} - const auto& mId = memberId.isEmpty() ? connection()->userId() :memberId; - if (const auto* plEvent = currentState().get()) { - return plEvent->powerLevelForUser(mId); - } - if (const auto* createEvent = creation()) { - return createEvent->senderId() == mId ? 100 : 0; - } - return 0; // That's rather weird but may happen, according to rvdh +int Room::powerLevelFor(const QString& eventTypeId, bool forceStateEvent) const +{ + const auto& ple = currentState().get(); + return forceStateEvent || isStateEvent(eventTypeId) ? ple->powerLevelForState(eventTypeId) + : ple->powerLevelForEvent(eventTypeId); } RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) @@ -1841,7 +1843,8 @@ Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, b void Room::updateData(SyncRoomData&& data, bool fromCache) { qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName(); - bool firstUpdate = d->baseState.empty(); + const bool firstUpdate = d->baseState.empty(); + const bool createEventPreviouslyMissing = creation() == nullptr; if (d->prevBatch && d->prevBatch->isEmpty()) *d->prevBatch = data.timelinePrevBatch; @@ -1862,6 +1865,20 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) roomChanges |= d->updateStatsFromSyncData(data, fromCache); if (roomChanges != 0) { + if (createEventPreviouslyMissing && creation() + && currentState().get() == d->defaultPowerLevels.get()) { + // Handle a special case when RoomCreateEvent just arrived but RoomPowerLevelsEvent + // did not. Usually that means that a power levels event is not in the room at all, + // which is a somewhat extreme but still valid situation. In such a case the spec says + // to rely on the default power levels save for the room creator who is effectively + // allowed to do everything. + // The entire defaultPowerLevels event gets replaced in order to maintain its constness + // everywhere else. + std::exchange(d->defaultPowerLevels, + std::make_unique(PowerLevelsEventContent{ + .users = { { creation()->senderId(), 100 } } })); + } + // First test for changes that can only come from /sync calls and not // other interactions (/members, /messages etc.) if ((roomChanges & Change::Topic) > 0) diff --git a/Quotient/room.h b/Quotient/room.h index f2d86c8e0..720b8d601 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -19,11 +19,12 @@ #include "events/accountdataevents.h" #include "events/encryptedevent.h" +#include "events/eventrelation.h" +#include "events/roomcreateevent.h" #include "events/roomkeyevent.h" #include "events/roommessageevent.h" -#include "events/roomcreateevent.h" +#include "events/roompowerlevelsevent.h" #include "events/roomtombstoneevent.h" -#include "events/eventrelation.h" #include #include @@ -671,20 +672,40 @@ class QUOTIENT_API Room : public QObject { /// \brief Get the current room state RoomStateView currentState() const; - //! \brief The effective power level of the given member in the room. - //! - //! Since a RoomPowerLevels state event may not always be available the following - //! is taken into account in line with the Matrix spec - //! https://spec.matrix.org/v1.8/client-server-api/#mroompower_levels: - //! - The users_default is assumed to be 0. - //! - The room creator is assumed to be 100. + //! \brief The effective power level of the given member in the room //! - //! If \p memberId is empty the power level of the local user will be returned. - //! If the room has been upgraded 0 will be returned to prevent further upgrade attempts. - //! - //! \sa RoomPowerLevelsEvent + //! This is normally the same as calling `RoomPowerLevelEvent::powerLevelForUser(userId)` but + //! takes into account the room context and works even if the room state has no power levels + //! event. It is THE recommended way to get a room member's power level to display in the UI. + //! \param memberId The room member ID to check; if empty, the local user will be checked + //! \sa RoomPowerLevelsEvent, https://spec.matrix.org/v1.11/client-server-api/#mroompower_levels Q_INVOKABLE int memberEffectivePowerLevel(const QString& memberId = {}) const; + //! \brief Get the power level required to send events of the given type + //! + //! \note This is a generic method that only gets the power level to send events with a given + //! type. Some operations have additional restrictions or enablers though: e.g., + //! room member changes (kicks, invites) have special power levels; on the other hand, + //! redactions of one's own messages are allowed regardless of the power level. To check + //! effective ability to perform an operation, use Room's can*() methods instead of + //! comparing the power levels (those are also slightly more efficient). + //! \note Unlike the template version below, this method determines at runtime whether an event + //! type is that of a state event, assuming unknown event types to be non-state; pass + //! `true` as the second parameter to override that. + //! \sa canSend, canRedact, canSwitchVersions + Q_INVOKABLE int powerLevelFor(const QString& eventTypeId, bool forceStateEvent = false) const; + + //! \brief Get the power level required to send events of the given type + //! + //! This is an optimised version of non-template powerLevelFor() (with the same caveat about + //! operations based on some event types) for cases when the event type is known at build time. + //! \tparam EvT the event type to get the power level for + template + int powerLevelFor() const + { + return currentState().get()->powerLevelForEventType(); + } + //! \brief Post a pre-created room message event //! //! Takes ownership of the event, deleting it once the matching one arrives with the sync. diff --git a/Quotient/roomstateview.h b/Quotient/roomstateview.h index 01ac1c087..e1a8b38a3 100644 --- a/Quotient/roomstateview.h +++ b/Quotient/roomstateview.h @@ -205,6 +205,7 @@ class QUOTIENT_API RoomStateView } private: - friend class Room; + friend class Room; // Factory class for RoomStateView + using QHash::QHash; }; } // namespace Quotient From d92f83f7109e889b8b12e508836adfb1ad2c589e Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Tue, 17 Sep 2024 15:57:32 +0200 Subject: [PATCH 5/5] Add a missing #include --- Quotient/events/event.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Quotient/events/event.h b/Quotient/events/event.h index 1eb5eaecc..d94e1711f 100644 --- a/Quotient/events/event.h +++ b/Quotient/events/event.h @@ -3,9 +3,12 @@ #pragma once +#include "single_key_value.h" + #include #include -#include "single_key_value.h" + +#include namespace Quotient { // === event_ptr_tt<> and basic type casting facilities ===