diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index be768aba1a18f015c4ceb79d0e5d97811c06ea6f..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 ) diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift index 9e06c687da0ac5b02cffb06b21754514681f3ee2..3538bc2536f23e493a298f2761fd3614711f77da 100644 --- a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift +++ b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift @@ -66,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>) } @@ -75,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 } @@ -88,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> } @@ -98,6 +102,7 @@ extension ChatEnvironment { messenger: .unimplemented, db: .unimplemented, sendMessage: .unimplemented, + sendImage: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented ) @@ -196,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 7c6c51f8ef4235d7d0777830cc8a4ac223ba1bf1..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() } diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift index 492d26d383857455b33dfa882851439c6c695973..01d0ed8faa3c37b0a2005ace0b7c98d13b2e6590 100644 --- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift @@ -240,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 + } + } }