From 8c2037cc548b4d7b2775b9ede627d6e02fcb9ac7 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 15 Nov 2022 15:29:17 +0300 Subject: [PATCH 1/2] Release 0.24.3 (#1636) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Manual device verification with Crypto V2 * Add manual user verification * Compiler flags * Prepare for new sprint * Support additionnal content in voice message * Add changelog * Add support for m.local_notification_settings. in account_data * Fix typo * Login with QR support check (#1592) * Expose `supportsQRLogin` on `MXMatrixVersions` * Expose supportedMatrixVersions to Swift * Refactor verification manager, requests, transactions * Incoming verification with Crypto V2 * Expose rest client method for generating login tokens through MSC3882 * Switch to unstable path for login token generation call * Refactor QR transactions * Mac-compatible MatrixSDKCrypto * QR code verification with Crypto V2 * Verification: Unencrypted events * Remove megolm decrypt cache build flag * Fix isLive flag in MXRoomMembers * Add changelog.d file * Exposed method to update client information * Changelog * Changelog fix * Prepare for new sprint * Prepare for new sprint * Disable a bunch of flaky crypto tests * Manual key export / import with Crypto V2 * Set local trust with Crypto V2 * Define MXCrypto and MXCrossSigning as protocols * Cross-sign self after restoring session * Threads: added support to read receipts (MSC3771) * Curate MXCrypto protocol methods * Delete devices endpoint * Complete MXCryptoV2 implementation * Changelog * Spelling fix * CryptoV2 changes * Add crypto version * Temporarily disable main queue creation * Prepare for new sprint * Prepare for new sprint * Threads: added support to read receipts (MSC3771) - Update after review * Threads: added support to notifications count (MSC3773) * Threads: added support to read receipts (MSC3771) - Fixed CI build error * Threads: added support to read receipts (MSC3771) - Update after review * Threads: add support to labs flag for read receipts * Doc: Update the synapse installation section with poetry usage * Threads: added support to read receipts (MSC3771) - Fixed CI build error * Room event decryption * Tests: Fix/comment some push tests due to default push rules changes on server * Tests: Update MXRestClientTests.testFilter() to match server expectation * Tests: Disable MXSpaceChildContentTests.testUpdateChildSuggestion() and testUpgradeSpaceChild() It seems we have race conditions that we could improve by adding some sleep(1) before REST request but they are still not 100% reliable. So skip them. testUpdateChildSuggestion is broken since 2022-09-16 without good reasons. testUpgradeSpaceChild() is bit more random * Tests: Disable tests that randomly fail Failure on the 3 last months: MXAggregatedReactionTests testUnreactOnEventReactedByOther: 5 MXCryptoSecretStorageTests testDeleteSecret: 4 MXAggregatedReactionTests testAggregationsListener: 2 * changelog * Tests: Rename testDefaultRoomMemberCountCondition into testDefaultEventMatchCondition as we test a different thing now * Threads notification count in main timeline including un participated threads * Crypto fixes * Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 * Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 - Update after review * Log decryption errors separately * Add polls to “acknowledgableEventTypes” * Add changelog.d file * Remove comment * Display number of unread messages above threads button * version++ Co-authored-by: Andy Uhnak Co-authored-by: Doug Co-authored-by: yostyle Co-authored-by: Gil Eluard Co-authored-by: Stefan Ceriu Co-authored-by: Alfonso Grillo Co-authored-by: Aleksandrs Proskurins Co-authored-by: gulekismail Co-authored-by: manuroe Co-authored-by: manuroe --- CHANGES.md | 27 + MatrixSDK.podspec | 2 +- MatrixSDK.xcodeproj/project.pbxproj | 52 ++ MatrixSDK/Background/MXBackgroundStore.swift | 12 +- MatrixSDK/Contrib/Swift/MXRestClient.swift | 5 +- .../RoomEvent/MXRoomEventDecryption.swift | 271 ++++++++++ .../CrossSigning/MXCrossSigningV2.swift | 6 +- .../CryptoMachine/MXCryptoMachine.swift | 196 +++---- .../CryptoMachine/MXCryptoProtocols.swift | 16 +- MatrixSDK/Crypto/Data/MXMegolmSessionData.m | 14 +- .../Engine/MXCryptoKeyBackupEngine.swift | 126 +++-- MatrixSDK/Crypto/KeyBackup/MXKeyBackup.m | 33 +- MatrixSDK/Crypto/MXCryptoV2.swift | 207 ++++---- .../MXKeyVerificationManagerV2.swift | 45 +- .../EventTimeline/Room/MXRoomEventTimeline.m | 39 +- MatrixSDK/Data/Filters/MXFilterJSONModel.h | 27 + MatrixSDK/Data/Filters/MXFilterJSONModel.m | 35 +- MatrixSDK/Data/Filters/MXRoomEventFilter.h | 5 + MatrixSDK/Data/Filters/MXRoomEventFilter.m | 12 + MatrixSDK/Data/MXRoom.h | 5 +- MatrixSDK/Data/MXRoom.m | 173 ++++-- MatrixSDK/Data/MXRoomSummary.m | 39 +- .../Data/Store/MXFileStore/MXFileStore.m | 138 ++--- .../Store/MXMemoryStore/MXMemoryRoomStore.h | 9 + .../Store/MXMemoryStore/MXMemoryRoomStore.m | 40 +- .../Data/Store/MXMemoryStore/MXMemoryStore.h | 24 +- .../Data/Store/MXMemoryStore/MXMemoryStore.m | 104 +++- .../Data/Store/MXMemoryStore/MXReceiptData.h | 9 +- .../Data/Store/MXMemoryStore/MXReceiptData.m | 11 +- MatrixSDK/Data/Store/MXNoStore/MXNoStore.m | 22 + MatrixSDK/Data/Store/MXStore.h | 28 +- MatrixSDK/JSONModels/MXEvent.h | 14 +- MatrixSDK/JSONModels/MXEvent.m | 41 ++ MatrixSDK/JSONModels/MXMatrixVersions.h | 5 + MatrixSDK/JSONModels/MXMatrixVersions.m | 5 + MatrixSDK/JSONModels/Sync/Room/MXRoomSync.h | 5 + MatrixSDK/JSONModels/Sync/Room/MXRoomSync.m | 16 + MatrixSDK/MXRestClient.h | 1 + MatrixSDK/MXRestClient.m | 9 +- MatrixSDK/MXSession.m | 14 +- MatrixSDK/MatrixSDKVersion.m | 2 +- .../Threads/MXThreadNotificationsCount.swift | 7 +- MatrixSDK/Threads/MXThreadingService.swift | 30 +- .../Utils/Logs/MXAnalyticsDestination.swift | 2 +- .../MXRoomEventDecryptionUnitTests.swift | 201 +++++++ .../CryptoMachine/DecryptedEvent+Stub.swift | 37 ++ .../MXCryptoMachineUnitTests.swift | 17 +- .../CryptoMachine/MXCryptoProtocolStubs.swift | 16 +- .../Data/MXMegolmSessionDataUnitTests.swift | 17 +- .../MXCryptoKeyBackupEngineUnitTests.swift | 27 +- .../JSONModels/MXEventFixtures.swift | 14 + .../MXMatrixVersionsUnitTests.swift | 7 +- MatrixSDKTests/MXNotificationCenterTests.m | 6 +- .../MXReceiptDataIntegrationTests.swift | 495 ++++++++++++++++++ MatrixSDKTests/MXRestClientTests.m | 46 +- .../MXRoomEventFilterUnitTests.swift | 254 +++++++++ .../MXThreadsNotificationCountTests.swift | 272 ++++++++++ .../TestPlans/AllWorkingTests.xctestplan | 17 +- MatrixSDKTests/TestPlans/UnitTests.xctestplan | 2 + .../UnitTestsWithSanitizers.xctestplan | 2 + Podfile.lock | 2 +- README.rst | 22 +- 62 files changed, 2775 insertions(+), 562 deletions(-) create mode 100644 MatrixSDK/Crypto/Algorithms/RoomEvent/MXRoomEventDecryption.swift create mode 100644 MatrixSDKTests/Crypto/Algorithms/RoomEvents/MXRoomEventDecryptionUnitTests.swift create mode 100644 MatrixSDKTests/Crypto/CryptoMachine/DecryptedEvent+Stub.swift create mode 100644 MatrixSDKTests/MXReceiptDataIntegrationTests.swift create mode 100644 MatrixSDKTests/MXRoomEventFilterUnitTests.swift create mode 100644 MatrixSDKTests/MXThreadsNotificationCountTests.swift diff --git a/CHANGES.md b/CHANGES.md index 4c4c9f50ea..a53ba2df9b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,30 @@ +## Changes in 0.24.3 (2022-11-15) + +✨ Features + +- Threads: added support to read receipts (MSC3771) ([#6663](https://github.com/vector-im/element-ios/issues/6663)) +- Threads: added support to notifications count (MSC3773) ([#6664](https://github.com/vector-im/element-ios/issues/6664)) +- Threads: added support to labs flag for read receipts ([#7029](https://github.com/vector-im/element-ios/issues/7029)) +- Threads: notification count in main timeline including un participated threads ([#7038](https://github.com/vector-im/element-ios/issues/7038)) + +🙌 Improvements + +- CryptoV2: Room event decryption ([#1627](https://github.com/matrix-org/matrix-ios-sdk/pull/1627)) +- CryptoV2: Bugfixes ([#1630](https://github.com/matrix-org/matrix-ios-sdk/pull/1630)) +- CryptoV2: Log decryption errors separately ([#1632](https://github.com/matrix-org/matrix-ios-sdk/pull/1632)) +- Adds the sending of read receipts for poll start/end events ([#1633](https://github.com/matrix-org/matrix-ios-sdk/pull/1633)) + +🐛 Bugfixes + +- Tests: Fix or disable flakey integration tests ([#1628](https://github.com/matrix-org/matrix-ios-sdk/pull/1628)) +- Threads: removed "unread_thread_notifications" from sync filters for server that doesn't support MSC3773 ([#7066](https://github.com/vector-im/element-ios/issues/7066)) +- Threads: Display number of unread messages above threads button ([#7076](https://github.com/vector-im/element-ios/issues/7076)) + +📄 Documentation + +- Doc: Update the synapse installation section with poetry usage ([#1625](https://github.com/matrix-org/matrix-ios-sdk/pull/1625)) + + ## Changes in 0.24.2 (2022-11-01) 🙌 Improvements diff --git a/MatrixSDK.podspec b/MatrixSDK.podspec index ca75013b4a..551c0276f5 100644 --- a/MatrixSDK.podspec +++ b/MatrixSDK.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = "MatrixSDK" - s.version = "0.24.2" + s.version = "0.24.3" s.summary = "The iOS SDK to build apps compatible with Matrix (https://www.matrix.org)" s.description = <<-DESC diff --git a/MatrixSDK.xcodeproj/project.pbxproj b/MatrixSDK.xcodeproj/project.pbxproj index 71a11f0442..0bb4c07285 100644 --- a/MatrixSDK.xcodeproj/project.pbxproj +++ b/MatrixSDK.xcodeproj/project.pbxproj @@ -665,6 +665,10 @@ 3A23A740256D322C00B9D00F /* MXAes.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A23A73D256D322C00B9D00F /* MXAes.m */; }; 3A23A741256D322C00B9D00F /* MXAes.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A23A73E256D322C00B9D00F /* MXAes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3A23A742256D322C00B9D00F /* MXAes.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A23A73E256D322C00B9D00F /* MXAes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3A2A3238291031A7005EF477 /* MXThreadsNotificationCountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2A3237291031A7005EF477 /* MXThreadsNotificationCountTests.swift */; }; + 3A2A3239291031A7005EF477 /* MXThreadsNotificationCountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2A3237291031A7005EF477 /* MXThreadsNotificationCountTests.swift */; }; + 3A4BB662291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4BB661291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift */; }; + 3A4BB663291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4BB661291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift */; }; 3A5787A528982D4600A0D8A8 /* MXBreadcrumbsRoomListDataFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5787A428982D4600A0D8A8 /* MXBreadcrumbsRoomListDataFetcher.swift */; }; 3A5787A628982D4600A0D8A8 /* MXBreadcrumbsRoomListDataFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5787A428982D4600A0D8A8 /* MXBreadcrumbsRoomListDataFetcher.swift */; }; 3A59A49D25A7A16F00DDA1FC /* MXOlmOutboundGroupSession.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A59A49B25A7A16E00DDA1FC /* MXOlmOutboundGroupSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -687,6 +691,8 @@ 3A858DE327529183006322C1 /* MXRoomCapabilityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A858DE027517C0E006322C1 /* MXRoomCapabilityType.swift */; }; 3A858DE8275511A4006322C1 /* MXRoomAliasAvailabilityCheckerResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A858DE7275511A4006322C1 /* MXRoomAliasAvailabilityCheckerResultTests.swift */; }; 3A858DE9275511A4006322C1 /* MXRoomAliasAvailabilityCheckerResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A858DE7275511A4006322C1 /* MXRoomAliasAvailabilityCheckerResultTests.swift */; }; + 3A96CD492901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96CD482901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift */; }; + 3A96CD4A2901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96CD482901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift */; }; 3A9E2B4328EB3960000DB2A7 /* MXMatrixVersionsUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9E2B4228EB3960000DB2A7 /* MXMatrixVersionsUnitTests.swift */; }; 3A9E2B4428EB3960000DB2A7 /* MXMatrixVersionsUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9E2B4228EB3960000DB2A7 /* MXMatrixVersionsUnitTests.swift */; }; 3AB5EBB4270B332B0058703A /* MXSpaceStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB5EBB3270B332B0058703A /* MXSpaceStore.swift */; }; @@ -1819,6 +1825,10 @@ ED01915928C64E0400ED3A69 /* MXForwardedRoomKeyEventContent.h in Headers */ = {isa = PBXBuildFile; fileRef = ED01915128C64E0400ED3A69 /* MXForwardedRoomKeyEventContent.h */; settings = {ATTRIBUTES = (Public, ); }; }; ED1AE92A2881AC7500D3432A /* MXWarnings.h in Headers */ = {isa = PBXBuildFile; fileRef = ED1AE9292881AC7100D3432A /* MXWarnings.h */; settings = {ATTRIBUTES = (Public, ); }; }; ED1AE92B2881AC7500D3432A /* MXWarnings.h in Headers */ = {isa = PBXBuildFile; fileRef = ED1AE9292881AC7100D3432A /* MXWarnings.h */; settings = {ATTRIBUTES = (Public, ); }; }; + ED1FE9062912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1FE9052912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift */; }; + ED1FE9072912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1FE9052912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift */; }; + ED1FE90B2912E13A0046F722 /* DecryptedEvent+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1FE90A2912E13A0046F722 /* DecryptedEvent+Stub.swift */; }; + ED1FE90C2912E13A0046F722 /* DecryptedEvent+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1FE90A2912E13A0046F722 /* DecryptedEvent+Stub.swift */; }; ED21F68528104DA2002FF83D /* MXMegolmEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED21F68428104DA2002FF83D /* MXMegolmEncryptionTests.swift */; }; ED21F68628104DA2002FF83D /* MXMegolmEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED21F68428104DA2002FF83D /* MXMegolmEncryptionTests.swift */; }; ED28068428F06C6C0070AE9F /* QrCode+Stub.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED28068328F06C6C0070AE9F /* QrCode+Stub.swift */; }; @@ -1965,6 +1975,8 @@ EDBCF33A281A8D3D00ED5044 /* MXSharedHistoryKeyService.m in Sources */ = {isa = PBXBuildFile; fileRef = EDBCF338281A8D3D00ED5044 /* MXSharedHistoryKeyService.m */; }; EDC2A0E628369E740039F3D6 /* CryptoTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = EDC2A0E528369E740039F3D6 /* CryptoTests.xctestplan */; }; EDC2A0E728369E740039F3D6 /* CryptoTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = EDC2A0E528369E740039F3D6 /* CryptoTests.xctestplan */; }; + EDCB65E22912AB0C00F55D4D /* MXRoomEventDecryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCB65E12912AB0C00F55D4D /* MXRoomEventDecryption.swift */; }; + EDCB65E32912AB0C00F55D4D /* MXRoomEventDecryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCB65E12912AB0C00F55D4D /* MXRoomEventDecryption.swift */; }; EDD4197E28DCAA5F007F3757 /* MXNativeKeyBackupEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = EDD4197D28DCAA5F007F3757 /* MXNativeKeyBackupEngine.h */; }; EDD4197F28DCAA5F007F3757 /* MXNativeKeyBackupEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = EDD4197D28DCAA5F007F3757 /* MXNativeKeyBackupEngine.h */; }; EDD4198128DCAA7B007F3757 /* MXNativeKeyBackupEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = EDD4198028DCAA7B007F3757 /* MXNativeKeyBackupEngine.m */; }; @@ -2569,6 +2581,8 @@ 3A108E6625826F52005EEBE9 /* MXKeyProviderUnitTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXKeyProviderUnitTests.m; sourceTree = ""; }; 3A23A73D256D322C00B9D00F /* MXAes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MXAes.m; sourceTree = ""; }; 3A23A73E256D322C00B9D00F /* MXAes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MXAes.h; sourceTree = ""; }; + 3A2A3237291031A7005EF477 /* MXThreadsNotificationCountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXThreadsNotificationCountTests.swift; sourceTree = ""; }; + 3A4BB661291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXRoomEventFilterUnitTests.swift; sourceTree = ""; }; 3A5787A428982D4600A0D8A8 /* MXBreadcrumbsRoomListDataFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXBreadcrumbsRoomListDataFetcher.swift; sourceTree = ""; }; 3A59A49B25A7A16E00DDA1FC /* MXOlmOutboundGroupSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MXOlmOutboundGroupSession.h; sourceTree = ""; }; 3A59A49C25A7A16F00DDA1FC /* MXOlmOutboundGroupSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MXOlmOutboundGroupSession.m; sourceTree = ""; }; @@ -2580,6 +2594,7 @@ 3A858DDB275120D1006322C1 /* MXHomeserverCapabilitiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXHomeserverCapabilitiesTests.swift; sourceTree = ""; }; 3A858DE027517C0E006322C1 /* MXRoomCapabilityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXRoomCapabilityType.swift; sourceTree = ""; }; 3A858DE7275511A4006322C1 /* MXRoomAliasAvailabilityCheckerResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXRoomAliasAvailabilityCheckerResultTests.swift; sourceTree = ""; }; + 3A96CD482901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXReceiptDataIntegrationTests.swift; sourceTree = ""; }; 3A9E2B4228EB3960000DB2A7 /* MXMatrixVersionsUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXMatrixVersionsUnitTests.swift; sourceTree = ""; }; 3AB5EBB3270B332B0058703A /* MXSpaceStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXSpaceStore.swift; sourceTree = ""; }; 3AB5EBB6270ED1C00058703A /* MXSpaceFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXSpaceFileStore.swift; sourceTree = ""; }; @@ -2998,6 +3013,8 @@ ED01915028C64E0400ED3A69 /* MXRoomKeyEventContent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MXRoomKeyEventContent.m; sourceTree = ""; }; ED01915128C64E0400ED3A69 /* MXForwardedRoomKeyEventContent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MXForwardedRoomKeyEventContent.h; sourceTree = ""; }; ED1AE9292881AC7100D3432A /* MXWarnings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXWarnings.h; sourceTree = ""; }; + ED1FE9052912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXRoomEventDecryptionUnitTests.swift; sourceTree = ""; }; + ED1FE90A2912E13A0046F722 /* DecryptedEvent+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DecryptedEvent+Stub.swift"; sourceTree = ""; }; ED21F68428104DA2002FF83D /* MXMegolmEncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXMegolmEncryptionTests.swift; sourceTree = ""; }; ED28068328F06C6C0070AE9F /* QrCode+Stub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QrCode+Stub.swift"; sourceTree = ""; }; ED28068628F06D360070AE9F /* MXQRCodeTransactionV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXQRCodeTransactionV2.swift; sourceTree = ""; }; @@ -3071,6 +3088,7 @@ EDBCF335281A8AB900ED5044 /* MXSharedHistoryKeyService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXSharedHistoryKeyService.h; sourceTree = ""; }; EDBCF338281A8D3D00ED5044 /* MXSharedHistoryKeyService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXSharedHistoryKeyService.m; sourceTree = ""; }; EDC2A0E528369E740039F3D6 /* CryptoTests.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = CryptoTests.xctestplan; sourceTree = ""; }; + EDCB65E12912AB0C00F55D4D /* MXRoomEventDecryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXRoomEventDecryption.swift; sourceTree = ""; }; EDD4197D28DCAA5F007F3757 /* MXNativeKeyBackupEngine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MXNativeKeyBackupEngine.h; sourceTree = ""; }; EDD4198028DCAA7B007F3757 /* MXNativeKeyBackupEngine.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MXNativeKeyBackupEngine.m; sourceTree = ""; }; EDD578DC2881C37C006739DD /* MXDeviceInfoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MXDeviceInfoSource.swift; sourceTree = ""; }; @@ -3430,6 +3448,7 @@ children = ( 32A151231DABB0A100400192 /* Megolm */, 327187821DA7CFF20071C818 /* Olm */, + ED1FE9082912D4340046F722 /* RoomEvent */, 322A51C11D9AC8FE00C8536D /* MXCryptoAlgorithms.h */, 322A51C21D9AC8FE00C8536D /* MXCryptoAlgorithms.m */, 3271877C1DA7CB2F0071C818 /* MXDecrypting.h */, @@ -4318,6 +4337,9 @@ B1EE98D328048ACF00AB63F0 /* MXGeoURIComponentsUnitTests.swift */, B1F04B152811EFF700103EBE /* MXBeaconAggregationsTests.swift */, 3A9E2B4228EB3960000DB2A7 /* MXMatrixVersionsUnitTests.swift */, + 3A96CD482901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift */, + 3A2A3237291031A7005EF477 /* MXThreadsNotificationCountTests.swift */, + 3A4BB661291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift */, ); path = MatrixSDKTests; sourceTree = ""; @@ -5262,6 +5284,22 @@ path = Common; sourceTree = ""; }; + ED1FE9082912D4340046F722 /* RoomEvent */ = { + isa = PBXGroup; + children = ( + EDCB65E12912AB0C00F55D4D /* MXRoomEventDecryption.swift */, + ); + path = RoomEvent; + sourceTree = ""; + }; + ED1FE9092912D4780046F722 /* RoomEvents */ = { + isa = PBXGroup; + children = ( + ED1FE9052912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift */, + ); + path = RoomEvents; + sourceTree = ""; + }; ED21F67A28104B9A002FF83D /* Crypto */ = { isa = PBXGroup; children = ( @@ -5285,6 +5323,7 @@ ED21F67B28104BA1002FF83D /* Algorithms */ = { isa = PBXGroup; children = ( + ED1FE9092912D4780046F722 /* RoomEvents */, ED21F67C28104BA7002FF83D /* Megolm */, ); path = Algorithms; @@ -5326,6 +5365,7 @@ EDA6933F290BA92E00223252 /* MXCryptoMachineUnitTests.swift */, ED2DD11B286C4F3E00F06731 /* MXCryptoRequestsUnitTests.swift */, ED8F1D312885AC5700F897E7 /* Device+Stub.swift */, + ED1FE90A2912E13A0046F722 /* DecryptedEvent+Stub.swift */, ); path = CryptoMachine; sourceTree = ""; @@ -7071,6 +7111,7 @@ B19A30C22404268600FB6F35 /* MXVerifyingAnotherUserQRCodeData.m in Sources */, EC8A53BD25B1BC77004E0802 /* MXCallRejectEventContent.m in Sources */, EC6D007B28E1F15400152144 /* MXDevice.m in Sources */, + EDCB65E22912AB0C00F55D4D /* MXRoomEventDecryption.swift in Sources */, ECCA02BE27348FE300B6F34F /* MXThread.swift in Sources */, ED7019E52886C32900FC31B9 /* MXSASTransactionV2.swift in Sources */, EC1848C72686174E00865E16 /* MXiOSAudioOutputRouteType.swift in Sources */, @@ -7135,6 +7176,7 @@ 322985D226FC9E61001890BC /* MXSessionTracker.swift in Sources */, 32B477822638133C00EA5800 /* MXFilterUnitTests.m in Sources */, 322985CF26FBAE7B001890BC /* TestObserver.swift in Sources */, + 3A96CD492901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift in Sources */, 32DC15D71A8DFF0D006F9AD3 /* MXNotificationCenterTests.m in Sources */, ED51943928462D130006EEC6 /* MXRoomStateUnitTests.swift in Sources */, 329571991B024D2B00ABB3BA /* MXMockCallStack.m in Sources */, @@ -7164,6 +7206,8 @@ 32C9B71823E81A1C00C6F30A /* MXCrossSigningVerificationTests.m in Sources */, 323C5A081A70E53500FB0549 /* MXToolsUnitTests.m in Sources */, 3281E89E19E299C000976E1A /* MXErrorUnitTests.m in Sources */, + ED1FE9062912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift in Sources */, + ED1FE90B2912E13A0046F722 /* DecryptedEvent+Stub.swift in Sources */, EC51019D26C41981007D6D88 /* MXSyncResponseUnitTests.swift in Sources */, EDB4209527DF822B0036AF39 /* MXEventsByTypesEnumeratorOnArrayTests.swift in Sources */, EC40385D28A16EDA0067D5B8 /* MXAes256KeyBackupTests.m in Sources */, @@ -7255,6 +7299,7 @@ 18121F7F273E837300B68ADF /* PollModels.swift in Sources */, 32C03CB62123076F00D92712 /* DirectRoomTests.m in Sources */, ED505DC128E1FD170079A3D3 /* MXCryptoKeyBackupEngineUnitTests.swift in Sources */, + 3A2A3238291031A7005EF477 /* MXThreadsNotificationCountTests.swift in Sources */, 329FB17C1A0A963700A5E88E /* MXRoomMemberTests.m in Sources */, ECE3DFA8270CF69500FB4C96 /* MockRoomSummary.swift in Sources */, EC1165CC27107F3E0089FA56 /* MXStoreRoomListDataManagerUnitTests.swift in Sources */, @@ -7262,6 +7307,7 @@ ED751DAA28EDE4F4003748C3 /* MXKeyVerificationManagerV2UnitTests.swift in Sources */, 32B477832638133C00EA5800 /* MXJSONModelUnitTests.m in Sources */, 321EA11D24893A0400E35B02 /* MXCryptoRecoveryServiceTests.m in Sources */, + 3A4BB662291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift in Sources */, 3A858DDD275121DB006322C1 /* MXHomeserverCapabilitiesTests.swift in Sources */, 3264DB941CECA72900B99881 /* MXAccountDataTests.m in Sources */, ED8F1D1E288590AF00F897E7 /* MXDeviceInfoUnitTests.swift in Sources */, @@ -7703,6 +7749,7 @@ B14EF2812397E90400758AF0 /* MXNotificationCenter.m in Sources */, EC60ED60265CFC2C00B39A4E /* MXSyncResponse.m in Sources */, EC6D007C28E1F15400152144 /* MXDevice.m in Sources */, + EDCB65E32912AB0C00F55D4D /* MXRoomEventDecryption.swift in Sources */, 3A108AB825812995005EEBE9 /* MXKeyProvider.m in Sources */, B14EF2822397E90400758AF0 /* MXDeviceList.m in Sources */, ED7019E62886C32900FC31B9 /* MXSASTransactionV2.swift in Sources */, @@ -7767,6 +7814,7 @@ 322985D326FC9E61001890BC /* MXSessionTracker.swift in Sources */, B1E09A392397FD7D0057C069 /* MXMyUserTests.m in Sources */, 322985D026FBAE7B001890BC /* TestObserver.swift in Sources */, + 3A96CD4A2901512C00F9A5AB /* MXReceiptDataIntegrationTests.swift in Sources */, B1E09A202397FCE90057C069 /* MXCurve25519KeyBackupTests.m in Sources */, ED7019DD2886C24100FC31B9 /* MXCrossSigningInfoSourceUnitTests.swift in Sources */, ED51943A28462D130006EEC6 /* MXRoomStateUnitTests.swift in Sources */, @@ -7796,6 +7844,8 @@ B1E09A3D2397FD820057C069 /* MXStoreFileStoreTests.m in Sources */, 32CEEF3E23AD134A0039BA98 /* MXCrossSigningTests.m in Sources */, 32EEA8402603CA140041425B /* MXRestClientExtensionsTests.m in Sources */, + ED1FE9072912D2EB0046F722 /* MXRoomEventDecryptionUnitTests.swift in Sources */, + ED1FE90C2912E13A0046F722 /* DecryptedEvent+Stub.swift in Sources */, EC51019E26C41981007D6D88 /* MXSyncResponseUnitTests.swift in Sources */, EDB4209627DF822B0036AF39 /* MXEventsByTypesEnumeratorOnArrayTests.swift in Sources */, EC40385E28A16EDA0067D5B8 /* MXAes256KeyBackupTests.m in Sources */, @@ -7887,6 +7937,7 @@ 18121F7B273E6E4200B68ADF /* PollBuilder.swift in Sources */, 18121F80273E837400B68ADF /* PollModels.swift in Sources */, ED505DC028E1FD160079A3D3 /* MXCryptoKeyBackupEngineUnitTests.swift in Sources */, + 3A2A3239291031A7005EF477 /* MXThreadsNotificationCountTests.swift in Sources */, B1E09A2F2397FD750057C069 /* MXErrorUnitTests.m in Sources */, B1660F1D260A20B900C3AA12 /* MXSpaceServiceTest.swift in Sources */, ECE3DFA9270CF69500FB4C96 /* MockRoomSummary.swift in Sources */, @@ -7894,6 +7945,7 @@ ED751DAB28EDE4F4003748C3 /* MXKeyVerificationManagerV2UnitTests.swift in Sources */, B1E09A2A2397FD680057C069 /* MatrixSDKTestsData.m in Sources */, B1E09A302397FD750057C069 /* MXHTTPClientTests.m in Sources */, + 3A4BB663291D93EA006F7585 /* MXRoomEventFilterUnitTests.swift in Sources */, EC383BC02542F1E4002FBBE6 /* MXBackgroundSyncServiceTests.swift in Sources */, 3A858DDE275121DC006322C1 /* MXHomeserverCapabilitiesTests.swift in Sources */, 321EA11E24893A0400E35B02 /* MXCryptoRecoveryServiceTests.m in Sources */, diff --git a/MatrixSDK/Background/MXBackgroundStore.swift b/MatrixSDK/Background/MXBackgroundStore.swift index efac7b8ead..e1649b8a9d 100644 --- a/MatrixSDK/Background/MXBackgroundStore.swift +++ b/MatrixSDK/Background/MXBackgroundStore.swift @@ -208,7 +208,7 @@ class MXBackgroundStore: NSObject, MXStore { return nil } - func getEventReceipts(_ roomId: String, eventId: String, sorted sort: Bool, completion: @escaping ([MXReceiptData]) -> Void) { + func getEventReceipts(_ roomId: String, eventId: String, threadId: String, sorted sort: Bool, completion: @escaping ([MXReceiptData]) -> Void) { DispatchQueue.main.async { completion([]) } @@ -218,10 +218,14 @@ class MXBackgroundStore: NSObject, MXStore { return false } - func getReceiptInRoom(_ roomId: String, forUserId userId: String) -> MXReceiptData? { + func getReceiptInRoom(_ roomId: String, threadId: String, forUserId userId: String) -> MXReceiptData? { return nil } + func getReceiptsInRoom(_ roomId: String, forUserId userId: String) -> [String: MXReceiptData] { + return [:] + } + func loadReceipts(forRoom roomId: String, completion: (() -> Void)? = nil) { DispatchQueue.main.async { completion?() @@ -231,6 +235,10 @@ class MXBackgroundStore: NSObject, MXStore { func localUnreadEventCount(_ roomId: String, threadId: String?, withTypeIn types: [Any]?) -> UInt { return 0 } + + func localUnreadEventCountPerThread(_ roomId: String, withTypeIn types: [Any]?) -> [String : NSNumber]! { + return [:] + } func newIncomingEvents(inRoom roomId: String, threadId: String?, withTypeIn types: [String]?) -> [MXEvent] { return [] diff --git a/MatrixSDK/Contrib/Swift/MXRestClient.swift b/MatrixSDK/Contrib/Swift/MXRestClient.swift index 85610c2676..f6fc351ac2 100644 --- a/MatrixSDK/Contrib/Swift/MXRestClient.swift +++ b/MatrixSDK/Contrib/Swift/MXRestClient.swift @@ -1730,13 +1730,14 @@ public extension MXRestClient { - parameters: - roomId: the id of the room. - eventId: the id of the event. + - threadId: the id of the thread (`nil` for unthreaded RR) - completion: A block object called when the operation completes. - response: Indicates whether the operation was successful. - returns: a `MXHTTPOperation` instance. */ - @nonobjc @discardableResult func sendReadReceipt(toRoom roomId: String, forEvent eventId: String, completion: @escaping (_ response: MXResponse) -> Void) -> MXHTTPOperation { - return __sendReadReceipt(roomId, eventId: eventId, success: currySuccess(completion), failure: curryFailure(completion)) + @nonobjc @discardableResult func sendReadReceipt(toRoom roomId: String, forEvent eventId: String, threadId: String?, completion: @escaping (_ response: MXResponse) -> Void) -> MXHTTPOperation { + return __sendReadReceipt(roomId, eventId: eventId, threadId: threadId, success: currySuccess(completion), failure: curryFailure(completion)) } diff --git a/MatrixSDK/Crypto/Algorithms/RoomEvent/MXRoomEventDecryption.swift b/MatrixSDK/Crypto/Algorithms/RoomEvent/MXRoomEventDecryption.swift new file mode 100644 index 0000000000..b7b4d8e500 --- /dev/null +++ b/MatrixSDK/Crypto/Algorithms/RoomEvent/MXRoomEventDecryption.swift @@ -0,0 +1,271 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if DEBUG +import MatrixSDKCrypto + +/// Object responsible for decrypting room events and dealing with undecryptable events +protocol MXRoomEventDecrypting: Actor { + + /// Decrypt a list of events + func decrypt(events: [MXEvent]) -> [MXEventDecryptionResult] + + /// Process an event that may contain room key and retry decryption if it does + /// + /// Note: room key could be contained in `m.room_key` or `m.forwarded_room_key` + func handlePossibleRoomKeyEvent(_ event: MXEvent) + + /// Retry decrypting events with specific session ids + /// + /// Note: this may be useful if we have just imported keys from backup / file + func retryUndecryptedEvents(sessionIds: [String]) + + /// Reset the store of undecrypted events + func resetUndecryptedEvents() +} + +/// Implementation of `MXRoomEventDecrypting` as an Actor +actor MXRoomEventDecryption: MXRoomEventDecrypting { + typealias SessionId = String + typealias EventId = String + + // `Megolm` error does not currently expose the type of "missing keys" error, so have to match against + // hardcoded non-localized error message. Will be changed in future PR + private static let MissingKeysMessage = "decryption failed because the room key is missing" + + private let handler: MXCryptoRoomEventDecrypting + private var undecryptedEvents: [SessionId: [EventId: MXEvent]] + private let log = MXNamedLog(name: "MXRoomEventDecryption") + + init(handler: MXCryptoRoomEventDecrypting) { + self.handler = handler + self.undecryptedEvents = [:] + } + + func decrypt(events: [MXEvent]) -> [MXEventDecryptionResult] { + let results = events.map(decrypt(event:)) + + let undecrypted = results.filter { + $0.clearEvent == nil || $0.error != nil + } + + if !undecrypted.isEmpty { + log.error("Unable to decrypt some event(s)", context: [ + "total": events.count, + "undecrypted": undecrypted.count + ]) + } else { + log.debug("Decrypted all \(events.count) event(s)") + } + + return results + } + + func handlePossibleRoomKeyEvent(_ event: MXEvent) { + guard let sessionId = roomKeySessionId(for: event) else { + return + } + + log.debug("Recieved a new room key as `\(event.type ?? "")` for session \(sessionId)") + let events = undecryptedEvents[sessionId]?.map(\.value) ?? [] + retryDecryption(events: events) + } + + func retryUndecryptedEvents(sessionIds: [String]) { + let events = sessionIds + .flatMap { + undecryptedEvents[$0]?.map { + $0.value + } ?? [] + } + retryDecryption(events: events) + } + + func resetUndecryptedEvents() { + undecryptedEvents = [:] + } + + // MARK: - Private + + private func decrypt(event: MXEvent) -> MXEventDecryptionResult { + guard + event.isEncrypted && event.clear == nil, + event.content?["algorithm"] as? String == kMXCryptoMegolmAlgorithm, + let sessionId = sessionId(for: event) + else { + log.debug("Ignoring unencrypted or non-room event") + + let result = MXEventDecryptionResult() + result.clearEvent = event.clear?.jsonDictionary() + return result + } + + do { + let decryptedEvent = try handler.decryptRoomEvent(event) + let result = try MXEventDecryptionResult(event: decryptedEvent) + log.debug("Successfully decrypted event `\(result.clearEvent["type"] ?? "unknown")`") + return result + + } catch let error as DecryptionError { + return handleDecryptionError(for: event, sessionId: sessionId, error: error) + } catch { + return handleGenericError(for: event, sessionId: sessionId, error: error) + } + } + + private func addUndecryptedEvent(_ event: MXEvent) { + guard let sessionId = sessionId(for: event) else { + return + } + + var events = undecryptedEvents[sessionId] ?? [:] + events[event.eventId] = event + undecryptedEvents[sessionId] = events + } + + private func removeUndecryptedEvent(_ event: MXEvent) { + guard let sessionId = sessionId(for: event) else { + return + } + undecryptedEvents[sessionId]?[event.eventId] = nil + } + + private func retryDecryption(events: [MXEvent]) { + guard !events.isEmpty else { + return + } + + log.debug("Re-decrypting \(events.count) event(s)") + + var results = [(MXEvent, MXEventDecryptionResult)]() + for event in events { + guard event.clear == nil else { + removeUndecryptedEvent(event) + continue + } + + let result = decrypt(event: event) + guard result.clearEvent != nil else { + log.error("Event still not decryptable", context: [ + "event_id": event.eventId ?? "unknown", + "session_id": sessionId(for: event), + "error": result.error.localizedDescription + ]) + continue + } + + removeUndecryptedEvent(event) + results.append((event, result)) + } + + Task { [results] in + await MainActor.run { + for (event, result) in results { + event.setClearData(result) + } + } + } + } + + private func roomKeySessionId(for event: MXEvent) -> String? { + if event.eventType == .roomKey, let content = MXRoomKeyEventContent(fromJSON: event.content) { + return content.sessionId + } else if event.eventType == .roomForwardedKey, let content = MXForwardedRoomKeyEventContent(fromJSON: event.content) { + return content.sessionId + } else { + return nil + } + } + + private func sessionId(for event: MXEvent) -> String? { + let sessionId = event.content["session_id"] ?? event.wireContent["session_id"] + guard let sessionId = sessionId as? String else { + log.failure("Event is missing session id") + return nil + } + return sessionId + } + + // MARK: - Error handling + + private func handleDecryptionError(for event: MXEvent, sessionId: String, error: DecryptionError) -> MXEventDecryptionResult { + switch error { + case .Identifier(let message): + log.error("Failed to decrypt event due to identifier", context: [ + "session_id": sessionId, + "message": message, + "error": error + ]) + return trackedDecryptionResult(for: event, error: error) + + case .Serialization(let message): + log.error("Failed to decrypt event due to serialization", context: [ + "session_id": sessionId, + "message": message, + "error": error + ]) + return trackedDecryptionResult(for: event, error: error) + + case .Megolm(let message): + if message == Self.MissingKeysMessage { + if undecryptedEvents[sessionId] == nil { + log.error("Failed to decrypt event(s) due to missing room keys", context: [ + "session_id": sessionId, + "message": message, + "error": error, + "details": "further errors for the same key will be supressed", + ]) + } + + let keysError = NSError( + domain: MXDecryptingErrorDomain, + code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue), + userInfo: [ + NSLocalizedDescriptionKey: MXDecryptingErrorUnknownInboundSessionIdReason + ] + ) + return trackedDecryptionResult(for: event, error: keysError) + } else { + log.error("Failed to decrypt event due to megolm error", context: [ + "session_id": sessionId, + "message": message, + "error": error + ]) + return trackedDecryptionResult(for: event, error: error) + } + } + } + + private func handleGenericError(for event: MXEvent, sessionId: String, error: Error) -> MXEventDecryptionResult { + log.error("Failed to decrypt event", context: [ + "session_id": sessionId, + "error": error + ]) + return trackedDecryptionResult(for: event, error: error) + } + + private func trackedDecryptionResult(for event: MXEvent, error: Error) -> MXEventDecryptionResult { + addUndecryptedEvent(event) + + let result = MXEventDecryptionResult() + result.error = error + return result + } +} + +#endif diff --git a/MatrixSDK/Crypto/CrossSigning/MXCrossSigningV2.swift b/MatrixSDK/Crypto/CrossSigning/MXCrossSigningV2.swift index c2336392cb..78d6778b99 100644 --- a/MatrixSDK/Crypto/CrossSigning/MXCrossSigningV2.swift +++ b/MatrixSDK/Crypto/CrossSigning/MXCrossSigningV2.swift @@ -121,7 +121,7 @@ class MXCrossSigningV2: NSObject, MXCrossSigning { Task { do { - try await crossSigning.updateTrackedUsers(users: [crossSigning.userId]) + try await crossSigning.downloadKeys(users: [crossSigning.userId]) myUserCrossSigningKeys = infoSource.crossSigningInfo(userId: crossSigning.userId) log.debug("Cross signing state refreshed") @@ -146,7 +146,7 @@ class MXCrossSigningV2: NSObject, MXCrossSigning { Task { do { - try await crossSigning.manuallyVerifyDevice(userId: crossSigning.userId, deviceId: deviceId) + try await crossSigning.verifyDevice(userId: crossSigning.userId, deviceId: deviceId) log.debug("Successfully cross-signed a device") await MainActor.run { @@ -170,7 +170,7 @@ class MXCrossSigningV2: NSObject, MXCrossSigning { Task { do { - try await crossSigning.manuallyVerifyUser(userId: userId) + try await crossSigning.verifyUser(userId: userId) log.debug("Successfully cross-signed a user") await MainActor.run { diff --git a/MatrixSDK/Crypto/CryptoMachine/MXCryptoMachine.swift b/MatrixSDK/Crypto/CryptoMachine/MXCryptoMachine.swift index 5202bb6d8f..4af745ec15 100644 --- a/MatrixSDK/Crypto/CryptoMachine/MXCryptoMachine.swift +++ b/MatrixSDK/Crypto/CryptoMachine/MXCryptoMachine.swift @@ -64,9 +64,6 @@ class MXCryptoMachine { private let syncQueue = MXTaskQueue() private var roomQueues = RoomQueues() - private var hasUploadedInitialKeys = false - private var initialKeysUploadCallback: (() -> Void)? - private let log = MXNamedLog(name: "MXCryptoMachine") init(userId: String, deviceId: String, restClient: MXRestClient, getRoomAction: @escaping GetRoomAction) throws { @@ -83,21 +80,46 @@ class MXCryptoMachine { setLogger(logger: self) } - func onInitialKeysUpload(callback: @escaping () -> Void) { - log.debug("->") + func start() async throws { + let details = """ + Starting the crypto machine for \(userId) + - device id : \(deviceId) + - ed25519 : \(deviceEd25519Key ?? "") + - curve25519 : \(deviceCurve25519Key ?? "") + """ + log.debug(details) - if hasUploadedInitialKeys { - callback() - } else { - initialKeysUploadCallback = callback + var keysUploadRequest: Request? + for request in try machine.outgoingRequests() { + guard case .keysUpload = request else { + continue + } + keysUploadRequest = request + break + } + + guard let request = keysUploadRequest else { + log.debug("There are no keys to upload") + return } + + try await handleRequest(request) + + log.debug("Keys successfully uploaded") } - private static func storeURL(for userId: String) throws -> URL { + func deleteAllData() throws { + let url = try Self.storeURL(for: machine.userId()) + try FileManager.default.removeItem(at: url) + } +} + +extension MXCryptoMachine { + static func storeURL(for userId: String) throws -> URL { let container: URL if let sharedContainerURL = FileManager.default.applicationGroupContainerURL() { container = sharedContainerURL - } else if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + } else if let url = platformDirectoryURL() { container = url } else { throw Error.invalidStorage @@ -108,9 +130,18 @@ class MXCryptoMachine { .appendingPathComponent(userId) } - func deleteAllData() throws { - let url = try Self.storeURL(for: machine.userId()) - try FileManager.default.removeItem(at: url) + private static func platformDirectoryURL() -> URL? { + #if os(OSX) + guard + let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first, + let identifier = Bundle.main.bundleIdentifier + else { + return nil + } + return applicationSupport.appendingPathComponent(identifier) + #else + return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first + #endif } } @@ -147,8 +178,6 @@ extension MXCryptoMachine: MXCryptoSyncing { deviceOneTimeKeysCounts: [String: NSNumber], unusedFallbackKeys: [String]? ) throws -> MXToDeviceSyncResponse { - log.debug("Recieving sync changes") - let events = toDevice?.jsonString() ?? "[]" let deviceChanges = DeviceLists( changed: deviceLists?.changed ?? [], @@ -197,7 +226,6 @@ extension MXCryptoMachine: MXCryptoSyncing { request: .init(body: body, deviceId: machine.deviceId()) ) try markRequestAsSent(requestId: requestId, requestType: .keysUpload, response: response.jsonString()) - broadcastInitialKeysUploadIfNecessary() case .keysQuery(let requestId, let users): let response = try await requests.queryKeys(users: users) @@ -266,15 +294,6 @@ extension MXCryptoMachine: MXCryptoSyncing { ) ) } - - private func broadcastInitialKeysUploadIfNecessary() { - guard !hasUploadedInitialKeys else { - return - } - hasUploadedInitialKeys = true - initialKeysUploadCallback?() - initialKeysUploadCallback = nil - } } extension MXCryptoMachine: MXCryptoDevicesSource { @@ -325,31 +344,18 @@ extension MXCryptoMachine: MXCryptoUserIdentitySource { } } - func updateTrackedUsers(users: [String]) async throws { - machine.updateTrackedUsers(users: users) - try await withThrowingTaskGroup(of: Void.self) { [weak self] group in - guard let self = self else { return } - - for request in try machine.outgoingRequests() { - guard case .keysQuery = request else { - continue - } - - group.addTask { - try await self.handleRequest(request) - } - } - - try await group.waitForAll() - } + func downloadKeys(users: [String]) async throws { + try await handleRequest( + .keysQuery(requestId: UUID().uuidString, users: users) + ) } - func manuallyVerifyUser(userId: String) async throws { + func verifyUser(userId: String) async throws { let request = try machine.verifyIdentity(userId: userId) try await requests.uploadSignatures(request: request) } - func manuallyVerifyDevice(userId: String, deviceId: String) async throws { + func verifyDevice(userId: String, deviceId: String) async throws { let request = try machine.verifyDevice(userId: userId, deviceId: deviceId) try await requests.uploadSignatures(request: request) } @@ -389,56 +395,6 @@ extension MXCryptoMachine: MXCryptoRoomEventEncrypting { return MXTools.deserialiseJSONString(event) as? [String: Any] ?? [:] } - func decryptRoomEvent(_ event: MXEvent) -> MXEventDecryptionResult { - guard let roomId = event.roomId, let eventString = event.jsonString() else { - log.failure("Invalid event") - - let result = MXEventDecryptionResult() - result.error = Error.invalidEvent - return result - } - - do { - let decryptedEvent = try machine.decryptRoomEvent(event: eventString, roomId: roomId) - let result = try MXEventDecryptionResult(event: decryptedEvent) - log.debug("Successfully decrypted event `\(result.clearEvent["type"] ?? "unknown")`") - return result - - // `Megolm` error does not currently expose the type of "missing keys" error, so have to match against - // hardcoded non-localized error message. Will be changed in future PR - } catch DecryptionError.Megolm(message: "decryption failed because the room key is missing") { - let result = MXEventDecryptionResult() - result.error = NSError( - domain: MXDecryptingErrorDomain, - code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue), - userInfo: [ - NSLocalizedDescriptionKey: MXDecryptingErrorUnknownInboundSessionIdReason - ] - ) - log.error("Failed decrypting due to missing key") - return result - } catch { - let result = MXEventDecryptionResult() - result.error = error - log.error("Failed decrypting", context: error) - return result - } - } - - func requestRoomKey(event: MXEvent) async throws { - guard let roomId = event.roomId, let eventString = event.jsonString() else { - throw Error.invalidEvent - } - - log.debug("->") - let result = try machine.requestRoomKey(event: eventString, roomId: roomId) - if let cancellation = result.cancellation { - try await handleRequest(cancellation) - } - try await handleRequest(result.keyRequest) - - } - func discardRoomKey(roomId: String) { do { try machine.discardRoomKey(roomId: roomId) @@ -449,6 +405,25 @@ extension MXCryptoMachine: MXCryptoRoomEventEncrypting { // MARK: - Private + private func updateTrackedUsers(users: [String]) async throws { + machine.updateTrackedUsers(users: users) + try await withThrowingTaskGroup(of: Void.self) { [weak self] group in + guard let self = self else { return } + + for request in try machine.outgoingRequests() { + guard case .keysQuery = request else { + continue + } + + group.addTask { + try await self.handleRequest(request) + } + } + + try await group.waitForAll() + } + } + private func getMissingSessions(users: [String]) async throws { guard let request = try machine.getMissingSessions(users: users), @@ -479,6 +454,29 @@ extension MXCryptoMachine: MXCryptoRoomEventEncrypting { } } +extension MXCryptoMachine: MXCryptoRoomEventDecrypting { + func decryptRoomEvent(_ event: MXEvent) throws -> DecryptedEvent { + guard let roomId = event.roomId, let eventString = event.jsonString() else { + log.failure("Invalid event") + throw Error.invalidEvent + } + return try machine.decryptRoomEvent(event: eventString, roomId: roomId) + } + + func requestRoomKey(event: MXEvent) async throws { + guard let roomId = event.roomId, let eventString = event.jsonString() else { + throw Error.invalidEvent + } + + log.debug("->") + let result = try machine.requestRoomKey(event: eventString, roomId: roomId) + if let cancellation = result.cancellation { + try await handleRequest(cancellation) + } + try await handleRequest(result.keyRequest) + } +} + extension MXCryptoMachine: MXCryptoCrossSigning { func crossSigningStatus() -> CrossSigningStatus { return machine.crossSigningStatus() @@ -783,7 +781,15 @@ extension MXCryptoMachine: MXCryptoBackup { extension MXCryptoMachine: Logger { func log(logLine: String) { + #if DEBUG + MXLog.debug("[MXCryptoMachine] \(logLine)") + #else + // Filtering out verbose logs for non-debug builds + guard !logLine.starts(with: "DEBUG") else { + return + } MXLog.debug("[MXCryptoMachine] \(logLine)") + #endif } func log(error: String) { diff --git a/MatrixSDK/Crypto/CryptoMachine/MXCryptoProtocols.swift b/MatrixSDK/Crypto/CryptoMachine/MXCryptoProtocols.swift index c2a6e670f1..912bfcd3f6 100644 --- a/MatrixSDK/Crypto/CryptoMachine/MXCryptoProtocols.swift +++ b/MatrixSDK/Crypto/CryptoMachine/MXCryptoProtocols.swift @@ -53,21 +53,25 @@ protocol MXCryptoUserIdentitySource: MXCryptoIdentity { func userIdentity(userId: String) -> UserIdentity? func isUserVerified(userId: String) -> Bool func isUserTracked(userId: String) -> Bool - func updateTrackedUsers(users: [String]) async throws - func manuallyVerifyUser(userId: String) async throws - func manuallyVerifyDevice(userId: String, deviceId: String) async throws + func downloadKeys(users: [String]) async throws + func verifyUser(userId: String) async throws + func verifyDevice(userId: String, deviceId: String) async throws func setLocalTrust(userId: String, deviceId: String, trust: LocalTrust) throws } -/// Event encryption and decryption +/// Room event encryption protocol MXCryptoRoomEventEncrypting: MXCryptoIdentity { func shareRoomKeysIfNecessary(roomId: String, users: [String], settings: EncryptionSettings) async throws func encryptRoomEvent(content: [AnyHashable: Any], roomId: String, eventType: String) throws -> [String: Any] - func decryptRoomEvent(_ event: MXEvent) -> MXEventDecryptionResult - func requestRoomKey(event: MXEvent) async throws func discardRoomKey(roomId: String) } +/// Room event decryption +protocol MXCryptoRoomEventDecrypting: MXCryptoIdentity { + func decryptRoomEvent(_ event: MXEvent) throws -> DecryptedEvent + func requestRoomKey(event: MXEvent) async throws +} + /// Cross-signing functionality protocol MXCryptoCrossSigning: MXCryptoUserIdentitySource { func crossSigningStatus() -> CrossSigningStatus diff --git a/MatrixSDK/Crypto/Data/MXMegolmSessionData.m b/MatrixSDK/Crypto/Data/MXMegolmSessionData.m index f2ed48e9cd..56cbdab214 100644 --- a/MatrixSDK/Crypto/Data/MXMegolmSessionData.m +++ b/MatrixSDK/Crypto/Data/MXMegolmSessionData.m @@ -52,9 +52,21 @@ + (id)modelFromJSON:(NSDictionary *)JSONDictionary - (NSDictionary *)JSONDictionary { + if (!_senderKey || !_roomId || !_sessionId || !_sessionKey || !_algorithm) + { + NSDictionary *details = @{ + @"sender_key": _senderKey ?: @"unknown", + @"room_id": _roomId ?: @"unknown", + @"session_id": _sessionId ?: @"unknown", + @"algorithm": _algorithm ?: @"unknown", + }; + MXLogErrorDetails(@"[MXMegolmSessionData] JSONDictionary: some properties are missing", details); + return nil; + } + return @{ @"sender_key": _senderKey, - @"sender_claimed_keys": _senderClaimedKeys, + @"sender_claimed_keys": _senderClaimedKeys ?: @[], @"room_id": _roomId, @"session_id": _sessionId, @"session_key":_sessionKey, diff --git a/MatrixSDK/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngine.swift b/MatrixSDK/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngine.swift index 089e5c4f72..4f70231530 100644 --- a/MatrixSDK/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngine.swift +++ b/MatrixSDK/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngine.swift @@ -41,28 +41,34 @@ class MXCryptoKeyBackupEngine: NSObject, MXKeyBackupEngine { } private let backup: MXCryptoBackup + private let roomEventDecryptor: MXRoomEventDecrypting private let log = MXNamedLog(name: "MXCryptoKeyBackupEngine") - init(backup: MXCryptoBackup) { + init(backup: MXCryptoBackup, roomEventDecryptor: MXRoomEventDecrypting) { self.backup = backup + self.roomEventDecryptor = roomEventDecryptor } // MARK: - Enable / Disable engine - func enableBackup(with keyBackupversion: MXKeyBackupVersion) throws { + func enableBackup(with keyBackupVersion: MXKeyBackupVersion) throws { log.debug("->") - guard let version = keyBackupversion.version else { + guard let version = keyBackupVersion.version else { log.error("Unknown backup version") throw Error.unknownBackupVersion } - let key = try MegolmV1BackupKey(keyBackupVersion: keyBackupversion) + let key = try MegolmV1BackupKey(keyBackupVersion: keyBackupVersion) try backup.enableBackup(key: key, version: version) log.debug("Backup enabled") } func disableBackup() { + guard enabled else { + return + } + log.debug("->") backup.disableBackup() } @@ -248,58 +254,73 @@ class MXCryptoKeyBackupEngine: NSObject, MXKeyBackupEngine { success: @escaping (UInt, UInt) -> Void, failure: @escaping (Swift.Error) -> Void ) { - let count = keysBackupData.rooms - .map { roomId, room in - room.sessions.count + Task(priority: .medium) { + + let count = keysBackupData.rooms + .map { roomId, room in + room.sessions.count + } + .reduce(0, +) + + log.debug("Importing \(count) encrypted sessions") + + let recoveryKey: BackupRecoveryKey + do { + let key = MXRecoveryKey.encode(privateKey) + recoveryKey = try BackupRecoveryKey.fromBase58(key: key) + } catch { + log.error("Failed creating recovery key") + failure(Error.invalidPrivateKey) + return } - .reduce(0, +) - - log.debug("Importing \(count) encrypted sessions") - - let sessions = keysBackupData.rooms - .flatMap { roomId, room in - room.sessions - .compactMap { sessionId, keyBackup in - decrypt( - keyBackupData:keyBackup, - keyBackupVersion: keyBackupVersion, - privateKey: privateKey, - forSession: sessionId, - inRoom: roomId - ) - } + + let date1 = Date() + + let sessions = keysBackupData.rooms + .flatMap { roomId, room in + room.sessions + .compactMap { sessionId, keyBackup in + decrypt( + keyBackupData:keyBackup, + keyBackupVersion: keyBackupVersion, + recoveryKey: recoveryKey, + forSession: sessionId, + inRoom: roomId + ) + } + } + + let duration1 = Date().timeIntervalSince(date1) * 1000 + log.debug("Decrypted \(sessions.count) sessions in \(duration1) ms") + + let date2 = Date() + + do { + let result = try backup.importDecryptedKeys(roomKeys: sessions, progressListener: self) + await roomEventDecryptor.retryUndecryptedEvents(sessionIds: sessions.map(\.sessionId)) + + let duration2 = Date().timeIntervalSince(date2) * 1000 + log.debug("Successfully imported \(result.imported) out of \(result.total) sessions in \(duration2) ms") + + await MainActor.run { + success(UInt(result.total), UInt(result.imported)) + } + } catch { + log.error("Failed importing sessions", context: error) + await MainActor.run { + failure(error) + } } - - log.debug("Decrypted \(sessions.count) sessions") - - do { - let result = try backup.importDecryptedKeys(roomKeys: sessions, progressListener: self) - log.debug("Successfully imported \(result.imported) out of \(result.total) sessions") - success(UInt(result.total), UInt(result.imported)) - } catch { - log.error("Failed importing sessions", context: error) - failure(error) } } private func decrypt( keyBackupData: MXKeyBackupData, keyBackupVersion: MXKeyBackupVersion, - privateKey: Data, + recoveryKey: BackupRecoveryKey, forSession sessionId: String, inRoom roomId: String ) -> MXMegolmSessionData? { - log.debug("->") - - let recoveryKey: BackupRecoveryKey - do { - let key = MXRecoveryKey.encode(privateKey) - recoveryKey = try BackupRecoveryKey.fromBase58(key: key) - } catch { - log.error("Failed creating recovery key") - return nil - } - guard let ciphertext = keyBackupData.sessionData["ciphertext"] as? String, let mac = keyBackupData.sessionData["mac"] as? String, @@ -332,8 +353,6 @@ class MXCryptoKeyBackupEngine: NSObject, MXKeyBackupEngine { data.sessionId = sessionId data.roomId = roomId data.isUntrusted = true // Asymmetric backups are untrusted by default - - log.debug("Decrypted key backup data") return data } @@ -343,8 +362,17 @@ class MXCryptoKeyBackupEngine: NSObject, MXKeyBackupEngine { return try backup.exportRoomKeys(passphrase: passphrase) } - func importRoomKeys(_ data: Data, passphrase: String) throws -> KeysImportResult { - return try backup.importRoomKeys(data, passphrase: passphrase, progressListener: self) + func importRoomKeys(_ data: Data, passphrase: String) async throws -> KeysImportResult { + let result = try backup.importRoomKeys(data, passphrase: passphrase, progressListener: self) + let sessionIds = result.keys + .flatMap { (roomId, senders) in + senders.flatMap { (sender, sessionIds) in + sessionIds + } + } + + await roomEventDecryptor.retryUndecryptedEvents(sessionIds: sessionIds) + return result } // MARK: - Private diff --git a/MatrixSDK/Crypto/KeyBackup/MXKeyBackup.m b/MatrixSDK/Crypto/KeyBackup/MXKeyBackup.m index d171fc2d1e..d4f30c0061 100644 --- a/MatrixSDK/Crypto/KeyBackup/MXKeyBackup.m +++ b/MatrixSDK/Crypto/KeyBackup/MXKeyBackup.m @@ -720,7 +720,7 @@ - (MXHTTPOperation*)restoreKeyBackup:(MXKeyBackupVersion*)keyBackupVersion NSData *privateKey = [self.engine validPrivateKeyForRecoveryKey:recoveryKey forKeyBackupVersion:keyBackupVersion error:&error]; if (error || !privateKey) { - MXLogDebug(@"[MXKeyBackup] restoreKeyBackup: Invalid recovery key. Error: %@", error); + MXLogErrorDetails(@"[MXKeyBackup] restoreKeyBackup: Invalid recovery key", error); if (failure) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -831,7 +831,7 @@ - (MXHTTPOperation*)restoreUsingPrivateKeyKeyBackup:(MXKeyBackupVersion*)keyBack if (![self.engine hasValidPrivateKeyForKeyBackupVersion:keyBackupVersion]) { - MXLogDebug(@"[MXKeyBackup] restoreUsingPrivateKeyKeyBackup. Error: Private key does not match: for: %@", keyBackupVersion); + MXLogError(@"[MXKeyBackup] restoreUsingPrivateKeyKeyBackup. Error: Private key does not match"); if (failure) { dispatch_async(dispatch_get_main_queue(), ^{ @@ -843,6 +843,7 @@ - (MXHTTPOperation*)restoreUsingPrivateKeyKeyBackup:(MXKeyBackupVersion*)keyBack failure(error); }); } + return; } // Launch the restore @@ -985,7 +986,7 @@ - (MXHTTPOperation *)trustKeyBackupVersion:(MXKeyBackupVersion *)keyBackupVersio } else { - MXLogDebug(@"[MXKeyBackup] trustKeyBackupVersion:withRecoveryKey: Invalid recovery key. Error: %@", error); + MXLogErrorDetails(@"[MXKeyBackup] trustKeyBackupVersion:withRecoveryKey: Invalid recovery key", error); if (failure) { @@ -1100,7 +1101,7 @@ - (BOOL)canBeRefreshed - (void)setState:(MXKeyBackupState)state { - MXLogDebug(@"[MXKeyBackup] setState: %@ -> %@", @(_state), @(state)); + MXLogDebug(@"[MXKeyBackup] setState: %@ -> %@", [self descriptionForState:_state], [self descriptionForState:state]); _state = state; @@ -1158,4 +1159,28 @@ - (MXHTTPOperation*)keyBackupForSession:(nullable NSString*)sessionId return operation; } +- (NSString *)descriptionForState:(MXKeyBackupState)state +{ + switch (state) { + case MXKeyBackupStateUnknown: + return @"Unknown"; + case MXKeyBackupStateCheckingBackUpOnHomeserver: + return @"CheckingBackUpOnHomeserver"; + case MXKeyBackupStateWrongBackUpVersion: + return @"WrongBackUpVersion"; + case MXKeyBackupStateDisabled: + return @"Disabled"; + case MXKeyBackupStateNotTrusted: + return @"NotTrusted"; + case MXKeyBackupStateEnabling: + return @"Enabling"; + case MXKeyBackupStateReadyToBackUp: + return @"ReadyToBackUp"; + case MXKeyBackupStateWillBackUp: + return @"WillBackUp"; + case MXKeyBackupStateBackingUp: + return @"BackingUp"; + }; +} + @end diff --git a/MatrixSDK/Crypto/MXCryptoV2.swift b/MatrixSDK/Crypto/MXCryptoV2.swift index 772d08b88b..34ba84aa2c 100644 --- a/MatrixSDK/Crypto/MXCryptoV2.swift +++ b/MatrixSDK/Crypto/MXCryptoV2.swift @@ -69,11 +69,11 @@ private class MXCryptoV2: NSObject, MXCrypto { private let cryptoQueue: DispatchQueue private let legacyStore: MXCryptoStore private let machine: MXCryptoMachine + private let roomEventDecryptor: MXRoomEventDecrypting private let deviceInfoSource: MXDeviceInfoSource private let trustLevelSource: MXTrustLevelSource private let backupEngine: MXCryptoKeyBackupEngine? private let keyVerification: MXKeyVerificationManagerV2 - private var undecryptableEvents = [String: MXEvent]() private var roomEventObserver: Any? private let log = MXNamedLog(name: "MXCryptoV2") @@ -129,6 +129,8 @@ private class MXCryptoV2: NSObject, MXCrypto { } ) + roomEventDecryptor = MXRoomEventDecryption(handler: machine) + deviceInfoSource = MXDeviceInfoSource(source: machine) trustLevelSource = MXTrustLevelSource( userIdentitySource: machine, @@ -141,7 +143,7 @@ private class MXCryptoV2: NSObject, MXCrypto { ) if MXSDKOptions.sharedInstance().enableKeyBackupWhenStartingMXCrypto { - let engine = MXCryptoKeyBackupEngine(backup: machine) + let engine = MXCryptoKeyBackupEngine(backup: machine, roomEventDecryptor: roomEventDecryptor) backupEngine = engine backup = MXKeyBackup( engine: engine, @@ -182,14 +184,6 @@ private class MXCryptoV2: NSObject, MXCrypto { ) log.debug("Initialized Crypto module") - - super.init() - - listenToRoomEvents(in: session) - } - - deinit { - session?.removeListener(roomEventObserver) } // MARK: - Crypto start / close @@ -199,22 +193,32 @@ private class MXCryptoV2: NSObject, MXCrypto { failure: ((Swift.Error) -> Void)? ) { log.debug("->") - // CryptoV2 will start immediately and finish configuring afterwards, - // because it is dependent on the sync loop being active - onComplete?() - - machine.onInitialKeysUpload { [weak self] in - guard let self = self else { return } - - self.crossSigning.refreshState(success: nil) - self.backup?.checkAndStart() - self.log.debug("Crypto has fully started") + Task { + do { + try await machine.start() + crossSigning.refreshState(success: nil) + backup?.checkAndStart() + + log.debug("Crypto module started") + await MainActor.run { + listenToRoomEvents() + onComplete?() + } + } catch { + log.error("Failed starting crypto module", context: error) + await MainActor.run { + failure?(error) + } + } } } public func close(_ deleteStore: Bool) { log.debug("->") - undecryptableEvents = [:] + session?.removeListener(roomEventObserver) + Task { + await roomEventDecryptor.resetUndecryptedEvents() + } if deleteStore { if let credentials = session?.credentials { @@ -250,9 +254,12 @@ private class MXCryptoV2: NSObject, MXCrypto { success: (([AnyHashable: Any], String) -> Void)?, failure: ((Swift.Error) -> Void)? ) -> MXHTTPOperation? { - let startDate = Date() log.debug("Encrypting content of type `\(eventType)`") + let startDate = Date() + let stopTracking = MXSDKOptions.sharedInstance().analyticsDelegate? + .startDurationTracking(forName: "MXCryptoV2", operation: "encryptEventContent") + guard let roomId = room.roomId else { log.failure("Missing room id") failure?(Error.missingRoom) @@ -280,6 +287,7 @@ private class MXCryptoV2: NSObject, MXCrypto { eventType: eventType ) + stopTracking?() let duration = Date().timeIntervalSince(startDate) * 1000 log.debug("Encrypted in \(duration) ms") @@ -301,10 +309,20 @@ private class MXCryptoV2: NSObject, MXCrypto { inTimeline timeline: String?, onComplete: (([MXEventDecryptionResult]) -> Void)? ) { - log.debug("->") - onComplete?( - events.map(decrypt(event:)) - ) + guard session?.isEventStreamInitialised == true else { + log.debug("Ignoring \(events.count) encrypted event(s) during initial sync in timeline \(timeline ?? "") (we most likely do not have the keys yet)") + let results = events.map { _ in MXEventDecryptionResult() } + onComplete?(results) + return + } + + Task { + log.debug("Decrypting \(events.count) event(s) in timeline \(timeline ?? "")") + let results = await roomEventDecryptor.decrypt(events: events) + await MainActor.run { + onComplete?(results) + } + } } func ensureEncryption( @@ -349,7 +367,7 @@ private class MXCryptoV2: NSObject, MXCrypto { let userId = event.sender, let deviceId = event.wireContent["device_id"] as? String else { - log.failure("Missing user id or device id") + log.error("Missing user id or device id") return nil; } return device(withDeviceId: deviceId, ofUser: userId) @@ -367,55 +385,48 @@ private class MXCryptoV2: NSObject, MXCrypto { // MARK: - Sync func handle(_ syncResponse: MXSyncResponse, onComplete: @escaping () -> Void) { - let uuid = UUID().uuidString let toDeviceCount = syncResponse.toDevice?.events.count ?? 0 + let devicesChanged = syncResponse.deviceLists?.changed?.count ?? 0 + let devicesLeft = syncResponse.deviceLists?.left?.count ?? 0 - log.debug("Handling new sync response \(uuid), \(toDeviceCount) to-device events") + MXLog.debug("[MXCryptoV2] --------------------------------") + log.debug("Handling new sync response with \(toDeviceCount) to-device event(s), \(devicesChanged) device(s) changed, \(devicesLeft) device(s) left") - Task { + Task(priority: .medium) { do { - let senders = syncResponse - .toDevice? - .events - .compactMap { $0.sender } - .filter { $0 != machine.userId } ?? [] - - try await machine.updateTrackedUsers(users: senders) - try await handle(syncResponse: syncResponse) + let toDevice = try machine.handleSyncResponse( + toDevice: syncResponse.toDevice, + deviceLists: syncResponse.deviceLists, + deviceOneTimeKeysCounts: syncResponse.deviceOneTimeKeysCount ?? [:], + unusedFallbackKeys: syncResponse.unusedFallbackKeys + ) + await handle(toDeviceEvents: toDevice.events) try await machine.processOutgoingRequests() } catch { log.error("Cannot handle sync", context: error) } - log.debug("Completing sync response \(uuid)") + log.debug("Completing sync response") + MXLog.debug("[MXCryptoV2] --------------------------------") await MainActor.run { onComplete() } } } - @MainActor - private func handle(syncResponse: MXSyncResponse) async throws { - let toDevice = try machine.handleSyncResponse( - toDevice: syncResponse.toDevice, - deviceLists: syncResponse.deviceLists, - deviceOneTimeKeysCounts: syncResponse.deviceOneTimeKeysCount ?? [:], - unusedFallbackKeys: syncResponse.unusedFallbackKeys - ) - + private func handle(toDeviceEvents: [MXEvent]) async { // Some of the to-device events processed by the machine require further updates // on the client side, not currently exposed through any convenient api. // These include new key verification events, or receiving backup key // which allows downloading room keys from backup. - for event in toDevice.events { - keyVerification.handleDeviceEvent(event) + for event in toDeviceEvents { + await keyVerification.handleDeviceEvent(event) restoreBackupIfPossible(event: event) + await roomEventDecryptor.handlePossibleRoomKeyEvent(event) } - backup?.maybeSend() - - if !toDevice.events.isEmpty { - retryUndecryptableEvents() + if backupEngine?.enabled == true && backupEngine?.hasKeysToBackup() == true { + backup?.maybeSend() } } @@ -438,7 +449,7 @@ private class MXCryptoV2: NSObject, MXCrypto { Task { do { - try await machine.manuallyVerifyDevice(userId: userId, deviceId: deviceId) + try await machine.verifyDevice(userId: userId, deviceId: deviceId) log.debug("Successfully marked device as verified") await MainActor.run { success?() @@ -477,22 +488,16 @@ private class MXCryptoV2: NSObject, MXCrypto { return } - log.debug("Setting user verification status manually") - - Task { - do { - try await machine.manuallyVerifyUser(userId: userId) - log.debug("Successfully marked user as verified") - await MainActor.run { - success?() - } - } catch { - log.error("Failed marking user as verified", context: error) - await MainActor.run { - failure?(error) - } + log.debug("Signing user") + crossSigning.signUser( + withUserId: userId, + success: { + success?() + }, + failure: { + failure?($0) } - } + ) } public func trustLevel(forUser userId: String) -> MXUserTrustLevel { @@ -538,7 +543,7 @@ private class MXCryptoV2: NSObject, MXCrypto { Task { do { - try await machine.updateTrackedUsers(users: userIds) + try await machine.downloadKeys(users: userIds) log.debug("Downloaded keys") await MainActor.run { @@ -581,7 +586,7 @@ private class MXCryptoV2: NSObject, MXCrypto { return } - Task.detached { [weak self] in + Task(priority: .medium) { [weak self] in guard let self = self else { return } do { @@ -613,20 +618,17 @@ private class MXCryptoV2: NSObject, MXCrypto { return } - Task.detached { [weak self] in - guard let self = self else { return } - + Task(priority: .medium) { do { - let result = try engine.importRoomKeys(keyFile, passphrase: password) + let result = try await engine.importRoomKeys(keyFile, passphrase: password) await MainActor.run { - self.retryUndecryptableEvents() - self.log.debug("Imported room keys") + log.debug("Imported room keys") success?(UInt(result.total), UInt(result.imported)) } } catch { await MainActor.run { - self.log.error("Failed importing room keys", context: error) + log.error("Failed importing room keys", context: error) failure?(error) } } @@ -638,14 +640,10 @@ private class MXCryptoV2: NSObject, MXCrypto { public func reRequestRoomKey(for event: MXEvent) { log.debug("->") - undecryptableEvents[event.eventId] = event Task { do { try await machine.requestRoomKey(event: event) - await MainActor.run { - retryUndecryptableEvents() - log.debug("Recieved room keys and re-decrypted event") - } + log.debug("Sent room key request") } catch { log.error("Failed requesting room key", context: error) } @@ -673,33 +671,27 @@ private class MXCryptoV2: NSObject, MXCrypto { // MARK: - Private - private func listenToRoomEvents(in session: MXSession) { + private func listenToRoomEvents() { + guard let session = session else { + return + } + roomEventObserver = session.listenToEvents(Array(MXKeyVerificationManagerV2.dmEventTypes)) { [weak self] event, direction, _ in guard let self = self else { return } if direction == .forwards && event.sender != session.myUserId { - Task { - try await self.machine.updateTrackedUsers(users: [event.sender]) - await self.keyVerification.handleRoomEvent(event) + Task(priority: .medium) { + if let userId = await self.keyVerification.handleRoomEvent(event), !self.machine.isUserTracked(userId: userId) { + // If we recieved a verification event from a new user we do not yet track + // we need to download their keys to be able to proceed with the verification flow + try await self.machine.downloadKeys(users: [userId]) + } try await self.machine.processOutgoingRequests() } } } } - private func decrypt(event: MXEvent) -> MXEventDecryptionResult { - guard event.isEncrypted && event.content?["algorithm"] as? String == kMXCryptoMegolmAlgorithm else { - log.debug("Ignoring non-room event") - return MXEventDecryptionResult() - } - - let result = machine.decryptRoomEvent(event) - if result.clearEvent == nil { - undecryptableEvents[event.eventId] = event - } - return result - } - private func restoreBackupIfPossible(event: MXEvent) { guard event.type == kMXEventTypeStringSecretSend @@ -728,20 +720,9 @@ private class MXCryptoV2: NSObject, MXCrypto { } } - private func retryUndecryptableEvents() { - for (eventId, event) in undecryptableEvents { - let result = decrypt(event: event) - if result.clearEvent != nil { - event.setClearData(result) - undecryptableEvents[eventId] = nil - } - } - } - private func getRoomUserIds(for room: MXRoom) async throws -> [String] { return try await room.members()?.members - .compactMap(\.userId) - .filter { $0 != machine.userId } ?? [] + .compactMap(\.userId) ?? [] } private func crossSigningInfo(userIds: [String]) -> [String: MXCrossSigningInfo] { diff --git a/MatrixSDK/Crypto/Verification/MXKeyVerificationManagerV2.swift b/MatrixSDK/Crypto/Verification/MXKeyVerificationManagerV2.swift index 9bc20acb95..f8b19cd1c6 100644 --- a/MatrixSDK/Crypto/Verification/MXKeyVerificationManagerV2.swift +++ b/MatrixSDK/Crypto/Verification/MXKeyVerificationManagerV2.swift @@ -291,25 +291,26 @@ class MXKeyVerificationManagerV2: NSObject, MXKeyVerificationManager { } @MainActor - func handleRoomEvent(_ event: MXEvent) { - guard Self.dmEventTypes.contains(where: { $0.identifier == event.type }) else { - return + func handleRoomEvent(_ event: MXEvent) -> String? { + guard isRoomVerificationEvent(event) else { + return nil } - log.debug("->") - if !event.isEncrypted, let roomId = event.roomId { handler.receiveUnencryptedVerificationEvent(event: event, roomId: roomId) + updatePendingVerification() } if event.type == kMXEventTypeStringRoomMessage && event.content?[kMXMessageTypeKey] as? String == kMXMessageTypeKeyVerificationRequest { handleIncomingRequest(userId: event.sender, flowId: event.eventId, transport: .directMessage) + return event.sender } else if event.type == kMXEventTypeStringKeyVerificationStart, let flowId = event.relatesTo.eventId { handleIncomingVerification(userId: event.sender, flowId: flowId, transport: .directMessage) + return event.sender + } else { + return nil } - - updatePendingVerification() } // MARK: - Update @@ -469,7 +470,15 @@ class MXKeyVerificationManagerV2: NSObject, MXKeyVerificationManager { switch verification { case .sasV1(let sas): log.debug("Tracking new SAS verification transaction") - _ = addSasTransaction(for: sas, transport: transport, notify: true) + let transaction = addSasTransaction(for: sas, transport: transport, notify: true) + if activeRequests[transaction.transactionId] != nil { + log.debug("Auto-accepting transaction that matches a pending request") + transaction.accept() + Task { + await updatePendingVerification() + } + } + case .qrCodeV1(let qrCode): if activeTransactions[flowId] is MXQRCodeTransaction { // This flow may happen if we have previously started a QR verification, but so has the other side, @@ -539,6 +548,26 @@ class MXKeyVerificationManagerV2: NSObject, MXKeyVerificationManager { } return roomId } + + private func isRoomVerificationEvent(_ event: MXEvent) -> Bool { + // Filter incoming events by allowed list of event types + guard Self.dmEventTypes.contains(where: { $0.identifier == event.type }) else { + return false + } + + // If it isn't a room message, it must be one of the direction verification events + guard event.type == MXEventType.roomMessage.identifier else { + return true + } + + // If the event does not have a message type, it cannot be accepted + guard let messageType = event.content[kMXMessageTypeKey] as? String else { + return false + } + + // Only requests are wrapped inside `m.room.message` types + return messageType == kMXMessageTypeKeyVerificationRequest + } } #endif diff --git a/MatrixSDK/Data/EventTimeline/Room/MXRoomEventTimeline.m b/MatrixSDK/Data/EventTimeline/Room/MXRoomEventTimeline.m index 03f3373da7..cf9dabcaad 100644 --- a/MatrixSDK/Data/EventTimeline/Room/MXRoomEventTimeline.m +++ b/MatrixSDK/Data/EventTimeline/Room/MXRoomEventTimeline.m @@ -466,26 +466,29 @@ - (void)handleJoinedRoomSync:(MXRoomSync *)roomSync onComplete:(void (^)(void))o uint64_t timestamp = 0; if (isRoomInitialSync && !_initialEventId) { - MXReceiptData *lastUserReadReceipt = [store getReceiptInRoom:_state.roomId forUserId:room.mxSession.myUserId]; - if (lastUserReadReceipt) + NSDictionary *lastUserReadReceiptList = [store getReceiptsInRoom:_state.roomId forUserId:room.mxSession.myUserId]; + for (MXReceiptData *lastUserReadReceipt in [lastUserReadReceiptList allValues]) { - timestamp = lastUserReadReceipt.ts; - - // find the last encrypted event in the events - __block MXEvent *lastEncryptedEvent = nil; - [roomSync.timeline.events enumerateObjectsWithOptions:NSEnumerationReverse - usingBlock:^(MXEvent * _Nonnull event, NSUInteger idx, BOOL * _Nonnull stop) { - if ([event.type isEqualToString:kMXEventTypeStringRoomEncrypted]) + if (lastUserReadReceipt) + { + timestamp = lastUserReadReceipt.ts; + + // find the last encrypted event in the events + __block MXEvent *lastEncryptedEvent = nil; + [roomSync.timeline.events enumerateObjectsWithOptions:NSEnumerationReverse + usingBlock:^(MXEvent * _Nonnull event, NSUInteger idx, BOOL * _Nonnull stop) { + if ([event.type isEqualToString:kMXEventTypeStringRoomEncrypted]) + { + *stop = YES; + lastEncryptedEvent = event; + } + }]; + + if (timestamp > lastEncryptedEvent.originServerTs) { - *stop = YES; - lastEncryptedEvent = event; + // we should at least decrypt the last encrypted event for the rooms whose read markers passed the last encrypted event + timestamp = lastEncryptedEvent.originServerTs; } - }]; - - if (timestamp > lastEncryptedEvent.originServerTs) - { - // we should at least decrypt the last encrypted event for the rooms whose read markers passed the last encrypted event - timestamp = lastEncryptedEvent.originServerTs; } } } @@ -703,7 +706,7 @@ - (void)addEvent:(MXEvent*)event direction:(MXTimelineDirection)direction fromSt } // Consider that a message sent by a user has been read by him - [room storeLocalReceipt:kMXEventTypeStringRead eventId:event.eventId userId:event.sender ts:event.originServerTs]; + [room storeLocalReceipt:kMXEventTypeStringRead eventId:event.eventId threadId:event.threadId ?: kMXEventTimelineMain userId:event.sender ts:event.originServerTs]; } // Store the event diff --git a/MatrixSDK/Data/Filters/MXFilterJSONModel.h b/MatrixSDK/Data/Filters/MXFilterJSONModel.h index 050c932ca9..a68b287154 100644 --- a/MatrixSDK/Data/Filters/MXFilterJSONModel.h +++ b/MatrixSDK/Data/Filters/MXFilterJSONModel.h @@ -84,4 +84,31 @@ */ + (MXFilterJSONModel*)syncFilterForLazyLoadingWithMessageLimit:(NSUInteger)messageLimit; +/** + Build a Matrix filter to retrieve a max given number of messages per room in /sync requests. + + @param messageLimit messageLimit the messages count limit. + @param unreadThreadNotifications enable/disable threads notifications count as per MSC3773. + @return the Matrix filter. + */ ++ (MXFilterJSONModel*)syncFilterWithMessageLimit:(NSUInteger)messageLimit unreadThreadNotifications:(BOOL)unreadThreadNotifications; + +/** + Build a Matrix filter to enable room members lazy loading in /sync requests. + + @param unreadThreadNotifications enable/disable threads notifications count as per MSC3773. + @return the Matrix filter. + */ ++ (MXFilterJSONModel*)syncFilterForLazyLoadingWithUnreadThreadNotifications:(BOOL)unreadThreadNotifications; + +/** + Build a Matrix filter to enable room members lazy loading in /sync requests + with a message count limit per room. + + @param messageLimit messageLimit the messages count limit. + @param unreadThreadNotifications enable/disable threads notifications count as per MSC3773. + @return the Matrix filter. + */ ++ (MXFilterJSONModel*)syncFilterForLazyLoadingWithMessageLimit:(NSUInteger)messageLimit unreadThreadNotifications:(BOOL)unreadThreadNotifications; + @end diff --git a/MatrixSDK/Data/Filters/MXFilterJSONModel.m b/MatrixSDK/Data/Filters/MXFilterJSONModel.m index 04d8d136cf..3c105e110a 100644 --- a/MatrixSDK/Data/Filters/MXFilterJSONModel.m +++ b/MatrixSDK/Data/Filters/MXFilterJSONModel.m @@ -100,34 +100,65 @@ - (BOOL)isEqual:(id)object #pragma mark - Factory + (MXFilterJSONModel*)syncFilterWithMessageLimit:(NSUInteger)messageLimit +{ + return [self syncFilterWithMessageLimit:messageLimit unreadThreadNotifications:NO]; +} + ++ (MXFilterJSONModel*)syncFilterForLazyLoading +{ + return [self syncFilterForLazyLoadingWithUnreadThreadNotifications:NO]; +} + ++ (MXFilterJSONModel*)syncFilterForLazyLoadingWithMessageLimit:(NSUInteger)messageLimit +{ + return [self syncFilterForLazyLoadingWithMessageLimit:messageLimit unreadThreadNotifications:NO]; +} + ++ (MXFilterJSONModel*)syncFilterWithMessageLimit:(NSUInteger)messageLimit unreadThreadNotifications:(BOOL)unreadThreadNotifications { MXFilterJSONModel *filter = [[MXFilterJSONModel alloc] init]; filter.room = [[MXRoomFilter alloc] init]; filter.room.timeline = [[MXRoomEventFilter alloc] init]; filter.room.timeline.limit = messageLimit; + // As per MSC3773, this parameter defaults to false so no need to send false + if (unreadThreadNotifications) + { + filter.room.timeline.unreadThreadNotifications = unreadThreadNotifications; + } return filter; } -+ (MXFilterJSONModel*)syncFilterForLazyLoading ++ (MXFilterJSONModel*)syncFilterForLazyLoadingWithUnreadThreadNotifications:(BOOL)unreadThreadNotifications { MXFilterJSONModel *filter = [[MXFilterJSONModel alloc] init]; filter.room = [[MXRoomFilter alloc] init]; filter.room.state = [[MXRoomEventFilter alloc] init]; filter.room.state.lazyLoadMembers = YES; + // As per MSC3773, this parameter defaults to false so no need to send false + if (unreadThreadNotifications) + { + filter.room.timeline = [[MXRoomEventFilter alloc] init]; + filter.room.timeline.unreadThreadNotifications = unreadThreadNotifications; + } return filter; } -+ (MXFilterJSONModel*)syncFilterForLazyLoadingWithMessageLimit:(NSUInteger)messageLimit ++ (MXFilterJSONModel*)syncFilterForLazyLoadingWithMessageLimit:(NSUInteger)messageLimit unreadThreadNotifications:(BOOL)unreadThreadNotifications { MXFilterJSONModel *filter = [[MXFilterJSONModel alloc] init]; filter.room = [[MXRoomFilter alloc] init]; filter.room.timeline = [[MXRoomEventFilter alloc] init]; filter.room.timeline.limit = messageLimit; + // As per MSC3773, this parameter defaults to false so no need to send false + if (unreadThreadNotifications) + { + filter.room.timeline.unreadThreadNotifications = unreadThreadNotifications; + } filter.room.state = [[MXRoomEventFilter alloc] init]; filter.room.state.lazyLoadMembers = YES; diff --git a/MatrixSDK/Data/Filters/MXRoomEventFilter.h b/MatrixSDK/Data/Filters/MXRoomEventFilter.h index 0dfad7a8f4..10d6e1dc3d 100644 --- a/MatrixSDK/Data/Filters/MXRoomEventFilter.h +++ b/MatrixSDK/Data/Filters/MXRoomEventFilter.h @@ -87,4 +87,9 @@ */ @property (nonatomic) BOOL lazyLoadMembers; +/** + Enable unread thread notifications count as per MSC3773. + */ +@property (nonatomic) BOOL unreadThreadNotifications; + @end diff --git a/MatrixSDK/Data/Filters/MXRoomEventFilter.m b/MatrixSDK/Data/Filters/MXRoomEventFilter.m index d7d234c878..c8b9d33572 100644 --- a/MatrixSDK/Data/Filters/MXRoomEventFilter.m +++ b/MatrixSDK/Data/Filters/MXRoomEventFilter.m @@ -143,6 +143,18 @@ - (BOOL)lazyLoadMembers return lazyLoadMembers; } +- (void)setUnreadThreadNotifications:(BOOL)unreadThreadNotifications +{ + dictionary[@"unread_thread_notifications"] = @(unreadThreadNotifications); +} + +- (BOOL)unreadThreadNotifications +{ + BOOL unreadThreadNotifications = NO; // Basic default value used by homeservers + MXJSONModelSetBoolean(unreadThreadNotifications, dictionary[@"unread_thread_notifications"]); + return unreadThreadNotifications; +} + - (void)setRelatedByTypes:(NSArray *)relatedByTypes { dictionary[kMXRoomEventFilterKeyRelatedByTypes] = relatedByTypes; diff --git a/MatrixSDK/Data/MXRoom.h b/MatrixSDK/Data/MXRoom.h index caf937ad78..eb6acb8758 100644 --- a/MatrixSDK/Data/MXRoom.h +++ b/MatrixSDK/Data/MXRoom.h @@ -1291,10 +1291,12 @@ Remove a tag applied on an event of the room Returns the read receipts list for an event, excluding the read receipt from the current user. @param eventId The event Id. + @param threadId The thread Id. Use `kMXEventTimelineMain` for the main timeline. @param sort YES to sort them from the latest to the oldest. @param completion Completion block containing the receipts for an event in a dedicated room. */ - (void)getEventReceipts:(nonnull NSString*)eventId + threadId:(nonnull NSString*)threadId sorted:(BOOL)sort completion:(nonnull void (^)(NSArray * _Nonnull))completion; @@ -1305,11 +1307,12 @@ Remove a tag applied on an event of the room @param receiptType the receipt type (like kMXEventTypeStringRead). @param eventId the id of the event. + @param threadId the id of the thread (nil for unthread read receipt). @param userId the user who generates the receipt. @param ts the receipt timestamp in ms since Epoch. @return YES if the receipt data is valid and has been stored. */ -- (BOOL)storeLocalReceipt:(NSString*)receiptType eventId:(NSString*)eventId userId:(NSString*)userId ts:(uint64_t)ts; +- (BOOL)storeLocalReceipt:(NSString*)receiptType eventId:(NSString*)eventId threadId:(nullable NSString*)threadId userId:(NSString*)userId ts:(uint64_t)ts; #pragma mark - Read marker handling diff --git a/MatrixSDK/Data/MXRoom.m b/MatrixSDK/Data/MXRoom.m index 74565722db..29fff051b0 100644 --- a/MatrixSDK/Data/MXRoom.m +++ b/MatrixSDK/Data/MXRoom.m @@ -3143,6 +3143,7 @@ - (BOOL)handleReceiptEvent:(MXEvent *)event inLiveTimeline:(id) { BOOL managedEvents = false; + NSString *threadId; for (NSString* eventId in event.content) { NSDictionary *eventDict, *readDict; @@ -3158,11 +3159,13 @@ - (BOOL)handleReceiptEvent:(MXEvent *)event inLiveTimeline:(id) NSNumber *ts; MXJSONModelSetNumber(ts, params[@"ts"]); + MXJSONModelSetString(threadId, params[@"thread_id"]); if (ts) { MXReceiptData *data = [[MXReceiptData alloc] init]; data.userId = userId; data.eventId = eventId; + data.threadId = threadId; data.ts = ts.longLongValue; managedEvents |= [mxSession.store storeReceipt:data inRoom:self.roomId]; @@ -3174,8 +3177,19 @@ - (BOOL)handleReceiptEvent:(MXEvent *)event inLiveTimeline:(id) // warn only if the receipts are not duplicated ones. if (managedEvents) { + MXThread *thread = [self.mxSession.threadingService threadWithId:threadId]; + // Notify listeners - [liveTimeline notifyListeners:event direction:direction]; + if (thread) + { + [thread liveTimeline:^(id liveTimeline) { + [liveTimeline notifyListeners:event direction:direction]; + }]; + } + else + { + [liveTimeline notifyListeners:event direction:direction]; + } } return managedEvents; @@ -3196,7 +3210,7 @@ - (void)acknowledgeEvent:(MXEvent*)event andUpdateReadMarker:(BOOL)updateReadMar // Retrieve the current read receipt event id NSString *currentReadReceiptEventId; NSString *myUserId = mxSession.myUserId; - MXReceiptData* currentData = [mxSession.store getReceiptInRoom:self.roomId forUserId:myUserId]; + MXReceiptData* currentData = [mxSession.store getReceiptInRoom:self.roomId threadId:event.threadId forUserId:myUserId]; if (currentData) { currentReadReceiptEventId = currentData.eventId; @@ -3224,8 +3238,9 @@ - (void)acknowledgeEvent:(MXEvent*)event andUpdateReadMarker:(BOOL)updateReadMar break; } - // Look for the first acknowledgeable event prior the event timestamp - if (nextEvent.originServerTs <= event.originServerTs && nextEvent.eventId) + // Look for the first acknowledgeable event prior the event timestamp within the same timeline + if (nextEvent.originServerTs <= event.originServerTs && nextEvent.eventId + && (event.threadId == nextEvent.threadId || [event.threadId isEqualToString:nextEvent.threadId])) { updatedReadReceiptEvent = nextEvent; @@ -3249,7 +3264,7 @@ - (void)acknowledgeEvent:(MXEvent*)event andUpdateReadMarker:(BOOL)updateReadMar if (updatedReadReceiptEvent) { // Update the oneself receipts - if ([self storeLocalReceipt:kMXEventTypeStringRead eventId:updatedReadReceiptEvent.eventId userId:myUserId ts:(uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000)] + if ([self storeLocalReceipt:kMXEventTypeStringRead eventId:updatedReadReceiptEvent.eventId threadId:event.threadId ?: kMXEventTimelineMain userId:myUserId ts:(uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000)] && [mxSession.store respondsToSelector:@selector(commit)]) { [mxSession.store commit]; @@ -3281,12 +3296,19 @@ - (void)acknowledgeEvent:(MXEvent*)event andUpdateReadMarker:(BOOL)updateReadMar if (readMarkerEventId) { - [self setReadMarker:readMarkerEventId withReadReceipt:updatedReadReceiptEvent.eventId]; + [self setReadMarker:readMarkerEventId withReadReceipt:updatedReadReceiptEvent.eventId threadId:event.threadId ?: kMXEventTimelineMain]; } else if (updatedReadReceiptEvent) { + NSString *threadId = nil; + if (MXSDKOptions.sharedInstance.enableThreads) + { + threadId = updatedReadReceiptEvent.threadId ?: kMXEventTimelineMain; + } + [mxSession.matrixRestClient sendReadReceipt:self.roomId eventId:updatedReadReceiptEvent.eventId + threadId:threadId success:nil failure:nil]; } @@ -3315,40 +3337,45 @@ - (void)markAllAsRead MXEvent *event; NSString* myUserId = mxSession.myUserId; - MXReceiptData *currentReceiptData = [mxSession.store getReceiptInRoom:self.roomId forUserId:myUserId]; + NSDictionary *receiptDataList = [mxSession.store getReceiptsInRoom:self.roomId forUserId:myUserId]; - // Prepare updated read receipt - @autoreleasepool + for (NSString *threadId in [receiptDataList allKeys]) { - id messagesEnumerator = [mxSession.store messagesEnumeratorForRoom:self.roomId withTypeIn:mxSession.acknowledgableEventTypes]; - - // Acknowledge the lastest valid event - while ((event = messagesEnumerator.nextEvent)) + MXReceiptData *currentReceiptData = receiptDataList[threadId]; + + // Prepare updated read receipt + @autoreleasepool { - // Sanity check on event id: Do not send read receipt on event without id - if (event.eventId && ([event.eventId hasPrefix:kMXRoomInviteStateEventIdPrefix] == NO)) + id messagesEnumerator = [mxSession.store messagesEnumeratorForRoom:self.roomId withTypeIn:mxSession.acknowledgableEventTypes]; + + // Acknowledge the lastest valid event + while ((event = messagesEnumerator.nextEvent)) { - // Check whether this is not the current position of the user - if (!currentReceiptData || ![currentReceiptData.eventId isEqualToString:event.eventId]) + // Sanity check on event id: Do not send read receipt on event without id + if (event.eventId && ([event.eventId hasPrefix:kMXRoomInviteStateEventIdPrefix] == NO)) { - // Update the oneself receipts - updatedReceiptData = [[MXReceiptData alloc] init]; - - updatedReceiptData.userId = myUserId; - updatedReceiptData.eventId = event.eventId; - updatedReceiptData.ts = (uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000); - - if ([mxSession.store storeReceipt:updatedReceiptData inRoom:self.roomId]) + // Check whether this is not the current position of the user + if (!currentReceiptData || ![currentReceiptData.eventId isEqualToString:event.eventId]) { - if ([mxSession.store respondsToSelector:@selector(commit)]) + // Update the oneself receipts + updatedReceiptData = [[MXReceiptData alloc] init]; + + updatedReceiptData.userId = myUserId; + updatedReceiptData.eventId = event.eventId; + updatedReceiptData.ts = (uint64_t) ([[NSDate date] timeIntervalSince1970] * 1000); + + if ([mxSession.store storeReceipt:updatedReceiptData inRoom:self.roomId]) { - [mxSession.store commit]; + if ([mxSession.store respondsToSelector:@selector(commit)]) + { + [mxSession.store commit]; + } } } + + // Break the loop + break; } - - // Break the loop - break; } } } @@ -3360,24 +3387,32 @@ - (void)markAllAsRead { // A non nil read receipt must be passed in order to not break notifications counters // homeserver side - readReceiptEventId = currentReceiptData.eventId; + readReceiptEventId = receiptDataList[kMXEventTimelineMain].eventId; } - [self setReadMarker:readMarkerEventId withReadReceipt:readReceiptEventId]; + [self setReadMarker:readMarkerEventId withReadReceipt:readReceiptEventId threadId:event.threadId ?: kMXEventTimelineMain]; } else if (updatedReceiptData) { + NSString *threadId = nil; + if (MXSDKOptions.sharedInstance.enableThreads) + { + threadId = event.threadId ?: kMXEventTimelineMain; + } + [mxSession.matrixRestClient sendReadReceipt:self.roomId eventId:updatedReceiptData.eventId + threadId:threadId success:nil failure:nil]; } } -- (void)getEventReceipts:(NSString *)eventId sorted:(BOOL)sort completion:(void (^)(NSArray * _Nonnull))completion +- (void)getEventReceipts:(NSString *)eventId threadId:(NSString *)threadId sorted:(BOOL)sort completion:(void (^)(NSArray * _Nonnull))completion { [mxSession.store getEventReceipts:self.roomId eventId:eventId + threadId:threadId sorted:sort completion:^(NSArray * _Nonnull receipts) { NSString *myUserId = self->mxSession.myUserId; @@ -3389,7 +3424,7 @@ - (void)getEventReceipts:(NSString *)eventId sorted:(BOOL)sort completion:(void }]; } -- (BOOL)storeLocalReceipt:(NSString *)receiptType eventId:(NSString *)eventId userId:(NSString *)userId ts:(uint64_t)ts +- (BOOL)storeLocalReceipt:(NSString *)receiptType eventId:(NSString *)eventId threadId:(NSString*)threadId userId:(NSString *)userId ts:(uint64_t)ts { // Sanity check if (!userId) @@ -3403,32 +3438,43 @@ - (BOOL)storeLocalReceipt:(NSString *)receiptType eventId:(NSString *)eventId us MXReceiptData* receiptData = [[MXReceiptData alloc] init]; receiptData.userId = userId; receiptData.eventId = eventId; + receiptData.threadId = threadId; receiptData.ts = ts; if ([mxSession.store storeReceipt:receiptData inRoom:_roomId]) { result = YES; + + NSDictionary *userModel = @{ @"thread_id": threadId ?: kMXEventTimelineMain, @"ts": @(receiptData.ts)}; + NSDictionary *jsonModel = @{ + @"type": kMXEventTypeStringReceipt, + @"room_id": _roomId, + @"content" : @{ + receiptData.eventId : @{ + kMXEventTypeStringRead: @{ + receiptData.userId: userModel + } + + } + } + }; // Notify SDK client about it with a local read receipt - MXEvent *receiptEvent = [MXEvent modelFromJSON: - @{ - @"type": kMXEventTypeStringReceipt, - @"room_id": _roomId, - @"content" : @{ - receiptData.eventId : @{ - kMXEventTypeStringRead: @{ - receiptData.userId: @{ - @"ts": @(receiptData.ts) - } - } - - } - } - }]; - - [self liveTimeline:^(id theLiveTimeline) { - [theLiveTimeline notifyListeners:receiptEvent direction:MXTimelineDirectionForwards]; - }]; + MXEvent *receiptEvent = [MXEvent modelFromJSON: jsonModel]; + + MXThread *thread = threadId ? [self.mxSession.threadingService threadWithId:threadId] : nil; + if (thread) + { + [thread liveTimeline:^(id theLiveTimeline) { + [theLiveTimeline notifyListeners:receiptEvent direction:MXTimelineDirectionForwards]; + }]; + } + else + { + [self liveTimeline:^(id theLiveTimeline) { + [theLiveTimeline notifyListeners:receiptEvent direction:MXTimelineDirectionForwards]; + }]; + } } return YES; @@ -3441,7 +3487,7 @@ - (void)moveReadMarkerToEventId:(NSString*)eventId // Sanity check on event id: Do not send read marker on event without id if (eventId && ![eventId hasPrefix:kMXEventLocalEventIdPrefix] && ![eventId hasPrefix:kMXRoomInviteStateEventIdPrefix]) { - [self setReadMarker:eventId withReadReceipt:nil]; + [self setReadMarker:eventId withReadReceipt:nil threadId:nil]; } } @@ -3449,14 +3495,14 @@ - (void)forgetReadMarker { // Retrieve the current position NSString *myUserId = mxSession.myUserId; - MXReceiptData* currentData = [mxSession.store getReceiptInRoom:self.roomId forUserId:myUserId]; + MXReceiptData* currentData = [mxSession.store getReceiptInRoom:self.roomId threadId:kMXEventTimelineMain forUserId:myUserId]; if (currentData) { - [self setReadMarker:currentData.eventId withReadReceipt:nil]; + [self setReadMarker:currentData.eventId withReadReceipt:nil threadId:nil]; } } -- (void)setReadMarker:(NSString*)eventId withReadReceipt:(NSString*)receiptEventId +- (void)setReadMarker:(NSString*)eventId withReadReceipt:(NSString*)receiptEventId threadId:(NSString*)threadId { _accountData.readMarkerEventId = eventId; @@ -3472,7 +3518,18 @@ - (void)setReadMarker:(NSString*)eventId withReadReceipt:(NSString*)receiptEvent } // Update data on the homeserver side - [mxSession.matrixRestClient sendReadMarker:self.roomId readMarkerEventId:eventId readReceiptEventId:receiptEventId success:nil failure:nil]; + [mxSession.matrixRestClient sendReadMarker:self.roomId readMarkerEventId:eventId readReceiptEventId:nil success:nil failure:nil]; + + if (receiptEventId) + { + // as per MSC3773, read markers do not yet support read receipts with thread ID. + // The read receipt with the right threadId should be sent by the client. + [mxSession.matrixRestClient sendReadReceipt:self.roomId + eventId:receiptEventId + threadId:MXSDKOptions.sharedInstance.enableThreads ? threadId: nil + success:nil + failure:nil]; + } } #pragma mark - Direct chats handling diff --git a/MatrixSDK/Data/MXRoomSummary.m b/MatrixSDK/Data/MXRoomSummary.m index 94ed030a4e..5ebedcc1d7 100644 --- a/MatrixSDK/Data/MXRoomSummary.m +++ b/MatrixSDK/Data/MXRoomSummary.m @@ -26,6 +26,7 @@ #import "MXTools.h" #import "MXEventRelations.h" #import "MXEventReplace.h" +#import "MXRoomSyncUnreadNotifications.h" #import "MXRoomSync.h" #import "MatrixSDKSwiftHeader.h" @@ -769,9 +770,12 @@ - (BOOL)updateLocalUnreadEventCount { BOOL updated = NO; - NSUInteger localUnreadEventCount = [self.mxSession.store localUnreadEventCount:self.roomId - threadId:nil - withTypeIn:self.mxSession.unreadEventTypes]; + NSDictionary *localUnreadEventCountPerThread = [self.mxSession.store localUnreadEventCountPerThread:self.roomId withTypeIn:self.mxSession.unreadEventTypes]; + NSUInteger localUnreadEventCount = 0; + for (NSNumber *unreadCount in localUnreadEventCountPerThread.allValues) + { + localUnreadEventCount += unreadCount.unsignedIntValue; + } if (self.localUnreadEventCount != localUnreadEventCount) { @@ -861,26 +865,23 @@ - (void)handleJoinedRoomSync:(MXRoomSync*)roomSync onComplete:(void (^)(void))on // Check for unread events in store and update the localUnreadEventCount value if needed updated |= [self updateLocalUnreadEventCount]; - // Store notification counts from unreadNotifications field in /sync response - if (roomSync.unreadNotifications) + // Store notification counts from unreadNotifications and unreadNotificationsPerThread fields in /sync response + if (roomSync.unreadNotifications || roomSync.unreadNotificationsPerThread) { - // Caution: the server may provide a not null count whereas we know locally the user has read all room messages - // (see for example this issue https://github.com/matrix-org/synapse/issues/2193). - // Patch: Ignore the server information when the user has read all messages. - if (roomSync.unreadNotifications.notificationCount && self.localUnreadEventCount == 0) + // compute the notification counts from unreadNotifications and unreadNotificationsPerThread fields in /sync response + NSUInteger notificationCount = roomSync.unreadNotifications.notificationCount; + NSUInteger highlightCount = roomSync.unreadNotifications.highlightCount; + for (MXRoomSyncUnreadNotifications *unreadNotifications in roomSync.unreadNotificationsPerThread.allValues) { - if (self.notificationCount != 0) - { - self->_notificationCount = 0; - self->_highlightCount = 0; - updated = YES; - } + notificationCount += unreadNotifications.notificationCount; + highlightCount += unreadNotifications.highlightCount; } - else if (self.notificationCount != roomSync.unreadNotifications.notificationCount - || self.highlightCount != roomSync.unreadNotifications.highlightCount) + + // store the new notification counts + if (self.notificationCount != notificationCount || self.highlightCount != highlightCount) { - self->_notificationCount = roomSync.unreadNotifications.notificationCount; - self->_highlightCount = roomSync.unreadNotifications.highlightCount; + self->_notificationCount = notificationCount; + self->_highlightCount = highlightCount; updated = YES; } } diff --git a/MatrixSDK/Data/Store/MXFileStore/MXFileStore.m b/MatrixSDK/Data/Store/MXFileStore/MXFileStore.m index 693d11edb8..35417eb17b 100644 --- a/MatrixSDK/Data/Store/MXFileStore/MXFileStore.m +++ b/MatrixSDK/Data/Store/MXFileStore/MXFileStore.m @@ -46,6 +46,7 @@ static NSString *const kMXFileStoreRoomStateFile = @"state"; static NSString *const kMXFileStoreRoomAccountDataFile = @"accountData"; static NSString *const kMXFileStoreRoomReadReceiptsFile = @"readReceipts"; +static NSString *const kMXFileStoreRoomThreadedReadReceiptsFile = @"threadedReadReceipts"; static NSUInteger preloadOptions; @@ -956,55 +957,80 @@ - (MXMemoryRoomOutgoingMessagesStore *)getOrCreateRoomOutgoingMessagesStore:(NSS return store; } -- (RoomReceiptsStore*)getOrCreateRoomReceiptsStore:(NSString *)roomId +- (RoomThreadedReceiptsStore*)getOrCreateRoomThreadedReceiptsStore:(NSString*)roomId { - RoomReceiptsStore *receiptsStore = roomReceiptsStores[roomId]; - if (nil == receiptsStore) + RoomThreadedReceiptsStore *threadedStore = roomThreadedReceiptsStores[roomId]; + if (nil == threadedStore) { // This object is global, which means that we will be able to open only one room at a time. // A per-room lock might be better. - @synchronized (roomReceiptsStores) { + @synchronized (roomThreadedReceiptsStores) { NSString *roomFile = [self readReceiptsFileForRoom:roomId forBackup:NO]; - if ([[NSFileManager defaultManager] fileExistsAtPath:roomFile]) + RoomReceiptsStore *receiptsStore = [self loadReceiptsStoreFromFileAt:roomFile forRoomWithId:roomId]; + + if (receiptsStore) { - @try + // if an old version of the receipts store exists we need first to port it to new version. + threadedStore = [RoomThreadedReceiptsStore new]; + threadedStore[kMXEventTimelineMain] = receiptsStore; + + // then save the new version of the receipts + NSString *newFile = [self threadedReadReceiptsFileForRoom:roomId forBackup:NO]; + if ([NSKeyedArchiver archiveRootObject:threadedStore toFile:newFile]) { - NSDate *startDate = [NSDate date]; - receiptsStore = [NSKeyedUnarchiver unarchiveObjectWithFile:roomFile]; - roomReceiptsStores[roomId] = receiptsStore; - if ([NSThread isMainThread]) - { - MXLogWarning(@"[MXFileStore] Loaded read receipts of room: %@ in %.0fms, in main thread", roomId, [[NSDate date] timeIntervalSinceDate:startDate] * 1000); - } + // this file is not needed anymore + [[NSFileManager defaultManager] removeItemAtPath:roomFile error:nil]; } - @catch (NSException *exception) + } + else + { + roomFile = [self threadedReadReceiptsFileForRoom:roomId forBackup:NO]; + threadedStore = [self loadReceiptsStoreFromFileAt:roomFile forRoomWithId:roomId]; + if (!threadedStore) { - NSDictionary *logDetails = @{ - @"roomId": roomId ?: @"", - @"exception": exception - }; - MXLogErrorDetails(@"[MXFileStore] Warning: loadReceipts file for room as been corrupted", logDetails); - - // We used to reset the store and force a full initial sync but this makes the app - // start very slowly. - // So, avoid this reset by considering there is no read receipts for this room which - // is not probably true. - // TODO: Can we live with that? - //[self deleteAllData]; - - receiptsStore = [RoomReceiptsStore new]; - roomReceiptsStores[roomId] = receiptsStore; + threadedStore = [RoomThreadedReceiptsStore new]; } } - else + roomThreadedReceiptsStores[roomId] = threadedStore; + } + } + + return threadedStore; +} + +- (NSMutableDictionary*)loadReceiptsStoreFromFileAt:(NSString*)filePath forRoomWithId:(NSString*)roomId +{ + NSMutableDictionary *store; + + if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) + { + @try + { + NSDate *startDate = [NSDate date]; + store = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath]; + if ([NSThread isMainThread]) { - receiptsStore = [RoomReceiptsStore new]; - roomReceiptsStores[roomId] = receiptsStore; + MXLogWarning(@"[MXFileStore] Loaded read receipts of room: %@ in %.0fms, in main thread", roomId, [[NSDate date] timeIntervalSinceDate:startDate] * 1000); } } + @catch (NSException *exception) + { + NSDictionary *logDetails = @{ + @"roomId": roomId ?: @"", + @"exception": exception + }; + MXLogErrorDetails(@"[MXFileStore] Warning: loadReceipts file for room as been corrupted", logDetails); + + // We used to reset the store and force a full initial sync but this makes the app + // start very slowly. + // So, avoid this reset by considering there is no read receipts for this room which + // is not probably true. + // TODO: Can we live with that? + //[self deleteAllData]; + } } - return receiptsStore; + return store; } #pragma mark - File paths @@ -1098,6 +1124,11 @@ - (NSString*)readReceiptsFileForRoom:(NSString*)roomId forBackup:(BOOL)backup return [[self folderForRoom:roomId forBackup:backup] stringByAppendingPathComponent:kMXFileStoreRoomReadReceiptsFile]; } +- (NSString*)threadedReadReceiptsFileForRoom:(NSString*)roomId forBackup:(BOOL)backup +{ + return [[self folderForRoom:roomId forBackup:backup] stringByAppendingPathComponent:kMXFileStoreRoomThreadedReadReceiptsFile]; +} + - (NSString*)metaDataFileForBackup:(BOOL)backup { if (!backup) @@ -2009,7 +2040,7 @@ - (void)saveGroups - (void)loadReceiptsForRoom:(NSString *)roomId completion:(void (^)(void))completion { dispatch_async(dispatchQueue, ^{ - [self getOrCreateRoomReceiptsStore:roomId]; + [self getOrCreateRoomThreadedReceiptsStore:roomId]; if (completion) { @@ -2049,39 +2080,10 @@ - (void)preloadRoomReceipts for (NSString *roomId in roomIDs) { - NSString *roomFile = [self readReceiptsFileForRoom:roomId forBackup:NO]; - - RoomReceiptsStore *receiptsStore; - @try - { - receiptsStore = [NSKeyedUnarchiver unarchiveObjectWithFile:roomFile]; - } - @catch (NSException *exception) - { - MXLogDebug(@"[MXFileStore] Warning: loadReceipts file for room %@ has been corrupted", roomId); - } - - if (receiptsStore) - { - //MXLogDebug(@" - %@: %tu", roomId, receiptsDict.count); - roomReceiptsStores[roomId] = receiptsStore; - } - else - { - MXLogDebug(@"[MXFileStore] Warning: MXFileStore has no receipts file for room %@", roomId); - - // We used to reset the store and force a full initial sync but this makes the app - // start very slowly. - // So, avoid this reset by considering there is no read receipts for this room which - // is not probably true. - // TODO: Can we live with that? - //[self deleteAllData]; - - roomReceiptsStores[roomId] = [RoomReceiptsStore new]; - } + roomThreadedReceiptsStores[roomId] = [self getOrCreateRoomThreadedReceiptsStore:roomId]; } - MXLogDebug(@"[MXFileStore] Loaded read receipts of %tu rooms in %.0fms", roomReceiptsStores.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000); + MXLogDebug(@"[MXFileStore] Loaded read receipts of %tu rooms in %.0fms", roomThreadedReceiptsStores.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000); } - (void)saveReceipts @@ -2104,13 +2106,13 @@ - (void)saveReceipts // Save rooms where there was changes for (NSString *roomId in roomsToCommit) { - RoomReceiptsStore *receiptsStore = self->roomReceiptsStores[roomId]; + RoomThreadedReceiptsStore *receiptsStore = self->roomThreadedReceiptsStores[roomId]; if (receiptsStore) { @synchronized (receiptsStore) { - NSString *file = [self readReceiptsFileForRoom:roomId forBackup:NO]; - NSString *backupFile = [self readReceiptsFileForRoom:roomId forBackup:YES]; + NSString *file = [self threadedReadReceiptsFileForRoom:roomId forBackup:NO]; + NSString *backupFile = [self threadedReadReceiptsFileForRoom:roomId forBackup:YES]; // Backup the file if (backupFile && [[NSFileManager defaultManager] fileExistsAtPath:file]) diff --git a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.h b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.h index d5a5196302..d085fbfcc1 100644 --- a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.h +++ b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.h @@ -92,6 +92,15 @@ */ - (id)enumeratorForMessagesWithTypeIn:(NSArray*)types; +/** + Get all events in thread since the root message event. + + @param threadId the thread id to find events in. + @param types a set of event types strings (MXEventTypeString). + @return the messages events after an event Id + */ +- (NSArray*)eventsInThreadWithThreadId:(NSString *)threadId except:(NSString *)userId withTypeIn:(NSSet*)types; + /** Get all events newer than the event with the passed id. diff --git a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.m b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.m index 823833d234..2da23e99d3 100644 --- a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.m +++ b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryRoomStore.m @@ -104,12 +104,50 @@ - (void)removeAllMessages return [[MXEventsByTypesEnumeratorOnArray alloc] initWithEventIds:[self allEventIds] andTypesIn:types dataSource:self]; } +- (NSArray*)eventsInThreadWithThreadId:(NSString *)threadId except:(NSString *)userId withTypeIn:(NSSet*)types +{ + NSMutableArray* list = [[NSMutableArray alloc] init]; + + if (threadId == nil || [threadId isEqualToString:kMXEventTimelineMain]) + { + MXLogWarning(@"[MXMemoryRoomStore] eventsInThreadWithThreadId: invalid thread ID %@", threadId); + return list; + } + + // Check messages from the most recent + for (NSInteger i = messages.count - 1; i >= 0 ; i--) + { + MXEvent *event = messages[i]; + + // Check if the event is the root event of the thread + if (NO == [event.eventId isEqualToString:threadId]) + { + // Keep events matching filters + BOOL typeAllowed = !types || [types containsObject:event.type]; + BOOL threadAllowed = [event.threadId isEqualToString:threadId]; + BOOL senderAllowed = ![event.sender isEqualToString:userId]; + if (typeAllowed && threadAllowed && senderAllowed) + { + [list insertObject:event atIndex:0]; + } + } + else + { + // We are done + break; + } + } + + return list; +} + - (NSArray*)eventsAfter:(NSString *)eventId threadId:(NSString *)threadId except:(NSString *)userId withTypeIn:(NSSet*)types { NSMutableArray* list = [[NSMutableArray alloc] init]; if (eventId) { + NSString *_threadId = ![threadId isEqualToString:kMXEventTimelineMain] ? threadId : nil; // Check messages from the most recent for (NSInteger i = messages.count - 1; i >= 0 ; i--) { @@ -119,7 +157,7 @@ - (void)removeAllMessages { // Keep events matching filters BOOL typeAllowed = !types || [types containsObject:event.type]; - BOOL threadAllowed = !threadId || [event.threadId isEqualToString:threadId]; + BOOL threadAllowed = (!_threadId && !event.isInThread) || [event.threadId isEqualToString:_threadId]; BOOL senderAllowed = ![event.sender isEqualToString:userId]; if (typeAllowed && threadAllowed && senderAllowed) { diff --git a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.h b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.h index 46ab2bf9fb..7fdb6d475c 100644 --- a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.h +++ b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.h @@ -24,6 +24,11 @@ */ typedef NSMutableDictionary RoomReceiptsStore; +/** + Receipts in a room by threadId. Keys are thread ID. + */ +typedef NSMutableDictionary RoomThreadedReceiptsStore; + /** `MXMemoryStore` is an implementation of the `MXStore` interface that stores events in memory. */ @@ -42,9 +47,9 @@ typedef NSMutableDictionary RoomReceiptsStore; // The keys are groups ids. NSMutableDictionary *groups; - // Dict of room receipts stores + // Dict of room threaded receipts stores // The keys are room ids. - NSMutableDictionary *roomReceiptsStores; + NSMutableDictionary *roomThreadedReceiptsStores; // Matrix filters // FilterId -> Filter JSON string @@ -73,11 +78,20 @@ typedef NSMutableDictionary RoomReceiptsStore; - (MXMemoryRoomOutgoingMessagesStore*)getOrCreateRoomOutgoingMessagesStore:(NSString*)roomId; /** - Interface to create or retrieve receipts for a room. + Interface to create or retrieve threaded receipts for a room. + + @param roomId the id of the room. + @return receipts dictionary by thread id. + */ +- (RoomThreadedReceiptsStore*)getOrCreateRoomThreadedReceiptsStore:(NSString*)roomId; + +/** + Interface to create or retrieve threaded receipts for a room. @param roomId the id of the room. - @return receipts dictionary by user id. + @param threadId the id of the thread. `kMXEventTimelineMain` for the main timeline. + @return receipts dictionary by thread id. */ -- (RoomReceiptsStore*)getOrCreateRoomReceiptsStore:(NSString*)roomId; +- (RoomReceiptsStore*)getOrCreateReceiptsStoreForRoomWithId:(NSString*)roomId threadId:(NSString* _Nullable)threadId; @end diff --git a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.m b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.m index 7ec517631a..9a3be1426f 100644 --- a/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.m +++ b/MatrixSDK/Data/Store/MXMemoryStore/MXMemoryStore.m @@ -49,7 +49,7 @@ - (instancetype)init { roomStores = [NSMutableDictionary dictionary]; roomOutgoingMessagesStores = [NSMutableDictionary dictionary]; - roomReceiptsStores = [NSMutableDictionary dictionary]; + roomThreadedReceiptsStores = [NSMutableDictionary dictionary]; users = [NSMutableDictionary dictionary]; groups = [NSMutableDictionary dictionary]; roomSummaryStore = [[MXMemoryRoomSummaryStore alloc] init]; @@ -108,9 +108,9 @@ - (void)deleteRoom:(NSString *)roomId [roomStores removeObjectForKey:roomId]; } - if (roomReceiptsStores[roomId]) + if (roomThreadedReceiptsStores[roomId]) { - [roomReceiptsStores removeObjectForKey:roomId]; + [roomThreadedReceiptsStores removeObjectForKey:roomId]; } [roomSummaryStore removeSummaryOfRoom:roomId]; @@ -190,7 +190,7 @@ - (void)stateOfRoom:(NSString *)roomId success:(void (^)(NSArray * _N - (void)loadReceiptsForRoom:(NSString *)roomId completion:(void (^)(void))completion { - [self getOrCreateRoomReceiptsStore:roomId]; + [self getOrCreateRoomThreadedReceiptsStore:roomId]; if (completion) { @@ -200,11 +200,11 @@ - (void)loadReceiptsForRoom:(NSString *)roomId completion:(void (^)(void))comple } } -- (void)getEventReceipts:(NSString *)roomId eventId:(NSString *)eventId sorted:(BOOL)sort completion:(void (^)(NSArray * _Nonnull))completion +- (void)getEventReceipts:(NSString *)roomId eventId:(NSString *)eventId threadId:(NSString *)threadId sorted:(BOOL)sort completion:(void (^)(NSArray * _Nonnull))completion { [self loadReceiptsForRoom:roomId completion:^{ - RoomReceiptsStore *receiptsStore = self->roomReceiptsStores[roomId]; - + RoomReceiptsStore *receiptsStore = [self getOrCreateReceiptsStoreForRoomWithId:roomId threadId:threadId]; + if (receiptsStore) { @synchronized (receiptsStore) @@ -240,7 +240,26 @@ - (void)getEventReceipts:(NSString *)roomId eventId:(NSString *)eventId sorted:( - (BOOL)storeReceipt:(MXReceiptData*)receipt inRoom:(NSString*)roomId { - RoomReceiptsStore *receiptsStore = [self getOrCreateRoomReceiptsStore:roomId]; + if (!receipt.threadId) + { + // Unthreaded RR are stored for main timeline and all threads. + RoomThreadedReceiptsStore *threadedStore = [self getOrCreateRoomThreadedReceiptsStore:roomId]; + + BOOL isStored = [self storeReceipt:receipt inRoom:roomId forThread:kMXEventTimelineMain]; + + for (NSString *threadId in threadedStore.allKeys) { + isStored |= [self storeReceipt:receipt inRoom:roomId forThread:threadId]; + } + + return isStored; + } + + return [self storeReceipt:receipt inRoom:roomId forThread:receipt.threadId]; +} + +- (BOOL)storeReceipt:(MXReceiptData*)receipt inRoom:(NSString*)roomId forThread:(NSString*)threadId +{ + RoomReceiptsStore *receiptsStore = [self getOrCreateReceiptsStoreForRoomWithId:roomId threadId:threadId]; MXReceiptData *curReceipt = receiptsStore[receipt.userId]; @@ -257,10 +276,10 @@ - (BOOL)storeReceipt:(MXReceiptData*)receipt inRoom:(NSString*)roomId return false; } -- (MXReceiptData *)getReceiptInRoom:(NSString*)roomId forUserId:(NSString*)userId +- (MXReceiptData *)getReceiptInRoom:(NSString *)roomId threadId:(NSString *)threadId forUserId:(NSString *)userId { - RoomReceiptsStore *receiptsStore = [self getOrCreateRoomReceiptsStore:roomId]; - + RoomReceiptsStore *receiptsStore = [self getOrCreateReceiptsStoreForRoomWithId:roomId threadId:threadId]; + if (receiptsStore) { MXReceiptData* data = receiptsStore[userId]; @@ -273,6 +292,25 @@ - (MXReceiptData *)getReceiptInRoom:(NSString*)roomId forUserId:(NSString*)userI return nil; } +- (NSMutableDictionary *)getReceiptsInRoom:(NSString*)roomId forUserId:(NSString*)userId +{ + NSMutableDictionary *receiptsData = [NSMutableDictionary new]; + RoomThreadedReceiptsStore *threadsStore = [self getOrCreateRoomThreadedReceiptsStore:roomId]; + + if (threadsStore) + { + for (NSString *threadId in [threadsStore allKeys]) { + MXReceiptData* data = threadsStore[threadId][userId]; + if (data) + { + receiptsData[threadId] = data; + } + } + } + + return receiptsData; +} + - (NSUInteger)localUnreadEventCount:(NSString*)roomId threadId:(NSString *)threadId withTypeIn:(NSArray*)types { NSArray *newEvents = [self newIncomingEventsInRoom:roomId threadId:threadId withTypeIn:types]; @@ -288,12 +326,26 @@ - (NSUInteger)localUnreadEventCount:(NSString*)roomId threadId:(NSString *)threa return result; } +- (NSDictionary *)localUnreadEventCountPerThread:(nonnull NSString*)roomId withTypeIn:(nullable NSArray*)types +{ + NSMutableDictionary *unreadEventCountPerThread = [NSMutableDictionary dictionary]; + + RoomThreadedReceiptsStore *threadedStore = [self getOrCreateRoomThreadedReceiptsStore:roomId]; + for (NSString *threadId in threadedStore.allKeys) + { + NSUInteger unreadCount = [self localUnreadEventCount:roomId threadId:threadId withTypeIn:types]; + unreadEventCountPerThread[threadId] = @(unreadCount); + } + + return unreadEventCountPerThread; +} + - (NSArray *)newIncomingEventsInRoom:(NSString *)roomId threadId:(NSString *)threadId withTypeIn:(NSArray *)types { MXMemoryRoomStore *store = [self getOrCreateRoomStore:roomId]; - RoomReceiptsStore *receiptsStore = [self getOrCreateRoomReceiptsStore:roomId]; + RoomReceiptsStore *receiptsStore = [self getOrCreateReceiptsStoreForRoomWithId:roomId threadId:threadId]; if (store == nil || receiptsStore == nil) { @@ -304,7 +356,14 @@ - (NSUInteger)localUnreadEventCount:(NSString*)roomId threadId:(NSString *)threa if (data == nil) { - return @[]; + if (receiptsStore.count > 0) + { + return [store eventsInThreadWithThreadId:threadId except:credentials.userId withTypeIn:[NSSet setWithArray:types]]; + } + else + { + return @[]; + } } // Check the current stored events (by ignoring oneself events) @@ -515,13 +574,26 @@ - (MXMemoryRoomOutgoingMessagesStore *)getOrCreateRoomOutgoingMessagesStore:(NSS return store; } -- (RoomReceiptsStore *)getOrCreateRoomReceiptsStore:(NSString *)roomId +- (RoomThreadedReceiptsStore*)getOrCreateRoomThreadedReceiptsStore:(NSString*)roomId { - RoomReceiptsStore *store = roomReceiptsStores[roomId]; + RoomThreadedReceiptsStore *store = roomThreadedReceiptsStores[roomId]; if (nil == store) + { + store = [RoomThreadedReceiptsStore new]; + roomThreadedReceiptsStores[roomId] = store; + } + return store; +} + +- (RoomReceiptsStore*)getOrCreateReceiptsStoreForRoomWithId:(NSString*)roomId threadId:(NSString*)threadId +{ + NSString *threadKey = threadId ?: kMXEventTimelineMain; + RoomThreadedReceiptsStore *threadedStore = [self getOrCreateRoomThreadedReceiptsStore:roomId]; + RoomReceiptsStore *store = threadedStore[threadKey]; + if (!store) { store = [RoomReceiptsStore new]; - roomReceiptsStores[roomId] = store; + threadedStore[threadKey] = store; } return store; } diff --git a/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.h b/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.h index 180201cf13..b5b3a092e9 100644 --- a/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.h +++ b/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.h @@ -21,12 +21,17 @@ /** The event id. */ -@property (nonatomic) NSString *userId; +@property (nonatomic, nonnull) NSString *userId; /** The event id. */ -@property (nonatomic) NSString *eventId; +@property (nonatomic, nonnull) NSString *eventId; + +/** + The thread id. This value shouldn't be nil as per https://github.com/matrix-org/matrix-spec-proposals/pull/3771 but you can set it to nil for unthreaded event. + */ +@property (nonatomic, nullable) NSString *threadId; /** The timestamp in ms since Epoch generated by the origin homeserver when it receives the event diff --git a/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.m b/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.m index 8fb948304c..8003a89720 100644 --- a/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.m +++ b/MatrixSDK/Data/Store/MXMemoryStore/MXReceiptData.m @@ -25,6 +25,10 @@ - (id)initWithCoder:(NSCoder *)aDecoder { _eventId = [aDecoder decodeObjectForKey:@"eventId"]; _userId = [aDecoder decodeObjectForKey:@"userId"]; + if ([aDecoder containsValueForKey:@"threadId"]) + { + _threadId = [aDecoder decodeObjectForKey:@"threadId"]; + } _ts = (uint64_t)[aDecoder decodeInt64ForKey:@"ts"]; } return self; @@ -35,6 +39,10 @@ -(void)encodeWithCoder:(NSCoder *)aCoder // All properties are mandatory except eventStreamToken [aCoder encodeObject:_eventId forKey:@"eventId"]; [aCoder encodeObject:_userId forKey:@"userId"]; + if (_threadId) + { + [aCoder encodeObject:_threadId forKey:@"threadId"]; + } [aCoder encodeInt64:(int64_t)_ts forKey:@"ts"]; // TODO need some new fields @@ -47,13 +55,14 @@ - (id)copyWithZone:(NSZone *)zone metaData->_ts = _ts; metaData->_eventId = [_eventId copyWithZone:zone]; metaData->_userId = [_userId copyWithZone:zone]; + metaData->_threadId = [_threadId copyWithZone:zone]; return metaData; } - (NSString *)description { - return [NSString stringWithFormat:@" userId: %@ - eventId: %@ - ts: %@", self, _userId, _eventId, @(_ts)]; + return [NSString stringWithFormat:@" userId: %@ - eventId: %@ - threadId: %@ - ts: %@", self, _userId, _eventId, _threadId, @(_ts)]; } @end diff --git a/MatrixSDK/Data/Store/MXNoStore/MXNoStore.m b/MatrixSDK/Data/Store/MXNoStore/MXNoStore.m index dcb868b5c8..55c1f847b0 100644 --- a/MatrixSDK/Data/Store/MXNoStore/MXNoStore.m +++ b/MatrixSDK/Data/Store/MXNoStore/MXNoStore.m @@ -411,6 +411,23 @@ - (MXReceiptData *)getReceiptInRoom:(NSString*)roomId forUserId:(NSString*)userI return nil; } +- (void)getEventReceipts:(nonnull NSString *)roomId eventId:(nonnull NSString *)eventId threadId:(nonnull NSString *)threadId sorted:(BOOL)sort completion:(nonnull void (^)(NSArray * _Nonnull))completion { + if (completion) + { + completion(@[]); + } +} + + +- (nullable MXReceiptData *)getReceiptInRoom:(nonnull NSString *)roomId threadId:(nonnull NSString *)threadId forUserId:(nonnull NSString *)userId { + return nil; +} + + +- (nonnull NSDictionary *)getReceiptsInRoom:(nonnull NSString *)roomId forUserId:(nonnull NSString *)userId { + return @{}; +} + - (void)loadReceiptsForRoom:(NSString *)roomId completion:(void (^)(void))completion { if (completion) @@ -426,6 +443,11 @@ - (NSUInteger)localUnreadEventCount:(NSString *)roomId threadId:(NSString *)thre return 0; } +- (NSDictionary *)localUnreadEventCountPerThread:(NSString *)roomId withTypeIn:(NSArray *)types +{ + return @{}; +} + - (NSArray *)newIncomingEventsInRoom:(NSString *)roomId threadId:(NSString *)threadId withTypeIn:(NSArray *)types { return @[]; diff --git a/MatrixSDK/Data/Store/MXStore.h b/MatrixSDK/Data/Store/MXStore.h index 57ef3bedd3..cf830a85d2 100644 --- a/MatrixSDK/Data/Store/MXStore.h +++ b/MatrixSDK/Data/Store/MXStore.h @@ -250,11 +250,13 @@ @param roomId The room Id. @param eventId The event Id. + @param threadId The thread Id. kMXEventTimelineMain for the main timeline. @param sort to sort them from the latest to the oldest @param completion Completion block containing the receipts for an event in a dedicated room. */ - (void)getEventReceipts:(nonnull NSString*)roomId eventId:(nonnull NSString*)eventId + threadId:(nonnull NSString*)threadId sorted:(BOOL)sort completion:(nonnull void (^)(NSArray * _Nonnull))completion; @@ -268,13 +270,23 @@ - (BOOL)storeReceipt:(nonnull MXReceiptData*)receipt inRoom:(nonnull NSString*)roomId; /** - Retrieve the receipt for a user in a room + Retrieve the receipt for a user within all threads in a room @param roomId The roomId @param userId The user identifier + @return all the currently stored receipts ordered by thread ID. + */ +- (nonnull NSDictionary *)getReceiptsInRoom:(nonnull NSString*)roomId forUserId:(nonnull NSString*)userId; + +/** + Retrieve the receipt for a user in a room within a specific thread. + + @param roomId The roomId + @param threadId The ID of the thread. kMXEventTimelineMain for the main timeline. + @param userId The user identifier @return the current stored receipt (nil by default). */ -- (MXReceiptData * _Nullable)getReceiptInRoom:(nonnull NSString*)roomId forUserId:(nonnull NSString*)userId; +- (nullable MXReceiptData *)getReceiptInRoom:(nonnull NSString*)roomId threadId:(nonnull NSString*)threadId forUserId:(nonnull NSString*)userId; /** Load receipts for a room asynchronously. @@ -297,6 +309,18 @@ */ - (NSUInteger)localUnreadEventCount:(nonnull NSString*)roomId threadId:(nullable NSString*)threadId withTypeIn:(nullable NSArray*)types; +/** + Count the unread events wrote in the store per thread. + + @discussion: The returned count is relative to the local storage. The actual unread messages + for a room may be higher than the returned value. + + @param roomId the room id. + @param types an array of event types strings (MXEventTypeString). + @return The number of unread events per thread which have their type listed in the provided array. + */ +- (nonnull NSDictionary *)localUnreadEventCountPerThread:(nonnull NSString*)roomId withTypeIn:(nullable NSArray*)types; + /** Incoming events since the last user receipt data. diff --git a/MatrixSDK/JSONModels/MXEvent.h b/MatrixSDK/JSONModels/MXEvent.h index aa3c38124f..3694226e1a 100644 --- a/MatrixSDK/JSONModels/MXEvent.h +++ b/MatrixSDK/JSONModels/MXEvent.h @@ -304,6 +304,11 @@ FOUNDATION_EXPORT NSString *const kMXJoinRulesContentKeyAllow; FOUNDATION_EXPORT NSString *const kMXJoinRulesContentKeyType; FOUNDATION_EXPORT NSString *const kMXJoinRulesContentKeyRoomId; +// Threads support + +FOUNDATION_EXPORT NSString *const kMXEventTimelineMain; +FOUNDATION_EXPORT NSString *const kMXEventUnthreaded; + /** The internal event state used to handle the different steps of the event sending. */ @@ -579,6 +584,13 @@ extern NSString *const kMXEventIdentifierKey; */ - (NSArray *)readReceiptEventIds; +/** + Returns the thread IDs for which a read receipt is defined in this event. + + This property is relevant only for events with 'kMXEventTypeStringReceipt' type. + */ +- (NSArray *)readReceiptThreadIds; + /** Returns the fully-qualified IDs of the users who sent read receipts with this event. @@ -645,7 +657,7 @@ extern NSString *const kMXEventIdentifierKey; Thread id for the event. This is actually the eventId of the thread's root event. nil if the event is not in a thread. */ -@property (nonatomic, readonly) NSString *threadId; +@property (nonatomic, readonly, nullable) NSString *threadId; #pragma mark - Crypto diff --git a/MatrixSDK/JSONModels/MXEvent.m b/MatrixSDK/JSONModels/MXEvent.m index 80d15868fe..6332557fb8 100644 --- a/MatrixSDK/JSONModels/MXEvent.m +++ b/MatrixSDK/JSONModels/MXEvent.m @@ -204,6 +204,11 @@ NSString *const kMXJoinRulesContentKeyType = @"type"; NSString *const kMXJoinRulesContentKeyRoomId = @"room_id"; +// Threads support + +NSString *const kMXEventTimelineMain = @"main"; +NSString *const kMXEventUnthreaded = @"unthreaded"; + #pragma mark - MXEvent @interface MXEvent () { @@ -574,6 +579,42 @@ - (NSArray *)readReceiptEventIds return list; } +- (NSArray *)readReceiptThreadIds +{ + NSMutableArray* list = nil; + + if (_wireEventType == MXEventTypeReceipt) + { + NSArray* eventIds = [_wireContent allKeys]; + list = [[NSMutableArray alloc] initWithCapacity:eventIds.count]; + + for (NSString* eventId in eventIds) + { + NSDictionary* eventDict = [_wireContent objectForKey:eventId]; + NSDictionary* readDict = [eventDict objectForKey:kMXEventTypeStringRead]; + + if (readDict) + { + NSArray* userDicts = [readDict allValues]; + + NSString *threadId; + for (NSDictionary *userDict in userDicts) + { + threadId = userDict[@"thread_id"]; + if (threadId) + { + break; + } + } + + [list addObject:threadId ?: kMXEventUnthreaded]; + } + } + } + + return list; +} + - (NSArray *)readReceiptSenders { NSMutableArray* list = nil; diff --git a/MatrixSDK/JSONModels/MXMatrixVersions.h b/MatrixSDK/JSONModels/MXMatrixVersions.h index d218ef32b5..b21a710c8a 100644 --- a/MatrixSDK/JSONModels/MXMatrixVersions.h +++ b/MatrixSDK/JSONModels/MXMatrixVersions.h @@ -108,6 +108,11 @@ extern const struct MXMatrixVersionsFeatureStruct MXMatrixVersionsFeature; */ @property (nonatomic, readonly) BOOL supportsQRLogin; +/** + Indicate if the server supports notifications for threads (MSC3773) + */ +@property (nonatomic, readonly) BOOL supportsNotificationsForThreads; + @end NS_ASSUME_NONNULL_END diff --git a/MatrixSDK/JSONModels/MXMatrixVersions.m b/MatrixSDK/JSONModels/MXMatrixVersions.m index 591e02c66f..4a76d69eb9 100644 --- a/MatrixSDK/JSONModels/MXMatrixVersions.m +++ b/MatrixSDK/JSONModels/MXMatrixVersions.m @@ -45,6 +45,7 @@ static NSString* const kJSONKeyMSC3881Unstable = @"org.matrix.msc3881"; static NSString* const kJSONKeyMSC3881 = @"org.matrix.msc3881.stable"; static NSString* const kJSONKeyMSC3882 = @"org.matrix.msc3882"; +static NSString* const kJSONKeyMSC3773 = @"org.matrix.msc3773"; @interface MXMatrixVersions () @@ -128,6 +129,10 @@ - (BOOL)supportsQRLogin return [self serverSupportsFeature:kJSONKeyMSC3882]; } +- (BOOL)supportsNotificationsForThreads { + return [self serverSupportsFeature:kJSONKeyMSC3773]; +} + #pragma mark - Private - (BOOL)serverSupportsVersion:(NSString *)version diff --git a/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.h b/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.h index 82cdb95753..9f13e19264 100644 --- a/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.h +++ b/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.h @@ -55,6 +55,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) MXRoomSyncUnreadNotifications *unreadNotifications; +/** + The notification counts per thread as per MSC3773. + */ +@property (nonatomic) NSDictionary *unreadNotificationsPerThread; + /** The room summary. Sent in case of lazy-loading of members. */ diff --git a/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.m b/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.m index 6867197724..5904fc33d8 100644 --- a/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.m +++ b/MatrixSDK/JSONModels/Sync/Room/MXRoomSync.m @@ -35,6 +35,22 @@ + (id)modelFromJSON:(NSDictionary *)JSONDictionary MXJSONModelSetMXJSONModel(roomSync.ephemeral, MXRoomSyncEphemeral, JSONDictionary[@"ephemeral"]); MXJSONModelSetMXJSONModel(roomSync.accountData, MXRoomSyncAccountData, JSONDictionary[@"account_data"]); MXJSONModelSetMXJSONModel(roomSync.unreadNotifications, MXRoomSyncUnreadNotifications, JSONDictionary[@"unread_notifications"]); + NSDictionary *threadNotifications; + MXJSONModelSetDictionary(threadNotifications, JSONDictionary[@"unread_thread_notifications"]); + if (threadNotifications) + { + NSMutableDictionary *unreadNotificationsPerThread = [NSMutableDictionary new]; + for (NSString *threadId in [threadNotifications allKeys]) + { + MXRoomSyncUnreadNotifications *unreadNotifications; + MXJSONModelSetMXJSONModel(unreadNotifications, MXRoomSyncUnreadNotifications, threadNotifications[threadId]); + if (unreadNotifications) + { + unreadNotificationsPerThread[threadId] = unreadNotifications; + } + } + roomSync.unreadNotificationsPerThread = unreadNotificationsPerThread; + } MXJSONModelSetMXJSONModel(roomSync.summary, MXRoomSyncSummary, JSONDictionary[@"summary"]); } return roomSync; diff --git a/MatrixSDK/MXRestClient.h b/MatrixSDK/MXRestClient.h index e09f87481f..3e02997084 100644 --- a/MatrixSDK/MXRestClient.h +++ b/MatrixSDK/MXRestClient.h @@ -2270,6 +2270,7 @@ Note: Clients should consider avoiding this endpoint for URLs posted in encrypte */ - (MXHTTPOperation*)sendReadReceipt:(NSString*)roomId eventId:(NSString*)eventId + threadId:(nullable NSString*)threadId success:(void (^)(void))success failure:(void (^)(NSError *error))failure NS_REFINED_FOR_SWIFT; diff --git a/MatrixSDK/MXRestClient.m b/MatrixSDK/MXRestClient.m index 1e95d180b6..281bfeb37d 100644 --- a/MatrixSDK/MXRestClient.m +++ b/MatrixSDK/MXRestClient.m @@ -4074,6 +4074,7 @@ - (MXHTTPOperation *)syncFromToken:(NSString*)token #pragma mark - read receipt - (MXHTTPOperation*)sendReadReceipt:(NSString*)roomId eventId:(NSString*)eventId + threadId:(NSString*)threadId success:(void (^)(void))success failure:(void (^)(NSError *error))failure { @@ -4081,11 +4082,17 @@ - (MXHTTPOperation*)sendReadReceipt:(NSString*)roomId apiPathPrefix, roomId, [MXTools encodeURIComponent:eventId]]; + + NSMutableDictionary *parameters = [NSMutableDictionary new]; + if (threadId) + { + parameters[@"thread_id"] = threadId; + } MXWeakify(self); return [httpClient requestWithMethod:@"POST" path:path - parameters:[[NSDictionary alloc] init] + parameters:parameters success:^(NSDictionary *JSONResponse) { MXStrongifyAndReturnIfNil(self); [self dispatchSuccess:success]; diff --git a/MatrixSDK/MXSession.m b/MatrixSDK/MXSession.m index 54984b3fcc..1a5494ee3b 100644 --- a/MatrixSDK/MXSession.m +++ b/MatrixSDK/MXSession.m @@ -288,7 +288,11 @@ - (id)initWithMatrixRestClient:(MXRestClient*)mxRestClient kMXEventTypeStringCallHangup, kMXEventTypeStringCallReject, kMXEventTypeStringCallNegotiate, - kMXEventTypeStringSticker + kMXEventTypeStringSticker, + kMXEventTypeStringPollStart, + kMXEventTypeStringPollEnd, + kMXEventTypeStringPollStartMSC3381, + kMXEventTypeStringPollEndMSC3381 ]; _unreadEventTypes = @[kMXEventTypeStringRoomName, @@ -2082,7 +2086,7 @@ - (void)handleBackgroundSyncCacheIfRequiredWithCompletion:(void (^)(void))comple { BOOL isInValidState = _state == MXSessionStateStoreDataReady || _state == MXSessionStatePaused; if (!isInValidState) { - NSString *message = [NSString stringWithFormat:@"[MXSession] state %@ is not valid to handle background sync cache, investigate why the method was called", [MXTools readableSessionState:_state]]; + NSString *message = [NSString stringWithFormat:@"[MXSession] handleBackgroundSyncCacheIfRequired: state %@ is not valid to handle background sync cache, investigate why the method was called", [MXTools readableSessionState:_state]]; MXLogFailure(message); if (completion) { @@ -4928,6 +4932,12 @@ - (void)onDidDecryptEvent:(NSNotification *)notification MXRoomSummary *summary = [self roomSummaryWithRoomId:event.roomId]; if (summary) { + if (!summary.lastMessage) + { + [summary resetLastMessage:nil failure:nil commit:YES]; + return; + } + [self eventWithEventId:summary.lastMessage.eventId inRoom:summary.roomId success:^(MXEvent *lastEvent) { diff --git a/MatrixSDK/MatrixSDKVersion.m b/MatrixSDK/MatrixSDKVersion.m index ba107ccada..c6a0aaa67f 100644 --- a/MatrixSDK/MatrixSDKVersion.m +++ b/MatrixSDK/MatrixSDKVersion.m @@ -16,4 +16,4 @@ #import -NSString *const MatrixSDKVersion = @"0.24.2"; +NSString *const MatrixSDKVersion = @"0.24.3"; diff --git a/MatrixSDK/Threads/MXThreadNotificationsCount.swift b/MatrixSDK/Threads/MXThreadNotificationsCount.swift index f9246dea6b..87aff24cd7 100644 --- a/MatrixSDK/Threads/MXThreadNotificationsCount.swift +++ b/MatrixSDK/Threads/MXThreadNotificationsCount.swift @@ -25,14 +25,19 @@ public class MXThreadNotificationsCount: NSObject { /// Number of highlighted threads in a specific room public let numberOfHighlightedThreads: UInt + /// Number of notifications in threads in a specific room + public let notificationsNumber: UInt + /// Initializer /// - Parameters: /// - numberOfNotifiedThreads: number of notified threads /// - numberOfHighlightedThreads: number of highlighted threads public init(numberOfNotifiedThreads: UInt, - numberOfHighlightedThreads: UInt) { + numberOfHighlightedThreads: UInt, + notificationsNumber: UInt) { self.numberOfNotifiedThreads = numberOfNotifiedThreads self.numberOfHighlightedThreads = numberOfHighlightedThreads + self.notificationsNumber = notificationsNumber super.init() } diff --git a/MatrixSDK/Threads/MXThreadingService.swift b/MatrixSDK/Threads/MXThreadingService.swift index f659614797..43fb3b8eb5 100644 --- a/MatrixSDK/Threads/MXThreadingService.swift +++ b/MatrixSDK/Threads/MXThreadingService.swift @@ -130,10 +130,17 @@ public class MXThreadingService: NSObject { /// - Parameter roomId: Room identifier /// - Returns: Notifications count public func notificationsCount(forRoom roomId: String) -> MXThreadNotificationsCount { - let notified = unsortedParticipatedThreads(inRoom: roomId).filter { $0.notificationCount > 0 }.count - let highlighted = unsortedThreads(inRoom: roomId).filter { $0.highlightCount > 0 }.count - return MXThreadNotificationsCount(numberOfNotifiedThreads: UInt(notified), - numberOfHighlightedThreads: UInt(highlighted)) + var notified: UInt = 0 + var highlighted: UInt = 0 + var notificationsNumber: UInt = 0 + for thread in unsortedThreads(inRoom: roomId) { + notified += thread.notificationCount > 0 ? 1 : 0 + highlighted += thread.highlightCount > 0 ? 1 : 0 + notificationsNumber += thread.notificationCount + } + return MXThreadNotificationsCount(numberOfNotifiedThreads: notified, + numberOfHighlightedThreads: highlighted, + notificationsNumber: notificationsNumber) } /// Method to check an event is a thread root or not @@ -168,6 +175,21 @@ public class MXThreadingService: NSObject { thread.markAsRead() notifyDidUpdateThreads() } + + @discardableResult + public func allThreads(inRoomWithId roomId: String, + onlyParticipated: Bool, + completion: @escaping ([MXThreadProtocol]) -> Void) -> MXHTTPOperation? { + return allThreads(inRoom: roomId, onlyParticipated: onlyParticipated) { response in + switch response { + case .success(let threads): + completion(threads) + case .failure(let error): + MXLog.warning("[MXThreadingService] allThreads failed with error: \(error)") + completion([]) + } + } + } @discardableResult public func allThreads(inRoom roomId: String, diff --git a/MatrixSDK/Utils/Logs/MXAnalyticsDestination.swift b/MatrixSDK/Utils/Logs/MXAnalyticsDestination.swift index b3a82cb2bb..c7b8ef31c8 100644 --- a/MatrixSDK/Utils/Logs/MXAnalyticsDestination.swift +++ b/MatrixSDK/Utils/Logs/MXAnalyticsDestination.swift @@ -45,7 +45,7 @@ class MXAnalyticsDestination: BaseDestination { return dictionary } else if let error = context as? Error { return [ - "error": error.localizedDescription + "error": error ] } else { return [ diff --git a/MatrixSDKTests/Crypto/Algorithms/RoomEvents/MXRoomEventDecryptionUnitTests.swift b/MatrixSDKTests/Crypto/Algorithms/RoomEvents/MXRoomEventDecryptionUnitTests.swift new file mode 100644 index 0000000000..99a7937577 --- /dev/null +++ b/MatrixSDKTests/Crypto/Algorithms/RoomEvents/MXRoomEventDecryptionUnitTests.swift @@ -0,0 +1,201 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +@testable import MatrixSDK + +#if DEBUG + +import MatrixSDKCrypto + +class MXRoomEventDecryptionUnitTests: XCTestCase { + class DecryptorStub: CryptoIdentityStub, MXCryptoRoomEventDecrypting { + enum Error: Swift.Error { + case cannotDecrypt + } + + var stubbedEvents = [String: DecryptedEvent]() + func decryptRoomEvent(_ event: MXEvent) throws -> DecryptedEvent { + guard let decrypted = stubbedEvents[event.eventId] else { + throw Error.cannotDecrypt + } + return decrypted + } + + func requestRoomKey(event: MXEvent) async throws { + } + } + + var decryptor: DecryptorStub! + var roomDecryptor: MXRoomEventDecryption! + + override func setUp() { + decryptor = DecryptorStub() + roomDecryptor = MXRoomEventDecryption(handler: decryptor) + } + + // MARK: - Decrypt + + func test_decrypt_returnsDecryptionResults() async { + let plain = [ + "text": "hello" + ] + let event = MXEvent.encryptedFixture( + id: "1", + sessionId: "123" + ) + decryptor.stubbedEvents = [ + "1": .stub(clearEvent: plain) + ] + + let results = await roomDecryptor.decrypt(events: [event]) + + XCTAssertEqual(results.first?.clearEvent as? [String: String], plain) + } + + func test_decrypt_returnsDecryptedAndErrorResults() async { + let plain = [ + "text": "hello" + ] + let events: [MXEvent] = [ + .encryptedFixture( + id: "1", + sessionId: "123" + ), + .encryptedFixture( + id: "2", + sessionId: "456" + ), + .encryptedFixture( + id: "3", + sessionId: "123" + ) + ] + + decryptor.stubbedEvents = [ + "2": .stub(clearEvent: plain) + ] + + let results = await roomDecryptor.decrypt(events: events) + + XCTAssertEqual(results.count, 3) + XCTAssertNotNil(results[0].error) + XCTAssertEqual(results[1].clearEvent as? [String: String], plain) + XCTAssertNotNil(results[2].error) + } + + // MARK: - Room key + + func test_handlePossibleRoomKeyEvent_doesNothingIfInvalidRoomKeyEvent() async { + let events = await prepareEventsForRedecryption() + let invalidEvent = MXEvent.fixture(id: 123) + + await roomDecryptor.handlePossibleRoomKeyEvent(invalidEvent) + await waitForDecryption() + + XCTAssertNil(events[0].clear) + XCTAssertNil(events[1].clear) + XCTAssertNil(events[2].clear) + } + + func test_handlePossibleRoomKeyEvent_decryptsMatchingEventsOnRoomKey() async { + let events = await prepareEventsForRedecryption() + let roomKey = MXEvent.roomKeyFixture(sessionId: "123") + + await roomDecryptor.handlePossibleRoomKeyEvent(roomKey) + await waitForDecryption() + + XCTAssertNotNil(events[0].clear) + XCTAssertNil(events[1].clear) + XCTAssertNotNil(events[2].clear) + } + + func test_handlePossibleRoomKeyEvent_decryptsMatchingEventsOnForwardedRoomKey() async { + let events = await prepareEventsForRedecryption() + let roomKey = MXEvent.forwardedRoomKeyFixture(sessionId: "123") + + await roomDecryptor.handlePossibleRoomKeyEvent(roomKey) + await waitForDecryption() + + XCTAssertNotNil(events[0].clear) + XCTAssertNil(events[1].clear) + XCTAssertNotNil(events[2].clear) + } + + // MARK: - Retry all + + func test_retryUndecryptedEvents() async { + let events = await prepareEventsForRedecryption() + + await roomDecryptor.retryUndecryptedEvents(sessionIds: ["123", "456"]) + await waitForDecryption() + + XCTAssertNotNil(events[0].clear) + XCTAssertNotNil(events[1].clear) + XCTAssertNotNil(events[2].clear) + } + + // MARK: - Helpers + + private func prepareEventsForRedecryption() async -> [MXEvent] { + // We assume two sessions, only one of which will later recieve a key + let session1 = "123" + let session2 = "456" + + // Prepare three events, encrypted with either of the two sessions + let events: [MXEvent] = [ + .encryptedFixture( + id: "1", + sessionId: session1 + ), + .encryptedFixture( + id: "2", + sessionId: session2 + ), + .encryptedFixture( + id: "3", + sessionId: session1 + ) + ] + + // Attempt to decrypt these events, which will produce errors + // and add them to an internal undecrypted events cache + let results = await roomDecryptor.decrypt(events: events) + for (event, result) in zip(events, results) { + event.setClearData(result) + } + + // Now stub out decryption result so that if these events are decrypted again + // we get the correct result + let decrypted = DecryptedEvent.stub(clearEvent: ["type": "m.decrypted"]) + decryptor.stubbedEvents = [ + "1": decrypted, + "2": decrypted, + "3": decrypted + ] + + return events + } + + private func waitForDecryption() async { + // When decrypting successfully, a notification will be triggered on the main thread, so we have to + // make sure we wait until this happens. We cannot listen to notifications directly, because + // repeated decryption failures will not trigger any. Instead simply wait a little while. + try! await Task.sleep(nanoseconds: 1_000_000) + } +} + +#endif diff --git a/MatrixSDKTests/Crypto/CryptoMachine/DecryptedEvent+Stub.swift b/MatrixSDKTests/Crypto/CryptoMachine/DecryptedEvent+Stub.swift new file mode 100644 index 0000000000..5d1c1e6586 --- /dev/null +++ b/MatrixSDKTests/Crypto/CryptoMachine/DecryptedEvent+Stub.swift @@ -0,0 +1,37 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +#if DEBUG + +import MatrixSDKCrypto + +extension DecryptedEvent { + static func stub( + clearEvent: [AnyHashable: Any] + ) -> DecryptedEvent { + return .init( + clearEvent: MXTools.serialiseJSONObject(clearEvent), + senderCurve25519Key: "", + claimedEd25519Key: nil, + forwardingCurve25519Chain: [], + verificationState: .trusted + ) + } +} + +#endif diff --git a/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoMachineUnitTests.swift b/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoMachineUnitTests.swift index c1984ba5d6..450f539d4c 100644 --- a/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoMachineUnitTests.swift +++ b/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoMachineUnitTests.swift @@ -16,20 +16,21 @@ import Foundation -#if DEBUG +#if DEBUG && os(iOS) import MatrixSDKCrypto @testable import MatrixSDK class MXCryptoMachineUnitTests: XCTestCase { + var userId = "@alice:matrix.org" var restClient: MXRestClient! var machine: MXCryptoMachine! override func setUp() { restClient = MXRestClientStub() machine = try! MXCryptoMachine( - userId: "@alice:matrix.org", + userId: userId, deviceId: "ABCD", restClient: restClient, getRoomAction: { @@ -37,6 +38,18 @@ class MXCryptoMachineUnitTests: XCTestCase { }) } + override func tearDown() { + do { + let url = try MXCryptoMachine.storeURL(for: userId) + guard FileManager.default.fileExists(atPath: url.path) else { + return + } + try FileManager.default.removeItem(at: url) + } catch { + XCTFail("Cannot tear down test - \(error)") + } + } + func test_handleSyncResponse_canProcessEmptyResponse() throws { let result = try machine.handleSyncResponse( toDevice: nil, diff --git a/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoProtocolStubs.swift b/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoProtocolStubs.swift index 724d399abf..7330babed9 100644 --- a/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoProtocolStubs.swift +++ b/MatrixSDKTests/Crypto/CryptoMachine/MXCryptoProtocolStubs.swift @@ -58,16 +58,16 @@ class UserIdentitySourceStub: CryptoIdentityStub, MXCryptoUserIdentitySource { } func isUserTracked(userId: String) -> Bool { - return false + return true } - func updateTrackedUsers(users: [String]) async throws { + func downloadKeys(users: [String]) async throws { } - func manuallyVerifyUser(userId: String) async throws { + func verifyUser(userId: String) async throws { } - func manuallyVerifyDevice(userId: String, deviceId: String) async throws { + func verifyDevice(userId: String, deviceId: String) async throws { } func setLocalTrust(userId: String, deviceId: String, trust: LocalTrust) throws { @@ -105,16 +105,16 @@ class CryptoCrossSigningStub: CryptoIdentityStub, MXCryptoCrossSigning { } func isUserTracked(userId: String) -> Bool { - return false + return true } - func updateTrackedUsers(users: [String]) async throws { + func downloadKeys(users: [String]) async throws { } - func manuallyVerifyUser(userId: String) async throws { + func verifyUser(userId: String) async throws { } - func manuallyVerifyDevice(userId: String, deviceId: String) async throws { + func verifyDevice(userId: String, deviceId: String) async throws { } func setLocalTrust(userId: String, deviceId: String, trust: LocalTrust) throws { diff --git a/MatrixSDKTests/Crypto/Data/MXMegolmSessionDataUnitTests.swift b/MatrixSDKTests/Crypto/Data/MXMegolmSessionDataUnitTests.swift index cbbb04308b..45f6ec5f37 100644 --- a/MatrixSDKTests/Crypto/Data/MXMegolmSessionDataUnitTests.swift +++ b/MatrixSDKTests/Crypto/Data/MXMegolmSessionDataUnitTests.swift @@ -71,7 +71,7 @@ class MXMegolmSessionDataUnitTests: XCTestCase { data.algorithm = "G" data.forwardingCurve25519KeyChain = ["H", "I"] - let json = data.jsonDictionary() as NSDictionary + let json = data.jsonDictionary() as? NSDictionary XCTAssertEqual(json, [ "sender_key": "A", @@ -85,4 +85,19 @@ class MXMegolmSessionDataUnitTests: XCTestCase { "untrusted": false ]) } + + func testInvalidJsonDictionary() { + let data = MXMegolmSessionData() + data.senderKey = nil + data.senderClaimedKeys = nil + data.roomId = nil + data.sessionId = nil + data.sessionKey = nil + data.algorithm = nil + data.forwardingCurve25519KeyChain = nil + + let json = data.jsonDictionary() as? NSDictionary + + XCTAssertNil(json) + } } diff --git a/MatrixSDKTests/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngineUnitTests.swift b/MatrixSDKTests/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngineUnitTests.swift index ea98ff422d..6c3ec3a150 100644 --- a/MatrixSDKTests/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngineUnitTests.swift +++ b/MatrixSDKTests/Crypto/KeyBackup/Engine/MXCryptoKeyBackupEngineUnitTests.swift @@ -22,12 +22,31 @@ import Foundation import MatrixSDKCrypto class MXCryptoKeyBackupEngineUnitTests: XCTestCase { + actor DecryptorSpy: MXRoomEventDecrypting { + func decrypt(events: [MXEvent]) -> [MXEventDecryptionResult] { + return [] + } + + func handlePossibleRoomKeyEvent(_ event: MXEvent) { + } + + var spySessionIds: [String] = [] + func retryUndecryptedEvents(sessionIds: [String]) { + spySessionIds = sessionIds + } + + func resetUndecryptedEvents() { + } + } + + var decryptor: DecryptorSpy! var backup: CryptoBackupStub! var engine: MXCryptoKeyBackupEngine! override func setUp() { + decryptor = DecryptorSpy() backup = CryptoBackupStub() - engine = MXCryptoKeyBackupEngine(backup: backup) + engine = MXCryptoKeyBackupEngine(backup: backup, roomEventDecryptor: decryptor) } func test_createsBackupKeyFromVersion() { @@ -199,7 +218,11 @@ class MXCryptoKeyBackupEngineUnitTests: XCTestCase { XCTAssertEqual(Set(sessionIds), ["1", "2", "3"]) XCTAssertEqual(Set(roomIds), ["A", "B"]) - exp.fulfill() + Task { + let sessionIds = await self.decryptor.spySessionIds + XCTAssertEqual(Set(sessionIds), ["1", "2", "3"]) + exp.fulfill() + } }, failure: { XCTFail("Importing failed with error \($0)") diff --git a/MatrixSDKTests/JSONModels/MXEventFixtures.swift b/MatrixSDKTests/JSONModels/MXEventFixtures.swift index a5d2b0cc4b..61bf6a1330 100644 --- a/MatrixSDKTests/JSONModels/MXEventFixtures.swift +++ b/MatrixSDKTests/JSONModels/MXEventFixtures.swift @@ -119,4 +119,18 @@ extension MXEvent { event.setClearData(result) return event } + + static func encryptedFixture( + id: String = "1", + sessionId: String = "123" + ) -> MXEvent { + return MXEvent(fromJSON: [ + "type": "m.room.encrypted", + "event_id": id, + "content": [ + "algorithm": kMXCryptoMegolmAlgorithm, + "session_id": sessionId + ] + ])! + } } diff --git a/MatrixSDKTests/MXMatrixVersionsUnitTests.swift b/MatrixSDKTests/MXMatrixVersionsUnitTests.swift index 2a2d377798..80822249ae 100644 --- a/MatrixSDKTests/MXMatrixVersionsUnitTests.swift +++ b/MatrixSDKTests/MXMatrixVersionsUnitTests.swift @@ -42,7 +42,8 @@ class MXMatrixVersionsUnitTests: XCTestCase { "org.matrix.msc3026.busy_presence": true, "org.matrix.msc2285.stable": true, "org.matrix.msc3881.stable": true, - "org.matrix.msc3882": true + "org.matrix.msc3882": true, + "org.matrix.msc3773": true ], "versions": [ "r0.0.1", @@ -77,6 +78,7 @@ class MXMatrixVersionsUnitTests: XCTestCase { "org.matrix.msc2285.stable": false, "org.matrix.msc3881.stable": false, "org.matrix.msc3882": false, + "org.matrix.msc3773": false ], "versions": [ "r0.0.1", @@ -153,6 +155,7 @@ class MXMatrixVersionsUnitTests: XCTestCase { XCTAssertFalse(versions.doesServerSupportSeparateAddAndBind, "versions shouldn't support separate Add And Bind") XCTAssertFalse(versions.supportsRemotelyTogglingPushNotifications, "versions shouldn't support remotely toggling push notifications") XCTAssertFalse(versions.supportsQRLogin, "versions shouldn't support QR login") + XCTAssertFalse(versions.supportsNotificationsForThreads, "versions shouldn't support notifications for threads") } func testFullSupportVersions() throws { @@ -172,6 +175,7 @@ class MXMatrixVersionsUnitTests: XCTestCase { XCTAssertTrue(versions.doesServerSupportSeparateAddAndBind, "versions should support separate Add And Bind") XCTAssertTrue(versions.supportsRemotelyTogglingPushNotifications, "versions should support remotely toggling push notifications") XCTAssertTrue(versions.supportsQRLogin, "versions should support QR login") + XCTAssertTrue(versions.supportsNotificationsForThreads, "versions shouldn support notifications for threads") } func testNoSupportVersions() throws { @@ -191,5 +195,6 @@ class MXMatrixVersionsUnitTests: XCTestCase { XCTAssertFalse(versions.doesServerSupportSeparateAddAndBind, "versions shouldn't support separate Add And Bind") XCTAssertFalse(versions.supportsRemotelyTogglingPushNotifications, "versions shouldn't support remotely toggling push notifications") XCTAssertFalse(versions.supportsQRLogin, "versions shouldn't support QR login") + XCTAssertFalse(versions.supportsNotificationsForThreads, "versions shouldn't support notifications for threads") } } diff --git a/MatrixSDKTests/MXNotificationCenterTests.m b/MatrixSDKTests/MXNotificationCenterTests.m index d4366425ae..a22465f6b4 100644 --- a/MatrixSDKTests/MXNotificationCenterTests.m +++ b/MatrixSDKTests/MXNotificationCenterTests.m @@ -275,7 +275,7 @@ - (void)testDefaultDisplayNameCondition }]; } -- (void)testDefaultRoomMemberCountCondition +- (void)testDefaultEventMatchCondition { [matrixSDKTestsData doMXSessionTestWithBobAndAliceInARoom:self readyToTest:^(MXSession *bobSession, MXRestClient *aliceRestClient, NSString *roomId, XCTestExpectation *expectation) { @@ -288,8 +288,8 @@ - (void)testDefaultRoomMemberCountCondition XCTAssert(rule.isDefault, @"The rule must be the server default rule. Rule: %@", rule); MXPushRuleCondition *condition = rule.conditions[0]; - XCTAssertEqualObjects(condition.kind, kMXPushRuleConditionStringRoomMemberCount, @"The default content rule with room_member_count condition must fire first"); - XCTAssertEqual(condition.kindType, MXPushRuleConditionTypeRoomMemberCount); + XCTAssertEqualObjects(condition.kind, kMXPushRuleConditionStringEventMatch, @"The default content rule with room_member_count condition must fire first"); + XCTAssertEqual(condition.kindType, MXPushRuleConditionTypeEventMatch); // Check the right event has been notified XCTAssertEqualObjects(event.content[kMXMessageBodyKey], messageFromAlice, @"The wrong messsage has been caught. event: %@", event); diff --git a/MatrixSDKTests/MXReceiptDataIntegrationTests.swift b/MatrixSDKTests/MXReceiptDataIntegrationTests.swift new file mode 100644 index 0000000000..f1463ba891 --- /dev/null +++ b/MatrixSDKTests/MXReceiptDataIntegrationTests.swift @@ -0,0 +1,495 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class MXReceiptDataIntegrationTests: XCTestCase { + + private var testData: MatrixSDKTestsData! + private var e2eTestData: MatrixSDKTestsE2EData! + + override func setUp() { + super.setUp() + testData = MatrixSDKTestsData() + e2eTestData = MatrixSDKTestsE2EData(matrixSDKTestsData: testData) + MXSDKOptions.sharedInstance().enableThreads = true + } + + override func tearDown() { + testData = nil + e2eTestData = nil + super.tearDown() + } + + // MARK - Tests + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Store Bob threaded RR and Alice threaded RR in the local store for main timeline + /// - Expect to have only Alice's RR stored for the main timeline + func testReadReceiptsStorageInMainTimeline() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + e2eTestData.doE2ETestWithAliceAndBob(inARoom: self, cryptedBob: true, warnOnUnknowDevices: false, aliceStore: aliceStore, bobStore: bobStore) { aliceSession, bobSession, roomId, expectation in + guard let bobSession = bobSession, + let aliceSession = aliceSession, + let bobRoom = bobSession.room(withRoomId: roomId), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + return + } + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success(let eventId): + + guard bobRoom.storeLocalReceipt(kMXEventTypeStringRead, eventId: eventId, threadId: kMXEventTimelineMain, userId: bobSession.myUserId, ts: UInt64(Date().timeIntervalSince1970 * 1000)) else { + XCTFail("failed to store bob RR in main timeline.") + expectation.fulfill() + return + } + + bobRoom.getEventReceipts(eventId ?? "", threadId: kMXEventTimelineMain, sorted: true) { receiptDataList in + guard receiptDataList.count == 0 else { + XCTFail("event should have no read receipt as off now.") + expectation.fulfill() + return + } + + guard bobRoom.storeLocalReceipt(kMXEventTypeStringRead, eventId: eventId, threadId: kMXEventTimelineMain, userId: aliceSession.myUserId, ts: UInt64(Date().timeIntervalSince1970 * 1000)) else { + XCTFail("failed to store alice RR in main timeline.") + expectation.fulfill() + return + } + + bobRoom.getEventReceipts(eventId ?? "", threadId: kMXEventTimelineMain, sorted: true) { receiptDataList in + guard receiptDataList.count == 1 else { + XCTFail("event should have just 1 read receipt in main timeline.") + expectation.fulfill() + return + } + + let aliceReceiptData = receiptDataList[0] + XCTAssertEqual(aliceReceiptData.userId, aliceSession.myUserId, "read receipt should be attributed to alice") + XCTAssertEqual(aliceReceiptData.threadId, kMXEventTimelineMain, "read receipt should be related to the main timeline") + + expectation.fulfill() + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Send a text message A1 as a thread of the message A + /// - Store Alice threaded RR in the local store for thread A + /// - Expect to have Alice's RR stored for the thread A from Bob's POV + /// - Expect to have Bob RR stored for the thread A from Alice's POV + func testReadReceiptsStorageInThread() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + e2eTestData.doE2ETestWithAliceAndBob(inARoom: self, cryptedBob: true, warnOnUnknowDevices: false, aliceStore: aliceStore, bobStore: bobStore) { aliceSession, bobSession, roomId, expectation in + guard let bobSession = bobSession, + let aliceSession = aliceSession, + let bobRoom = bobSession.room(withRoomId: roomId), + let aliceRoom = aliceSession.room(withRoomId: roomId), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + return + } + + let bobThreadingService = bobSession.threadingService + let aliceThreadingService = aliceSession.threadingService + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success(let eventId): + guard let threadId = eventId else { + XCTFail("Failed to setup test conditions") + expectation.fulfill() + return + } + + bobRoom.sendTextMessage("Thread message", threadId: threadId, localEcho: &localEcho) { response2 in + switch response2 { + case .success(let eventId): + guard let eventId = eventId else { + XCTFail("eventId must not be nil") + expectation.fulfill() + return + } + + guard bobRoom.storeLocalReceipt(kMXEventTypeStringRead, eventId: eventId, threadId: threadId, userId: aliceSession.myUserId, ts: UInt64(Date().timeIntervalSince1970 * 1000)) else { + XCTFail("failed to store alice RR in main timeline.") + expectation.fulfill() + return + } + + bobRoom.getEventReceipts(eventId, threadId: threadId, sorted: false) { receiptDataList in + XCTAssertEqual(receiptDataList.count, 1, "event should have just 1 read receipt for the thread") + guard let receiptData = receiptDataList.first else { + XCTFail("event should have at least 1 read receipt") + return + } + XCTAssertEqual(receiptData.userId, aliceSession.myUserId, "read receipt should be attributed to alice") + XCTAssertEqual(receiptData.threadId, threadId, "read receipt should be related to current thread") + } + + bobThreadingService.addDelegate(MockThreadingServiceDelegate(withNewThreadBlock: { _ in + bobThreadingService.removeAllDelegates() + aliceThreadingService.allThreads(inRoom: aliceRoom.roomId, completion: { response in + switch response { + case .success(let threads): + guard let thread = threads.first else { + XCTFail("Thread must be created") + expectation.fulfill() + return + } + + XCTAssertEqual(thread.id, threadId, "Thread must have the correct id") + XCTAssertEqual(thread.roomId, aliceRoom.roomId, "Thread must have the correct room id") + XCTAssertEqual(thread.lastMessage?.eventId, eventId, "Thread last message must have the correct event id") + XCTAssertNotNil(thread.rootMessage, "Thread must have the root event") + XCTAssertEqual(thread.numberOfReplies, 1, "Thread must have only 1 reply") + + aliceRoom.getEventReceipts(eventId, threadId: threadId, sorted: false, completion: { receiptList in + guard let readReceipt = receiptList.first else { + XCTFail("The RR list should contain at least 1 read receipt") + expectation.fulfill() + return + } + XCTAssertEqual(receiptList.count, 1, "The RR list should contain only 1 read receipt") + XCTAssertEqual(readReceipt.threadId, threadId, "The RR should be related to crrent thread") + XCTAssertEqual(readReceipt.userId, bobSession.myUserId, "The RR should be sent by Bob") + + expectation.fulfill() + }) + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + }) + })) + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Send a text message A1 as a thread of the message A + /// - Store Alice unthreaded RR in the local store + /// - Expect to have Alice's RR stored for the main timeline + /// - Expect to have Alice's RR stored for the thread A + func testUnthreadedReadReceiptsStorage() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + e2eTestData.doE2ETestWithAliceAndBob(inARoom: self, cryptedBob: true, warnOnUnknowDevices: false, aliceStore: aliceStore, bobStore: bobStore) { aliceSession, bobSession, roomId, expectation in + guard let bobSession = bobSession, + let aliceSession = aliceSession, + let bobRoom = bobSession.room(withRoomId: roomId), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + return + } + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success(let eventId): + guard let threadId = eventId else { + XCTFail("Failed to setup test conditions") + expectation.fulfill() + return + } + + bobRoom.sendTextMessage("Thread message", threadId: threadId, localEcho: &localEcho) { response2 in + switch response2 { + case .success(let eventId): + guard let eventId = eventId else { + XCTFail("eventId must not be nil") + expectation.fulfill() + return + } + + guard bobRoom.storeLocalReceipt(kMXEventTypeStringRead, eventId: eventId, threadId: nil, userId: aliceSession.myUserId, ts: UInt64(Date().timeIntervalSince1970 * 1000)) else { + XCTFail("failed to store alice RR in main timeline.") + expectation.fulfill() + return + } + + bobRoom.getEventReceipts(eventId, threadId: kMXEventTimelineMain, sorted: false) { receiptDataList in + XCTAssertEqual(receiptDataList.count, 1, "event should have just 1 read receipt for the main timeline") + guard let receiptData = receiptDataList.first else { + XCTFail("event should have at least 1 read receipt") + return + } + XCTAssertEqual(receiptData.userId, aliceSession.myUserId, "read receipt should be attributed to alice") + XCTAssertNil(receiptData.threadId, "read receipt should be unthreaded") + } + + bobRoom.getEventReceipts(eventId, threadId: threadId, sorted: false) { receiptDataList in + XCTAssertEqual(receiptDataList.count, 1, "event should have just 1 read receipt for the thread") + guard let receiptData = receiptDataList.first else { + XCTFail("event should have at least 1 read receipt") + return + } + XCTAssertEqual(receiptData.userId, aliceSession.myUserId, "read receipt should be attributed to alice") + XCTAssertNil(receiptData.threadId, "read receipt should be unthreaded") + } + + expectation.fulfill() + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Acknowledge message from Alice's main timeline + /// - Wait for Alice's RR in Bob's main timeline + /// - Expect to retrieve Alice's RR in Bob's main timeline + func testAcknowledgeMessageInMainTimeline() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + e2eTestData.doE2ETestWithAliceAndBob(inARoom: self, cryptedBob: true, warnOnUnknowDevices: false, aliceStore: aliceStore, bobStore: bobStore) { aliceSession, bobSession, roomId, expectation in + guard let bobSession = bobSession, + let aliceSession = aliceSession, + let bobRoom = bobSession.room(withRoomId: roomId), + let aliceRoom = aliceSession.room(withRoomId: roomId), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + return + } + + aliceRoom.liveTimeline { timeline in + let _ = timeline?.listenToEvents([.roomMessage], { event, direction, roomState in + aliceRoom.acknowledgeEvent(event, andUpdateReadMarker: true) + }) + } + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success(let eventId): + + guard let eventId = eventId else { + XCTFail("eventId shouldn't be nil") + expectation.fulfill() + return + } + + bobRoom.liveTimeline { timeline in + let _ = timeline?.listenToEvents([.receipt], { event, direction, roomState in + bobRoom.getEventReceipts(eventId, threadId: kMXEventTimelineMain, sorted: true) { receiptDataList in + guard !receiptDataList.isEmpty else { + return + } + + let receiptData = receiptDataList[0] + XCTAssertEqual(receiptData.threadId, kMXEventTimelineMain, "The RR should be related to crrent thread") + XCTAssertEqual(receiptData.userId, aliceSession.myUserId, "The RR should be sent by Bob") + + expectation.fulfill() + } + }) + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } + + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Send a text message A1 as a thread of the message A + /// - Acknowledge message A1 from Alice's thread timeline + /// - Wait for Alice's threaded RR in Bob's thread timeline + /// - Expect to retrieve Alice's threaded RR in Bob's thread timeline + func testAcknowledgeMessageInThread() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + e2eTestData.doE2ETestWithAliceAndBob(inARoom: self, cryptedBob: true, warnOnUnknowDevices: false, aliceStore: aliceStore, bobStore: bobStore) { aliceSession, bobSession, roomId, expectation in + guard let bobSession = bobSession, + let aliceSession = aliceSession, + let bobRoom = bobSession.room(withRoomId: roomId), + let aliceRoom = aliceSession.room(withRoomId: roomId), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + return + } + + let bobThreadingService = bobSession.threadingService + let aliceThreadingService = aliceSession.threadingService + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success(let eventId): + guard let threadId = eventId else { + XCTFail("Failed to setup test conditions") + expectation.fulfill() + return + } + + bobRoom.sendTextMessage("Thread message", threadId: threadId, localEcho: &localEcho) { response2 in + switch response2 { + case .success: + + bobRoom.liveTimeline({ timeline in + }) + + bobThreadingService.addDelegate(MockThreadingServiceDelegate(withNewThreadBlock: { _ in + bobThreadingService.removeAllDelegates() + aliceThreadingService.allThreads(inRoom: aliceRoom.roomId, completion: { response in + switch response { + case .success: + + guard let bobThread = bobSession.threadingService.thread(withId: threadId) else { + XCTFail("Unable to retrieve thread within Bob's session") + expectation.fulfill() + return + } + + guard let aliceThread = aliceSession.threadingService.thread(withId: threadId) else { + XCTFail("Unable to retrieve thread within Alice's session") + expectation.fulfill() + return + } + + aliceThread.liveTimeline({ timeline in + let _ = timeline.listenToEvents([.roomMessage], { event, direction, roomState in + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + aliceSession.matrixRestClient.sendReadReceipt(toRoom: event.roomId, forEvent: event.eventId, threadId: threadId) { _ in } + } + }) + }) + + bobRoom.sendTextMessage("Thread message 2", threadId: threadId, localEcho: &localEcho) { response3 in + switch response3 { + case .success(let eventId): + guard let eventId = eventId else { + XCTFail("eventId shouldn't be nil") + expectation.fulfill() + return + } + + bobThread.liveTimeline({ timeline in + let _ = timeline.listenToEvents([.receipt], { event, direction, roomState in + bobRoom.getEventReceipts(eventId, threadId: threadId, sorted: true) { receiptDataList in + guard !receiptDataList.isEmpty else { + return + } + + let receiptData = receiptDataList[0] + XCTAssertEqual(receiptData.threadId, threadId, "The RR should be related to crrent thread") + XCTAssertEqual(receiptData.userId, aliceSession.myUserId, "The RR should be sent by Bob") + + expectation.fulfill() + } + }) + }) + case .failure(let error): + XCTFail("Failed to send message within thread: \(error)") + expectation.fulfill() + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + }) + })) + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } +} + +private class MockThreadingServiceDelegate: MXThreadingServiceDelegate { + + private let newThreadBlock: ((MXThread) -> Void) + private static var instance: MockThreadingServiceDelegate? + + init(withNewThreadBlock newThreadBlock: @escaping (MXThread) -> Void) { + self.newThreadBlock = newThreadBlock + // do not allow this to be deallocated + Self.instance = self + } + + func threadingService(_ service: MXThreadingService, + didCreateNewThread thread: MXThread, + direction: MXTimelineDirection) { + newThreadBlock(thread) + } + +} diff --git a/MatrixSDKTests/MXRestClientTests.m b/MatrixSDKTests/MXRestClientTests.m index 2406d6445d..7cd11d8b54 100644 --- a/MatrixSDKTests/MXRestClientTests.m +++ b/MatrixSDKTests/MXRestClientTests.m @@ -1112,7 +1112,8 @@ - (void)testAddAndRemoveTag #pragma mark - Filter operations - (void)testFilter { - [self.matrixSDKTestsData doMXRestClientTestWithAlice:self readyToTest:^(MXRestClient *aliceRestClient, XCTestExpectation *expectation) { + [self.matrixSDKTestsData doMXRestClientTestWithBobAndAliceInARoom:self + readyToTest:^(MXRestClient *bobRestClient, MXRestClient *aliceRestClient, NSString *roomId, XCTestExpectation *expectation) { MXFilterJSONModel *filter = [[MXFilterJSONModel alloc] init]; @@ -1120,15 +1121,13 @@ - (void)testFilter filter.eventFormat = @"federation"; filter.room = [[MXRoomFilter alloc] init]; - filter.room.rooms = @[@"!aroom:matrix:org"]; - filter.room.notRooms = @[@"!notaroom:matrix:org"]; + filter.room.rooms = @[roomId]; filter.room.ephemeral = [[MXRoomEventFilter alloc] init]; filter.room.ephemeral.containsURL = NO; filter.room.ephemeral.types = @[@"atype"]; filter.room.ephemeral.notTypes = @[@"notatype"]; - filter.room.ephemeral.rooms = @[@"!aroom_ephemeral:matrix:org"]; - filter.room.ephemeral.notRooms = @[@"!notaroom_ephemeral:matrix:org"]; + filter.room.ephemeral.rooms = @[roomId];; filter.room.ephemeral.senders = @[@"@asender:matrix.org"]; filter.room.ephemeral.notSenders = @[@"@notasender:matrix.org"]; @@ -1333,42 +1332,7 @@ - (void)testPushRules // Check data sent by the home server has been correcltly modelled XCTAssertTrue([pushRules.global isKindOfClass:[MXPushRulesSet class]]); - XCTAssertNotNil(pushRules.global.content); - XCTAssertTrue([pushRules.global.content isKindOfClass:[NSArray class]]); - - MXPushRule *pushRule = pushRules.global.content[0]; - XCTAssertTrue([pushRule isKindOfClass:[MXPushRule class]]); - - XCTAssertNotNil(pushRule.actions); - - MXPushRuleAction *pushAction = pushRule.actions[0]; - XCTAssertTrue([pushAction isKindOfClass:[MXPushRuleAction class]]); - - // Test a rule with room_member_count condition. There must be one for 1:1 in underride rules - MXPushRule *roomMemberCountRule; - for (MXPushRule *pushRule in pushRules.global.underride) - { - if (pushRule.conditions.count) - { - MXPushRuleCondition *condition = pushRule.conditions[0]; - if (condition.kindType == MXPushRuleConditionTypeRoomMemberCount) - { - roomMemberCountRule = pushRule; - break; - } - } - } - XCTAssertNotNil(roomMemberCountRule); - - MXPushRuleCondition *condition = roomMemberCountRule.conditions[0]; - XCTAssertNotNil(condition); - XCTAssertEqualObjects(condition.kind, kMXPushRuleConditionStringRoomMemberCount); - - XCTAssertEqual(condition.kindType, MXPushRuleConditionTypeRoomMemberCount); - - XCTAssertNotNil(condition.parameters); - NSNumber *number= condition.parameters[@"is"]; - XCTAssertEqual(number.intValue, 2); + // TODO: Check new default push rules [expectation fulfill]; diff --git a/MatrixSDKTests/MXRoomEventFilterUnitTests.swift b/MatrixSDKTests/MXRoomEventFilterUnitTests.swift new file mode 100644 index 0000000000..c58558dd43 --- /dev/null +++ b/MatrixSDKTests/MXRoomEventFilterUnitTests.swift @@ -0,0 +1,254 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MXRoomEventFilterUnitTests: XCTestCase { + + private enum FilterType { + case lazyLoading + case lazyLoadingWithMessageLimit + case messageLimit + } + + private enum Constant { + static let messageLimit: UInt = 30 + } + + private static let emptyVersions: [String: Any] = + [ + "unstable_features": [:], + "versions": [] + ] + + private static let fullSupportVersions: [String: Any] = + [ + "unstable_features": [ + "org.matrix.msc2716": true, + "io.element.e2ee_forced.private": true, + "io.element.e2ee_forced.public": true, + "org.matrix.msc3030": true, + "org.matrix.e2e_cross_signing": true, + "org.matrix.msc2432": true, + "io.element.e2ee_forced.trusted_private": true, + "org.matrix.msc3440.stable": true, + "org.matrix.msc3827.stable": true, + "fi.mau.msc2815": true, + "uk.half-shot.msc2666.mutual_rooms": true, + "org.matrix.label_based_filtering": true, + "org.matrix.msc3026.busy_presence": true, + "org.matrix.msc2285.stable": true, + "org.matrix.msc3881.stable": true, + "org.matrix.msc3882": true, + "org.matrix.msc3773": true + ], + "versions": [ + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.5.0", + "r0.6.0", + "r0.6.1", + "v1.1", + "v1.2" + ] + ] + + private static let noSupportVersions: [String: Any] = + [ + "unstable_features": [ + "org.matrix.msc2716": false, + "io.element.e2ee_forced.private": false, + "io.element.e2ee_forced.public": false, + "org.matrix.msc3030": false, + "org.matrix.e2e_cross_signing": false, + "org.matrix.msc2432": false, + "io.element.e2ee_forced.trusted_private": false, + "org.matrix.msc3440.stable": false, + "org.matrix.msc3827.stable": false, + "fi.mau.msc2815": false, + "uk.half-shot.msc2666.mutual_rooms": false, + "org.matrix.label_based_filtering": false, + "org.matrix.msc3026.busy_presence": false, + "org.matrix.msc2285.stable": false, + "org.matrix.msc3881.stable": false, + "org.matrix.msc3882": false, + "org.matrix.msc3773": false + ], + "versions": [ + "r0.0.1", + "r0.1.0", + "r0.2.0", + "r0.3.0", + "r0.4.0", + "r0.6.1", + "v1.1", + "v1.2" + ] + ] + + // MARK: - Properties + + private var testData: MatrixSDKTestsData! + + // MARK: - Setup + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + try super.setUpWithError() + testData = MatrixSDKTestsData() + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + testData = nil + try super.tearDownWithError() + } + + // MARK: - Tests + + func testDefaultFilters() throws { + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(), + ofType: .lazyLoading, + supportsNotificationsForThreads: false) + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withMessageLimit: Constant.messageLimit), + ofType: .lazyLoadingWithMessageLimit, + supportsNotificationsForThreads: false) + validate(filter: MXFilterJSONModel.syncFilter(withMessageLimit: Constant.messageLimit), + ofType: .messageLimit, + supportsNotificationsForThreads: false) + } + + func testEmptyVersions() throws { + guard let versions = MXMatrixVersions(fromJSON: Self.emptyVersions) else { + XCTFail("Unable to instantiate MXMatrixVersions") + return + } + + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withUnreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .lazyLoading, + supportsNotificationsForThreads: false) + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withMessageLimit: Constant.messageLimit, unreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .lazyLoadingWithMessageLimit, + supportsNotificationsForThreads: false) + validate(filter: MXFilterJSONModel.syncFilter(withMessageLimit: Constant.messageLimit, unreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .messageLimit, + supportsNotificationsForThreads: false) + } + + func testFullSupportVersions() throws { + guard let versions = MXMatrixVersions(fromJSON: Self.fullSupportVersions) else { + XCTFail("Unable to instantiate MXMatrixVersions") + return + } + + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withUnreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .lazyLoading, + supportsNotificationsForThreads: true) + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withMessageLimit: Constant.messageLimit, unreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .lazyLoadingWithMessageLimit, + supportsNotificationsForThreads: true) + validate(filter: MXFilterJSONModel.syncFilter(withMessageLimit: Constant.messageLimit, unreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .messageLimit, + supportsNotificationsForThreads: true) + } + + func testNoSupportVersions() throws { + guard let versions = MXMatrixVersions(fromJSON: Self.noSupportVersions) else { + XCTFail("Unable to instantiate MXMatrixVersions") + return + } + + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withUnreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .lazyLoading, + supportsNotificationsForThreads: false) + validate(filter: MXFilterJSONModel.syncFilterForLazyLoading(withMessageLimit: Constant.messageLimit, unreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .lazyLoadingWithMessageLimit, + supportsNotificationsForThreads: false) + validate(filter: MXFilterJSONModel.syncFilter(withMessageLimit: Constant.messageLimit, unreadThreadNotifications: versions.supportsNotificationsForThreads), + ofType: .messageLimit, + supportsNotificationsForThreads: false) + } + + // MARK: - Private + + private func validate(filter: MXFilterJSONModel?, ofType filterType: FilterType, supportsNotificationsForThreads: Bool) { + guard let filter = filter else { + XCTFail("Failed to create sync filter of type \(String(describing: filterType))") + return + } + XCTAssertNil(filter.eventFields) + XCTAssertNil(filter.eventFormat) + XCTAssertNil(filter.presence) + XCTAssertNil(filter.accountData) + + guard let roomFilter = filter.room else { + XCTFail("No room filter found in filter \(String(describing: filterType))") + return + } + + switch filterType { + case .messageLimit: + XCTAssertNil(roomFilter.ephemeral) + XCTAssertNil(roomFilter.state) + guard let timeline = roomFilter.timeline else { + XCTFail("No room timeline found in filter \(String(describing: filterType))") + return + } + XCTAssertEqual(timeline.limit, Constant.messageLimit) + XCTAssertEqual(timeline.unreadThreadNotifications, supportsNotificationsForThreads) + if !supportsNotificationsForThreads { + XCTAssertNil(timeline.dictionary["unread_thread_notifications"]) + } + case .lazyLoadingWithMessageLimit: + XCTAssertNil(roomFilter.ephemeral) + guard let roomState = roomFilter.state else { + XCTFail("No room state found in filter \(String(describing: filterType))") + return + } + XCTAssertEqual(roomState.lazyLoadMembers, true) + guard let timeline = roomFilter.timeline else { + XCTFail("No room timeline found in filter \(String(describing: filterType))") + return + } + XCTAssertEqual(timeline.limit, Constant.messageLimit) + XCTAssertEqual(timeline.unreadThreadNotifications, supportsNotificationsForThreads) + if !supportsNotificationsForThreads { + XCTAssertNil(timeline.dictionary["unread_thread_notifications"]) + } + case .lazyLoading: + XCTAssertNil(roomFilter.ephemeral) + guard let roomState = roomFilter.state else { + XCTFail("No room state found in filter \(String(describing: filterType))") + return + } + XCTAssertEqual(roomState.lazyLoadMembers, true) + if supportsNotificationsForThreads { + guard let timeline = roomFilter.timeline else { + XCTFail("No room timeline found in filter \(String(describing: filterType))") + return + } + XCTAssertEqual(timeline.unreadThreadNotifications, supportsNotificationsForThreads) + } else { + XCTAssertNil(roomFilter.timeline) + } + } + } + +} + diff --git a/MatrixSDKTests/MXThreadsNotificationCountTests.swift b/MatrixSDKTests/MXThreadsNotificationCountTests.swift new file mode 100644 index 0000000000..ab9be4855f --- /dev/null +++ b/MatrixSDKTests/MXThreadsNotificationCountTests.swift @@ -0,0 +1,272 @@ +// +// Copyright 2022 The Matrix.org Foundation C.I.C +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class MXThreadsNotificationCountTests: XCTestCase { + + private var testData: MatrixSDKTestsData! + private var e2eTestData: MatrixSDKTestsE2EData! + + override func setUp() { + super.setUp() + testData = MatrixSDKTestsData() + e2eTestData = MatrixSDKTestsE2EData(matrixSDKTestsData: testData) + MXSDKOptions.sharedInstance().enableThreads = true + } + + override func tearDown() { + testData = nil + e2eTestData = nil + super.tearDown() + } + + // MARK - Tests + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Expect to have 1 unread message in Alice room summary + func testUnreadCountForRoomWithUnreadMessageInMainTimeline() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + testData.doMXRestClientTestWithBobAndAlice(inARoom: self) { aliceRestClient, bobRestClient, roomId, expectation in + guard let bobRestClient = bobRestClient, + let aliceRestClient = aliceRestClient, + let roomId = roomId, + let bobSession = MXSession(matrixRestClient: bobRestClient), + let aliceSession = MXSession(matrixRestClient: aliceRestClient), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + expectation?.fulfill() + return + } + + guard let filter = MXFilterJSONModel.syncFilter(withMessageLimit: 30, unreadThreadNotifications: true) else { + XCTFail("Unable to instantiate filter") + expectation.fulfill() + return + } + + bobSession.setStore(bobStore) { response in + guard !response.isFailure else { + XCTFail("Failed to set store for Bob's session") + expectation.fulfill() + return + } + + bobSession.start(withSyncFilter: filter) { response in + guard !response.isFailure else { + XCTFail("Failed to start Bob's session") + expectation.fulfill() + return + } + + guard let bobRoom = bobSession.room(withRoomId: roomId) else { + XCTFail("Failed to get room from Bob's POV") + expectation.fulfill() + return + } + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success: + aliceSession.setStore(aliceStore) { response in + guard !response.isFailure else { + XCTFail("Failed to set store for Alice's session") + bobSession.close() + expectation.fulfill() + return + } + + NotificationCenter.default.addObserver(forName: NSNotification.Name.mxSessionStateDidChange, object: aliceSession, queue: OperationQueue.main) { notification in + guard aliceSession.state == .running else { + return + } + + guard let summary = aliceSession.roomSummary(withRoomId: roomId) else { + XCTFail("Failed to retrieve room summary") + bobSession.close() + aliceSession.close() + expectation.fulfill() + return + } + + XCTAssertEqual(1, summary.notificationCount) + + bobSession.close() + aliceSession.close() + expectation.fulfill() + } + + aliceSession.start(withSyncFilter: filter) { response in + guard !response.isFailure else { + XCTFail("Failed to start Alice's session") + bobSession.close() + expectation.fulfill() + return + } + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } + } + } + + /// - Create a Bob session + /// - Create a Alice session + /// - Create an initial room for both + /// - Send a text message A in main timeline + /// - Send a text message A1 as a thread of the message A + /// - Expect to have 2 unread messages in Alice room summary + func testUnreadCountForRoomWithUnreadMessageInMainTimelineAndThread() { + let bobStore = MXMemoryStore() + let aliceStore = MXMemoryStore() + + testData.doMXRestClientTestWithBobAndAlice(inARoom: self) { aliceRestClient, bobRestClient, roomId, expectation in + guard let bobRestClient = bobRestClient, + let aliceRestClient = aliceRestClient, + let roomId = roomId, + let bobSession = MXSession(matrixRestClient: bobRestClient), + let aliceSession = MXSession(matrixRestClient: aliceRestClient), + let expectation = expectation else { + XCTFail("Failed to setup test conditions") + expectation?.fulfill() + return + } + + guard let filter = MXFilterJSONModel.syncFilter(withMessageLimit: 30, unreadThreadNotifications: true) else { + XCTFail("Unable to instantiate filter") + expectation.fulfill() + return + } + + bobSession.setStore(bobStore) { response in + guard !response.isFailure else { + XCTFail("Failed to set store for Bob's session") + expectation.fulfill() + return + } + + bobSession.start(withSyncFilter: filter) { response in + guard !response.isFailure else { + XCTFail("Failed to start Bob's session") + expectation.fulfill() + return + } + + guard let bobRoom = bobSession.room(withRoomId: roomId) else { + XCTFail("Failed to get room from Bob's POV") + expectation.fulfill() + return + } + + var localEcho: MXEvent? + bobRoom.sendTextMessage("Root message", threadId: nil, localEcho: &localEcho) { response in + switch response { + case .success(let eventId): + + guard let threadId = eventId else { + XCTFail("Failed to setup test conditions") + expectation.fulfill() + return + } + + bobRoom.sendTextMessage("Thread message", threadId: threadId, localEcho: &localEcho) { response2 in + switch response2 { + case .success: + aliceSession.setStore(aliceStore) { response in + guard !response.isFailure else { + XCTFail("Failed to set store for Alice's session") + bobSession.close() + expectation.fulfill() + return + } + + NotificationCenter.default.addObserver(forName: NSNotification.Name.mxSessionStateDidChange, object: aliceSession, queue: OperationQueue.main) { notification in + guard aliceSession.state == .running else { + return + } + + guard let summary = aliceSession.roomSummary(withRoomId: roomId) else { + XCTFail("Failed to retrieve room summary") + bobSession.close() + aliceSession.close() + expectation.fulfill() + return + } + + XCTAssertEqual(2, summary.notificationCount) + + bobSession.close() + aliceSession.close() + expectation.fulfill() + } + + aliceSession.start(withSyncFilter: filter) { response in + guard !response.isFailure else { + XCTFail("Failed to start Alice's session") + bobSession.close() + expectation.fulfill() + return + } + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + + case .failure(let error): + XCTFail("Failed to setup test conditions: \(error)") + expectation.fulfill() + } + } + } + } + } + } +} + +private class MockThreadingServiceDelegate: MXThreadingServiceDelegate { + + private let newThreadBlock: ((MXThread) -> Void) + private static var instance: MockThreadingServiceDelegate? + + init(withNewThreadBlock newThreadBlock: @escaping (MXThread) -> Void) { + self.newThreadBlock = newThreadBlock + // do not allow this to be deallocated + Self.instance = self + } + + func threadingService(_ service: MXThreadingService, + didCreateNewThread thread: MXThread, + direction: MXTimelineDirection) { + newThreadBlock(thread) + } + +} diff --git a/MatrixSDKTests/TestPlans/AllWorkingTests.xctestplan b/MatrixSDKTests/TestPlans/AllWorkingTests.xctestplan index 24fd2e67a5..fb12ca99dc 100644 --- a/MatrixSDKTests/TestPlans/AllWorkingTests.xctestplan +++ b/MatrixSDKTests/TestPlans/AllWorkingTests.xctestplan @@ -24,23 +24,25 @@ "MXAggregatedEditsTests\/testEditsFromInitialSync", "MXAggregatedEditsTests\/testFormatedEditsFromInitialSync", "MXAggregatedEditsTests\/testFormattedEditSendAndReceive", - "MXAggregatedEditsUnitTests", - "MXAggregatedReferenceUnitTests", - "MXAsyncTaskQueueUnitTests", - "MXAuthenticationSessionUnitTests", "MXAggregatedEditsTests\/testFormattedEditServerSide", + "MXAggregatedEditsUnitTests", "MXAggregatedReactionTests\/testAggregationsFromInitialSync", "MXAggregatedReactionTests\/testReactionsWhenPaginatingFromAGappyInitialSync", "MXAggregatedReactionTests\/testReactionsWhenPaginatingFromAGappySync", "MXAggregatedReactionTests\/testUnreactAfterInitialSync", + "MXAggregatedReactionTests\/testUnreactOnEventReactedByOther", + "MXAggregatedReferenceTests\/testE2EFetchAllReferenceEvents", "MXAggregatedReferenceTests\/testReferenceFromInitialSync", + "MXAggregatedReferenceUnitTests", + "MXAsyncTaskQueueUnitTests", + "MXAuthenticationSessionUnitTests", "MXAutoDiscoveryTests\/testAutoDiscoveryNotJSON", "MXAutoDiscoveryTests\/testAutoDiscoverySuccessful", "MXAutoDiscoveryTests\/testAutoDiscoverySuccessfulWithNoContentType", + "MXBackgroundSyncServiceTests\/testStoreWithGappyAndOutdatedSync()", "MXBackgroundTaskUnitTests", "MXBeaconInfoUnitTests", "MXCredentialsUnitTests", - "MXBackgroundSyncServiceTests\/testStoreWithGappyAndOutdatedSync()", "MXCrossSigningTests", "MXCrossSigningVerificationTests", "MXCrossSigningVerificationTests\/testVerificationByDMFullFlow", @@ -53,6 +55,7 @@ "MXCryptoRecoveryServiceTests", "MXCryptoRequestsUnitTests", "MXCryptoSecretShareTests\/testSecretRequestCancellation", + "MXCryptoSecretStorageTests\/testDeleteSecret", "MXCryptoShareTests", "MXCryptoTests", "MXErrorUnitTests", @@ -80,8 +83,8 @@ "MXMyUserTests\/testIdenticon", "MXOlmDeviceUnitTests", "MXPeekingRoomTests\/testPeekingOnNonWorldReadable", - "MXPollRelationTests\/testBobAndAliceAnswer", "MXPollAggregatorTest\/testEditing()", + "MXPollRelationTests\/testBobAndAliceAnswer", "MXPushRuleUnitTests", "MXQRCodeDataUnitTests", "MXReplyEventParserUnitTests", @@ -106,6 +109,8 @@ "MXSelfSignedHomeserverTests\/testTrustedCertificate", "MXSessionTests", "MXSharedHistoryKeyManagerUnitTests", + "MXSpaceChildContentTests\/testUpdateChildSuggestion()", + "MXSpaceChildContentTests\/testUpgradeSpaceChild()", "MXStoreFileStoreTests", "MXStoreMemoryStoreTests", "MXStoreNoStoreTests\/testMXNoStoreSeveralPaginateBacks", diff --git a/MatrixSDKTests/TestPlans/UnitTests.xctestplan b/MatrixSDKTests/TestPlans/UnitTests.xctestplan index 92b4111c37..8e1de35c72 100644 --- a/MatrixSDKTests/TestPlans/UnitTests.xctestplan +++ b/MatrixSDKTests/TestPlans/UnitTests.xctestplan @@ -43,6 +43,7 @@ "MXCrossSigningInfoUnitTests", "MXCrossSigningV2UnitTests", "MXCryptoKeyBackupEngineUnitTests", + "MXCryptoMachineUnitTests", "MXCryptoRequestsUnitTests", "MXDeviceInfoSourceUnitTests", "MXDeviceInfoUnitTests", @@ -74,6 +75,7 @@ "MXQRCodeTransactionV2UnitTests", "MXReplyEventParserUnitTests", "MXResponseUnitTests", + "MXRoomEventDecryptionUnitTests", "MXRoomKeyEventContentUnitTests", "MXRoomKeyInfoFactoryUnitTests", "MXRoomStateUnitTests", diff --git a/MatrixSDKTests/TestPlans/UnitTestsWithSanitizers.xctestplan b/MatrixSDKTests/TestPlans/UnitTestsWithSanitizers.xctestplan index d04e534148..7c5554d7d2 100644 --- a/MatrixSDKTests/TestPlans/UnitTestsWithSanitizers.xctestplan +++ b/MatrixSDKTests/TestPlans/UnitTestsWithSanitizers.xctestplan @@ -53,6 +53,7 @@ "MXCrossSigningInfoUnitTests", "MXCrossSigningV2UnitTests", "MXCryptoKeyBackupEngineUnitTests", + "MXCryptoMachineUnitTests", "MXCryptoRequestsUnitTests", "MXDeviceInfoSourceUnitTests", "MXDeviceInfoUnitTests", @@ -82,6 +83,7 @@ "MXQRCodeTransactionV2UnitTests", "MXReplyEventParserUnitTests", "MXResponseUnitTests", + "MXRoomEventDecryptionUnitTests", "MXRoomKeyEventContentUnitTests", "MXRoomKeyInfoFactoryUnitTests", "MXRoomStateUnitTests", diff --git a/Podfile.lock b/Podfile.lock index b862f27406..13e47dbf52 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -73,4 +73,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7805b1fe65269b6ac6667a7f347f324e8970c050 -COCOAPODS: 1.11.2 +COCOAPODS: 1.11.3 diff --git a/README.rst b/README.rst index 74dff902cb..cf1c8f89b2 100644 --- a/README.rst +++ b/README.rst @@ -478,22 +478,26 @@ Unit tests classes use the suffix "UnitTests" to differentiate them. A unit test Out of the box, the tests use one of the homeservers (located at http://localhost:8080) of the "Demo Federation of Homeservers" -(https://github.com/matrix-org/synapse#running-a-demo-federation-of-synapses). +(https://matrix-org.github.io/synapse/develop/development/demo.html?highlight=demo#synapse-demo-setup). You first need to follow instructions to set up Synapse in development mode at https://github.com/matrix-org/synapse#synapse-development. -If you have already installed all dependencies, the steps are:: +The cookbook is:: + $ pip install --user pipx + $ pipx install poetry + $ python3 -m pipx ensurepath # To run if `pipx install poetry` complained about PATH not being correctly set $ git clone https://github.com/matrix-org/synapse.git $ cd synapse - $ virtualenv -p python3 env - $ source env/bin/activate - (env) $ python -m pip install --no-use-pep517 -e . + $ poetry install --extras all -Every time you want to launch these test homeservers, type:: +To launch these test homeservers, type from the synapse root folder:: - $ virtualenv -p python3 env - $ source env/bin/activate - (env) $ demo/start.sh --no-rate-limit + $ poetry run ./demo/start.sh --no-rate-limit + +To stop and reset the servers:: + + $ poetry run ./demo/stop.sh + $ poetry run ./demo/clean.sh You can now run tests from the Xcode Test navigator tab or select the MatrixSDKTests scheme and click on the "Test" action. From 1cfbb06730d6cd0385a4e35eb9f57e0f6528e33d Mon Sep 17 00:00:00 2001 From: gulekismail Date: Tue, 15 Nov 2022 15:29:42 +0300 Subject: [PATCH 2/2] finish version++ --- Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Podfile.lock b/Podfile.lock index 13e47dbf52..b862f27406 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -73,4 +73,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 7805b1fe65269b6ac6667a7f347f324e8970c050 -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2