Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libQuotient for openSUSE:Factory 
checked in at 2026-02-16 13:11:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libQuotient (Old)
 and      /work/SRC/openSUSE:Factory/.libQuotient.new.1977 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libQuotient"

Mon Feb 16 13:11:53 2026 rev:23 rq:1333229 version:0.9.6

Changes:
--------
--- /work/SRC/openSUSE:Factory/libQuotient/libQuotient.changes  2025-09-20 
22:05:37.898574935 +0200
+++ /work/SRC/openSUSE:Factory/.libQuotient.new.1977/libQuotient.changes        
2026-02-16 13:18:12.340281435 +0100
@@ -1,0 +2,21 @@
+Sun Feb 15 21:09:13 UTC 2026 - Christophe Marin <[email protected]>
+
+- Update to 0.9.6
+  * Connection object name now actually includes device id once
+    authentication is done
+  * emit SSSSHandler::keyBackupUnlocked when receiving backup
+    key from another device
+  * Deprecate encryption pickle operations in AccountSettings
+  * Improving build configuration with libQuotient
+  * Assume authenticated media API is available before the homeserver
+    returns versions - helps with avatars sometimes not showing
+  * Fix potential undefined behaviour when marking messages as read
+  * Make stable branch compatible with Qt 6.10
+  * Add missing hasOlmSession check in sendToDevice
+  * Connection::join(AndGet)Room(): return the right room object
+  * Make sure the original messages get hidden when an edit is
+    available
+  * Performance improvement for rooms with many leaves
+    accumulating over time
+
+-------------------------------------------------------------------

Old:
----
  libQuotient-0.9.5.tar.gz

New:
----
  libQuotient-0.9.6.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libQuotient.spec ++++++
--- /var/tmp/diff_new_pack.rc6U4m/_old  2026-02-16 13:18:13.048311664 +0100
+++ /var/tmp/diff_new_pack.rc6U4m/_new  2026-02-16 13:18:13.052311835 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package libQuotient
 #
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -29,7 +29,7 @@
 %define sonum 0.9
 %define rname libQuotient
 Name:           libQuotient%{?pkg_suffix}
-Version:        0.9.5
+Version:        0.9.6
 Release:        0
 Summary:        Library for Qt Matrix Clients
 License:        LGPL-2.1-only

++++++ libQuotient-0.9.5.tar.gz -> libQuotient-0.9.6.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/.github/workflows/ci.yml 
new/libQuotient-0.9.6/.github/workflows/ci.yml
--- old/libQuotient-0.9.5/.github/workflows/ci.yml      2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/.github/workflows/ci.yml      2026-02-15 
21:24:53.000000000 +0100
@@ -34,6 +34,8 @@
           qt-version: '6.4'
         - os: windows-latest
           qt-version: '6.4'
+        - os: macos-14
+          qt-version: '6.10'
 
     env:
       GCC_VERSION: -13
@@ -203,7 +205,7 @@
       run: |
         CTEST_ARGS="--test-dir $BUILD_PATH --output-on-failure"
         if [[ '${{ runner.os }}' != 'Linux' ]]; then
-            CTEST_ARGS="$CTEST_ARGS -E testolmaccount"
+            CTEST_ARGS="$CTEST_ARGS -LE needs_mock_server"
         else
             . autotests/setup-tests.sh
         fi
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/CMakeLists.txt 
new/libQuotient-0.9.6/CMakeLists.txt
--- old/libQuotient-0.9.5/CMakeLists.txt        2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/CMakeLists.txt        2026-02-15 21:24:53.000000000 
+0100
@@ -4,7 +4,7 @@
 endif()
 
 set(API_VERSION "0.9")
-project(Quotient VERSION "${API_VERSION}.5" LANGUAGES CXX)
+project(Quotient VERSION "${API_VERSION}.6" LANGUAGES CXX)
 set(PRE_STAGE "")
 string(JOIN ~ FULL_VERSION ${PROJECT_VERSION} ${PRE_STAGE})
 
@@ -403,9 +403,12 @@
 if (NOT DEFINED BUILD_SHARED_LIBS)
     set(BUILD_SHARED_LIBS OFF)
 endif()
