Update the SDK (#3196)

* Update the SDK.

* Fix API breaks on send failures and propagate the new type.

* Handle new SDK ShieldState.

* Set up the ClientBuilder's `cachePath` option.

* Delete the cacheDirectory during logout/clearCache.

* Add unit tests for RestorationToken decoding and SessionDirectories generation.
This commit is contained in:
Doug 2024-08-27 11:06:26 +01:00 committed by GitHub
parent ebef0d1fbe
commit 812c5d5c61
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 403 additions and 82 deletions

View File

@ -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 = "<group>"; };
077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = "<group>"; };
07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinedRoomProxy.swift; sourceTree = "<group>"; };
0825EAFD47332DD459DE893F /* SessionDirectoriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectoriesTests.swift; sourceTree = "<group>"; };
08283301736A6FE9D558B2CB /* AppLockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelProtocol.swift; sourceTree = "<group>"; };
0833F51229E166BCA141D004 /* RoomRolesAndPermissionsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsFlowCoordinator.swift; sourceTree = "<group>"; };
086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; };
@ -1478,6 +1483,7 @@
42C8C368A611B9CB79C7F5FA /* RoomPollsHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreen.swift; sourceTree = "<group>"; };
436A0D98D372B17EAE9AA999 /* GlobalSearchScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenModels.swift; sourceTree = "<group>"; };
43A84EE187D0C772E18A4E39 /* VoiceMessageCacheProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageCacheProtocol.swift; sourceTree = "<group>"; };
43C2067FF58B4996323EB40C /* SessionDirectories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDirectories.swift; sourceTree = "<group>"; };
4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareToMapsAppActivity.swift; sourceTree = "<group>"; };
44ABA63DBE7F76C58260B43B /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = "<group>"; };
44C314C00533E2C297796B60 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -1878,6 +1884,7 @@
A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = "<group>"; };
A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = "<group>"; };
A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = "<group>"; };
A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = "<group>"; };
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = "<group>"; };
@ -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" */ = {

View File

@ -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"
}
},
{

View File

@ -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.";

View File

@ -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.

View File

@ -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
}
}

View File

@ -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()

View File

@ -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))

View File

@ -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)

View File

@ -35,11 +35,13 @@ struct TimelineStyler<Content: View>: 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
}()

View File

@ -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<LoginHomeserver, Never> { 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<UserSessionProtocol, AuthenticationServiceError> {
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:

View File

@ -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)

View File

@ -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<UserSessionProtocol, QRCodeLoginServiceError> {
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):

View File

@ -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<CompoundIcons, Image> {
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)
}

View File

@ -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))
}
}()

View File

@ -56,7 +56,7 @@ extension EventBasedTimelineItemProtocol {
}
var hasFailedToSend: Bool {
properties.deliveryStatus == .sendingFailed
properties.deliveryStatus?.isSendingFailed == true
}
var hasFailedDecryption: Bool {

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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<UserSessionProtocol, UserSessionStoreError> {
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
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)
}
}
}

View File

@ -40,7 +40,7 @@ protocol UserSessionStoreProtocol {
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
/// 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<UserSessionProtocol, UserSessionStoreError>
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
/// Logs out of the specified session.
func logout(userSession: UserSessionProtocol)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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?
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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