Skip to content
Snippets Groups Projects
Commit 6e220b88 authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Restore contacts from backup

parent 4ee10c7a
No related branches found
No related tags found
2 merge requests!112Restore contacts from backup,!102Release 1.0.0
This commit is part of merge request !102. Comments created here will be created in the context of that merge request.
...@@ -24,7 +24,7 @@ public struct RestoreState: Equatable { ...@@ -24,7 +24,7 @@ public struct RestoreState: Equatable {
public init( public init(
file: File? = nil, file: File? = nil,
fileImportFailure: String? = nil, fileImportFailure: String? = nil,
restoreFailure: String? = nil, restoreFailures: [String] = [],
focusedField: Field? = nil, focusedField: Field? = nil,
isImportingFile: Bool = false, isImportingFile: Bool = false,
passphrase: String = "", passphrase: String = "",
...@@ -32,7 +32,7 @@ public struct RestoreState: Equatable { ...@@ -32,7 +32,7 @@ public struct RestoreState: Equatable {
) { ) {
self.file = file self.file = file
self.fileImportFailure = fileImportFailure self.fileImportFailure = fileImportFailure
self.restoreFailure = restoreFailure self.restoreFailures = restoreFailures
self.focusedField = focusedField self.focusedField = focusedField
self.isImportingFile = isImportingFile self.isImportingFile = isImportingFile
self.passphrase = passphrase self.passphrase = passphrase
...@@ -41,7 +41,7 @@ public struct RestoreState: Equatable { ...@@ -41,7 +41,7 @@ public struct RestoreState: Equatable {
public var file: File? public var file: File?
public var fileImportFailure: String? public var fileImportFailure: String?
public var restoreFailure: String? public var restoreFailures: [String]
@BindableState public var focusedField: Field? @BindableState public var focusedField: Field?
@BindableState public var isImportingFile: Bool @BindableState public var isImportingFile: Bool
@BindableState public var passphrase: String @BindableState public var passphrase: String
...@@ -53,7 +53,7 @@ public enum RestoreAction: Equatable, BindableAction { ...@@ -53,7 +53,7 @@ public enum RestoreAction: Equatable, BindableAction {
case fileImport(Result<URL, NSError>) case fileImport(Result<URL, NSError>)
case restoreTapped case restoreTapped
case finished case finished
case failed(NSError) case failed([NSError])
case binding(BindingAction<RestoreState>) case binding(BindingAction<RestoreState>)
} }
...@@ -125,7 +125,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm ...@@ -125,7 +125,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
guard let backupData = state.file?.data, backupData.count > 0 else { return .none } guard let backupData = state.file?.data, backupData.count > 0 else { return .none }
let backupPassphrase = state.passphrase let backupPassphrase = state.passphrase
state.isRestoring = true state.isRestoring = true
state.restoreFailure = nil state.restoreFailures = []
return Effect.result { return Effect.result {
do { do {
let result = try env.messenger.restoreBackup( let result = try env.messenger.restoreBackup(
...@@ -140,10 +140,33 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm ...@@ -140,10 +140,33 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
phone: facts.get(.phone)?.value, phone: facts.get(.phone)?.value,
createdAt: env.now() createdAt: env.now()
)) ))
let lookupResult = try env.messenger.lookupContacts.callAsFunction(
ids: result.restoredContacts
)
try lookupResult.contacts.forEach { contact in
let facts = try contact.getFacts()
try env.db().saveContact(XXModels.Contact(
id: try contact.getId(),
marshaled: contact.data,
username: facts.get(.username)?.value,
email: facts.get(.email)?.value,
phone: facts.get(.phone)?.value,
authStatus: .friend,
createdAt: env.now()
))
}
guard lookupResult.errors.isEmpty else {
return .success(.failed(lookupResult.errors))
}
return .success(.finished) return .success(.finished)
} catch { } catch {
try? env.messenger.destroy() var errors = [error as NSError]
return .success(.failed(error as NSError)) do {
try env.messenger.destroy()
} catch {
errors.append(error as NSError)
}
return .success(.failed(errors))
} }
} }
.subscribe(on: env.bgQueue) .subscribe(on: env.bgQueue)
...@@ -154,9 +177,9 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm ...@@ -154,9 +177,9 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
state.isRestoring = false state.isRestoring = false
return .none return .none
case .failed(let error): case .failed(let errors):
state.isRestoring = false state.isRestoring = false
state.restoreFailure = error.localizedDescription state.restoreFailures = errors.map(\.localizedDescription)
return .none return .none
case .binding(_): case .binding(_):
......
...@@ -21,7 +21,7 @@ public struct RestoreView: View { ...@@ -21,7 +21,7 @@ public struct RestoreView: View {
var isRestoring: Bool var isRestoring: Bool
var focusedField: RestoreState.Field? var focusedField: RestoreState.Field?
var fileImportFailure: String? var fileImportFailure: String?
var restoreFailure: String? var restoreFailures: [String]
init(state: RestoreState) { init(state: RestoreState) {
file = state.file.map { .init(name: $0.name, size: $0.data.count) } file = state.file.map { .init(name: $0.name, size: $0.data.count) }
...@@ -30,7 +30,7 @@ public struct RestoreView: View { ...@@ -30,7 +30,7 @@ public struct RestoreView: View {
isRestoring = state.isRestoring isRestoring = state.isRestoring
focusedField = state.focusedField focusedField = state.focusedField
fileImportFailure = state.fileImportFailure fileImportFailure = state.fileImportFailure
restoreFailure = state.restoreFailure restoreFailures = state.restoreFailures
} }
} }
...@@ -38,6 +38,30 @@ public struct RestoreView: View { ...@@ -38,6 +38,30 @@ public struct RestoreView: View {
WithViewStore(store, observe: ViewState.init) { viewStore in WithViewStore(store, observe: ViewState.init) { viewStore in
NavigationView { NavigationView {
Form { Form {
fileSection(viewStore)
if viewStore.file != nil {
restoreSection(viewStore)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
viewStore.send(.finished)
} label: {
Text("Cancel")
}
.disabled(viewStore.isImportingFile || viewStore.isRestoring)
}
}
.navigationTitle("Restore")
.onChange(of: viewStore.focusedField) { focusedField = $0 }
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
}
.navigationViewStyle(.stack)
}
}
@ViewBuilder func fileSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View {
Section { Section {
if let file = viewStore.file { if let file = viewStore.file {
HStack(alignment: .bottom) { HStack(alignment: .bottom) {
...@@ -45,8 +69,7 @@ public struct RestoreView: View { ...@@ -45,8 +69,7 @@ public struct RestoreView: View {
Spacer() Spacer()
Text(format(byteCount: file.size)) Text(format(byteCount: file.size))
} }
} } else {
Button { Button {
viewStore.send(.importFileTapped) viewStore.send(.importFileTapped)
} label: { } label: {
...@@ -62,16 +85,22 @@ public struct RestoreView: View { ...@@ -62,16 +85,22 @@ public struct RestoreView: View {
viewStore.send(.fileImport(result.mapError { $0 as NSError })) viewStore.send(.fileImport(result.mapError { $0 as NSError }))
} }
) )
.disabled(viewStore.isRestoring)
if let failure = viewStore.fileImportFailure {
Text("Error: \(failure)")
} }
} header: { } header: {
Text("File") Text("File")
} }
.disabled(viewStore.isRestoring)
if viewStore.file != nil { if let failure = viewStore.fileImportFailure {
Section {
Text(failure)
} header: {
Text("Error")
}
}
}
@ViewBuilder func restoreSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View {
Section { Section {
SecureField("Passphrase", text: viewStore.binding( SecureField("Passphrase", text: viewStore.binding(
get: \.passphrase, get: \.passphrase,
...@@ -81,6 +110,7 @@ public struct RestoreView: View { ...@@ -81,6 +110,7 @@ public struct RestoreView: View {
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
.disableAutocorrection(true) .disableAutocorrection(true)
.focused($focusedField, equals: .passphrase) .focused($focusedField, equals: .passphrase)
.disabled(viewStore.isRestoring)
Button { Button {
viewStore.send(.restoreTapped) viewStore.send(.restoreTapped)
...@@ -93,31 +123,19 @@ public struct RestoreView: View { ...@@ -93,31 +123,19 @@ public struct RestoreView: View {
} }
} }
} }
if let failure = viewStore.restoreFailure {
Text("Error: \(failure)")
}
} header: { } header: {
Text("Backup") Text("Restore")
}
.disabled(viewStore.isRestoring)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
viewStore.send(.finished)
} label: {
Text("Cancel")
}
.disabled(viewStore.isRestoring)
} }
if !viewStore.restoreFailures.isEmpty {
Section {
ForEach(Array(viewStore.restoreFailures.enumerated()), id: \.offset) { _, failure in
Text(failure)
} }
.navigationTitle("Restore") .font(.footnote)
.onChange(of: viewStore.focusedField) { focusedField = $0 } } header: {
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } Text("Error")
} }
.navigationViewStyle(.stack)
} }
} }
...@@ -133,7 +151,19 @@ public struct RestoreView: View { ...@@ -133,7 +151,19 @@ public struct RestoreView: View {
public struct RestoreView_Previews: PreviewProvider { public struct RestoreView_Previews: PreviewProvider {
public static var previews: some View { public static var previews: some View {
RestoreView(store: Store( RestoreView(store: Store(
initialState: RestoreState(), initialState: RestoreState(
file: .init(name: "preview", data: Data()),
fileImportFailure: nil,
restoreFailures: [
"Preview failure 1",
"Preview failure 2",
"Preview failure 3",
],
focusedField: nil,
isImportingFile: false,
passphrase: "",
isRestoring: false
),
reducer: .empty, reducer: .empty,
environment: () environment: ()
)) ))
......
...@@ -85,15 +85,22 @@ final class RestoreFeatureTests: XCTestCase { ...@@ -85,15 +85,22 @@ final class RestoreFeatureTests: XCTestCase {
] ]
let restoreResult = MessengerRestoreBackup.Result( let restoreResult = MessengerRestoreBackup.Result(
restoredParams: BackupParams(username: "restored-username"), restoredParams: BackupParams(username: "restored-username"),
restoredContacts: [] restoredContacts: [
"contact-1-id".data(using: .utf8)!,
"contact-2-id".data(using: .utf8)!,
"contact-3-id".data(using: .utf8)!,
]
) )
let lookedUpContacts = [XXClient.Contact.stub(1), .stub(2), .stub(3)]
let now = Date() let now = Date()
let contactId = "contact-id".data(using: .utf8)! let contactId = "contact-id".data(using: .utf8)!
var udFacts: [Fact] = [] var udFacts: [Fact] = []
var didRestoreWithData: [Data] = [] var didRestoreWithData: [Data] = []
var didRestoreWithPassphrase: [String] = [] var didRestoreWithPassphrase: [String] = []
var didSaveContact: [XXModels.Contact] = [] var didSaveContact: [XXModels.Contact] = []
var didLookupContactIds: [[Data]] = []
let store = TestStore( let store = TestStore(
initialState: RestoreState( initialState: RestoreState(
...@@ -126,6 +133,14 @@ final class RestoreFeatureTests: XCTestCase { ...@@ -126,6 +133,14 @@ final class RestoreFeatureTests: XCTestCase {
ud.getFacts.run = { udFacts } ud.getFacts.run = { udFacts }
return ud return ud
} }
store.environment.messenger.lookupContacts.run = { contactIds in
didLookupContactIds.append(contactIds)
return .init(
contacts: lookedUpContacts,
failedIds: [],
errors: []
)
}
store.environment.db.run = { store.environment.db.run = {
var db: Database = .unimplemented var db: Database = .unimplemented
db.saveContact.run = { contact in db.saveContact.run = { contact in
...@@ -145,13 +160,43 @@ final class RestoreFeatureTests: XCTestCase { ...@@ -145,13 +160,43 @@ final class RestoreFeatureTests: XCTestCase {
XCTAssertNoDifference(didRestoreWithData, [backupData]) XCTAssertNoDifference(didRestoreWithData, [backupData])
XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase]) XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase])
XCTAssertNoDifference(didSaveContact, [Contact( XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts])
XCTAssertNoDifference(didSaveContact, [
Contact(
id: contactId, id: contactId,
username: restoreResult.restoredParams.username, username: restoreResult.restoredParams.username,
email: restoredFacts.get(.email)?.value, email: restoredFacts.get(.email)?.value,
phone: restoredFacts.get(.phone)?.value, phone: restoredFacts.get(.phone)?.value,
createdAt: now createdAt: now
)]) ),
Contact(
id: "contact-\(1)-id".data(using: .utf8)!,
marshaled: "contact-\(1)-data".data(using: .utf8)!,
username: "contact-\(1)-username",
email: "contact-\(1)-email",
phone: "contact-\(1)-phone",
authStatus: .friend,
createdAt: now
),
Contact(
id: "contact-\(2)-id".data(using: .utf8)!,
marshaled: "contact-\(2)-data".data(using: .utf8)!,
username: "contact-\(2)-username",
email: "contact-\(2)-email",
phone: "contact-\(2)-phone",
authStatus: .friend,
createdAt: now
),
Contact(
id: "contact-\(3)-id".data(using: .utf8)!,
marshaled: "contact-\(3)-data".data(using: .utf8)!,
username: "contact-\(3)-username",
email: "contact-\(3)-email",
phone: "contact-\(3)-phone",
authStatus: .friend,
createdAt: now
),
])
store.receive(.finished) { store.receive(.finished) {
$0.isRestoring = false $0.isRestoring = false
...@@ -195,9 +240,28 @@ final class RestoreFeatureTests: XCTestCase { ...@@ -195,9 +240,28 @@ final class RestoreFeatureTests: XCTestCase {
XCTAssertEqual(didDestroyMessenger, 1) XCTAssertEqual(didDestroyMessenger, 1)
store.receive(.failed(failure as NSError)) { store.receive(.failed([failure as NSError])) {
$0.isRestoring = false $0.isRestoring = false
$0.restoreFailure = failure.localizedDescription $0.restoreFailures = [failure.localizedDescription]
}
}
} }
private extension XXClient.Contact {
static func stub(_ id: Int) -> XXClient.Contact {
var contact = XXClient.Contact.unimplemented(
"contact-\(id)-data".data(using: .utf8)!
)
contact.getIdFromContact.run = { _ in
"contact-\(id)-id".data(using: .utf8)!
}
contact.getFactsFromContact.run = { _ in
[
Fact(type: .username, value: "contact-\(id)-username"),
Fact(type: .email, value: "contact-\(id)-email"),
Fact(type: .phone, value: "contact-\(id)-phone"),
]
}
return contact
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment