diff --git a/Examples/xx-messenger/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift b/Examples/xx-messenger/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift new file mode 100644 index 0000000000000000000000000000000000000000..84abe3c5edf058f205978bc836758a52bd379e0f --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift @@ -0,0 +1,128 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct ReceiveFileHandler { + public struct ProgressError: Error { + public init(message: String) { + self.message = message + } + + public var message: String + } + + public typealias OnError = (Error) -> Void + + public var run: (@escaping OnError) -> Cancellable + + public func callAsFunction(onError: @escaping OnError) -> Cancellable { + run(onError) + } +} + +extension ReceiveFileHandler { + public static func live( + messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> ReceiveFileHandler { + ReceiveFileHandler { onError in + func receiveFile(_ file: ReceivedFile) { + do { + let date = now() + try db().saveFileTransfer(XXModels.FileTransfer( + id: file.transferId, + contactId: file.senderId, + name: file.name, + type: file.type, + data: nil, + progress: 0, + isIncoming: true, + createdAt: date + )) + try db().saveMessage(XXModels.Message( + senderId: file.senderId, + recipientId: try messenger.e2e.tryGet().getContact().getId(), + groupId: nil, + date: date, + status: .received, + isUnread: false, + text: "", + fileTransferId: file.transferId + )) + try messenger.receiveFile(.init( + transferId: file.transferId, + callbackIntervalMS: 500 + )) { info in + switch info { + case .progress(let transmitted, let total): + updateProgress( + transferId: file.transferId, + transmitted: transmitted, + total: total + ) + + case .finished(let data): + saveData( + transferId: file.transferId, + data: data + ) + + case .failed(.receiveError(let error)): + onError(error) + + case .failed(.callbackError(let error)): + onError(error) + + case .failed(.progressError(let message)): + onError(ProgressError(message: message)) + } + } + } catch { + onError(error) + } + } + + func updateProgress(transferId: Data, transmitted: Int, total: Int) { + do { + if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first { + transfer.progress = total > 0 ? Float(transmitted) / Float(total) : 0 + try db().saveFileTransfer(transfer) + } + } catch { + onError(error) + } + } + + func saveData(transferId: Data, data: Data) { + do { + if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first { + transfer.progress = 1 + transfer.data = data + try db().saveFileTransfer(transfer) + } + } catch { + onError(error) + } + } + + return messenger.registerReceiveFileCallback(.init { result in + switch result { + case .success(let file): + receiveFile(file) + + case .failure(let error): + onError(error) + } + }) + } + } +} + +extension ReceiveFileHandler { + public static let unimplemented = ReceiveFileHandler( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/SendImage/SendImage.swift b/Examples/xx-messenger/Sources/AppCore/SendImage/SendImage.swift new file mode 100644 index 0000000000000000000000000000000000000000..a457dcd49e11c3e06975a7151a6bd8a9dd839412 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/SendImage/SendImage.swift @@ -0,0 +1,119 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct SendImage { + public struct ProgressError: Error, Equatable { + public init(message: String) { + self.message = message + } + + public var message: String + } + + public typealias OnError = (Error) -> Void + public typealias Completion = () -> Void + + public var run: (Data, Data, @escaping OnError, @escaping Completion) -> Void + + public func callAsFunction( + _ image: Data, + to recipientId: Data, + onError: @escaping OnError, + completion: @escaping Completion + ) { + run(image, recipientId, onError, completion) + } +} + +extension SendImage { + public static func live( + messenger: Messenger, + db: DBManagerGetDB, + now: @escaping () -> Date + ) -> SendImage { + SendImage { image, recipientId, onError, completion in + func updateProgress(transferId: Data, progress: Float) { + do { + if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first { + transfer.progress = progress + try db().saveFileTransfer(transfer) + } + } catch { + onError(error) + } + } + + let file = FileSend( + name: "image.jpg", + type: "image", + preview: nil, + contents: image + ) + let params = MessengerSendFile.Params( + file: file, + recipientId: recipientId, + retry: 2, + callbackIntervalMS: 500 + ) + do { + let date = now() + let myContactId = try messenger.e2e.tryGet().getContact().getId() + let transferId = try messenger.sendFile(params) { info in + switch info { + case .progress(let transferId, let transmitted, let total): + updateProgress( + transferId: transferId, + progress: total > 0 ? Float(transmitted) / Float(total) : 0 + ) + + case .finished(let transferId): + updateProgress( + transferId: transferId, + progress: 1 + ) + + case .failed(_, .error(let error)): + onError(error) + + case .failed(_, .progressError(let message)): + onError(ProgressError(message: message)) + + case .failed(_, .close(let error)): + onError(error) + } + } + try db().saveFileTransfer(XXModels.FileTransfer( + id: transferId, + contactId: myContactId, + name: file.name, + type: file.type, + data: image, + progress: 0, + isIncoming: false, + createdAt: date + )) + try db().saveMessage(XXModels.Message( + senderId: myContactId, + recipientId: recipientId, + groupId: nil, + date: date, + status: .sent, + isUnread: false, + text: "", + fileTransferId: transferId + )) + } catch { + onError(error) + } + } + } +} + +extension SendImage { + public static let unimplemented = SendImage( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/ImagePicker.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/ImagePicker.swift new file mode 100644 index 0000000000000000000000000000000000000000..c0b5fbadd05543205ae7c87dfbdc5daa5d1a1f2b --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/ImagePicker.swift @@ -0,0 +1,49 @@ +import UIKit +import SwiftUI + +public struct ImagePicker: UIViewControllerRepresentable { + public init(onImport: @escaping (UIImage) -> Void) { + self.onImport = onImport + } + + var onImport: (UIImage) -> Void + @Environment(\.presentationMode) private var presentationMode + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public func makeUIViewController(context: Context) -> UIImagePickerController { + let controller = UIImagePickerController() + controller.delegate = context.coordinator + return controller + } + + public func updateUIViewController( + _ uiViewController: UIImagePickerController, + context: Context + ) {} +} + +extension ImagePicker { + public final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + init(_ picker: ImagePicker) { + self.picker = picker + super.init() + } + + public func imagePickerController( + _ controller: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any] + ) { + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + DispatchQueue.main.async { + self.picker.onImport(image) + } + } + picker.presentationMode.wrappedValue.dismiss() + } + + let picker: ImagePicker + } +} diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index c4ff9b27f027a0eef0d02eaeab6e8e7db519025c..2b1f9964c4b900bee5596eb7fee1e1f59ef4f7c8 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -99,6 +99,11 @@ extension AppEnvironment { db: dbManager.getDB, now: Date.init ), + sendImage: .live( + messenger: messenger, + db: dbManager.getDB, + now: Date.init + ), mainQueue: mainQueue, bgQueue: bgQueue ) @@ -113,6 +118,11 @@ extension AppEnvironment { messenger: messenger, db: dbManager.getDB ), + receiveFileHandler: .live( + messenger: messenger, + db: dbManager.getDB, + now: Date.init + ), backupStorage: backupStorage, log: .live(), mainQueue: mainQueue, diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index 7796f592bcd5c795c20d4a281d243cd03a7b8df9..56f692c6c1b1cb83f010ec479fb2d82058f4b25e 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift @@ -50,6 +50,7 @@ struct AppEnvironment { var messenger: Messenger var authHandler: AuthCallbackHandler var messageListener: MessageListenerHandler + var receiveFileHandler: ReceiveFileHandler var backupStorage: BackupStorage var log: Logger var mainQueue: AnySchedulerOf<DispatchQueue> @@ -66,6 +67,7 @@ extension AppEnvironment { messenger: .unimplemented, authHandler: .unimplemented, messageListener: .unimplemented, + receiveFileHandler: .unimplemented, backupStorage: .unimplemented, log: .unimplemented, mainQueue: .unimplemented, @@ -98,6 +100,10 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> cancellables.append(env.messageListener(onError: { error in env.log(.error(error as NSError)) })) + cancellables.append(env.receiveFileHandler(onError: { error in + env.log(.error(error as NSError)) + })) + cancellables.append(env.messenger.registerBackupCallback(.init { data in try? env.backupStorage.store(data) })) diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift index 46cfdbfa7103029615bd525abd78d8f54dfa3a10..3538bc2536f23e493a298f2761fd3614711f77da 100644 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift @@ -18,13 +18,15 @@ public struct ChatState: Equatable, Identifiable { date: Date, senderId: Data, text: String, - status: XXModels.Message.Status + status: XXModels.Message.Status, + fileTransfer: XXModels.FileTransfer? = nil ) { self.id = id self.date = date self.senderId = senderId self.text = text self.status = status + self.fileTransfer = fileTransfer } public var id: Int64 @@ -32,6 +34,7 @@ public struct ChatState: Equatable, Identifiable { public var senderId: Data public var text: String public var status: XXModels.Message.Status + public var fileTransfer: XXModels.FileTransfer? } public init( @@ -63,6 +66,7 @@ public enum ChatAction: Equatable, BindableAction { case didFetchMessages(IdentifiedArrayOf<ChatState.Message>) case sendTapped case sendFailed(String) + case imagePicked(Data) case dismissSendFailureTapped case binding(BindingAction<ChatState>) } @@ -72,12 +76,14 @@ public struct ChatEnvironment { messenger: Messenger, db: DBManagerGetDB, sendMessage: SendMessage, + sendImage: SendImage, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue> ) { self.messenger = messenger self.db = db self.sendMessage = sendMessage + self.sendImage = sendImage self.mainQueue = mainQueue self.bgQueue = bgQueue } @@ -85,6 +91,7 @@ public struct ChatEnvironment { public var messenger: Messenger public var db: DBManagerGetDB public var sendMessage: SendMessage + public var sendImage: SendImage public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> } @@ -95,6 +102,7 @@ extension ChatEnvironment { messenger: .unimplemented, db: .unimplemented, sendMessage: .unimplemented, + sendImage: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented ) @@ -112,31 +120,50 @@ public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment> let myContactId = try env.messenger.e2e.tryGet().getContact().getId() state.myContactId = myContactId let queryChat: XXModels.Message.Query.Chat + let receivedFileTransfersQuery: XXModels.FileTransfer.Query + let sentFileTransfersQuery: XXModels.FileTransfer.Query switch state.id { case .contact(let contactId): queryChat = .direct(myContactId, contactId) + receivedFileTransfersQuery = .init( + contactId: contactId, + isIncoming: true + ) + sentFileTransfersQuery = .init( + contactId: myContactId, + isIncoming: false + ) } - let query = XXModels.Message.Query(chat: queryChat) - return try env.db().fetchMessagesPublisher(query) - .assertNoFailure() - .map { messages in - messages.compactMap { message in - guard let id = message.id else { return nil } - return ChatState.Message( - id: id, - date: message.date, - senderId: message.senderId, - text: message.text, - status: message.status - ) - } + let messagesQuery = XXModels.Message.Query(chat: queryChat) + return Publishers.CombineLatest3( + try env.db().fetchMessagesPublisher(messagesQuery), + try env.db().fetchFileTransfersPublisher(receivedFileTransfersQuery), + try env.db().fetchFileTransfersPublisher(sentFileTransfersQuery) + ) + .map { messages, receivedFileTransfers, sentFileTransfers in + (messages, receivedFileTransfers + sentFileTransfers) + } + .assertNoFailure() + .map { messages, fileTransfers in + messages.compactMap { message in + guard let id = message.id else { return nil } + return ChatState.Message( + id: id, + date: message.date, + senderId: message.senderId, + text: message.text, + status: message.status, + fileTransfer: fileTransfers.first { $0.id == message.fileTransferId } + ) } - .map { IdentifiedArrayOf<ChatState.Message>(uniqueElements: $0) } - .map(ChatAction.didFetchMessages) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: FetchEffectId.self, cancelInFlight: true) + } + .removeDuplicates() + .map { IdentifiedArrayOf<ChatState.Message>(uniqueElements: $0) } + .map(ChatAction.didFetchMessages) + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: FetchEffectId.self, cancelInFlight: true) } catch { state.failure = error.localizedDescription return .none @@ -174,6 +201,28 @@ public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment> state.sendFailure = failure return .none + case .imagePicked(let data): + let chatId = state.id + return Effect.run { subscriber in + switch chatId { + case .contact(let recipientId): + env.sendImage( + data, + to: recipientId, + onError: { error in + subscriber.send(.sendFailed(error.localizedDescription)) + }, + completion: { + subscriber.send(completion: .finished) + } + ) + } + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + case .dismissSendFailureTapped: state.sendFailure = nil return .none diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift index 23596dc1ceecc06d65cc6342318278f84313f03a..6058b61bc86fe9c54f25483e7de2a05c5a35dc9d 100644 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift @@ -8,6 +8,7 @@ public struct ChatView: View { } let store: Store<ChatState, ChatAction> + @State var isPresentingImagePicker = false struct ViewState: Equatable { var myContactId: Data? @@ -87,12 +88,28 @@ public struct ChatView: View { )) .textFieldStyle(.roundedBorder) - Button { - viewStore.send(.sendTapped) - } label: { - Image(systemName: "paperplane.fill") + if viewStore.text.isEmpty == false { + Button { + viewStore.send(.sendTapped) + } label: { + Image(systemName: "paperplane.fill") + } + .buttonStyle(.borderedProminent) + } else { + Button { + isPresentingImagePicker = true + } label: { + Image(systemName: "photo.on.rectangle.angled") + } + .buttonStyle(.borderedProminent) + .sheet(isPresented: $isPresentingImagePicker) { + ImagePicker { image in + if let data = image.jpegData(compressionQuality: 0.7) { + viewStore.send(.imagePicked(data)) + } + } + } } - .buttonStyle(.borderedProminent) } .padding() } @@ -112,6 +129,10 @@ public struct ChatView: View { message.senderId == myContactId ? .trailing : .leading } + var paddingEdge: Edge.Set { + message.senderId == myContactId ? .leading : .trailing + } + var textColor: Color? { message.senderId == myContactId ? Color.white : nil } @@ -123,20 +144,38 @@ public struct ChatView: View { .font(.footnote) .frame(maxWidth: .infinity, alignment: alignment) - Text(message.text) - .foregroundColor(textColor) - .padding(.horizontal, 16) - .padding(.vertical, 8) - .background { - if message.senderId == myContactId { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(Color.blue) - } else { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(Material.ultraThick) + VStack(alignment: .leading) { + if let fileTransfer = message.fileTransfer { + Text("\(fileTransfer.name) (\(fileTransfer.type))") + if fileTransfer.progress < 1 { + ProgressView(value: fileTransfer.progress) + } + if fileTransfer.type == "image", + let data = fileTransfer.data, + let image = UIImage(data: data) { + Image(uiImage: image) + .resizable() + .scaledToFit() + .padding(.bottom, 8) } + } else { + Text(message.text) } - .frame(maxWidth: .infinity, alignment: alignment) + } + .foregroundColor(textColor) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background { + if message.senderId == myContactId { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Color.blue) + } else { + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(Material.ultraThick) + } + } + .frame(maxWidth: .infinity, alignment: alignment) + .padding(paddingEdge, 60) } .padding(.horizontal) } @@ -178,6 +217,47 @@ public struct ChatView_Previews: PreviewProvider { text: "Hi!", status: .sent ), + .init( + id: 3, + date: Date(), + senderId: "contact-id".data(using: .utf8)!, + text: "", + status: .received, + fileTransfer: .init( + id: Data(), + contactId: Data(), + name: "received_file.jpeg", + type: "image", + progress: 0.75, + isIncoming: true + ) + ), + .init( + id: 4, + date: Date(), + senderId: "my-contact-id".data(using: .utf8)!, + text: "", + status: .sent, + fileTransfer: .init( + id: Data(), + contactId: Data(), + name: "sent_file.jpeg", + type: "image", + data: { + let bounds = CGRect(origin: .zero, size: .init(width: 4, height: 3)) + let format = UIGraphicsImageRendererFormat() + format.scale = 1 + let renderer = UIGraphicsImageRenderer(bounds: bounds, format: format) + let image = renderer.image { ctx in + UIColor.systemMint.setFill() + ctx.fill(bounds) + } + return image.jpegData(compressionQuality: 0.72) + }(), + progress: 1, + isIncoming: true + ) + ), ], failure: "Something went wrong when fetching messages from database.", sendFailure: "Something went wrong when sending message." diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index f2015ebc2434ecaada1ba9abd4d345cea91bd03e..6433dd330faac74045cad472798cd8c06a79cb40 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -151,6 +151,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> try env.messenger.listenForMessages() } + if env.messenger.isFileTransferRunning() == false { + try env.messenger.startFileTransfer() + } + if env.messenger.isLoggedIn() == false { if try env.messenger.isRegistered() == false { return .success(.messenger(.didStartUnregistered)) diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AppCoreTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AppCoreTests.swift deleted file mode 100644 index 9a5658d4caa59cea2181061ff1ca1d2d6d38c5e4..0000000000000000000000000000000000000000 --- a/Examples/xx-messenger/Tests/AppCoreTests/AppCoreTests.swift +++ /dev/null @@ -1,9 +0,0 @@ -import XCTest -@testable import AppCore - -@MainActor -final class AppCoreTests: XCTestCase { - func testExample() async throws { - XCTAssert(true) - } -} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/ReceiveFileHandler/ReceiveFileHandlerTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/ReceiveFileHandler/ReceiveFileHandlerTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..6d54a9cfcc27db703a70f723b29bbc0833e1f5b1 --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/ReceiveFileHandler/ReceiveFileHandlerTests.swift @@ -0,0 +1,214 @@ +import CustomDump +import XCTest +import XXMessengerClient +import XXClient +import XXModels +@testable import AppCore + +final class ReceiveFileHandlerTests: XCTestCase { + func testReceiveFile() { + let currentDate = Date(timeIntervalSince1970: 123) + let myContactId = "my-contact-id".data(using: .utf8)! + let receivedFile = ReceivedFile.stub() + + var actions: [Action] = [] + var receiveFileCallback: ReceiveFileCallback? + var receivingFileCallback: MessengerReceiveFile.Callback? + + var messenger: Messenger = .unimplemented + messenger.registerReceiveFileCallback.run = { callback in + actions.append(.didRegisterReceiveFileCallback) + receiveFileCallback = callback + return Cancellable { actions.append(.didCancelReceiveFileCallback) } + } + messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in myContactId } + return contact + } + return e2e + } + messenger.receiveFile.run = { params, callback in + actions.append(.didReceiveFile(params)) + receivingFileCallback = callback + } + + var db: DBManagerGetDB = .unimplemented + db.run = { + var db: Database = .unimplemented + db.saveFileTransfer.run = { model in + actions.append(.didSaveFileTransfer(model)) + return model + } + db.saveMessage.run = { model in + actions.append(.didSaveMessage(model)) + return model + } + db.fetchFileTransfers.run = { query in + actions.append(.didFetchFileTransfers(query)) + return [ + FileTransfer( + id: receivedFile.transferId, + contactId: receivedFile.senderId, + name: receivedFile.name, + type: receivedFile.type, + data: nil, + progress: 0, + isIncoming: true, + createdAt: currentDate + ) + ] + } + return db + } + + let handler = ReceiveFileHandler.live( + messenger: messenger, + db: db, + now: { currentDate } + ) + + XCTAssertNoDifference(actions, []) + + actions = [] + let cancellable = handler(onError: { error in + actions.append(.didCatchError(error as NSError)) + }) + + XCTAssertNoDifference(actions, [ + .didRegisterReceiveFileCallback + ]) + + actions = [] + let error = NSError(domain: "receive-file", code: 1) + receiveFileCallback?.handle(.failure(error)) + + XCTAssertNoDifference(actions, [ + .didCatchError(error) + ]) + + actions = [] + receiveFileCallback?.handle(.success(receivedFile)) + + XCTAssertNoDifference(actions, [ + .didSaveFileTransfer(FileTransfer( + id: receivedFile.transferId, + contactId: receivedFile.senderId, + name: receivedFile.name, + type: receivedFile.type, + data: nil, + progress: 0, + isIncoming: true, + createdAt: currentDate + )), + .didSaveMessage(Message( + networkId: nil, + senderId: receivedFile.senderId, + recipientId: myContactId, + groupId: nil, + date: currentDate, + status: .received, + isUnread: false, + text: "", + replyMessageId: nil, + roundURL: nil, + fileTransferId: receivedFile.transferId + )), + .didReceiveFile(MessengerReceiveFile.Params( + transferId: receivedFile.transferId, + callbackIntervalMS: 500 + )), + ]) + + actions = [] + let receivingFileError = NSError(domain: "receiving-file", code: 2) + receivingFileCallback?(.failed(.receiveError(receivingFileError))) + + XCTAssertNoDifference(actions, [ + .didCatchError(receivingFileError) + ]) + + actions = [] + let receivingFileCallbackError = NSError(domain: "receiving-file-callback", code: 3) + receivingFileCallback?(.failed(.callbackError(receivingFileCallbackError))) + + XCTAssertNoDifference(actions, [ + .didCatchError(receivingFileCallbackError) + ]) + + actions = [] + let receivingFileProgressError = "receiving-file-progress" + receivingFileCallback?(.failed(.progressError(receivingFileProgressError))) + + XCTAssertNoDifference(actions, [ + .didCatchError(ReceiveFileHandler.ProgressError(message: receivingFileProgressError) as NSError) + ]) + + actions = [] + receivingFileCallback?(.progress(transmitted: 1, total: 2)) + + XCTAssertNoDifference(actions, [ + .didFetchFileTransfers(.init(id: [receivedFile.transferId])), + .didSaveFileTransfer(FileTransfer( + id: receivedFile.transferId, + contactId: receivedFile.senderId, + name: receivedFile.name, + type: receivedFile.type, + data: nil, + progress: 0.5, + isIncoming: true, + createdAt: currentDate + )), + ]) + + actions = [] + let fileData = "file-data".data(using: .utf8)! + receivingFileCallback?(.finished(fileData)) + + XCTAssertNoDifference(actions, [ + .didFetchFileTransfers(.init(id: [receivedFile.transferId])), + .didSaveFileTransfer(FileTransfer( + id: receivedFile.transferId, + contactId: receivedFile.senderId, + name: receivedFile.name, + type: receivedFile.type, + data: fileData, + progress: 1, + isIncoming: true, + createdAt: currentDate + )), + ]) + + actions = [] + cancellable.cancel() + + XCTAssertNoDifference(actions, [ + .didCancelReceiveFileCallback + ]) + } +} + +private enum Action: Equatable { + case didRegisterReceiveFileCallback + case didCancelReceiveFileCallback + case didCatchError(NSError) + case didSaveFileTransfer(XXModels.FileTransfer) + case didSaveMessage(XXModels.Message) + case didReceiveFile(MessengerReceiveFile.Params) + case didFetchFileTransfers(XXModels.FileTransfer.Query) +} + +private extension ReceivedFile { + static func stub() -> ReceivedFile { + ReceivedFile( + transferId: "received-file-transferId".data(using: .utf8)!, + senderId: "received-file-senderId".data(using: .utf8)!, + preview: "received-file-preview".data(using: .utf8)!, + name: "received-file-name", + type: "received-file-type", + size: 1234 + ) + } +} diff --git a/Examples/xx-messenger/Tests/AppCoreTests/SendImage/SendImageTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/SendImage/SendImageTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d1cb1741abe2ffdbd2f4481800ca8529943cb9df --- /dev/null +++ b/Examples/xx-messenger/Tests/AppCoreTests/SendImage/SendImageTests.swift @@ -0,0 +1,163 @@ +import CustomDump +import XCTest +import XXClient +import XXMessengerClient +import XXModels +@testable import AppCore + +final class SendImageTests: XCTestCase { + func testSend() { + let image = "image-data".data(using: .utf8)! + let recipientId = "recipient-id".data(using: .utf8)! + let myContactId = "my-contact-id".data(using: .utf8)! + let transferId = "transfer-id".data(using: .utf8)! + let currentDate = Date(timeIntervalSince1970: 123) + + var actions: [Action] = [] + var sendFileCallback: MessengerSendFile.Callback? + + var messenger: Messenger = .unimplemented + messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in myContactId } + return contact + } + return e2e + } + messenger.sendFile.run = { params, callback in + actions.append(.didSendFile(params)) + sendFileCallback = callback + return transferId + } + var db: DBManagerGetDB = .unimplemented + db.run = { + var db: Database = .unimplemented + db.saveFileTransfer.run = { model in + actions.append(.didSaveFileTransfer(model)) + return model + } + db.saveMessage.run = { model in + actions.append(.didSaveMessage(model)) + return model + } + db.fetchFileTransfers.run = { query in + actions.append(.didFetchFileTransfers(query)) + return [.stub(withProgress: 0)] + } + return db + } + let send: SendImage = .live(messenger: messenger, db: db, now: { currentDate }) + + actions = [] + send( + image, + to: recipientId, + onError: { error in + actions.append(.didFail(error as NSError)) + }, + completion: { + actions.append(.didComplete) + } + ) + + XCTAssertNoDifference(actions, [ + .didSendFile(.init( + file: .init( + name: "image.jpg", + type: "image", + preview: nil, + contents: image + ), + recipientId: recipientId, + retry: 2, + callbackIntervalMS: 500 + )), + .didSaveFileTransfer(.init( + id: transferId, + contactId: myContactId, + name: "image.jpg", + type: "image", + data: image, + progress: 0, + isIncoming: false, + createdAt: currentDate + )), + .didSaveMessage(.init( + senderId: myContactId, + recipientId: recipientId, + groupId: nil, + date: currentDate, + status: .sent, + isUnread: false, + text: "", + fileTransferId: transferId + )), + ]) + + actions = [] + let sendError = NSError(domain: "send-error", code: 1) + sendFileCallback?(.failed(id: transferId, .error(sendError))) + + XCTAssertNoDifference(actions, [ + .didFail(sendError), + ]) + + actions = [] + let closeError = NSError(domain: "close-error", code: 2) + sendFileCallback?(.failed(id: transferId, .close(closeError))) + + XCTAssertNoDifference(actions, [ + .didFail(closeError), + ]) + + actions = [] + let progressError = "progress-error" + sendFileCallback?(.failed(id: transferId, .progressError(progressError))) + + XCTAssertNoDifference(actions, [ + .didFail(SendImage.ProgressError(message: progressError) as NSError), + ]) + + actions = [] + sendFileCallback?(.progress(id: transferId, transmitted: 1, total: 2)) + + XCTAssertNoDifference(actions, [ + .didFetchFileTransfers(.init(id: [transferId])), + .didSaveFileTransfer(.stub(withProgress: 0.5)), + ]) + + actions = [] + sendFileCallback?(.finished(id: transferId)) + + XCTAssertNoDifference(actions, [ + .didFetchFileTransfers(.init(id: [transferId])), + .didSaveFileTransfer(.stub(withProgress: 1)), + ]) + } +} + +private enum Action: Equatable { + case didFail(NSError) + case didComplete + case didSendFile(MessengerSendFile.Params) + case didSaveFileTransfer(XXModels.FileTransfer) + case didSaveMessage(XXModels.Message) + case didFetchFileTransfers(XXModels.FileTransfer.Query) +} + +private extension XXModels.FileTransfer { + static func stub(withProgress progress: Float) -> XXModels.FileTransfer { + XXModels.FileTransfer( + id: Data(), + contactId: Data(), + name: "", + type: "", + data: nil, + progress: progress, + isIncoming: false, + createdAt: Date(timeIntervalSince1970: 0) + ) + } +} diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift index 098a026c2c665c803b9b1ef00d4c6dbbe9250f0a..5ec16fc51cbb5f6b7ca0ce23696860b4460a8df3 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift @@ -34,6 +34,10 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.receiveFileHandler.run = { _ in + actions.append(.didStartReceiveFileHandler) + return Cancellable {} + } store.environment.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} @@ -49,6 +53,7 @@ final class AppFeatureTests: XCTestCase { .didMakeDB, .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, ]) @@ -83,6 +88,10 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.receiveFileHandler.run = { _ in + actions.append(.didStartReceiveFileHandler) + return Cancellable {} + } store.environment.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} @@ -98,6 +107,7 @@ final class AppFeatureTests: XCTestCase { .didMakeDB, .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, .didLoadMessenger, ]) @@ -132,6 +142,10 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.receiveFileHandler.run = { _ in + actions.append(.didStartReceiveFileHandler) + return Cancellable {} + } store.environment.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} @@ -148,6 +162,7 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, .didLoadMessenger, ]) @@ -182,6 +197,10 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.receiveFileHandler.run = { _ in + actions.append(.didStartReceiveFileHandler) + return Cancellable {} + } store.environment.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} @@ -198,6 +217,7 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, .didLoadMessenger, ]) @@ -229,6 +249,10 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.receiveFileHandler.run = { _ in + actions.append(.didStartReceiveFileHandler) + return Cancellable {} + } store.environment.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} @@ -245,6 +269,7 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, ]) @@ -331,6 +356,10 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.receiveFileHandler.run = { _ in + actions.append(.didStartReceiveFileHandler) + return Cancellable {} + } store.environment.messenger.registerBackupCallback.run = { _ in actions.append(.didRegisterBackupCallback) return Cancellable {} @@ -346,6 +375,7 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, ]) @@ -356,6 +386,7 @@ final class AppFeatureTests: XCTestCase { var actions: [Action]! var authHandlerOnError: [AuthCallbackHandler.OnError] = [] var messageListenerOnError: [MessageListenerHandler.OnError] = [] + var fileHandlerOnError: [ReceiveFileHandler.OnError] = [] var backupCallback: [UpdateBackupFunc] = [] let store = TestStore( @@ -383,6 +414,13 @@ final class AppFeatureTests: XCTestCase { actions.append(.didCancelMessageListener) } } + store.environment.receiveFileHandler.run = { onError in + fileHandlerOnError.append(onError) + actions.append(.didStartReceiveFileHandler) + return Cancellable { + actions.append(.didCancelReceiveFileHandler) + } + } store.environment.messenger.registerBackupCallback.run = { callback in backupCallback.append(callback) actions.append(.didRegisterBackupCallback) @@ -406,6 +444,7 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, ]) @@ -420,28 +459,36 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didCancelAuthHandler, .didCancelMessageListener, + .didCancelReceiveFileHandler, .didCancelBackupCallback, .didStartAuthHandler, .didStartMessageListener, + .didStartReceiveFileHandler, .didRegisterBackupCallback, ]) actions = [] - struct AuthError: Error {} - let authError = AuthError() + let authError = NSError(domain: "auth-handler-error", code: 1) authHandlerOnError.first?(authError) XCTAssertNoDifference(actions, [ - .didLog(.error(authError as NSError)) + .didLog(.error(authError)) ]) actions = [] - struct MessageError: Error {} - let messageError = MessageError() + let messageError = NSError(domain: "message-listener-error", code: 2) messageListenerOnError.first?(messageError) XCTAssertNoDifference(actions, [ - .didLog(.error(messageError as NSError)) + .didLog(.error(messageError)) + ]) + + actions = [] + let fileError = NSError(domain: "receive-file-error", code: 3) + fileHandlerOnError.first?(fileError) + + XCTAssertNoDifference(actions, [ + .didLog(.error(fileError)) ]) actions = [] @@ -458,6 +505,7 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didCancelAuthHandler, .didCancelMessageListener, + .didCancelReceiveFileHandler, .didCancelBackupCallback, ]) } @@ -467,10 +515,12 @@ private enum Action: Equatable { case didMakeDB case didStartAuthHandler case didStartMessageListener + case didStartReceiveFileHandler case didRegisterBackupCallback case didLoadMessenger case didCancelAuthHandler case didCancelMessageListener + case didCancelReceiveFileHandler case didCancelBackupCallback case didLog(Logger.Message) case didStoreBackup(Data) diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift index 7f0633021b525d1e26517db1ffc87ed073fae3d6..01d0ed8faa3c37b0a2005ace0b7c98d13b2e6590 100644 --- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift @@ -21,6 +21,8 @@ final class ChatFeatureTests: XCTestCase { var didFetchMessagesWithQuery: [XXModels.Message.Query] = [] let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>() + var didFetchFileTransfersWithQuery: [XXModels.FileTransfer.Query] = [] + let fileTransfersPublisher = PassthroughSubject<[XXModels.FileTransfer], Error>() store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate @@ -39,6 +41,10 @@ final class ChatFeatureTests: XCTestCase { didFetchMessagesWithQuery.append(query) return messagesPublisher.eraseToAnyPublisher() } + db.fetchFileTransfersPublisher.run = { query in + didFetchFileTransfersWithQuery.append(query) + return fileTransfersPublisher.eraseToAnyPublisher() + } return db } @@ -49,7 +55,25 @@ final class ChatFeatureTests: XCTestCase { XCTAssertNoDifference(didFetchMessagesWithQuery, [ .init(chat: .direct(myContactId, contactId)) ]) + XCTAssertNoDifference(didFetchFileTransfersWithQuery, [ + .init(contactId: contactId, isIncoming: true), + .init(contactId: myContactId, isIncoming: false), + ]) + let receivedFileTransfer = FileTransfer( + id: "file-transfer-1-id".data(using: .utf8)!, + contactId: contactId, + name: "file-transfer-1-name", + type: "file-transfer-1-type", + isIncoming: true + ) + let sentFileTransfer = FileTransfer( + id: "file-transfer-2-id".data(using: .utf8)!, + contactId: myContactId, + name: "file-transfer-2-name", + type: "file-transfer-2-type", + isIncoming: false + ) messagesPublisher.send([ .init( id: nil, @@ -69,7 +93,8 @@ final class ChatFeatureTests: XCTestCase { date: Date(timeIntervalSince1970: 1), status: .received, isUnread: false, - text: "Message 1" + text: "Message 1", + fileTransferId: receivedFileTransfer.id ), .init( id: 2, @@ -79,9 +104,14 @@ final class ChatFeatureTests: XCTestCase { date: Date(timeIntervalSince1970: 2), status: .sent, isUnread: false, - text: "Message 2" + text: "Message 2", + fileTransferId: sentFileTransfer.id ), ]) + fileTransfersPublisher.send([ + receivedFileTransfer, + sentFileTransfer, + ]) let expectedMessages = IdentifiedArrayOf<ChatState.Message>(uniqueElements: [ .init( @@ -89,14 +119,16 @@ final class ChatFeatureTests: XCTestCase { date: Date(timeIntervalSince1970: 1), senderId: contactId, text: "Message 1", - status: .received + status: .received, + fileTransfer: receivedFileTransfer ), .init( id: 2, date: Date(timeIntervalSince1970: 2), senderId: myContactId, text: "Message 2", - status: .sent + status: .sent, + fileTransfer: sentFileTransfer ), ]) @@ -105,6 +137,7 @@ final class ChatFeatureTests: XCTestCase { } messagesPublisher.send(completion: .finished) + fileTransfersPublisher.send(completion: .finished) } func testStartFailure() { @@ -207,4 +240,70 @@ final class ChatFeatureTests: XCTestCase { $0.sendFailure = nil } } + + func testSendImage() { + struct SendImageParams: Equatable { + var image: Data + var recipientId: Data + } + var didSendImageWithParams: [SendImageParams] = [] + var sendImageCompletion: SendImage.Completion? + + let store = TestStore( + initialState: ChatState(id: .contact("contact-id".data(using: .utf8)!)), + reducer: chatReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.sendImage.run = { image, recipientId, _, completion in + didSendImageWithParams.append(.init(image: image, recipientId: recipientId)) + sendImageCompletion = completion + } + + let image = "image-data".data(using: .utf8)! + store.send(.imagePicked(image)) + + XCTAssertNoDifference(didSendImageWithParams, [ + .init(image: image, recipientId: "contact-id".data(using: .utf8)!) + ]) + + sendImageCompletion?() + } + + func testSendImageFailure() { + var sendImageOnError: SendImage.OnError? + var sendImageCompletion: SendImage.Completion? + + let store = TestStore( + initialState: ChatState( + id: .contact("contact-id".data(using: .utf8)!) + ), + reducer: chatReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.sendImage.run = { _, _, onError, completion in + sendImageOnError = onError + sendImageCompletion = completion + } + + store.send(.imagePicked(Data())) + + let error = NSError(domain: "test", code: 123) + sendImageOnError?(error) + + store.receive(.sendFailed(error.localizedDescription)) { + $0.sendFailure = error.localizedDescription + } + + sendImageCompletion?() + + store.send(.dismissSendFailureTapped) { + $0.sendFailure = nil + } + } } diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 3a57414bb46df0058870adb5078aae2dfc41bf7c..6c23ff8b718191771ce8dbf30ef4777cfd7303f9 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -22,6 +22,7 @@ final class HomeFeatureTests: XCTestCase { var messengerDidStartWithTimeout: [Int] = [] var messengerDidConnect = 0 var messengerDidListenForMessages = 0 + var messengerDidStartFileTransfer = 0 store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate @@ -30,6 +31,8 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.connect.run = { messengerDidConnect += 1 } store.environment.messenger.isListeningForMessages.run = { false } store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } + store.environment.messenger.isFileTransferRunning.run = { false } + store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { false } @@ -38,6 +41,7 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidListenForMessages, 1) + XCTAssertNoDifference(messengerDidStartFileTransfer, 1) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { @@ -55,6 +59,7 @@ final class HomeFeatureTests: XCTestCase { var messengerDidStartWithTimeout: [Int] = [] var messengerDidConnect = 0 var messengerDidListenForMessages = 0 + var messengerDidStartFileTransfer = 0 var messengerDidLogIn = 0 var messengerDidResumeBackup = 0 @@ -65,6 +70,8 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.connect.run = { messengerDidConnect += 1 } store.environment.messenger.isListeningForMessages.run = { false } store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } + store.environment.messenger.isFileTransferRunning.run = { false } + store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } @@ -85,6 +92,7 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidListenForMessages, 1) + XCTAssertNoDifference(messengerDidStartFileTransfer, 1) XCTAssertNoDifference(messengerDidLogIn, 1) XCTAssertNoDifference(messengerDidResumeBackup, 1) @@ -112,6 +120,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isListeningForMessages.run = { true } + store.environment.messenger.isFileTransferRunning.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } @@ -203,6 +212,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isListeningForMessages.run = { true } + store.environment.messenger.isFileTransferRunning.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { throw error } @@ -229,6 +239,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isListeningForMessages.run = { true } + store.environment.messenger.isFileTransferRunning.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { throw error } diff --git a/Sources/XXClient/Models/FileSend.swift b/Sources/XXClient/Models/FileSend.swift index 1d925e6f4cd728b5fc364d53240c6ca61ceee4fa..8f95c675602101631240e3f74f4ec1878b2dcd20 100644 --- a/Sources/XXClient/Models/FileSend.swift +++ b/Sources/XXClient/Models/FileSend.swift @@ -4,7 +4,7 @@ public struct FileSend: Equatable { public init( name: String, type: String, - preview: Data, + preview: Data?, contents: Data ) { self.name = name @@ -15,7 +15,7 @@ public struct FileSend: Equatable { public var name: String public var type: String - public var preview: Data + public var preview: Data? public var contents: Data } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsFileTransferRunning.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsFileTransferRunning.swift new file mode 100644 index 0000000000000000000000000000000000000000..23a940802243ba95300f206285103adb9b7e73ad --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsFileTransferRunning.swift @@ -0,0 +1,21 @@ +import XCTestDynamicOverlay + +public struct MessengerIsFileTransferRunning { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } +} + +extension MessengerIsFileTransferRunning { + public static func live(_ env: MessengerEnvironment) -> MessengerIsFileTransferRunning { + MessengerIsFileTransferRunning { env.fileTransfer.get() != nil } + } +} + +extension MessengerIsFileTransferRunning { + public static let unimplemented = MessengerIsFileTransferRunning( + run: XCTUnimplemented("\(Self.self)", placeholder: false) + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift index d565ebb2b6515f00bacd776be88f83f7bcd7401c..a6f1ee0d3eb409f6c18fca2c0c62860a97721dfb 100644 --- a/Sources/XXMessengerClient/Messenger/Messenger.swift +++ b/Sources/XXMessengerClient/Messenger/Messenger.swift @@ -5,6 +5,7 @@ public struct Messenger { public var e2e: Stored<E2E?> public var ud: Stored<UserDiscovery?> public var backup: Stored<Backup?> + public var fileTransfer: Stored<FileTransfer?> public var isCreated: MessengerIsCreated public var create: MessengerCreate public var restoreBackup: MessengerRestoreBackup @@ -41,6 +42,7 @@ public struct Messenger { public var setLogLevel: MessengerSetLogLevel public var startLogging: MessengerStartLogging public var registerReceiveFileCallback: MessengerRegisterReceiveFileCallback + public var isFileTransferRunning: MessengerIsFileTransferRunning public var startFileTransfer: MessengerStartFileTransfer public var sendFile: MessengerSendFile public var receiveFile: MessengerReceiveFile @@ -53,6 +55,7 @@ extension Messenger { e2e: env.e2e, ud: env.ud, backup: env.backup, + fileTransfer: env.fileTransfer, isCreated: .live(env), create: .live(env), restoreBackup: .live(env), @@ -89,6 +92,7 @@ extension Messenger { setLogLevel: .live(env), startLogging: .live(env), registerReceiveFileCallback: .live(env), + isFileTransferRunning: .live(env), startFileTransfer: .live(env), sendFile: .live(env), receiveFile: .live(env) @@ -102,6 +106,7 @@ extension Messenger { e2e: .unimplemented(), ud: .unimplemented(), backup: .unimplemented(), + fileTransfer: .unimplemented(), isCreated: .unimplemented, create: .unimplemented, restoreBackup: .unimplemented, @@ -138,6 +143,7 @@ extension Messenger { setLogLevel: .unimplemented, startLogging: .unimplemented, registerReceiveFileCallback: .unimplemented, + isFileTransferRunning: .unimplemented, startFileTransfer: .unimplemented, sendFile: .unimplemented, receiveFile: .unimplemented diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsFileTransferRunningTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsFileTransferRunningTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..3ac1fd3008b4f3c0dec329a49351c0f540d67c4f --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsFileTransferRunningTests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import XXMessengerClient + +final class MessengerIsFileTransferRunningTests: XCTestCase { + func testIsRunning() { + var env: MessengerEnvironment = .unimplemented + env.fileTransfer.get = { .unimplemented } + let isRunning: MessengerIsFileTransferRunning = .live(env) + + XCTAssertTrue(isRunning()) + } + + func testIsNotRunning() { + var env: MessengerEnvironment = .unimplemented + env.fileTransfer.get = { nil } + let isRunning: MessengerIsFileTransferRunning = .live(env) + + XCTAssertFalse(isRunning()) + } +}