diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 59bd47cc3..40c451ca9 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -509,6 +509,7 @@ 755395927DDD6EBDDA5E217A /* SettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */; }; 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */; }; 756EA0D663261889EF64E6D4 /* VoiceMessageRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9CBF577B9711CFBB4FA40D /* VoiceMessageRecordingView.swift */; }; + 7573D682F089205F7F1D96CF /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; }; 762DAF94846C7AC8550F1CC1 /* MediaPlayerProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */; }; 762DB0973865293F0C3D3D7B /* SessionVerificationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */; }; 763D69741D58D2B650BC1FC9 /* CallScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F37FA1A5D55633E1942B153B /* CallScreenCoordinator.swift */; }; @@ -642,6 +643,7 @@ 90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE40D4A5DD857AC16EED945A /* URLSession.swift */; }; 90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; }; 90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; + 914BDF61447C723F104BCE33 /* SessionDirectories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2067FF58B4996323EB40C /* SessionDirectories.swift */; }; 915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; }; 91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; }; 91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; }; @@ -870,6 +872,7 @@ C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C4FE0E11A907C8999F92D5A8 /* TimelineStartRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */; }; C55A44C99F64A479ABA85B46 /* RoomScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */; }; + C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */; }; C58E305C380D3ADDF7912180 /* StickerRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 818695BED971753243FEF897 /* StickerRoomTimelineItem.swift */; }; C5A07E2D88BE7D51DCECD166 /* LoginScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0B159AFFBBD8ECFD0E37FA /* LoginScreenModels.swift */; }; C67FCC854F3A6FC7A2EC04D0 /* MediaUploadPreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */; }; @@ -899,6 +902,7 @@ CBD2ABE4C1A47ECD99E1488E /* NotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421FA93BCC2840E66E4F306F /* NotificationSettingsScreenViewModelProtocol.swift */; }; CBFF4F1BFA90B46241B8106C /* Strings+SAS.swift in Sources */ = {isa = PBXBuildFile; fileRef = B172057567E049007A5C4D92 /* Strings+SAS.swift */; }; CC0D088F505F33A20DC5590F /* RoomStateEventStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */; }; + CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */; }; CC961529F9F1854BEC3272C9 /* LayoutMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */; }; CCBEC2100CAF2EEBE9DB4156 /* TemplateScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */; }; CD0088B763CD970CF1CBF8CB /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E97E9615A158C76B2AB77 /* DateTests.swift */; }; @@ -1215,6 +1219,7 @@ 07579F9C29001E40715F3014 /* NotificationSettingsChatType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsChatType.swift; sourceTree = ""; }; 077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = ""; }; 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedRoomProxy.swift; sourceTree = ""; }; + 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectoriesTests.swift; sourceTree = ""; }; 08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = ""; }; 0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsFlowCoordinator.swift; sourceTree = ""; }; 086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = ""; }; @@ -1478,6 +1483,7 @@ 42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = ""; }; 436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = ""; }; 43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = ""; }; + 43C2067FF58B4996323EB40C /* SessionDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectories.swift; sourceTree = ""; }; 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = ""; }; 44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 44C314C00533E2C297796B60 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1878,6 +1884,7 @@ A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; + A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = ""; }; A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = ""; }; @@ -3763,6 +3770,7 @@ 347D708104CCEF771428C9A3 /* PollFormScreenViewModelTests.swift */, 25E7E9B7FEAB6169D960C206 /* QRCodeLoginScreenViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, + A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */, 41D041A857614A9AE13C7795 /* RoomChangePermissionsScreenViewModelTests.swift */, 8F841F219ACDFC1D3F42FEFB /* RoomChangeRolesScreenViewModelTests.swift */, 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */, @@ -3786,6 +3794,7 @@ 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */, F08776C48FFB47CACF64ED10 /* ServerConfirmationScreenViewModelTests.swift */, EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */, + 0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */, A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */, DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */, 3D487C1185D658F8B15B8F55 /* SettingsViewModelTests.swift */, @@ -4916,6 +4925,7 @@ isa = PBXGroup; children = ( 3558A15CFB934F9229301527 /* RestorationToken.swift */, + 43C2067FF58B4996323EB40C /* SessionDirectories.swift */, 0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */, BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */, ); @@ -5932,6 +5942,7 @@ 414F50CFCFEEE2611127DCFB /* RestorationToken.swift in Sources */, 17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */, 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */, + 7573D682F089205F7F1D96CF /* SessionDirectories.swift in Sources */, 422E8D182CA688D4565CD1E1 /* String.swift in Sources */, CBFF4F1BFA90B46241B8106C /* Strings+SAS.swift in Sources */, ECA636DAF071C611FDC2BB57 /* Strings+Untranslated.swift in Sources */, @@ -6020,6 +6031,7 @@ D415764645491F10344FC6AC /* Publisher.swift in Sources */, BDC4EB54CC3036730475CB8B /* QRCodeLoginScreenViewModelTests.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, + C5627BCC3EBBB96A943B6D93 /* RestorationTokenTests.swift in Sources */, 9B03943616A1147539DF7F08 /* RoomChangePermissionsScreenViewModelTests.swift in Sources */, D2825E013A8ECFB66D9A1DE6 /* RoomChangeRolesScreenViewModelTests.swift in Sources */, 9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */, @@ -6043,6 +6055,7 @@ 53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */, 89658A44C9FC19B58FD1C226 /* ServerConfirmationScreenViewModelTests.swift in Sources */, 93875ADD456142D20823ED24 /* ServerSelectionViewModelTests.swift in Sources */, + CC1C948F67A5510A340FD7F0 /* SessionDirectoriesTests.swift in Sources */, 86675910612A12409262DFBD /* SessionVerificationStateMachineTests.swift in Sources */, 755727E0B756430DFFEC4732 /* SessionVerificationViewModelTests.swift in Sources */, 206F0DBAB6AF042CA1FF2C0D /* SettingsViewModelTests.swift in Sources */, @@ -6718,6 +6731,7 @@ 85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */, 0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */, BD782053BE4C3D2F0BDE5699 /* ServiceLocator.swift in Sources */, + 914BDF61447C723F104BCE33 /* SessionDirectories.swift in Sources */, 237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */, AE1A73B24D63DA3D63DC4EE3 /* SessionVerificationControllerProxyMock.swift in Sources */, 94A65DD8A353DF112EBEF67A /* SessionVerificationControllerProxyProtocol.swift in Sources */, @@ -7665,7 +7679,7 @@ repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift"; requirement = { kind = exactVersion; - version = 1.0.40; + version = 1.0.42; }; }; 701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = { diff --git a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 760bbaa7e..74f7fd10e 100644 --- a/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/element-hq/matrix-rust-components-swift", "state" : { - "revision" : "5d9f1865a71badfe6d9f7c3232b6cf23b12f8add", - "version" : "1.0.40" + "revision" : "ccae0615642728bbadcd051e4851d96ab298bab2", + "version" : "1.0.42" } }, { diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 035db99a4..651d51141 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -257,6 +257,7 @@ "error_some_messages_have_not_been_sent" = "Some messages have not been sent"; "error_unknown" = "Sorry, an error occurred"; "event_shield_reason_authenticity_not_guaranteed" = "The authenticity of this encrypted message can't be guaranteed on this device."; +"event_shield_reason_previously_verified" = "Encrypted by a previously-verified user."; "event_shield_reason_sent_in_clear" = "Not encrypted."; "event_shield_reason_unknown_device" = "Encrypted by an unknown or deleted device."; "event_shield_reason_unsigned_device" = "Encrypted by a device not verified by its owner."; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 4bb5bb57f..fcdf07d65 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -572,6 +572,8 @@ internal enum L10n { internal static var errorUnknown: String { return L10n.tr("Localizable", "error_unknown") } /// The authenticity of this encrypted message can't be guaranteed on this device. internal static var eventShieldReasonAuthenticityNotGuaranteed: String { return L10n.tr("Localizable", "event_shield_reason_authenticity_not_guaranteed") } + /// Encrypted by a previously-verified user. + internal static var eventShieldReasonPreviouslyVerified: String { return L10n.tr("Localizable", "event_shield_reason_previously_verified") } /// Not encrypted. internal static var eventShieldReasonSentInClear: String { return L10n.tr("Localizable", "event_shield_reason_sent_in_clear") } /// Encrypted by an unknown or deleted device. diff --git a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift index 256bb31b2..673559d1e 100644 --- a/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/SDKGeneratedMocks.swift @@ -4804,17 +4804,17 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } } - //MARK: - sessionPath + //MARK: - sessionPaths - var sessionPathPathUnderlyingCallsCount = 0 - open var sessionPathPathCallsCount: Int { + var sessionPathsDataPathCachePathUnderlyingCallsCount = 0 + open var sessionPathsDataPathCachePathCallsCount: Int { get { if Thread.isMainThread { - return sessionPathPathUnderlyingCallsCount + return sessionPathsDataPathCachePathUnderlyingCallsCount } else { var returnValue: Int? = nil DispatchQueue.main.sync { - returnValue = sessionPathPathUnderlyingCallsCount + returnValue = sessionPathsDataPathCachePathUnderlyingCallsCount } return returnValue! @@ -4822,29 +4822,29 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } set { if Thread.isMainThread { - sessionPathPathUnderlyingCallsCount = newValue + sessionPathsDataPathCachePathUnderlyingCallsCount = newValue } else { DispatchQueue.main.sync { - sessionPathPathUnderlyingCallsCount = newValue + sessionPathsDataPathCachePathUnderlyingCallsCount = newValue } } } } - open var sessionPathPathCalled: Bool { - return sessionPathPathCallsCount > 0 + open var sessionPathsDataPathCachePathCalled: Bool { + return sessionPathsDataPathCachePathCallsCount > 0 } - open var sessionPathPathReceivedPath: String? - open var sessionPathPathReceivedInvocations: [String] = [] + open var sessionPathsDataPathCachePathReceivedArguments: (dataPath: String, cachePath: String?)? + open var sessionPathsDataPathCachePathReceivedInvocations: [(dataPath: String, cachePath: String?)] = [] - var sessionPathPathUnderlyingReturnValue: ClientBuilder! - open var sessionPathPathReturnValue: ClientBuilder! { + var sessionPathsDataPathCachePathUnderlyingReturnValue: ClientBuilder! + open var sessionPathsDataPathCachePathReturnValue: ClientBuilder! { get { if Thread.isMainThread { - return sessionPathPathUnderlyingReturnValue + return sessionPathsDataPathCachePathUnderlyingReturnValue } else { var returnValue: ClientBuilder? = nil DispatchQueue.main.sync { - returnValue = sessionPathPathUnderlyingReturnValue + returnValue = sessionPathsDataPathCachePathUnderlyingReturnValue } return returnValue! @@ -4852,26 +4852,26 @@ open class ClientBuilderSDKMock: MatrixRustSDK.ClientBuilder { } set { if Thread.isMainThread { - sessionPathPathUnderlyingReturnValue = newValue + sessionPathsDataPathCachePathUnderlyingReturnValue = newValue } else { DispatchQueue.main.sync { - sessionPathPathUnderlyingReturnValue = newValue + sessionPathsDataPathCachePathUnderlyingReturnValue = newValue } } } } - open var sessionPathPathClosure: ((String) -> ClientBuilder)? + open var sessionPathsDataPathCachePathClosure: ((String, String?) -> ClientBuilder)? - open override func sessionPath(path: String) -> ClientBuilder { - sessionPathPathCallsCount += 1 - sessionPathPathReceivedPath = path + open override func sessionPaths(dataPath: String, cachePath: String?) -> ClientBuilder { + sessionPathsDataPathCachePathCallsCount += 1 + sessionPathsDataPathCachePathReceivedArguments = (dataPath: dataPath, cachePath: cachePath) DispatchQueue.main.async { - self.sessionPathPathReceivedInvocations.append(path) + self.sessionPathsDataPathCachePathReceivedInvocations.append((dataPath: dataPath, cachePath: cachePath)) } - if let sessionPathPathClosure = sessionPathPathClosure { - return sessionPathPathClosure(path) + if let sessionPathsDataPathCachePathClosure = sessionPathsDataPathCachePathClosure { + return sessionPathsDataPathCachePathClosure(dataPath, cachePath) } else { - return sessionPathPathReturnValue + return sessionPathsDataPathCachePathReturnValue } } diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index 0776498ff..21a04a74f 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -68,6 +68,22 @@ extension URL: ExpressibleByStringLiteral { return url } + /// The base directory where all application support data is stored. + static var cachesBaseDirectory: URL { + let url = appGroupContainerDirectory + .appendingPathComponent("Library", isDirectory: true) + .appendingPathComponent("Caches", isDirectory: true) + .appendingPathComponent(InfoPlistReader.main.baseBundleIdentifier, isDirectory: true) + .appendingPathComponent("Sessions", isDirectory: true) + + try? FileManager.default.createDirectoryIfNeeded(at: url) + + // Caches are excluded from backups automatically anyway. + // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html + + return url + } + var globalProxy: String? { if let proxySettingsUnmanaged = CFNetworkCopySystemProxySettings() { let proxySettings = proxySettingsUnmanaged.takeRetainedValue() diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index cdd2dc69d..d83edd886 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -559,7 +559,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { fatalError("Only events can have send info.") } - if eventTimelineItem.properties.deliveryStatus == .sendingFailed { + if case .sendingFailed = eventTimelineItem.properties.deliveryStatus { + // In the future we will show different errors for the various failure reasons. displayAlert(.sendingFailed) } else if let authenticityMessage = eventTimelineItem.properties.encryptionAuthenticity?.message { displayAlert(.encryptionAuthenticity(authenticityMessage)) diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index 853dfef82..1d322191d 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -150,7 +150,7 @@ private extension TimelineItemSendInfo { itemID = timelineItem.id localizedString = timelineItem.localizedSendInfo - status = if adjustedDeliveryStatus == .sendingFailed { + status = if case .sendingFailed = adjustedDeliveryStatus { .sendingFailed } else if let authenticity = timelineItem.properties.encryptionAuthenticity { .encryptionAuthenticity(authenticity) diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index 86c620dab..94e4f4792 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -35,11 +35,13 @@ struct TimelineStyler: View { var body: some View { mainContent .onChange(of: timelineItem.properties.deliveryStatus) { newStatus in - if newStatus == .sendingFailed { + if case .sendingFailed = newStatus { guard task == nil else { return } task = Task { + // Add a short delay so that an immediate failure when retrying + // shows as sending for long enough to be visible to the user. try? await Task.sleep(for: .milliseconds(700)) if !Task.isCancelled { adjustedDeliveryStatus = newStatus @@ -101,7 +103,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { static let failed: TextRoomTimelineItem = { var result = base - result.properties.deliveryStatus = .sendingFailed + result.properties.deliveryStatus = .sendingFailed(.unknown) return result }() diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index fada236e6..956435d03 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -20,7 +20,7 @@ import MatrixRustSDK class AuthenticationService: AuthenticationServiceProtocol { private var client: Client? - private var sessionDirectory: URL + private var sessionDirectories: SessionDirectories private let passphrase: String private let userSessionStore: UserSessionStoreProtocol @@ -31,7 +31,7 @@ class AuthenticationService: AuthenticationServiceProtocol { var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) { - sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString) + sessionDirectories = .init() passphrase = encryptionKeyProvider.generateKey().base64EncodedString() self.userSessionStore = userSessionStore self.appSettings = appSettings @@ -149,20 +149,24 @@ class AuthenticationService: AuthenticationServiceProtocol { slidingSyncProxy: appSettings.slidingSyncProxyURL, sessionDelegate: userSessionStore.clientSessionDelegate, appHooks: appHooks) - .sessionPath(path: sessionDirectory.path(percentEncoded: false)) + .sessionPaths(dataPath: sessionDirectories.dataPath, + cachePath: sessionDirectories.cachePath) .passphrase(passphrase: passphrase) } private func rotateSessionDirectory() { - if FileManager.default.directoryExists(at: sessionDirectory) { - try? FileManager.default.removeItem(at: sessionDirectory) + if FileManager.default.directoryExists(at: sessionDirectories.dataDirectory) { + try? FileManager.default.removeItem(at: sessionDirectories.dataDirectory) + } + if FileManager.default.directoryExists(at: sessionDirectories.cacheDirectory) { + try? FileManager.default.removeItem(at: sessionDirectories.cacheDirectory) } - sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString) + sessionDirectories = .init() } private func userSession(for client: Client) async -> Result { - switch await userSessionStore.userSession(for: client, sessionDirectory: sessionDirectory, passphrase: passphrase) { + switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { case .success(let clientProxy): return .success(clientProxy) case .failure: diff --git a/ElementX/Sources/Services/Keychain/KeychainController.swift b/ElementX/Sources/Services/Keychain/KeychainController.swift index b95f53463..749f69c1b 100644 --- a/ElementX/Sources/Services/Keychain/KeychainController.swift +++ b/ElementX/Sources/Services/Keychain/KeychainController.swift @@ -120,6 +120,7 @@ class KeychainController: KeychainControllerProtocol { } let restorationToken = RestorationToken(session: session, sessionDirectory: oldToken.sessionDirectory, + cacheDirectory: oldToken.cacheDirectory, passphrase: oldToken.passphrase, pusherNotificationClientIdentifier: oldToken.pusherNotificationClientIdentifier) setRestorationToken(restorationToken, forUsername: session.userId) diff --git a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift index 85bf3efe1..e9dee279a 100644 --- a/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift +++ b/ElementX/Sources/Services/QRCode/QRCodeLoginService.swift @@ -20,7 +20,7 @@ import Foundation import MatrixRustSDK final class QRCodeLoginService: QRCodeLoginServiceProtocol { - private var sessionDirectory: URL + private var sessionDirectories: SessionDirectories private let passphrase: String private let userSessionStore: UserSessionStoreProtocol @@ -36,7 +36,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol { userSessionStore: UserSessionStoreProtocol, appSettings: AppSettings, appHooks: AppHooks) { - sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString) + sessionDirectories = .init() passphrase = encryptionKeyProvider.generateKey().base64EncodedString() self.userSessionStore = userSessionStore self.appSettings = appSettings @@ -83,20 +83,24 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol { slidingSyncProxy: appSettings.slidingSyncProxyURL, sessionDelegate: userSessionStore.clientSessionDelegate, appHooks: appHooks) - .sessionPath(path: sessionDirectory.path(percentEncoded: false)) + .sessionPaths(dataPath: sessionDirectories.dataPath, + cachePath: sessionDirectories.cachePath) .passphrase(passphrase: passphrase) } private func rotateSessionDirectory() { - if FileManager.default.directoryExists(at: sessionDirectory) { - try? FileManager.default.removeItem(at: sessionDirectory) + if FileManager.default.directoryExists(at: sessionDirectories.dataDirectory) { + try? FileManager.default.removeItem(at: sessionDirectories.dataDirectory) + } + if FileManager.default.directoryExists(at: sessionDirectories.cacheDirectory) { + try? FileManager.default.removeItem(at: sessionDirectories.cacheDirectory) } - sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString) + sessionDirectories = .init() } private func userSession(for client: Client) async -> Result { - switch await userSessionStore.userSession(for: client, sessionDirectory: sessionDirectory, passphrase: passphrase) { + switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) { case .success(let session): return .success(session) case .failure(let error): diff --git a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift index 78df4fbd5..7fc1a51ff 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemContent/EncryptionAuthenticity.swift @@ -28,6 +28,7 @@ enum EncryptionAuthenticity: Hashable { case unknownDevice(color: Color) case unsignedDevice(color: Color) case unverifiedIdentity(color: Color) + case previouslyVerified(color: Color) case sentInClear(color: Color) var message: String { @@ -40,6 +41,8 @@ enum EncryptionAuthenticity: Hashable { L10n.eventShieldReasonUnsignedDevice case .unverifiedIdentity: L10n.eventShieldReasonUnverifiedIdentity + case .previouslyVerified: + L10n.eventShieldReasonPreviouslyVerified case .sentInClear: L10n.eventShieldReasonSentInClear } @@ -51,6 +54,7 @@ enum EncryptionAuthenticity: Hashable { .unknownDevice(let color), .unsignedDevice(let color), .unverifiedIdentity(let color), + .previouslyVerified(let color), .sentInClear(let color): color } @@ -59,7 +63,7 @@ enum EncryptionAuthenticity: Hashable { var icon: KeyPath { switch self { case .notGuaranteed: \.info - case .unknownDevice, .unsignedDevice, .unverifiedIdentity: \.helpSolid + case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .previouslyVerified: \.helpSolid case .sentInClear: \.lockOff } } @@ -87,6 +91,8 @@ extension EncryptionAuthenticity { self = .unsignedDevice(color: color) case .unverifiedIdentity: self = .unverifiedIdentity(color: color) + case .previouslyVerified: + self = .previouslyVerified(color: color) case .sentInClear: self = .sentInClear(color: color) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 77f5466fb..7e627ffd5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -68,7 +68,20 @@ enum TimelineItemProxy { enum TimelineItemDeliveryStatus: Hashable { case sending case sent - case sendingFailed + case sendingFailed(SendFailureReason) + + enum SendFailureReason: Hashable { + case verifiedUserHasUnsignedDevice(devices: [String: [String]]) + case verifiedUserChangedIdentity(users: [String]) + case unknown + } + + var isSendingFailed: Bool { + switch self { + case .sending, .sent: false + case .sendingFailed: true + } + } } /// A light wrapper around event timeline items returned from Rust. @@ -88,11 +101,15 @@ class EventTimelineItemProxy { switch localSendState { case .sendingFailed(_, let isRecoverable): - return isRecoverable ? .sending : .sendingFailed + return isRecoverable ? .sending : .sendingFailed(.unknown) case .notSentYet: return .sending case .sent: return .sent + case .verifiedUserHasUnsignedDevice(devices: let devices): + return .sendingFailed(.verifiedUserHasUnsignedDevice(devices: devices)) + case .verifiedUserChangedIdentity(users: let users): + return .sendingFailed(.verifiedUserChangedIdentity(users: users)) } }() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index d017409f5..c7b71e159 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -56,7 +56,7 @@ extension EventBasedTimelineItemProtocol { } var hasFailedToSend: Bool { - properties.deliveryStatus == .sendingFailed + properties.deliveryStatus?.isSendingFailed == true } var hasFailedDecryption: Bool { diff --git a/ElementX/Sources/Services/UserSession/RestorationToken.swift b/ElementX/Sources/Services/UserSession/RestorationToken.swift index 59162f994..96955feb3 100644 --- a/ElementX/Sources/Services/UserSession/RestorationToken.swift +++ b/ElementX/Sources/Services/UserSession/RestorationToken.swift @@ -20,6 +20,7 @@ import MatrixRustSDK struct RestorationToken: Equatable { let session: MatrixRustSDK.Session let sessionDirectory: URL + let cacheDirectory: URL let passphrase: String? let pusherNotificationClientIdentifier: String? } @@ -29,10 +30,22 @@ extension RestorationToken: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) let session = try container.decode(MatrixRustSDK.Session.self, forKey: .session) - let sessionDirectory = try container.decodeIfPresent(URL.self, forKey: .sessionDirectory) + let dataDirectory = try container.decodeIfPresent(URL.self, forKey: .sessionDirectory) + let cacheDirectory = try container.decodeIfPresent(URL.self, forKey: .cacheDirectory) + + let sessionDirectories = if let dataDirectory { + if let cacheDirectory { + SessionDirectories(dataDirectory: dataDirectory, cacheDirectory: cacheDirectory) + } else { + SessionDirectories(dataDirectory: dataDirectory) + } + } else { + SessionDirectories(userID: session.userId) + } self = try .init(session: session, - sessionDirectory: sessionDirectory ?? .legacySessionDirectory(for: session.userId), + sessionDirectory: sessionDirectories.dataDirectory, + cacheDirectory: sessionDirectories.cacheDirectory, passphrase: container.decodeIfPresent(String.self, forKey: .passphrase), pusherNotificationClientIdentifier: container.decodeIfPresent(String.self, forKey: .pusherNotificationClientIdentifier)) } @@ -66,18 +79,3 @@ extension MatrixRustSDK.Session: Codable { case accessToken, refreshToken, userId, deviceId, homeserverUrl, oidcData, slidingSyncProxy } } - -// MARK: Migrations - -private extension URL { - /// Gets the store directory of a legacy session that hasn't been migrated to the new token format. - /// - /// This should only be used to fill in the missing value when restoring a token as older versions of - /// the SDK set the session directory for us, based on the user's ID. Newer sessions now use a UUID, - /// which is generated app side during authentication. - static func legacySessionDirectory(for userID: String) -> URL { - // Rust sanitises the user ID replacing invalid characters with an _ - let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_") - return .sessionsBaseDirectory.appendingPathComponent(sanitisedUserID) - } -} diff --git a/ElementX/Sources/Services/UserSession/SessionDirectories.swift b/ElementX/Sources/Services/UserSession/SessionDirectories.swift new file mode 100644 index 000000000..89f8149dd --- /dev/null +++ b/ElementX/Sources/Services/UserSession/SessionDirectories.swift @@ -0,0 +1,61 @@ +// +// Copyright 2024 New Vector Ltd +// +// 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 + +struct SessionDirectories: Hashable, Codable { + let dataDirectory: URL + let cacheDirectory: URL + + var dataPath: String { dataDirectory.path(percentEncoded: false) } + var cachePath: String { cacheDirectory.path(percentEncoded: false) } +} + +extension SessionDirectories { + /// Creates a fresh set of session directories for a new user. + init() { + let sessionDirectoryName = UUID().uuidString + dataDirectory = .sessionsBaseDirectory.appending(component: sessionDirectoryName) + cacheDirectory = .cachesBaseDirectory.appending(component: sessionDirectoryName) + } + + /// Creates the session directories for a user who signed in before the data directory was stored. + init(userID: String) { + dataDirectory = .legacySessionDirectory(for: userID) + cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) + } + + /// Creates the session directories for a user who has a single session directory stored without a separate caches directory. + init(dataDirectory: URL) { + self.dataDirectory = dataDirectory + cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent) + } +} + +// MARK: Migrations + +private extension URL { + /// Gets the store directory of a legacy session that hasn't been migrated to the new token format. + /// + /// This should only be used to fill in the missing value when restoring a token as older versions of + /// the SDK set the session directory for us, based on the user's ID. Newer sessions now use a UUID, + /// which is generated app side during authentication. + static func legacySessionDirectory(for userID: String) -> URL { + // Rust sanitises the user ID replacing invalid characters with an _ + let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_") + return .sessionsBaseDirectory.appendingPathComponent(sanitisedUserID) + } +} diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index b3d37345a..5dd5470a0 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -64,20 +64,21 @@ class UserSessionStore: UserSessionStoreProtocol { // On any restoration failure reset the token and restart keychainController.removeRestorationTokenForUsername(credentials.userID) - deleteSessionDirectory(for: credentials) + deleteSessionDirectories(for: credentials) return .failure(error) } } - func userSession(for client: Client, sessionDirectory: URL, passphrase: String?) async -> Result { + func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result { do { let session = try client.session() let userID = try client.userId() let clientProxy = await setupProxyForClient(client) keychainController.setRestorationToken(RestorationToken(session: session, - sessionDirectory: sessionDirectory, + sessionDirectory: sessionDirectories.dataDirectory, + cacheDirectory: sessionDirectories.cacheDirectory, passphrase: passphrase, pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier), forUsername: userID) @@ -95,7 +96,7 @@ class UserSessionStore: UserSessionStoreProtocol { keychainController.removeRestorationTokenForUsername(userID) if let credentials { - deleteSessionDirectory(for: credentials) + deleteSessionDirectories(for: credentials) } } @@ -133,7 +134,8 @@ class UserSessionStore: UserSessionStoreProtocol { slidingSync: appSettings.simplifiedSlidingSyncEnabled ? .simplified : .restored, sessionDelegate: keychainController, appHooks: appHooks) - .sessionPath(path: credentials.restorationToken.sessionDirectory.path(percentEncoded: false)) + .sessionPaths(dataPath: credentials.restorationToken.sessionDirectory.path(percentEncoded: false), + cachePath: credentials.restorationToken.cacheDirectory.path(percentEncoded: false)) .username(username: credentials.userID) .homeserverUrl(url: homeserverURL) .passphrase(passphrase: credentials.restorationToken.passphrase) @@ -156,22 +158,36 @@ class UserSessionStore: UserSessionStoreProtocol { appSettings: appSettings) } - private func deleteSessionDirectory(for credentials: KeychainCredentials) { + private func deleteSessionDirectories(for credentials: KeychainCredentials) { do { try FileManager.default.removeItem(at: credentials.restorationToken.sessionDirectory) } catch { MXLog.failure("Failed deleting the session data: \(error)") } + do { + try FileManager.default.removeItem(at: credentials.restorationToken.cacheDirectory) + } catch { + MXLog.failure("Failed deleting the session caches: \(error)") + } } private func deleteCaches(for credentials: KeychainCredentials) { do { - let sessionDirectoryContents = try FileManager.default.contentsOfDirectory(at: credentials.restorationToken.sessionDirectory, includingPropertiesForKeys: nil) - for url in sessionDirectoryContents where url.path.contains(matrixSDKStateKey) { - try FileManager.default.removeItem(at: url) - } + try deleteContentsOfDirectory(at: credentials.restorationToken.sessionDirectory) } catch { - MXLog.failure("Failed clearing caches: \(error)") + MXLog.failure("Failed clearing state store: \(error)") + } + do { + try deleteContentsOfDirectory(at: credentials.restorationToken.cacheDirectory) + } catch { + MXLog.failure("Failed clearing event cache store: \(error)") + } + } + + private func deleteContentsOfDirectory(at url: URL) throws { + let sessionDirectoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) + for url in sessionDirectoryContents where url.path.contains(matrixSDKStateKey) { + try FileManager.default.removeItem(at: url) } } } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift index 5cbf20b65..dd72d92cb 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStoreProtocol.swift @@ -40,7 +40,7 @@ protocol UserSessionStoreProtocol { func restoreUserSession() async -> Result /// Creates a user session for a new client from the SDK along with the passphrase used for the data stores. - func userSession(for client: Client, sessionDirectory: URL, passphrase: String?) async -> Result + func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result /// Logs out of the specified session. func logout(userSession: UserSessionProtocol) diff --git a/NSE/Sources/Other/NSEUserSession.swift b/NSE/Sources/Other/NSEUserSession.swift index 3f17b810d..736713532 100644 --- a/NSE/Sources/Other/NSEUserSession.swift +++ b/NSE/Sources/Other/NSEUserSession.swift @@ -39,7 +39,8 @@ final class NSEUserSession { slidingSync: simplifiedSlidingSyncEnabled ? .simplified : .restored, sessionDelegate: clientSessionDelegate, appHooks: appHooks) - .sessionPath(path: credentials.restorationToken.sessionDirectory.path(percentEncoded: false)) + .sessionPaths(dataPath: credentials.restorationToken.sessionDirectory.path(percentEncoded: false), + cachePath: credentials.restorationToken.cacheDirectory.path(percentEncoded: false)) .username(username: credentials.userID) .homeserverUrl(url: homeserverURL) .passphrase(passphrase: credentials.restorationToken.passphrase) diff --git a/NSE/SupportingFiles/target.yml b/NSE/SupportingFiles/target.yml index 41d2741d5..c900f6a60 100644 --- a/NSE/SupportingFiles/target.yml +++ b/NSE/SupportingFiles/target.yml @@ -111,3 +111,4 @@ targets: - path: ../../ElementX/Sources/Services/Notification/Proxy - path: ../../ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift - path: ../../ElementX/Sources/Services/UserSession/RestorationToken.swift + - path: ../../ElementX/Sources/Services/UserSession/SessionDirectories.swift diff --git a/UnitTests/Sources/KeychainControllerTests.swift b/UnitTests/Sources/KeychainControllerTests.swift index aa3ec23e0..b65cf436a 100644 --- a/UnitTests/Sources/KeychainControllerTests.swift +++ b/UnitTests/Sources/KeychainControllerTests.swift @@ -41,6 +41,7 @@ class KeychainControllerTests: XCTestCase { oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy"), sessionDirectory: .homeDirectory.appending(component: UUID().uuidString), + cacheDirectory: .homeDirectory.appending(component: UUID().uuidString), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusherClientID") keychain.setRestorationToken(restorationToken, forUsername: username) @@ -60,6 +61,7 @@ class KeychainControllerTests: XCTestCase { oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy"), sessionDirectory: .homeDirectory.appending(component: UUID().uuidString), + cacheDirectory: .homeDirectory.appending(component: UUID().uuidString), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusherClientID") keychain.setRestorationToken(restorationToken, forUsername: username) @@ -85,6 +87,7 @@ class KeychainControllerTests: XCTestCase { oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy"), sessionDirectory: .homeDirectory.appending(component: UUID().uuidString), + cacheDirectory: .homeDirectory.appending(component: UUID().uuidString), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusherClientID") keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com") @@ -109,6 +112,7 @@ class KeychainControllerTests: XCTestCase { oidcData: "oidcData", slidingSyncProxy: "https://my.sync.proxy"), sessionDirectory: .homeDirectory.appending(component: UUID().uuidString), + cacheDirectory: .homeDirectory.appending(component: UUID().uuidString), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusherClientID") keychain.setRestorationToken(restorationToken, forUsername: "@test\(index):example.com") @@ -141,6 +145,7 @@ class KeychainControllerTests: XCTestCase { oidcData: "oidcData", slidingSyncProxy: nil), sessionDirectory: .homeDirectory.appending(component: UUID().uuidString), + cacheDirectory: .homeDirectory.appending(component: UUID().uuidString), passphrase: "passphrase", pusherNotificationClientIdentifier: "pusherClientID") keychain.setRestorationToken(restorationToken, forUsername: username) diff --git a/UnitTests/Sources/RestorationTokenTests.swift b/UnitTests/Sources/RestorationTokenTests.swift new file mode 100644 index 000000000..f6936f73d --- /dev/null +++ b/UnitTests/Sources/RestorationTokenTests.swift @@ -0,0 +1,108 @@ +// +// Copyright 2024 New Vector Ltd +// +// 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 + +@testable import ElementX +import MatrixRustSDK + +class RestorationTokenTests: XCTestCase { + func testDecodeFromTokenV1() throws { + // Given an encoded restoration token in the original format that only contains a Session from the SDK. + let originalToken = RestorationTokenV1(session: Session(accessToken: "1234", + refreshToken: nil, + userId: "@user:example.com", + deviceId: "D3V1C3", + homeserverUrl: "https://matrix.example.com", + oidcData: nil, + slidingSyncProxy: "https://sync.example.com")) + let data = try JSONEncoder().encode(originalToken) + + // When decoding the data to the current restoration token format. + let decodedToken = try JSONDecoder().decode(RestorationToken.self, from: data) + + // Then the output should be a valid token with the expected store directories. + XCTAssertEqual(decodedToken.session, originalToken.session, "The session should not be changed.") + XCTAssertNil(decodedToken.passphrase, "There should not be a passphrase.") + XCTAssertNil(decodedToken.pusherNotificationClientIdentifier, "There should not be a push notification client ID.") + XCTAssertEqual(decodedToken.sessionDirectory, .sessionsBaseDirectory.appending(component: "@user_example.com"), + "The session directory should match the original location set by the Rust SDK from our base directory.") + XCTAssertEqual(decodedToken.cacheDirectory, .cachesBaseDirectory.appending(component: "@user_example.com"), + "The cache directory should be derived from the session directory but in the caches directory.") + } + + func testDecodeFromTokenV4() throws { + // Given an encoded restoration token in the 4th format that contains a stored session directory. + let sessionDirectoryName = UUID().uuidString + let originalToken = RestorationTokenV4(session: Session(accessToken: "1234", + refreshToken: "5678", + userId: "@user:example.com", + deviceId: "D3V1C3", + homeserverUrl: "https://matrix.example.com", + oidcData: "data-from-mas", + slidingSyncProxy: "https://sync.example.com"), + sessionDirectory: .sessionsBaseDirectory.appending(component: sessionDirectoryName), + passphrase: "passphrase", + pusherNotificationClientIdentifier: "pusher-identifier") + let data = try JSONEncoder().encode(originalToken) + + // When decoding the data to the current restoration token format. + let decodedToken = try JSONDecoder().decode(RestorationToken.self, from: data) + + // Then the output should be a valid token with the expected store directories. + XCTAssertEqual(decodedToken.session, originalToken.session, "The session should not be changed.") + XCTAssertEqual(decodedToken.passphrase, originalToken.passphrase, "The passphrase should not be changed.") + XCTAssertEqual(decodedToken.pusherNotificationClientIdentifier, originalToken.pusherNotificationClientIdentifier, + "The push notification client identifier should not be changed.") + XCTAssertEqual(decodedToken.sessionDirectory, originalToken.sessionDirectory, "The session directory should not be changed.") + XCTAssertEqual(decodedToken.cacheDirectory, .cachesBaseDirectory.appending(component: sessionDirectoryName), + "The cache directory should be derived from the session directory but in the caches directory.") + } + + func testDecodeFromCurrentToken() throws { + // Given an encoded restoration token in the current format. + let sessionDirectoryName = UUID().uuidString + let originalToken = RestorationToken(session: Session(accessToken: "1234", + refreshToken: "5678", + userId: "@user:example.com", + deviceId: "D3V1C3", + homeserverUrl: "https://matrix.example.com", + oidcData: "data-from-mas", + slidingSyncProxy: nil), + sessionDirectory: .sessionsBaseDirectory.appending(component: sessionDirectoryName), + cacheDirectory: .cachesBaseDirectory.appending(component: sessionDirectoryName), + passphrase: "passphrase", + pusherNotificationClientIdentifier: "pusher-identifier") + let data = try JSONEncoder().encode(originalToken) + + // When decoding the data. + let decodedToken = try JSONDecoder().decode(RestorationToken.self, from: data) + + // Then the output should be a valid token. + XCTAssertEqual(decodedToken, originalToken, "The token should remain identical.") + } +} + +struct RestorationTokenV1: Equatable, Codable { + let session: MatrixRustSDK.Session +} + +struct RestorationTokenV4: Equatable, Codable { + let session: MatrixRustSDK.Session + let sessionDirectory: URL + let passphrase: String? + let pusherNotificationClientIdentifier: String? +} diff --git a/UnitTests/Sources/SessionDirectoriesTests.swift b/UnitTests/Sources/SessionDirectoriesTests.swift new file mode 100644 index 000000000..a803cca92 --- /dev/null +++ b/UnitTests/Sources/SessionDirectoriesTests.swift @@ -0,0 +1,63 @@ +// +// Copyright 2024 New Vector Ltd +// +// 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 + +@testable import ElementX + +class SessionDirectoriesTests: XCTestCase { + func testInitWithUserID() { + // Given only a user ID. + let userID = "@user:matrix.org" + + // When creating the session directories using this. + let sessionDirectories = SessionDirectories(userID: userID) + + // Then the directories should be generated in the correct location, using an escaped version of the user ID + XCTAssertEqual(sessionDirectories.dataDirectory, .sessionsBaseDirectory.appending(component: "@user_matrix.org")) + XCTAssertEqual(sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: "@user_matrix.org")) + } + + func testInitWithDataDirectory() { + // Given only a session directory without a caches directory. + let sessionDirectoryName = UUID().uuidString + let sessionDirectory = URL.applicationSupportBaseDirectory.appending(component: sessionDirectoryName) + + // When creating the session directories using this. + let sessionDirectories = SessionDirectories(dataDirectory: sessionDirectory) + + // Then the data directory should remain unchanged and the caches directory should be generated. + XCTAssertEqual(sessionDirectories.dataDirectory, sessionDirectory) + XCTAssertEqual(sessionDirectories.cacheDirectory, .cachesBaseDirectory.appending(component: sessionDirectoryName)) + } + + func testPathOutput() { + // Given session directories created from paths with spaces in them. + let originalDataPath = "/Users/John Smith/Data" + let originalCachePath = "/Users/John Smith/Caches" + let dataDirectory = URL(filePath: originalDataPath) + let cacheDirectory = URL(filePath: originalCachePath) + let sessionDirectories = SessionDirectories(dataDirectory: dataDirectory, cacheDirectory: cacheDirectory) + + // When getting the paths from the session directories struct. + let returnedDataPath = sessionDirectories.dataPath + let returnedCachePath = sessionDirectories.cachePath + + // Then the paths should not be escaped. + XCTAssertEqual(returnedDataPath, originalDataPath) + XCTAssertEqual(returnedCachePath, originalCachePath) + } +} diff --git a/UnitTests/Sources/TextBasedRoomTimelineTests.swift b/UnitTests/Sources/TextBasedRoomTimelineTests.swift index 119543d31..791077ec4 100644 --- a/UnitTests/Sources/TextBasedRoomTimelineTests.swift +++ b/UnitTests/Sources/TextBasedRoomTimelineTests.swift @@ -70,7 +70,7 @@ final class TextBasedRoomTimelineTests: XCTestCase { sender: .init(id: UUID().uuidString), content: .init(body: "Test")) timelineItem.properties.isEdited = true - timelineItem.properties.deliveryStatus = .sendingFailed + timelineItem.properties.deliveryStatus = .sendingFailed(.unknown) let editedCount = L10n.commonEditedSuffix.count XCTAssertEqual(timelineItem.additionalWhitespaces(), timestamp.count + editedCount + 5) } diff --git a/project.yml b/project.yml index f9549e3b5..cd8b60a02 100644 --- a/project.yml +++ b/project.yml @@ -60,7 +60,7 @@ packages: # Element/Matrix dependencies MatrixRustSDK: url: https://github.com/element-hq/matrix-rust-components-swift - exactVersion: 1.0.40 + exactVersion: 1.0.42 # path: ../matrix-rust-sdk Compound: url: https://github.com/element-hq/compound-ios