-configure_file(cmake/${PROJECT_NAME}Config.cmake.in
+configure_package_config_file(cmake/${PROJECT_NAME}Config.cmake.in
     
"${CMAKE_CURRENT_BINARY_DIR}/${QUOTIENT_LIB_NAME}/${QUOTIENT_LIB_NAME}Config.cmake"
-    @ONLY
+    INSTALL_DESTINATION
+    "${CMAKE_INSTALL_PREFIX}/${CMakeFilesLocation}"
+    NO_SET_AND_CHECK_MACRO
+    NO_CHECK_REQUIRED_COMPONENTS_MACRO
 )
 
 install(EXPORT ${QUOTIENT_LIB_NAME}Targets
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/connection.cpp 
new/libQuotient-0.9.6/Quotient/connection.cpp
--- old/libQuotient-0.9.5/Quotient/connection.cpp       2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/connection.cpp       2026-02-15 
21:24:53.000000000 +0100
@@ -157,6 +157,7 @@
                                    const QString& deviceId)
 {
     d->ensureHomeserver(userId, LoginFlowTypes::Password).then([=, this] {
+        setObjectName(userId % u"(?)");
         d->loginToServer(LoginFlowTypes::Password, makeUserIdentifier(userId),
                          password, /*token*/ QString(), deviceId, 
initialDeviceName);
     });
@@ -173,6 +174,7 @@
                                 const QString& deviceId)
 {
     Q_ASSERT(d->data->baseUrl().isValid() && 
d->supportsLoginFlow(LoginFlowTypes::Token));
+    setObjectName(loginToken % u"(?)");
     d->loginToServer(LoginFlowTypes::Token, std::nullopt /*user is encoded in 
loginToken*/,
                      QString() /*password*/, loginToken, deviceId, 
initialDeviceName);
 }
@@ -362,11 +364,9 @@
     auto result = promise.future();
     promise.start();
     if (data->baseUrl().isValid() && (flowType.isEmpty() || 
supportsLoginFlow(flowType))) {
-        q->setObjectName(userId % u"(?)");
         promise.finish(); // Perfect, we're already good to go
     } else if (userId.startsWith(u'@') && userId.indexOf(u':') != -1) {
         // Try to ascertain the homeserver URL and flows
-        q->setObjectName(userId % u"(?)");
         q->resolveServer(userId);
         if (!flowType.isEmpty())
             QtFuture::connect(q, &Connection::loginFlowsChanged)
@@ -672,13 +672,13 @@
     // If the room object is not there, provideRoom() will create it in Join 
state. Using
     // the continuation ensures that the room is provided before any client 
connections.
     return callApi<JoinRoomJob>(roomAlias, serverNames, serverNames)
-        .then([this](const QString& roomId) { provideRoom(roomId); });
+        .then([this](const QString& roomId) { provideRoom(roomId, 
JoinState::Join); });
 }
 
 QFuture<Room*> Connection::joinAndGetRoom(const QString& roomAlias, const 
QStringList& serverNames)
 {
     return callApi<JoinRoomJob>(roomAlias, serverNames, serverNames)
-        .then([this](const QString& roomId) { return provideRoom(roomId); });
+        .then([this](const QString& roomId) { return provideRoom(roomId, 
JoinState::Join); });
 }
 
 QFuture<Room *> Connection::waitForNewRoom(const QString &roomId)
@@ -1567,7 +1567,7 @@
     const auto data =
         d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor()
                          : 
QJsonDocument(rootObj).toJson(QJsonDocument::Compact);
-    qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
+    qCDebug(PROFILER).noquote() << "Cache for" << objectName() << "generated 
in" << et;
 
     outFile.write(data.data(), data.size());
     qCDebug(MAIN) << "State cache saved to" << outFile.fileName();
@@ -1855,20 +1855,25 @@
                               const QString& targetDeviceId, const Event& 
event,
                               bool encrypted)
 {
-    if (encrypted && !d->encryptionData) {
+    if (!encrypted) {
+        sendToDevices(event.matrixType(), {{targetUserId, {{targetDeviceId, 
event.contentJson()}}}});
+        return;
+    }
+
+    if (!d->encryptionData) {
         qWarning(E2EE) << "E2EE is off for" << objectName()
                        << "- no encrypted to-device message will be sent";
         return;
     }
-
-    const auto contentJson =
-        encrypted
-            ? d->encryptionData->assembleEncryptedContent(event.fullJson(),
-                                                          targetUserId,
-                                                          targetDeviceId)
-            : event.contentJson();
-    sendToDevices(encrypted ? EncryptedEvent::TypeId : event.matrixType(),
-                  { { targetUserId, { { targetDeviceId, contentJson } } } });
+    if (!d->encryptionData->hasOlmSession(targetUserId, targetDeviceId)) {
+        qWarning(E2EE) << "Olm session for" << targetUserId << '/' << 
targetDeviceId
+                       << "is missing, to-device message won't be sent";
+        return;
+    }
+    sendToDevices(EncryptedEvent::TypeId,
+                  {{targetUserId,
+                    {{targetDeviceId, 
d->encryptionData->assembleEncryptedContent(
+                                          event.fullJson(), targetUserId, 
targetDeviceId)}}}});
 }
 
 bool Connection::isVerifiedSession(const QByteArray& megolmSessionId) const
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/connection.h 
new/libQuotient-0.9.6/Quotient/connection.h
--- old/libQuotient-0.9.5/Quotient/connection.h 2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/Quotient/connection.h 2026-02-15 21:24:53.000000000 
+0100
@@ -89,7 +89,7 @@
 [[deprecated("Compare login flow types instead")]]
 inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs)
 {
-    return !(lhs == rhs);
+    QT_IGNORE_DEPRECATIONS (return !(lhs == rhs);)
 }
 
 class Connection;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/e2ee/sssshandler.cpp 
new/libQuotient-0.9.6/Quotient/e2ee/sssshandler.cpp
--- old/libQuotient-0.9.5/Quotient/e2ee/sssshandler.cpp 2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/e2ee/sssshandler.cpp 2026-02-15 
21:24:53.000000000 +0100
@@ -148,6 +148,7 @@
     Q_ASSERT(m_connection);
     m_connection->requestKeyFromDevices(MegolmBackupKey).then([this](const 
QByteArray& key) {
         loadMegolmBackup(key);
+        emit keyBackupUnlocked();
     });
     for (auto k : {CrossSigningUserSigningKey, CrossSigningSelfSigningKey, 
CrossSigningMasterKey})
         m_connection->requestKeyFromDevices(k);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/eventitem.h 
new/libQuotient-0.9.6/Quotient/eventitem.h
--- old/libQuotient-0.9.5/Quotient/eventitem.h  2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/Quotient/eventitem.h  2026-02-15 21:24:53.000000000 
+0100
@@ -23,16 +23,15 @@
      * All values except Redacted and Hidden are mutually exclusive.
      */
     enum Code : uint16_t {
-        Normal = 0x0, ///< No special designation
-        Submitted = 0x01, ///< The event has just been submitted for sending
-        FileUploaded = 0x02, ///< The file attached to the event has been
-                             ///  uploaded to the server
-        Departed = 0x03, ///< The event has left the client
-        ReachedServer = 0x04, ///< The server has received the event
-        SendingFailed = 0x05, ///< The server could not receive the event
-        Redacted = 0x08, ///< The event has been redacted
-        Replaced = 0x10, ///< The event has been replaced
-        Hidden = 0x100, ///< The event should not be shown in the timeline
+        Normal = 0x0,         //!< No special designation
+        Submitted = 0x01,     //!< The event has just been submitted for 
sending
+        FileUploaded = 0x02,  //!< The file attached to the event has been 
uploaded to the server
+        Departed = 0x03,      //!< The event has left the client
+        ReachedServer = 0x04, //!< The server has received the event
+        SendingFailed = 0x05, //!< The server could not receive the event
+        Redacted = 0x08,      //!< The event has been redacted
+        Replaced = 0x10,      //!< The event has been replaced
+        Hidden = 0x100,       //!< The event should not be shown in the 
timeline
     };
     Q_ENUM_NS(Code)
 } // namespace EventStatus
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/eventcontent.cpp 
new/libQuotient-0.9.6/Quotient/events/eventcontent.cpp
--- old/libQuotient-0.9.5/Quotient/events/eventcontent.cpp      2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/events/eventcontent.cpp      2026-02-15 
21:24:53.000000000 +0100
@@ -133,11 +133,11 @@
     const auto relatesTo = 
fromJson<std::optional<EventRelation>>(json[RelatesToKey]);
 
     const auto actualJson = relatesTo.has_value() && relatesTo->type == 
EventRelation::ReplacementType
-                                ? json.value("m.new_content"_L1).toObject()
+                                ? json.value(NewContentKey).toObject()
                                 : json;
     // Special-casing the custom matrix.org's (actually, Element's) way
     // of sending HTML messages.
