From cdbab6ec074338c339a050e0eeb37310e71eded8 Mon Sep 17 00:00:00 2001 From: Djorkaeff Alexandre Date: Thu, 15 Feb 2024 16:44:15 -0300 Subject: [PATCH] Add ErrorActionHandler --- .../ActionHandler/ErrorActionHandler.swift | 29 ++++++++++++++ .../Database/Database.swift | 8 ++++ .../Database/RocketChatDatabase.swift | 29 ++++++++++++++ .../Loaders/MessageSender.swift | 17 ++++++--- .../Loaders/MessagesLoader.swift | 13 ++++--- .../Loaders/RoomsLoader.swift | 6 +-- .../Localizable.xcstrings | 6 +++ .../Views/LoggedInView.swift | 1 + .../Views/MessageActionView.swift | 36 ++++++++++++++++++ .../Views/MessageListView.swift | 11 +++++- .../Views/MessageView.swift | 38 +++++++++++++++++-- ios/RocketChatRN.xcodeproj/project.pbxproj | 18 +++++++++ 12 files changed, 193 insertions(+), 19 deletions(-) create mode 100644 ios/RocketChat Watch App/ActionHandler/ErrorActionHandler.swift create mode 100644 ios/RocketChat Watch App/Views/MessageActionView.swift diff --git a/ios/RocketChat Watch App/ActionHandler/ErrorActionHandler.swift b/ios/RocketChat Watch App/ActionHandler/ErrorActionHandler.swift new file mode 100644 index 000000000..5e5ba8955 --- /dev/null +++ b/ios/RocketChat Watch App/ActionHandler/ErrorActionHandler.swift @@ -0,0 +1,29 @@ +protocol ErrorActionHandling { + func handle(error: RocketChatError) +} + +final class ErrorActionHandler { + @Dependency private var database: Database + @Dependency private var serversDB: ServersDatabase + @Dependency private var router: AppRouting + + private let server: Server + + init(server: Server) { + self.server = server + } +} + +extension ErrorActionHandler: ErrorActionHandling { + func handle(error: RocketChatError) { + switch error { + case .unauthorized: + router.route(to: .serverList) + + database.remove() + serversDB.remove(server) + default: + break + } + } +} diff --git a/ios/RocketChat Watch App/Database/Database.swift b/ios/RocketChat Watch App/Database/Database.swift index 4ec290d73..dab71fee3 100644 --- a/ios/RocketChat Watch App/Database/Database.swift +++ b/ios/RocketChat Watch App/Database/Database.swift @@ -8,6 +8,8 @@ protocol ServersDatabase { func user(id: String) -> LoggedUser? func servers() -> [Server] + func remove(_ server: Server) + func save() func process(updatedServer: WatchMessage.Server) @@ -67,6 +69,12 @@ final class DefaultDatabase: ServersDatabase { return (try? viewContext.fetch(request)) ?? [] } + func remove(_ server: Server) { + viewContext.delete(server) + + save() + } + func process(updatedServer: WatchMessage.Server) { if let server = server(url: updatedServer.url) { server.url = updatedServer.url diff --git a/ios/RocketChat Watch App/Database/RocketChatDatabase.swift b/ios/RocketChat Watch App/Database/RocketChatDatabase.swift index dda326381..bf8b2e67e 100644 --- a/ios/RocketChat Watch App/Database/RocketChatDatabase.swift +++ b/ios/RocketChat Watch App/Database/RocketChatDatabase.swift @@ -6,10 +6,14 @@ protocol Database { func room(id: String) -> Room? func message(id: String) -> Message? func createTempMessage(msg: String, in room: Room, for loggedUser: LoggedUser) -> String + func updateMessage(_ id: String, status: String) + func remove(_ message: Message) func process(subscription: SubscriptionsResponse.Subscription) func process(subscription: SubscriptionsResponse.Subscription?, in updatedRoom: RoomsResponse.Room) func process(updatedMessage: MessageResponse, in room: Room) + + func remove() } final class RocketChatDatabase: Database { @@ -102,6 +106,19 @@ final class RocketChatDatabase: Database { return id } + func updateMessage(_ id: String, status: String) { + let message = message(id: id) ?? createMessage(id: id) + message.status = status + + save() + } + + func remove(_ message: Message) { + viewContext.delete(message) + + save() + } + func user(id: String) -> User? { let user = User(context: viewContext) user.id = id @@ -225,4 +242,16 @@ final class RocketChatDatabase: Database { save() } + + func remove() { + guard let path = container.persistentStoreDescriptions.first?.url?.path else { + return + } + + do { + try FileManager.default.removeItem(atPath: path) + } catch { + print(error) + } + } } diff --git a/ios/RocketChat Watch App/Loaders/MessageSender.swift b/ios/RocketChat Watch App/Loaders/MessageSender.swift index 0d7890771..952f20e0c 100644 --- a/ios/RocketChat Watch App/Loaders/MessageSender.swift +++ b/ios/RocketChat Watch App/Loaders/MessageSender.swift @@ -3,6 +3,7 @@ import Foundation protocol MessageSending { func sendMessage(_ msg: String, in room: Room) + func resendMessage(messageID: String, msg: String, in room: Room) } final class MessageSender { @@ -18,15 +19,21 @@ final class MessageSender { extension MessageSender: MessageSending { func sendMessage(_ msg: String, in room: Room) { - guard let rid = room.id else { return } - let messageID = database.createTempMessage(msg: msg, in: room, for: server.loggedUser) + resendMessage(messageID: messageID, msg: msg, in: room) + } + + func resendMessage(messageID: String, msg: String, in room: Room) { + guard let rid = room.id else { return } + client.sendMessage(id: messageID, rid: rid, msg: msg) .receive(on: DispatchQueue.main) - .subscribe(Subscribers.Sink { completion in - if case .failure(let error) = completion { - print(error) + .subscribe(Subscribers.Sink { [weak self] completion in + guard let self else { return } + + if case .failure = completion { + self.database.updateMessage(messageID, status: "error") } } receiveValue: { [weak self] messageResponse in guard let self else { diff --git a/ios/RocketChat Watch App/Loaders/MessagesLoader.swift b/ios/RocketChat Watch App/Loaders/MessagesLoader.swift index f4dcf7ef8..ec13ede2e 100644 --- a/ios/RocketChat Watch App/Loaders/MessagesLoader.swift +++ b/ios/RocketChat Watch App/Loaders/MessagesLoader.swift @@ -16,6 +16,7 @@ final class MessagesLoader { @Dependency private var client: RocketChatClientProtocol @Dependency private var database: Database @Dependency private var serversDB: ServersDatabase + @Dependency private var errorActionHandler: ErrorActionHandling private var roomID: String? @@ -32,9 +33,9 @@ final class MessagesLoader { client.syncMessages(rid: rid, updatedSince: date) .receive(on: DispatchQueue.main) - .sink { completion in + .sink { [weak self] completion in if case .failure(let error) = completion { - print(error) + self?.errorActionHandler.handle(error: error) } } receiveValue: { [weak self] messagesResponse in let messages = messagesResponse.result.updated @@ -57,9 +58,9 @@ final class MessagesLoader { client.getHistory(rid: rid, t: room.t ?? "", latest: date) .receive(on: DispatchQueue.main) - .sink { completion in + .sink { [weak self] completion in if case .failure(let error) = completion { - print(error) + self?.errorActionHandler.handle(error: error) } } receiveValue: { [weak self] messagesResponse in let messages = messagesResponse.messages @@ -84,9 +85,9 @@ final class MessagesLoader { client.sendRead(rid: rid) .receive(on: DispatchQueue.main) - .sink { completion in + .sink { [weak self] completion in if case .failure(let error) = completion { - print(error) + self?.errorActionHandler.handle(error: error) } } receiveValue: { _ in diff --git a/ios/RocketChat Watch App/Loaders/RoomsLoader.swift b/ios/RocketChat Watch App/Loaders/RoomsLoader.swift index f4e85e8ed..a91b8db03 100644 --- a/ios/RocketChat Watch App/Loaders/RoomsLoader.swift +++ b/ios/RocketChat Watch App/Loaders/RoomsLoader.swift @@ -11,6 +11,7 @@ final class RoomsLoader { @Dependency private var client: RocketChatClientProtocol @Dependency private var database: Database @Dependency private var serversDB: ServersDatabase + @Dependency private var errorActionHandler: ErrorActionHandling private var timer: Timer? private var cancellable = CancelBag() @@ -31,10 +32,9 @@ final class RoomsLoader { client.getSubscriptions(updatedSince: updatedSince) ) .receive(on: DispatchQueue.main) - .sink { completion in + .sink { [weak self] completion in if case .failure(let error) = completion { - // TODO: LOGOUT - print(error) + self?.errorActionHandler.handle(error: error) } } receiveValue: { (roomsResponse, subscriptionsResponse) in let rooms = roomsResponse.update diff --git a/ios/RocketChat Watch App/Localizable.xcstrings b/ios/RocketChat Watch App/Localizable.xcstrings index 12f5812a9..5ab06c5aa 100644 --- a/ios/RocketChat Watch App/Localizable.xcstrings +++ b/ios/RocketChat Watch App/Localizable.xcstrings @@ -1,11 +1,17 @@ { "sourceLanguage" : "en", "strings" : { + "Delete" : { + + }, "Load More..." : { }, "Message" : { + }, + "Resend" : { + }, "Rooms" : { diff --git a/ios/RocketChat Watch App/Views/LoggedInView.swift b/ios/RocketChat Watch App/Views/LoggedInView.swift index 0288ce95a..79bc5e240 100644 --- a/ios/RocketChat Watch App/Views/LoggedInView.swift +++ b/ios/RocketChat Watch App/Views/LoggedInView.swift @@ -17,6 +17,7 @@ struct LoggedInView: View { Store.register(Database.self, factory: database) Store.register(RocketChatClientProtocol.self, factory: RocketChatClient(server: server)) Store.register(MessageSending.self, factory: MessageSender(server: server)) + Store.register(ErrorActionHandling.self, factory: ErrorActionHandler(server: server)) } var body: some View { diff --git a/ios/RocketChat Watch App/Views/MessageActionView.swift b/ios/RocketChat Watch App/Views/MessageActionView.swift new file mode 100644 index 000000000..5292e43e5 --- /dev/null +++ b/ios/RocketChat Watch App/Views/MessageActionView.swift @@ -0,0 +1,36 @@ +import SwiftUI + +struct MessageActionView: View { + @Environment(\.dismiss) private var dismiss + + private let action: (MessageAction) -> Void + private let message: Message + + init(message: Message, action: @escaping (MessageAction) -> Void) { + self.action = action + self.message = message + } + + var body: some View { + VStack { + Button(action: { + dismiss() + + guard let messageID = message.id, let msg = message.msg else { return } + + action(.resend(messageID, msg)) + }, label: { + Text("Resend") + }) + Button(action: { + dismiss() + + action(.delete(message)) + }, label: { + Text("Delete") + .foregroundStyle(.red) + }) + } + .padding() + } +} diff --git a/ios/RocketChat Watch App/Views/MessageListView.swift b/ios/RocketChat Watch App/Views/MessageListView.swift index 5c5ce8f1a..55fb451c7 100644 --- a/ios/RocketChat Watch App/Views/MessageListView.swift +++ b/ios/RocketChat Watch App/Views/MessageListView.swift @@ -54,7 +54,16 @@ struct MessageListView: View { MessageView( client: client, viewModel: .init(message: message, previousMessage: previousMessage, server: server, lastOpen: lastOpen) - ) + ) { action in + switch action { + case .resend(let id, let msg): + messageSender.resendMessage(messageID: id, msg: msg, in: room) + + lastOpen = nil + case .delete(let message): + database.remove(message) + } + } } MessageComposerView(room: room) { diff --git a/ios/RocketChat Watch App/Views/MessageView.swift b/ios/RocketChat Watch App/Views/MessageView.swift index fcd4a866a..e8546a074 100644 --- a/ios/RocketChat Watch App/Views/MessageView.swift +++ b/ios/RocketChat Watch App/Views/MessageView.swift @@ -1,11 +1,19 @@ import SwiftUI +enum MessageAction { + case resend(String, String) + case delete(Message) +} + struct MessageView: View { @ObservedObject private var viewModel: MessageViewModel + @State private var message: Message? + private let action: (MessageAction) -> Void private let client: RocketChatClientProtocol - init(client: RocketChatClientProtocol, viewModel: MessageViewModel) { + init(client: RocketChatClientProtocol, viewModel: MessageViewModel, action: @escaping (MessageAction) -> Void) { + self.action = action self.client = client self.viewModel = viewModel } @@ -68,9 +76,25 @@ struct MessageView: View { .font(.caption.italic()) .foregroundStyle(.primary) } else if let text = viewModel.message.msg { - Text(text) - .font(.caption) - .foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary) + HStack { + Text(text) + .font(.caption) + .foregroundStyle(viewModel.message.status == "temp" ? .secondary : .primary) + + if viewModel.message.status == "error" { + Button( + action: { + message = viewModel.message + }, + label: { + Image(systemName: "exclamationmark.circle") + .font(.caption) + .foregroundStyle(.red) + } + ) + .buttonStyle(PlainButtonStyle()) + } + } } if let attachments = viewModel.message.attachments?.allObjects as? Array { ForEach(attachments) { attachment in @@ -78,5 +102,11 @@ struct MessageView: View { } } } + .sheet(item: $message) { message in + MessageActionView( + message: message, + action: action + ) + } } } diff --git a/ios/RocketChatRN.xcodeproj/project.pbxproj b/ios/RocketChatRN.xcodeproj/project.pbxproj index e6feb9ed6..a48b0af6f 100644 --- a/ios/RocketChatRN.xcodeproj/project.pbxproj +++ b/ios/RocketChatRN.xcodeproj/project.pbxproj @@ -19,6 +19,8 @@ 1E01C82D2511337700FEF824 /* RoomKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E01C82C2511337700FEF824 /* RoomKey.swift */; }; 1E0426E6251A5467008F022C /* RoomType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0426E5251A5467008F022C /* RoomType.swift */; }; 1E0426E7251A54B4008F022C /* RoomType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0426E5251A5467008F022C /* RoomType.swift */; }; + 1E06561B2B7E91FB0081B01F /* ErrorActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E06561A2B7E91FB0081B01F /* ErrorActionHandler.swift */; }; + 1E06561D2B7E9C1C0081B01F /* MessageActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E06561C2B7E9C1C0081B01F /* MessageActionView.swift */; }; 1E068CFE24FD2DC700A0FFC1 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E068CFD24FD2DC700A0FFC1 /* AppGroup.swift */; }; 1E068CFF24FD2DC700A0FFC1 /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E068CFD24FD2DC700A0FFC1 /* AppGroup.swift */; }; 1E068D0124FD2E0500A0FFC1 /* AppGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E068D0024FD2E0500A0FFC1 /* AppGroup.m */; }; @@ -336,6 +338,8 @@ 1E01C82A2511335A00FEF824 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 1E01C82C2511337700FEF824 /* RoomKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomKey.swift; sourceTree = ""; }; 1E0426E5251A5467008F022C /* RoomType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomType.swift; sourceTree = ""; }; + 1E06561A2B7E91FB0081B01F /* ErrorActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorActionHandler.swift; sourceTree = ""; }; + 1E06561C2B7E9C1C0081B01F /* MessageActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageActionView.swift; sourceTree = ""; }; 1E068CFD24FD2DC700A0FFC1 /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; 1E068D0024FD2E0500A0FFC1 /* AppGroup.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppGroup.m; sourceTree = ""; }; 1E1C2F7F250FCB69005DCE7D /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; @@ -572,6 +576,14 @@ name = RocketChatRN; sourceTree = ""; }; + 1E0656192B7E91F00081B01F /* ActionHandler */ = { + isa = PBXGroup; + children = ( + 1E06561A2B7E91FB0081B01F /* ErrorActionHandler.swift */, + ); + path = ActionHandler; + sourceTree = ""; + }; 1E068CFB24FD2DAF00A0FFC1 /* AppGroup */ = { isa = PBXGroup; children = ( @@ -797,6 +809,7 @@ 1E4AFC262B5B23C600E2AA7D /* RetryView.swift */, 1EDB30F12B5B453A00532C7E /* LoggedInView.swift */, 1E638E982B5F0A2900E645E4 /* ChatScrollView.swift */, + 1E06561C2B7E9C1C0081B01F /* MessageActionView.swift */, ); path = Views; sourceTree = ""; @@ -813,6 +826,7 @@ 1ED0388F2B507B4C00C007D4 /* RocketChat Watch App */ = { isa = PBXGroup; children = ( + 1E0656192B7E91F00081B01F /* ActionHandler */, 1EDFD0FB2B589FC4002FEE5F /* DependencyInjection */, 1EDFD0F82B589B82002FEE5F /* Loaders */, 1E29A31E2B5871BE0093C03C /* Formatters */, @@ -1855,6 +1869,7 @@ files = ( 1E29A3242B5874FF0093C03C /* MessageComposerView.swift in Sources */, 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */, + 1E06561D2B7E9C1C0081B01F /* MessageActionView.swift in Sources */, 1E29A3162B5868DF0093C03C /* MessageListView.swift in Sources */, 1E4AFC172B5AF09C00E2AA7D /* Store.swift in Sources */, 1E29A2F42B585B070093C03C /* SubscriptionsResponse.swift in Sources */, @@ -1874,6 +1889,7 @@ 1E29A3202B5871C80093C03C /* RoomFormatter.swift in Sources */, 1EDFD0FA2B589B8F002FEE5F /* MessagesLoader.swift in Sources */, 1E29A3102B5865B80093C03C /* RoomViewModel.swift in Sources */, + 1E06561B2B7E91FB0081B01F /* ErrorActionHandler.swift in Sources */, 1E29A2FC2B585B070093C03C /* SendMessageRequest.swift in Sources */, 1E29A30C2B585D1D0093C03C /* String+Extensions.swift in Sources */, 1ED033CD2B55D671004F4930 /* RocketChatDatabase.swift in Sources */, @@ -2325,6 +2341,7 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.watchkitapp; PRODUCT_NAME = Rocket.Chat; PROVISIONING_PROFILE_SPECIFIER = "match AppStore chat.rocket.reactnative.watchkitapp"; @@ -2372,6 +2389,7 @@ LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = chat.rocket.reactnative.watchkitapp; PRODUCT_NAME = Rocket.Chat; PROVISIONING_PROFILE_SPECIFIER = "match AppStore chat.rocket.reactnative.watchkitapp";