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 {
public init(
file: File? = nil,
fileImportFailure: String? = nil,
restoreFailure: String? = nil,
restoreFailures: [String] = [],
focusedField: Field? = nil,
isImportingFile: Bool = false,
passphrase: String = "",
......@@ -32,7 +32,7 @@ public struct RestoreState: Equatable {
) {
self.file = file
self.fileImportFailure = fileImportFailure
self.restoreFailure = restoreFailure
self.restoreFailures = restoreFailures
self.focusedField = focusedField
self.isImportingFile = isImportingFile
self.passphrase = passphrase
......@@ -41,7 +41,7 @@ public struct RestoreState: Equatable {
public var file: File?
public var fileImportFailure: String?
public var restoreFailure: String?
public var restoreFailures: [String]
@BindableState public var focusedField: Field?
@BindableState public var isImportingFile: Bool
@BindableState public var passphrase: String
......@@ -53,7 +53,7 @@ public enum RestoreAction: Equatable, BindableAction {
case fileImport(Result<URL, NSError>)
case restoreTapped
case finished
case failed(NSError)
case failed([NSError])
case binding(BindingAction<RestoreState>)
}
......@@ -125,7 +125,7 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
guard let backupData = state.file?.data, backupData.count > 0 else { return .none }
let backupPassphrase = state.passphrase
state.isRestoring = true
state.restoreFailure = nil
state.restoreFailures = []
return Effect.result {
do {
let result = try env.messenger.restoreBackup(
......@@ -140,10 +140,33 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
phone: facts.get(.phone)?.value,
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)
} catch {
try? env.messenger.destroy()
return .success(.failed(error as NSError))
var errors = [error as NSError]
do {
try env.messenger.destroy()
} catch {
errors.append(error as NSError)
}
return .success(.failed(errors))
}
}
.subscribe(on: env.bgQueue)
......@@ -154,9 +177,9 @@ public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironm
state.isRestoring = false
return .none
case .failed(let error):
case .failed(let errors):
state.isRestoring = false
state.restoreFailure = error.localizedDescription
state.restoreFailures = errors.map(\.localizedDescription)
return .none
case .binding(_):
......
......@@ -21,7 +21,7 @@ public struct RestoreView: View {
var isRestoring: Bool
var focusedField: RestoreState.Field?
var fileImportFailure: String?
var restoreFailure: String?
var restoreFailures: [String]
init(state: RestoreState) {
file = state.file.map { .init(name: $0.name, size: $0.data.count) }
......@@ -30,7 +30,7 @@ public struct RestoreView: View {
isRestoring = state.isRestoring
focusedField = state.focusedField
fileImportFailure = state.fileImportFailure
restoreFailure = state.restoreFailure
restoreFailures = state.restoreFailures
}
}
......@@ -38,6 +38,30 @@ public struct RestoreView: View {
WithViewStore(store, observe: ViewState.init) { viewStore in
NavigationView {
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 {
if let file = viewStore.file {
HStack(alignment: .bottom) {
......@@ -45,8 +69,7 @@ public struct RestoreView: View {
Spacer()
Text(format(byteCount: file.size))
}
}
} else {
Button {
viewStore.send(.importFileTapped)
} label: {
......@@ -62,16 +85,22 @@ public struct RestoreView: View {
viewStore.send(.fileImport(result.mapError { $0 as NSError }))
}
)
if let failure = viewStore.fileImportFailure {
Text("Error: \(failure)")
.disabled(viewStore.isRestoring)
}
} header: {
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 {
SecureField("Passphrase", text: viewStore.binding(
get: \.passphrase,
......@@ -81,6 +110,7 @@ public struct RestoreView: View {
.textInputAutocapitalization(.never)
.disableAutocorrection(true)
.focused($focusedField, equals: .passphrase)
.disabled(viewStore.isRestoring)
Button {
viewStore.send(.restoreTapped)
......@@ -93,31 +123,19 @@ public struct RestoreView: View {
}
}
}
if let failure = viewStore.restoreFailure {
Text("Error: \(failure)")
}
} header: {
Text("Backup")
}
.disabled(viewStore.isRestoring)
}
}
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
viewStore.send(.finished)
} label: {
Text("Cancel")
}
.disabled(viewStore.isRestoring)
Text("Restore")
}
if !viewStore.restoreFailures.isEmpty {
Section {
ForEach(Array(viewStore.restoreFailures.enumerated()), id: \.offset) { _, failure in
Text(failure)
}
.navigationTitle("Restore")
.onChange(of: viewStore.focusedField) { focusedField = $0 }
.onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) }
.font(.footnote)
} header: {
Text("Error")
}
.navigationViewStyle(.stack)
}
}
......@@ -133,7 +151,19 @@ public struct RestoreView: View {
public struct RestoreView_Previews: PreviewProvider {
public static var previews: some View {
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,
environment: ()
))
......
......@@ -85,15 +85,22 @@ final class RestoreFeatureTests: XCTestCase {
]
let restoreResult = MessengerRestoreBackup.Result(
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 contactId = "contact-id".data(using: .utf8)!
var udFacts: [Fact] = []
var didRestoreWithData: [Data] = []
var didRestoreWithPassphrase: [String] = []
var didSaveContact: [XXModels.Contact] = []
var didLookupContactIds: [[Data]] = []
let store = TestStore(
initialState: RestoreState(
......@@ -126,6 +133,14 @@ final class RestoreFeatureTests: XCTestCase {
ud.getFacts.run = { udFacts }
return ud
}
store.environment.messenger.lookupContacts.run = { contactIds in
didLookupContactIds.append(contactIds)
return .init(
contacts: lookedUpContacts,
failedIds: [],
errors: []
)
}
store.environment.db.run = {
var db: Database = .unimplemented
db.saveContact.run = { contact in
......@@ -145,13 +160,43 @@ final class RestoreFeatureTests: XCTestCase {
XCTAssertNoDifference(didRestoreWithData, [backupData])
XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase])
XCTAssertNoDifference(didSaveContact, [Contact(
XCTAssertNoDifference(didLookupContactIds, [restoreResult.restoredContacts])
XCTAssertNoDifference(didSaveContact, [
Contact(
id: contactId,
username: restoreResult.restoredParams.username,
email: restoredFacts.get(.email)?.value,
phone: restoredFacts.get(.phone)?.value,
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) {
$0.isRestoring = false
......@@ -195,9 +240,28 @@ final class RestoreFeatureTests: XCTestCase {
XCTAssertEqual(didDestroyMessenger, 1)
store.receive(.failed(failure as NSError)) {
store.receive(.failed([failure as NSError])) {
$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