-    if (actualJson["format"_L1].toString() == HtmlContentTypeId) {
+    if (actualJson[FormatKey].toString() == HtmlContentTypeId) {
         mimeType = HtmlMimeType;
         body = actualJson[FormattedBodyKey].toString();
     } else {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/eventrelation.h 
new/libQuotient-0.9.6/Quotient/events/eventrelation.h
--- old/libQuotient-0.9.5/Quotient/events/eventrelation.h       2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/events/eventrelation.h       2026-02-15 
21:24:53.000000000 +0100
@@ -10,9 +10,16 @@
 constexpr inline auto RelatesToKey = "m.relates_to"_L1;
 constexpr inline auto RelTypeKey = "rel_type"_L1;
 constexpr inline auto IsFallingBackKey = "is_falling_back"_L1;
+constexpr inline auto NewContentKey = "m.new_content"_L1;
+constexpr inline auto RelationsKey = "m.relations"_L1;
 
+//! \brief Data about one relation of an event to an upstream event (e.g. a 
reply or a reaction)
+//!
+//! This structure contains all information pertaining to a single event 
relation. In terms of
+//! CS API it corresponds to the contents of `m.relates_to` and 
`m.in_reply_to` JSON objects.
 struct QUOTIENT_API EventRelation {
-    using reltypeid_t = QLatin1String;
+    using typeid_t = QLatin1String;
+    using reltypeid_t [[deprecated("Use typeid_t")]] = typeid_t;
 
     QString type;
     QString eventId;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/roomevent.cpp 
new/libQuotient-0.9.6/Quotient/events/roomevent.cpp
--- old/libQuotient-0.9.5/Quotient/events/roomevent.cpp 2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/events/roomevent.cpp 2026-02-15 
21:24:53.000000000 +0100
@@ -5,6 +5,9 @@
 
 #include "encryptedevent.h"
 #include "redactionevent.h"
+#include "roomcreateevent.h"
+#include "roommemberevent.h"
+#include "roompowerlevelsevent.h"
 #include "stateevent.h"
 
 #include "../logging_categories_p.h"
@@ -44,6 +47,57 @@
     return isRedacted() ? _redactedBecause->reason() : QString {};
 }
 
+event_ptr_tt<RoomEvent> RoomEvent::makeRedacted(const RedactionEvent 
&redaction) const
+{
+    // The logic below faithfully follows the spec despite quite a few of
+    // the preserved keys being only relevant for homeservers. Just in case.
+    static const QStringList TopLevelKeysToKeep{
+        EventIdKey,  TypeKey,          RoomIdKey,        SenderKey,
+        StateKeyKey, ContentKey,       "hashes"_L1,      "signatures"_L1,
+        "depth"_L1,  "prev_events"_L1, "auth_events"_L1, "origin_server_ts"_L1
+    };
+
+    auto originalJson = this->fullJson();
+    for (auto it = originalJson.begin(); it != originalJson.end();) {
+        if (!TopLevelKeysToKeep.contains(it.key()))
+            it = originalJson.erase(it);
+        else
+            ++it;
+    }
+    if (!this->is<RoomCreateEvent>()) { // See MSC2176 on create events
+        static const QHash<QString, QStringList> ContentKeysToKeepPerType{
+            { RedactionEvent::TypeId, { "redacts"_L1 } },
+            { RoomMemberEvent::TypeId,
+              { "membership"_L1, "join_authorised_via_users_server"_L1 } },
+            { RoomPowerLevelsEvent::TypeId,
+              { "ban"_L1, "events"_L1, "events_default"_L1, "invite"_L1,
+                "kick"_L1, "redact"_L1, "state_default"_L1, "users"_L1,
+                "users_default"_L1 } },
+            // TODO: Replace with RoomJoinRules::TypeId etc. once available
+            { "m.room.join_rules"_L1, { "join_rule"_L1, "allow"_L1 } },
+            { "m.room.history_visibility"_L1, { "history_visibility"_L1 } }
+        };
+
+        if (const auto contentKeysToKeep = 
ContentKeysToKeepPerType.value(this->matrixType());
+            !contentKeysToKeep.isEmpty()) {
+            editSubobject(originalJson, ContentKey, 
[&contentKeysToKeep](QJsonObject& content) {
+                for (auto it = content.begin(); it != content.end();) {
+                    if (!contentKeysToKeep.contains(it.key()))
+                        it = content.erase(it);
+                    else
+                        ++it;
+                }
+            });
+        } else {
+            originalJson.remove(ContentKey);
+            originalJson.remove(PrevContentKey);
+        }
+    }
+    replaceSubvalue(originalJson, UnsignedKey, RedactedCauseKey, 
redaction.fullJson());
+
+    return loadEvent<RoomEvent>(originalJson);
+}
+
 QString RoomEvent::transactionId() const
 {
     return unsignedPart<QString>("transaction_id"_L1);
@@ -79,6 +133,7 @@
     Q_ASSERT(id().isEmpty());
     Q_ASSERT(!newId.isEmpty());
     editJson().insert(EventIdKey, newId);
+    _id = newId;
     qCDebug(EVENTS) << "Event txnId -> id:" << transactionId() << "->" << id();
     Q_ASSERT(id() == newId);
 }
@@ -115,3 +170,19 @@
 {
     return containsEventType(StateEvent::BaseMetaType.derivedTypes(), 
eventTypeId);
 }
+
+event_ptr_tt<RoomEvent> RoomEvent::makeReplaced(const RoomEvent &replacement) 
const
+{
+    // See 
https://spec.matrix.org/latest/client-server-api/#applying-mnew_content
+    auto newContent = replacement.contentPart<QJsonObject>(NewContentKey);
+    addParam<IfNotEmpty>(newContent, RelatesToKey, 
this->contentPart<QJsonObject>(RelatesToKey));
+    auto originalJson = this->fullJson();
+    originalJson.insert(ContentKey, newContent);
+    editSubobject(originalJson, UnsignedKey, [&replacement](QJsonObject 
&unsignedData) {
+        replaceSubvalue(unsignedData, RelationsKey, 
EventRelation::ReplacementType,
+                        replacement.id());
+    });
+
+    return loadEvent<RoomEvent>(originalJson);
+}
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/events/roomevent.h 
new/libQuotient-0.9.6/Quotient/events/roomevent.h
--- old/libQuotient-0.9.5/Quotient/events/roomevent.h   2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/events/roomevent.h   2026-02-15 
21:24:53.000000000 +0100
@@ -5,6 +5,8 @@
 
 #include "event.h"
 
+#include "eventrelation.h"
+
 #include <QtCore/QDateTime>
 
 namespace Quotient {
@@ -44,6 +46,13 @@
     }
     QString redactionReason() const;
 
+    //! \brief Make a redacted event
+    //!
+    //! This applies the redaction procedure as defined by the CS API 
specification to the event's
+    //! JSON and returns the resulting new event.
+    //! \note It is the responsibility of the caller to dispose of the 
original event after that.
+    event_ptr_tt<RoomEvent> makeRedacted(const RedactionEvent &redaction) 
const;
+
     //! The transaction_id JSON value for the event.
     QString transactionId() const;
 
@@ -78,11 +87,22 @@
     const EncryptedEvent* originalEvent() const { return _originalEvent.get(); 
}
     const QJsonObject encryptedJson() const;
 
+    //! \brief Make a replaced event
+    //!
+    //! \returns a clone of `*this` with content taken from \p replacement as 
described in
+    //!          
https://spec.matrix.org/latest/client-server-api/#applying-mnew_content
+    //! \note Disposal of the original event after that is on the caller.
+    event_ptr_tt<RoomEvent> makeReplaced(const RoomEvent &replacementEvent) 
const;
+
 protected:
     explicit RoomEvent(const QJsonObject& json);
     void dumpTo(QDebug dbg) const override;
 
+    virtual void afterRelationChange() {}
+
 private:
+    QString _id;
+
     // RedactionEvent is an incomplete type here so we cannot inline
     // constructors using it and also destructors (with 'using', in 
particular).
     event_ptr_tt<RedactionEvent> _redactedBecause;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/libQuotient-0.9.5/Quotient/events/roommessageevent.cpp 
new/libQuotient-0.9.6/Quotient/events/roommessageevent.cpp
--- old/libQuotient-0.9.5/Quotient/events/roommessageevent.cpp  2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/events/roommessageevent.cpp  2026-02-15 
21:24:53.000000000 +0100
@@ -110,7 +110,7 @@
             }
             newContentJson.insert(BodyKey, plainBody);
             newContentJson.insert(MsgTypeKey, jsonMsgType);
-            json.insert("m.new_content"_L1, newContentJson);
+            json.insert(NewContentKey, newContentJson);
             json.insert(BodyKey, "* "_L1 + plainBody);
         }
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/jobs/downloadfilejob.cpp 
new/libQuotient-0.9.6/Quotient/jobs/downloadfilejob.cpp
--- old/libQuotient-0.9.5/Quotient/jobs/downloadfilejob.cpp     2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/jobs/downloadfilejob.cpp     2026-02-15 
21:24:53.000000000 +0100
@@ -46,7 +46,7 @@
                                      const QString& mediaId)
 {
     QT_IGNORE_DEPRECATIONS( // For GetContentJob
-        return hsData.checkMatrixSpecVersion(u"v1.11")
+        return hsData.checkMatrixSpecVersion(u"v1.11") || 
hsData.supportedSpecVersions.empty() // Assume that if we're called early, that 
the server uses auth media
                    ? GetContentAuthedJob::makeRequestUrl(hsData, serverName, 
mediaId)
                    : GetContentJob::makeRequestUrl(hsData, serverName, 
mediaId);)
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/libQuotient-0.9.5/Quotient/jobs/mediathumbnailjob.cpp 
new/libQuotient-0.9.6/Quotient/jobs/mediathumbnailjob.cpp
--- old/libQuotient-0.9.5/Quotient/jobs/mediathumbnailjob.cpp   2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/jobs/mediathumbnailjob.cpp   2026-02-15 
21:24:53.000000000 +0100
@@ -22,7 +22,7 @@
                                        std::optional<bool> animated)
 {
     QT_IGNORE_DEPRECATIONS( // For GetContentThumbnailJob
-        return hsData.checkMatrixSpecVersion(u"v1.11")
+        return hsData.checkMatrixSpecVersion(u"v1.11") || 
hsData.supportedSpecVersions.empty() // Assume that if we're called early, that 
the server uses auth media
                    ? GetContentThumbnailAuthedJob::makeRequestUrl(hsData, 
serverName, mediaId,
                                                                   
requestedSize.width(),
                                                                   
requestedSize.height(),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/libQuotient-0.9.5/Quotient/keyverificationsession.cpp 
new/libQuotient-0.9.6/Quotient/keyverificationsession.cpp
--- old/libQuotient-0.9.5/Quotient/keyverificationsession.cpp   2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/Quotient/keyverificationsession.cpp   2026-02-15 
21:24:53.000000000 +0100
@@ -204,12 +204,15 @@
 EmojiStore loadEmojiStore()
 {
     Q_INIT_RESOURCE(libquotientemojis);
-    QFile dataFile(":/sas-emoji.json"_L1);
-    dataFile.open(QFile::ReadOnly);
-    auto data = dataFile.readAll();
-    Q_CLEANUP_RESOURCE(libquotientemojis);
-    return fromJson<EmojiStore>(
-        QJsonDocument::fromJson(data).array());
+    if (QFile dataFile(":/sas-emoji.json"_L1); dataFile.open(QFile::ReadOnly)) 
{
+        auto data = dataFile.readAll();
+        Q_CLEANUP_RESOURCE(libquotientemojis);
+        return fromJson<EmojiStore>(QJsonDocument::fromJson(data).array());
+    } else {
+        qCritical(MAIN)
+            << "Could not open the file with SAS emoji definitions; key 
verification will not work";
+        return {};
+    }
 }
 
 EmojiEntry emojiForCode(int code, const QString& language)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/room.cpp 
new/libQuotient-0.9.6/Quotient/room.cpp
--- old/libQuotient-0.9.5/Quotient/room.cpp     2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/Quotient/room.cpp     2026-02-15 21:24:53.000000000 
+0100
@@ -128,7 +128,7 @@
     // For storing a list of current member names for the purpose of 
disambiguation.
     QMultiHash<QString, QString> memberNameMap;
     QStringList membersInvited;
-    QStringList membersLeft;
+    QSet<QString> membersLeft;
     QStringList membersTyping;
 
     QHash<QString, QSet<QString>> eventIdReadUsers;
@@ -198,6 +198,9 @@
     /// A map from event/txn ids to information about the long operation;
     /// used for both download and upload operations
     QHash<QString, FileTransferPrivateInfo> fileTransfers;
+    //! A map of orphans of replacement events that are still looking for 
their related event in the
+    //! past. The key is the target event ID, and the value is the replacement 
event ID.
+    QHash<QString, QString> orphanedReplacementEvents;
 
     const RoomMessageEvent* getEventWithFile(const QString& eventId) const;
 
@@ -207,6 +210,7 @@
                               const RoomEvent* curEvent);
     Change processStateEvent(const RoomEvent& curEvent,
                              const RoomEvent* oldEvent);
+    void processRedactionsAndEdits(RoomEvents &events);
 
     void insertMemberIntoMap(const QString& memberId);
     void removeMemberFromMap(const QString& memberId);
@@ -330,7 +334,7 @@
     /*! Apply a new revision of the event to the timeline
      *
      * Tries to find an event in the timeline and replace it with the new
-     * content passed in \p newMessage.
+     * content passed in \p newEvent.
      * \return true if the event has been found and replaced; false otherwise
      */
     bool processReplacement(const RoomMessageEvent& newEvent);
@@ -1059,7 +1063,9 @@
 
 void Room::markAllMessagesAsRead()
 {
-    d->markMessagesAsRead(d->timeline.crbegin());
+    if (!d->timeline.empty()) {
+        d->markMessagesAsRead(d->timeline.crbegin());
+    }
 }
 
 bool Room::canSwitchVersions() const
@@ -1158,13 +1164,13 @@
 }
 
 const Room::RelatedEvents Room::relatedEvents(
-    const QString& evtId, EventRelation::reltypeid_t relType) const
+    const QString& evtId, EventRelation::typeid_t relType) const
 {
     return d->relations.value({ evtId, relType });
 }
 
 const Room::RelatedEvents Room::relatedEvents(
-    const RoomEvent& evt, EventRelation::reltypeid_t relType) const
+    const RoomEvent& evt, EventRelation::typeid_t relType) const
 {
     return relatedEvents(evt.id(), relType);
 }
