diff --git a/NextcloudTalk/Chat/BaseChatViewController.swift b/NextcloudTalk/Chat/BaseChatViewController.swift index 6fc3dd74e..0b367fc75 100644 --- a/NextcloudTalk/Chat/BaseChatViewController.swift +++ b/NextcloudTalk/Chat/BaseChatViewController.swift @@ -1360,6 +1360,15 @@ import Toast NotificationPresenter.shared().present(text: NSLocalizedString("Message unpinned", comment: ""), dismissAfterDelay: 5.0, includedStyle: .success) } + internal func prepareVoiceMessageForResend(_ message: NCChatMessage) { + message.sendingFailed = false + message.isOfflineMessage = false + + // Like a resent text message, the message is sent now: it needs to be placed at the end of the chat + // and must not expire (getTemporaryMessages) based on the timestamp of the failed attempt + message.timestamp = Int(Date().timeIntervalSince1970) + } + func didPressResend(for message: NCChatMessage) { // Make sure there's no unread message separator, as the indexpath could be invalid after removing a message self.removeUnreadMessagesSeparator() @@ -1371,6 +1380,13 @@ import Toast if message.messageType != kMessageTypeVoiceMessage { self.sendChatMessage(message: originalMessage, withParentMessage: message.parent, messageParameters: message.messageParametersJSONString ?? "", silently: message.isSilent) } else { + // Show and store the message as sending again, so it can be marked as failed again + self.prepareVoiceMessageForResend(message) + + RLMRealm.writeTransaction { realm in + realm.addOrUpdate(NCChatMessage(value: message)) + } + if NCDatabaseManager.sharedInstance().roomHasTalkCapability(.chatReferenceId, for: room) { self.appendTemporaryMessage(temporaryMessage: message) } @@ -2245,11 +2261,33 @@ import Toast try await ChatFileUploader.upload(upload) NCLog.log("Successfully uploaded and shared \(upload.fileName)") } catch { + if let referenceId = upload.referenceId { + self.markTemporaryMessageAsFailed(referenceId: referenceId) + } + self.presentUploadError(error, for: upload) } } } + internal func markTemporaryMessageAsFailed(referenceId: String) { + // Allow to resend or delete the message, instead of showing it as sending until it expires + RLMRealm.writeTransaction { _ in + if let managedTemporaryMessage = NCChatMessage.objects(where: "referenceId = %@ AND isTemporary = true", referenceId).firstObject() as? NCChatMessage { + managedTemporaryMessage.sendingFailed = true + managedTemporaryMessage.isOfflineMessage = false + } + } + + self.modifyMessageWith(referenceId: referenceId) { message in + // The message might have been received from the server in the meantime + guard message.isTemporary else { return } + + message.sendingFailed = true + message.isOfflineMessage = false + } + } + private func presentUploadError(_ error: Error, for upload: ChatFileUpload) { let message: String @@ -2270,6 +2308,9 @@ import Toast case ChatFileUploadError.tooManyRequests: NCLog.log("Too many requests while uploading") message = NSLocalizedString("Too many requests, please try again later", comment: "") + case ChatFileUploadError.uploadFailed(_, let errorDescription) where !errorDescription.isEmpty: + NCLog.log("Failed to upload \(upload.fileName). Error: \(errorDescription)") + message = errorDescription default: NCLog.log("Failed to upload \(upload.fileName). Error: \(error.localizedDescription)") message = NSLocalizedString("Unknown error occurred", comment: "") diff --git a/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift b/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift index 83424e8f9..c765ae6dd 100644 --- a/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift +++ b/NextcloudTalkTests/Unit/Chat/UnitBaseChatViewControllerTest.swift @@ -222,4 +222,58 @@ final class UnitBaseChatViewControllerTest: TestBaseRealm { XCTAssertEqual(baseController.getCellHeight(for: testMessage, with: 300), 171.0) } + func testMarkTemporaryMessageAsFailed() throws { + let temporaryMessage = NCChatMessage() + temporaryMessage.internalId = "temp-1" + temporaryMessage.referenceId = "reference-1" + temporaryMessage.messageType = "voice-message" + temporaryMessage.isTemporary = true + + // A message received from the server with the same reference id + let receivedMessage = NCChatMessage() + receivedMessage.messageId = 1 + receivedMessage.internalId = "internal-1" + receivedMessage.referenceId = "reference-1" + + try? realm.transaction { + realm.add(temporaryMessage) + realm.add(receivedMessage) + } + + baseController.markTemporaryMessageAsFailed(referenceId: "reference-1") + + XCTAssertTrue(temporaryMessage.sendingFailed) + XCTAssertFalse(temporaryMessage.isOfflineMessage) + XCTAssertFalse(receivedMessage.sendingFailed) + } + + func testMarkTemporaryMessageAsFailedIgnoresReceivedMessage() throws { + let receivedMessage = NCChatMessage() + receivedMessage.messageId = 1 + receivedMessage.internalId = "internal-1" + receivedMessage.referenceId = "reference-1" + + try? realm.transaction { + realm.add(receivedMessage) + } + + baseController.markTemporaryMessageAsFailed(referenceId: "reference-1") + + XCTAssertFalse(receivedMessage.sendingFailed) + } + + func testPrepareVoiceMessageForResendAssignsNewTimestamp() throws { + let message = NCChatMessage() + message.sendingFailed = true + message.isOfflineMessage = true + message.timestamp = Int(Date().timeIntervalSince1970) - (60 * 60 * 13) + + let before = Int(Date().timeIntervalSince1970) + baseController.prepareVoiceMessageForResend(message) + + XCTAssertFalse(message.sendingFailed) + XCTAssertFalse(message.isOfflineMessage) + XCTAssertGreaterThanOrEqual(message.timestamp, before) + } + }