@@ -2249,7 +2255,7 @@
 
 QString Room::postPlainText(const QString& plainText)
 {
-    return postMessage(plainText, MessageEventType::Text);
+    return postText<MessageEventType::Text>(plainText);
 }
 
 QString Room::postHtmlMessage(const QString& plainText, const QString& html,
@@ -2262,7 +2268,7 @@
 
 QString Room::postHtmlText(const QString& plainText, const QString& html)
 {
-    return postHtmlMessage(plainText, html);
+    return postText<MessageEventType::Text>(plainText, html);
 }
 
 QString Room::postReaction(const QString& eventId, const QString& key)
@@ -2276,44 +2282,42 @@
     // Remote URL will only be known after upload; fill in the local path
     // to enable the preview while the event is pending.
     q->uploadFile(txnId, localUrl);
-    // Below, the upload job is used as a context object to clean up 
connections
-    const auto& transferJob = fileTransfers.value(txnId).job;
-    connect(q, &Room::fileTransferCompleted, transferJob,
-        [this, txnId](const QString& tId, const QUrl&,
-                      const FileSourceInfo& fileMetadata) {
+    if (const auto& transferJob = fileTransfers.value(txnId).job) {
+        // Below, the upload job is used as a context object to clean up 
connections
+        connect(q, &Room::fileTransferCompleted, transferJob,
+                [this, txnId](const QString& tId, const QUrl&, const 
FileSourceInfo& fileMetadata) {
+                    if (tId != txnId)
+                        return;
+
+                    const auto it = q->findPendingEvent(txnId);
+                    if (it != unsyncedEvents.end()) {
+                        it->setFileUploaded(fileMetadata);
+                        emit q->pendingEventChanged(int(it - 
unsyncedEvents.begin()));
+                        doSendEvent(it);
+                    } else {
+                        // Normally in this situation we should instruct
+                        // the media server to delete the file; alas, there's 
no
+                        // API specced for that.
+                        qCWarning(MAIN) << "File uploaded to" << 
getUrlFromSourceInfo(fileMetadata)
+                                        << "but the event referring to it was "
+                                           "cancelled";
+                    }
+                });
+        connect(q, &Room::fileTransferFailed, transferJob, [this, txnId](const 
QString& tId) {
             if (tId != txnId)
                 return;
 
             const auto it = q->findPendingEvent(txnId);
-            if (it != unsyncedEvents.end()) {
-                it->setFileUploaded(fileMetadata);
-                emit q->pendingEventChanged(int(it - unsyncedEvents.begin()));
-                doSendEvent(it);
-            } else {
-                // Normally in this situation we should instruct
-                // the media server to delete the file; alas, there's no
-                // API specced for that.
-                qCWarning(MAIN)
-                    << "File uploaded to" << getUrlFromSourceInfo(fileMetadata)
-                    << "but the event referring to it was "
-                       "cancelled";
-            }
+            if (it == unsyncedEvents.end())
+                return;
+
+            const auto idx = int(it - unsyncedEvents.begin());
+            emit q->pendingEventAboutToDiscard(idx);
+            // See #286 on why `it` may not be valid here.
+            unsyncedEvents.erase(unsyncedEvents.begin() + idx);
+            emit q->pendingEventDiscarded();
         });
-    connect(q, &Room::fileTransferFailed, transferJob,
-            [this, txnId](const QString& tId) {
-                if (tId != txnId)
-                    return;
-
-                const auto it = q->findPendingEvent(txnId);
-                if (it == unsyncedEvents.end())
-                    return;
-
-                const auto idx = int(it - unsyncedEvents.begin());
-                emit q->pendingEventAboutToDiscard(idx);
-                // See #286 on why `it` may not be valid here.
-                unsyncedEvents.erase(unsyncedEvents.begin() + idx);
-                emit q->pendingEventDiscarded();
-            });
+    }
 
     return txnId;
 }
@@ -2536,16 +2540,24 @@
     // This is required because toLocalFile doesn't work on android and 
toString doesn't work on the desktop
     auto fileName = localFilename.isLocalFile() ? localFilename.toLocalFile() 
: localFilename.toString();
     FileSourceInfo fileMetadata;
+    // NB: tempFile needs to live at least until Connection::uploadFile() 
opens it
     QTemporaryFile tempFile;
     if (usesEncryption()) {
-        tempFile.open();
-        QFile file(fileName);
-        file.open(QFile::ReadOnly);
-        QByteArray data;
-        std::tie(fileMetadata, data) = encryptFile(file.readAll());
-        tempFile.write(data);
-        tempFile.close();
-        fileName = QFileInfo(tempFile).absoluteFilePath();
+        if (tempFile.open()) {
+            if (QFile file(fileName); file.open(QFile::ReadOnly)) {
+                QByteArray data;
+                std::tie(fileMetadata, data) = encryptFile(file.readAll());
+                tempFile.write(data);
+                fileName = QFileInfo(tempFile).absoluteFilePath();
+            } else
+                d->failedTransfer(id, u"Could not open a temporary file for 
encryption"_s);
+
+            tempFile.close();
+        } else
+            d->failedTransfer(id, u"Could not open the file for uploading"_s);
+
+        if (d->fileTransfers.value(id).status == FileTransferInfo::Failed)
+            return;
     }
     auto job = connection()->uploadFile(fileName, overrideContentType);
     if (isJobPending(job)) {
@@ -2702,64 +2714,6 @@
             << "Decrypted" << totalDecrypted << "events in" << et;
 }
 
-//! \brief Make a redacted event
-//!
-//! This applies the redaction procedure as defined by the CS API specification
-//! to the event's JSON and returns the resulting new event. It is
-//! the responsibility of the caller to dispose of the original event after 
that.
-RoomEventPtr makeRedacted(const RoomEvent& target,
-                          const RedactionEvent& redaction)
-{
-    // The logic below faithfully follows the spec despite quite a few of
-    // the preserved keys being only relevant for homeservers. Just in case.
-    static const QStringList TopLevelKeysToKeep{
-        EventIdKey,  TypeKey,          RoomIdKey,        SenderKey,
-        StateKeyKey, ContentKey,       "hashes"_L1,      "signatures"_L1,
-        "depth"_L1,  "prev_events"_L1, "auth_events"_L1, "origin_server_ts"_L1
-    };
-
-    auto originalJson = target.fullJson();
-    for (auto it = originalJson.begin(); it != originalJson.end();) {
-        if (!TopLevelKeysToKeep.contains(it.key()))
-            it = originalJson.erase(it);
-        else
-            ++it;
-    }
-    if (!target.is<RoomCreateEvent>()) { // See MSC2176 on create events
-        static const QHash<QString, QStringList> ContentKeysToKeepPerType{
-            { RedactionEvent::TypeId, { "redacts"_L1 } },
-            { RoomMemberEvent::TypeId,
-              { "membership"_L1, "join_authorised_via_users_server"_L1 } },
-            { RoomPowerLevelsEvent::TypeId,
-              { "ban"_L1, "events"_L1, "events_default"_L1, "invite"_L1,
-                "kick"_L1, "redact"_L1, "state_default"_L1, "users"_L1,
-                "users_default"_L1 } },
-            // TODO: Replace with RoomJoinRules::TypeId etc. once available
-            { "m.room.join_rules"_L1, { "join_rule"_L1, "allow"_L1 } },
-            { "m.room.history_visibility"_L1, { "history_visibility"_L1 } }
-        };
-
-        if (const auto contentKeysToKeep = 
ContentKeysToKeepPerType.value(target.matrixType());
-            !contentKeysToKeep.isEmpty()) //
-        {
-            editSubobject(originalJson, ContentKey, 
[&contentKeysToKeep](QJsonObject& content) {
-                for (auto it = content.begin(); it != content.end();) {
-                    if (!contentKeysToKeep.contains(it.key()))
-                        it = content.erase(it);
-                    else
-                        ++it;
-                }
-            });
-        } else {
-            originalJson.remove(ContentKey);
-            originalJson.remove(PrevContentKey);
-        }
-    }
-    replaceSubvalue(originalJson, UnsignedKey, RedactedCauseKey, 
redaction.fullJson());
-
-    return loadEvent<RoomEvent>(originalJson);
-}
-
 bool Room::Private::processRedaction(const RedactionEvent& redaction)
 {
     // Can't use findInTimeline because it returns a const iterator, and
@@ -2781,7 +2735,7 @@
 
     // Make a new event from the redacted JSON and put it in the timeline
     // instead of the redacted one. oldEvent will be deleted on return.
-    auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction));
+    auto oldEvent = ti.replaceEvent(ti->makeRedacted(redaction));
     qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << 
redaction.id();
     if (oldEvent->isStateEvent()) {
         // Check whether the old event was a part of current state; if it was,
@@ -2815,27 +2769,11 @@
     return true;
 }
 
-/** Make a replaced event
- *
- * Takes \p target and returns a copy of it with content taken from
- * \p replacement. Disposal of the original event after that is on the caller.
- */
-RoomEventPtr makeReplaced(const RoomEvent& target,
-                          const RoomMessageEvent& replacement)
-{
-    auto newContent = replacement.contentPart<QJsonObject>("m.new_content"_L1);
-    addParam<IfNotEmpty>(newContent, RelatesToKey, 
target.contentPart<QJsonObject>(RelatesToKey));
-    auto originalJson = target.fullJson();
-    originalJson[ContentKey] = newContent;
-    editSubobject(originalJson, UnsignedKey, [&replacement](QJsonObject& 
unsignedData) {
-        replaceSubvalue(unsignedData, "m.relations"_L1, "m.replace"_L1, 
replacement.id());
-    });
-
-    return loadEvent<RoomEvent>(originalJson);
-}
-
-bool Room::Private::processReplacement(const RoomMessageEvent& newEvent)
+bool Room::Private::processReplacement(const RoomMessageEvent &newEvent)
 {
+    if (QUO_ALARM_X(newEvent.isStateEvent(), "Replacing state events is not 
allowed"))
+        return false;
+
     // Can't use findInTimeline because it returns a const iterator, and
     // we need to change the underlying TimelineItem.
     const auto pIdx = eventsIndex.constFind(newEvent.replacedEvent());
@@ -2847,7 +2785,7 @@
     auto& ti = timeline[Timeline::size_type(*pIdx - q->minTimelineIndex())];
     const auto* const rme = ti.viewAs<RoomMessageEvent>();
     if (!rme) {
-        qCWarning(STATE) << "Ignoring attempt to replace a non-message event"
+        qCWarning(STATE) << "Replacing a non-message event is implemented in 
libQuotient 0.10"
                          << ti->id();
         return false;
     }
@@ -2859,7 +2797,7 @@
 
     // Make a new event from the redacted JSON and put it in the timeline
     // instead of the redacted one. oldEvent will be deleted on return.
-    auto oldEvent = ti.replaceEvent(makeReplaced(*ti, newEvent));
+    auto oldEvent = ti.replaceEvent(ti->makeReplaced(newEvent));
     qCDebug(STATE) << "Replaced" << oldEvent->id() << "with" << newEvent.id();
     emit q->replacedEvent(ti.event(), std::to_address(oldEvent));
     return true;
@@ -2896,19 +2834,6 @@
         emit q->updatedEvent(content.eventId);
 }
 
-namespace {
-/// Whether the event is a redaction or a replacement
-inline bool isEditing(const RoomEventPtr& ep)
-{
-    return QUO_CHECK(ep != nullptr)
-           && ep->switchOnType([](const RedactionEvent&) { return true; },
-                               [](const RoomMessageEvent& rme) {
-                                   return !rme.replacedEvent().isEmpty();
-                               },
-                               false);
-}
-}
-
 Room::Timeline::size_type 
Room::Private::mergePendingEvent(PendingEvents::iterator localEchoIt,
                                                            
RoomEvents::iterator remoteEchoIt)
 {
@@ -2944,50 +2869,11 @@
         return Change::None;
 
     decryptIncomingEvents(events);
+    processRedactionsAndEdits(events);
 
     QElapsedTimer et;
     et.start();
 
-    {
-        using namespace std::ranges;
-        // Pre-process redactions and edits so that events that get
-        // redacted/replaced in the same batch landed in the timeline already
-        // treated.
-        // NB: We have to store redacting/replacing events to the timeline too 
-
-        // see #220.
-        auto it = find_if(events, isEditing);
-        for (const auto& eptr : subrange(it, events.end())) {
-            if (auto* r = eventCast<RedactionEvent>(eptr)) {
-                // Try to find the target in the timeline, then in the batch.
-                if (processRedaction(*r))
-                    continue;
-                if (auto targetIt = find(events, r->redactedEvent(), 
&RoomEvent::id);
-                    targetIt != events.end())
-                    *targetIt = makeRedacted(**targetIt, *r);
-                else
-                    qCDebug(STATE)
-                        << "Redaction" << r->id() << "ignored: target event"
-                        << r->redactedEvent() << "is not found";
-                // If the target event comes later, it comes already redacted.
-            }
-            if (auto* msg = eventCast<RoomMessageEvent>(eptr);
-                    msg && !msg->replacedEvent().isEmpty()) {
-                if (processReplacement(*msg))
-                    continue;
-                if (auto targetIt = find(events.begin(), it, 
msg->replacedEvent(), &RoomEvent::id);
-                    targetIt != it)
-                    *targetIt = makeReplaced(**targetIt, *msg);
-                else // FIXME: hide the replacing event when target arrives 
later
-                    qCDebug(EVENTS)
-                        << "Replacing event" << msg->id()
-                        << "ignored: target event" << msg->replacedEvent()
-                        << "is not found";
-                // Same as with redactions above, the replaced event coming
-                // later will come already with the new content.
-            }
-        }
-    }
-
     // State changes arrive as a part of timeline; the current room state gets
     // updated before merging events to the timeline because that's what
     // clients historically expect. This may eventually change though if we
@@ -3100,6 +2986,7 @@
 
     const auto timelineSize = timeline.size();
     decryptIncomingEvents(events);
+    processRedactionsAndEdits(events);
 
     QElapsedTimer et;
     et.start();
@@ -3174,7 +3061,7 @@
             case Membership::Leave:
                 if (rme.membership() == Membership::Invite
                     || rme.membership() == Membership::Join) {
-                    membersLeft.removeOne(rme.userId());
+                    membersLeft.remove(rme.userId());
                     Q_ASSERT(!membersLeft.contains(rme.userId()));
                 }
                 break;
@@ -3290,8 +3177,7 @@
             case Membership::Knock:
             case Membership::Ban:
             case Membership::Leave:
-                if (!membersLeft.contains(evt.userId()))
-                    membersLeft.append(evt.userId());
+                membersLeft.insert(evt.userId());
                 break;
             case Membership::Undefined:
                 qCWarning(MEMBERS) << "Ignored undefined membership type";
@@ -3314,6 +3200,89 @@
         Change::Other);
 }
 
+void Room::Private::processRedactionsAndEdits(RoomEvents &events)
+{
+    using namespace std::ranges;
+    // Pre-process redactions and edits so that events that get 
redacted/replaced in the same batch
+    // landed in the timeline already treated.
+    // NB: We have to store redacting/replacing events to the timeline too - 
see #220.
+    for (auto &evt : events) {
+        if (const auto *r = eventCast<RedactionEvent>(evt)) {
+            // Try to find the target in the timeline
+            if (processRedaction(*r))
+                continue;
+            // Otherwise, check this batch for the event
+            if (auto targetIt = find(events, r->redactedEvent(), 
&RoomEvent::id);
+                targetIt != events.end())
+                *targetIt = (*targetIt)->makeRedacted(*r);
+            else
+                qCDebug(STATE) << "Redaction" << r->id() << "ignored: target 
event"
+                               << r->redactedEvent() << "is not found";
+            // If the target event comes later, we expected it to come already 
redacted
+            continue;
+            // We don't expect redaction events to participate in edits, 
although The Spec doesn't
+            // say anything about it
+        }
+
+        const auto* const rme = eventCast<const RoomMessageEvent>(evt);
+        // Check if we have orphaned replacements for the event just received; 
if yes, edit it
+        if (const auto replacementEventId = 
orphanedReplacementEvents.take(evt->id());
+            !replacementEventId.isEmpty()) {
+            // Check the replacement validity (see
+            // 
https://spec.matrix.org/v1.17/client-server-api/#validity-of-replacement-events),
+            // with a caveat that 0.9 can only deal with message event 
replacements because of
+            // back-comp; if the replacement is invalid, the old event will 
remain intact and the
+            // replacement we now know is invalid will be hidden anyway
+            if (!rme) [[unlikely]]
+                qDebug(EVENTS).noquote() << "This version of libQuotient 
doesn't support "
+                                            "replacement of non-message 
events, skipping"
+                                         << evt->id();
+            else if (rme->relatesTo()) [[unlikely]]
+                qDebug(EVENTS).noquote() << "Attempt to replace event" << 
rme->id()
+                                         << "that is itself a replacement - 
skipping";
+            else if (const auto replacementIt = 
q->findInTimeline(replacementEventId);
+                     replacementIt != historyEdge()) {
+                if (evt->metaType() != (*replacementIt)->metaType()) 
[[unlikely]]
+                    qCDebug(EVENTS).noquote()
+                        << "Attempt to replace" << evt->matrixType() << "with"
+                        << (*replacementIt)->matrixType() << "- skipping";
+                else if (evt->senderId() != (*replacementIt)->senderId()) 
[[unlikely]]
+                    qCDebug(EVENTS).noquote()
+                        << "Attempt to replace an event from" << 
evt->senderId()
+                        << "with one from" << (*replacementIt)->senderId() << 
"- skipping";
+                else {
+                    qCDebug(EVENTS) << "Found parent" << evt->id() << "for 
replacement event"
+                                    << (*replacementIt)->id();
+                    evt = evt->makeReplaced(**replacementIt);
+                }
+            }
+            continue;
+        }
+
+        if (rme) {
+            // Check if the event is a replacement, which we hopefully have 
the target for; if we
+            // don't then we store it in orphaned replacements and postpone 
any action until
+            // the original event becomes known
+            if (const auto replacedEventId = rme->replacedEvent(); 
!replacedEventId.isEmpty()) {
+                // Try to find the target in the timeline
+                if (processReplacement(*rme))
+                    continue;
+                // Otherwise, check the current batch for the event
+                if (auto targetIt = find(events, replacedEventId, 
&RoomEvent::id);
+                    targetIt != events.end())
+                    *targetIt = (*targetIt)->makeReplaced(*evt);
+                else if (evt->isStateEvent()){
+                    qCDebug(EVENTS)
+                        << "Processing of replacement event" << evt->id()
+                        << "postponed: target event" << rme->replacedEvent()
+                        << "is not found, will check for it in older batches 
when they come";
+                    orphanedReplacementEvents[rme->replacedEvent()] = 
evt->id();
+                }
+            }
+        }
+    }
+}
+
 Room::Changes Room::processEphemeralEvent(EventPtr&& event)
 {
     Changes changes {};
@@ -3468,7 +3437,7 @@
         shortlist = buildShortlist(membersInvited);
 
     if (shortlist.front().isEmpty())
-        shortlist = buildShortlist(membersLeft);
+        shortlist = buildShortlist(membersLeft.values());
 
     QStringList names;
     for (const auto& u : shortlist) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/room.h 
new/libQuotient-0.9.6/Quotient/room.h
--- old/libQuotient-0.9.5/Quotient/room.h       2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/Quotient/room.h       2026-02-15 21:24:53.000000000 
+0100
@@ -389,9 +389,9 @@
     PendingEvents::const_iterator findPendingEvent(const QString& txnId) const;
 
     const RelatedEvents relatedEvents(const QString& evtId,
-                                      EventRelation::reltypeid_t relType) 
const;
+                                      EventRelation::typeid_t relType) const;
     const RelatedEvents relatedEvents(const RoomEvent& evt,
-                                      EventRelation::reltypeid_t relType) 
const;
+                                      EventRelation::typeid_t relType) const;
 
     const RoomCreateEvent* creation() const;
     const RoomTombstoneEvent* tombstone() const;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/Quotient/settings.h 
new/libQuotient-0.9.6/Quotient/settings.h
--- old/libQuotient-0.9.5/Quotient/settings.h   2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/Quotient/settings.h   2026-02-15 21:24:53.000000000 
+0100
@@ -154,7 +154,9 @@
     QUrl homeserver() const;
     void setHomeserver(const QUrl& url);
 
+    [[deprecated("Client code shouldn't use the pickle; and the library stores 
it in a keychain")]]
     QByteArray encryptionAccountPickle();
+    [[deprecated("Client code shouldn't use the pickle; and the library stores 
it in a keychain")]]
     void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle);
     Q_INVOKABLE void clearEncryptionAccountPickle();
 };
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/autotests/CMakeLists.txt 
new/libQuotient-0.9.6/autotests/CMakeLists.txt
--- old/libQuotient-0.9.5/autotests/CMakeLists.txt      2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/autotests/CMakeLists.txt      2026-02-15 
21:24:53.000000000 +0100
@@ -9,16 +9,19 @@
 add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" )
 
 function(QUOTIENT_ADD_TEST)
-    cmake_parse_arguments(ARG "" "NAME" "" ${ARGN})
+    cmake_parse_arguments(ARG "NEEDS_MOCK_SERVER" "NAME" "" ${ARGN})
     add_executable(${ARG_NAME} ${ARG_NAME}.cpp testutils.h testutils.cpp)
     target_link_libraries(${ARG_NAME} ${Qt}::Core ${Qt}::Test 
${QUOTIENT_LIB_NAME})
     add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME})
     add_dependencies(autotests ${ARG_NAME})
+    if (ARG_NEEDS_MOCK_SERVER)
+        set_property(TEST ${ARG_NAME} APPEND PROPERTY LABELS needs_mock_server)
+    endif()
 endfunction()
 
 quotient_add_test(NAME callcandidateseventtest)
 quotient_add_test(NAME utiltests)
-quotient_add_test(NAME testolmaccount)
+quotient_add_test(NAME testolmaccount NEEDS_MOCK_SERVER)
 quotient_add_test(NAME testgroupsession)
 quotient_add_test(NAME testolmsession)
 if (NOT MSVC)
@@ -30,4 +33,5 @@
 quotient_add_test(NAME testcrosssigning)
 quotient_add_test(NAME testkeyimport)
 quotient_add_test(NAME testsettings)
-quotient_add_test(NAME testthread)
+quotient_add_test(NAME testthread NEEDS_MOCK_SERVER)
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/autotests/testcrosssigning.cpp 
new/libQuotient-0.9.6/autotests/testcrosssigning.cpp
--- old/libQuotient-0.9.5/autotests/testcrosssigning.cpp        2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/autotests/testcrosssigning.cpp        2026-02-15 
21:24:53.000000000 +0100
@@ -20,7 +20,7 @@
         path = path.left(path.lastIndexOf(QDir::separator()));
         path += "/cross_signing_data.json"_L1;
         QFile file(path);
-        file.open(QIODevice::ReadOnly);
+        QVERIFY(file.open(QIODevice::ReadOnly));
         auto data = file.readAll();
         QVERIFY(!data.isEmpty());
         auto jobMock = Mocked<QueryKeysJob>(QHash<QString, QStringList>{});
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/autotests/testkeyimport.cpp 
new/libQuotient-0.9.6/autotests/testkeyimport.cpp
--- old/libQuotient-0.9.5/autotests/testkeyimport.cpp   2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/autotests/testkeyimport.cpp   2026-02-15 
21:24:53.000000000 +0100
@@ -25,7 +25,7 @@
     path = path.left(path.lastIndexOf(QDir::separator()));
     path += "/key-export.data"_L1;
     QFile file(path);
-    file.open(QIODevice::ReadOnly);
+    QVERIFY(file.open(QIODevice::ReadOnly));
     auto data = file.readAll();
     QVERIFY(!data.isEmpty());
     const auto result = keyImport.decrypt(QString::fromUtf8(data), 
u"123passphrase"_s);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/autotests/testutils.h 
new/libQuotient-0.9.6/autotests/testutils.h
--- old/libQuotient-0.9.5/autotests/testutils.h 2025-09-20 13:57:20.000000000 
+0200
+++ new/libQuotient-0.9.6/autotests/testutils.h 2026-02-15 21:24:53.000000000 
+0100
@@ -25,10 +25,10 @@
 inline event_ptr_tt<EventT> loadEventFromFile(const QString &eventFileName)
 {
     if (!eventFileName.isEmpty()) {
-        QFile testEventFile;
-        testEventFile.setFileName(QLatin1StringView(DATA_DIR) + u'/' + 
eventFileName);
-        testEventFile.open(QIODevice::ReadOnly);
-        return 
loadEvent<EventT>(QJsonDocument::fromJson(testEventFile.readAll()).object());
+        if (QFile testEventFile(QStringLiteral(DATA_DIR "/") + eventFileName);
+            testEventFile.open(QIODevice::ReadOnly)) {
+            return 
loadEvent<EventT>(QJsonDocument::fromJson(testEventFile.readAll()).object());
+        }
     }
     return nullptr;
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libQuotient-0.9.5/cmake/QuotientConfig.cmake.in 
new/libQuotient-0.9.6/cmake/QuotientConfig.cmake.in
--- old/libQuotient-0.9.5/cmake/QuotientConfig.cmake.in 2025-09-20 
13:57:20.000000000 +0200
+++ new/libQuotient-0.9.6/cmake/QuotientConfig.cmake.in 2026-02-15 
21:24:53.000000000 +0100
@@ -1,5 +1,11 @@
+@PACKAGE_INIT@
+
 include(CMakeFindDependencyMacro)
 
+find_dependency(@Qt@Core)
+if (@Qt@Core_VERSION VERSION_GREATER_EQUAL 6.10)
+    find_dependency(@Qt@CorePrivate)
+endif()
 find_dependency(@Qt@Gui)
 find_dependency(@Qt@Network)
 find_dependency(@Qt@Keychain)
@@ -7,6 +13,10 @@
 find_dependency(OpenSSL)
 find_dependency(@Qt@Sql)
 
+if(NOT @BUILD_SHARED_LIBS@ AND @Qt@Core_VERSION VERSION_GREATER_EQUAL 6.10)
+    find_dependency(@Qt@CorePrivate)
+endif()
+
 include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]")
 
 if (NOT QUOTIENT_FORCE_NAMESPACED_INCLUDES)

Reply via email to