From 3344e58716f6ef2aeba768cb5b693fd3cfa96ccf Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Tue, 14 Feb 2023 13:41:24 +1100 Subject: [PATCH] Cleaned up some logic around sync messages Added logic to indicate when a sync message failed to send (and the ability to retry) Added the retry/resync button to the long press message menu Updated sync messages to run via the MessageSendJob Updated the delivery status to always show on the last outgoing message Updated the logic to update the delivery status when retrying to send a failed message Removed the convoluted recursion logic for turning self-send messages into sync messages --- .../Context Menu/ContextMenuVC+Action.swift | 21 +++ .../ConversationVC+Interaction.swift | 72 ++++--- .../Conversations/ConversationViewModel.swift | 9 + .../Message Cells/VisibleMessageCell.swift | 2 +- .../GIFs/GifPickerViewController.swift | 2 +- .../GIFs/GiphyAPI.swift | 2 + .../Translations/de.lproj/Localizable.strings | 6 + .../Translations/en.lproj/Localizable.strings | 6 + .../Translations/es.lproj/Localizable.strings | 6 + .../Translations/fa.lproj/Localizable.strings | 6 + .../Translations/fi.lproj/Localizable.strings | 6 + .../Translations/fr.lproj/Localizable.strings | 6 + .../Translations/hi.lproj/Localizable.strings | 6 + .../Translations/hr.lproj/Localizable.strings | 6 + .../id-ID.lproj/Localizable.strings | 6 + .../Translations/it.lproj/Localizable.strings | 6 + .../Translations/ja.lproj/Localizable.strings | 6 + .../Translations/nl.lproj/Localizable.strings | 6 + .../Translations/pl.lproj/Localizable.strings | 6 + .../pt_BR.lproj/Localizable.strings | 6 + .../Translations/ru.lproj/Localizable.strings | 6 + .../Translations/si.lproj/Localizable.strings | 6 + .../Translations/sk.lproj/Localizable.strings | 6 + .../Translations/sv.lproj/Localizable.strings | 6 + .../Translations/th.lproj/Localizable.strings | 6 + .../vi-VN.lproj/Localizable.strings | 6 + .../zh-Hant.lproj/Localizable.strings | 6 + .../zh_CN.lproj/Localizable.strings | 6 + .../UserNotificationsAdaptee.swift | 2 + .../Database/Models/RecipientState.swift | 38 ++-- .../Jobs/Types/FailedMessageSendsJob.swift | 6 +- .../Jobs/Types/MessageReceiveJob.swift | 2 +- .../Jobs/Types/MessageSendJob.swift | 13 +- .../Jobs/Types/SendReadReceiptsJob.swift | 3 +- SessionMessagingKit/Messages/Message.swift | 12 +- .../MessageReceiver+VisibleMessages.swift | 13 +- .../MessageSender+Convenience.swift | 39 +++- .../Sending & Receiving/MessageSender.swift | 178 ++++++++++++------ .../Shared Models/MessageViewModel.swift | 13 +- .../Themes/Theme+ClassicDark.swift | 1 + .../Themes/Theme+ClassicLight.swift | 1 + .../Style Guide/Themes/Theme+Colors.swift | 1 + .../Style Guide/Themes/Theme+OceanDark.swift | 1 + .../Style Guide/Themes/Theme+OceanLight.swift | 1 + SessionUIKit/Style Guide/Themes/Theme.swift | 1 + 45 files changed, 434 insertions(+), 131 deletions(-) diff --git a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift index 904978ab3..346dbc0c7 100644 --- a/Session/Conversations/Context Menu/ContextMenuVC+Action.swift +++ b/Session/Conversations/Context Menu/ContextMenuVC+Action.swift @@ -34,6 +34,17 @@ extension ContextMenuVC { } // MARK: - Actions + + static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { + return Action( + icon: UIImage(systemName: "arrow.triangle.2.circlepath"), + title: (cellViewModel.state == .failedToSync ? + "context_menu_resync".localized() : + "context_menu_resend".localized() + ), + accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message") + ) { delegate?.retry(cellViewModel) } + } static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action { return Action( @@ -126,6 +137,14 @@ extension ContextMenuVC { case .standardOutgoing, .standardIncoming: break } + let canRetry: Bool = ( + cellViewModel.variant == .standardOutgoing && ( + cellViewModel.state == .failed || ( + cellViewModel.threadVariant == .contact && + cellViewModel.state == .failedToSync + ) + ) + ) let canReply: Bool = ( cellViewModel.variant != .standardOutgoing || ( cellViewModel.state != .failed && @@ -180,6 +199,7 @@ extension ContextMenuVC { }() let generatedActions: [Action] = [ + (canRetry ? Action.retry(cellViewModel, delegate) : nil), (canReply ? Action.reply(cellViewModel, delegate) : nil), (canCopy ? Action.copy(cellViewModel, delegate) : nil), (canSave ? Action.save(cellViewModel, delegate) : nil), @@ -201,6 +221,7 @@ extension ContextMenuVC { // MARK: - Delegate protocol ContextMenuActionDelegate { + func retry(_ cellViewModel: MessageViewModel) func reply(_ cellViewModel: MessageViewModel) func copy(_ cellViewModel: MessageViewModel) func copySessionID(_ cellViewModel: MessageViewModel) diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 3b1f3e89c..e03ed17e4 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -7,6 +7,7 @@ import PhotosUI import Sodium import PromiseKit import GRDB +import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit import SignalUtilitiesKit @@ -828,7 +829,7 @@ extension ConversationVC: } func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) { - guard cellViewModel.variant != .standardOutgoing || cellViewModel.state != .failed else { + guard cellViewModel.variant != .standardOutgoing || (cellViewModel.state != .failed && cellViewModel.state != .failedToSync) else { // Show the failed message sheet showFailedMessageSheet(for: cellViewModel) return @@ -1450,30 +1451,34 @@ extension ConversationVC: // MARK: --action handling func showFailedMessageSheet(for cellViewModel: MessageViewModel) { - let sheet = UIAlertController(title: cellViewModel.mostRecentFailureText, message: nil, preferredStyle: .actionSheet) - sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) - sheet.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { _ in - Storage.shared.writeAsync { db in - try Interaction - .filter(id: cellViewModel.id) - .deleteAll(db) - } - })) - sheet.addAction(UIAlertAction(title: "Resend", style: .default, handler: { _ in - Storage.shared.writeAsync { [weak self] db in - guard - let threadId: String = self?.viewModel.threadData.threadId, - let interaction: Interaction = try? Interaction.fetchOne(db, id: cellViewModel.id), - let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) - else { return } - - try MessageSender.send( - db, - interaction: interaction, - in: thread - ) - } - })) + let sheet = UIAlertController( + title: (cellViewModel.state == .failedToSync ? + "MESSAGE_DELIVERY_FAILED_SYNC_TITLE".localized() : + "MESSAGE_DELIVERY_FAILED_TITLE".localized() + ), + message: cellViewModel.mostRecentFailureText, + preferredStyle: .actionSheet + ) + sheet.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil)) + + if cellViewModel.state != .failedToSync { + sheet.addAction(UIAlertAction(title: "TXT_DELETE_TITLE".localized(), style: .destructive, handler: { _ in + Storage.shared.writeAsync { db in + try Interaction + .filter(id: cellViewModel.id) + .deleteAll(db) + } + })) + } + + sheet.addAction(UIAlertAction( + title: (cellViewModel.state == .failedToSync ? + "context_menu_resync".localized() : + "context_menu_resend".localized() + ), + style: .default, + handler: { [weak self] _ in self?.retry(cellViewModel) } + )) // HACK: Extracting this info from the error string is pretty dodgy let prefix: String = "HTTP request failed at destination (Service node " @@ -1557,6 +1562,23 @@ extension ConversationVC: } // MARK: - ContextMenuActionDelegate + + func retry(_ cellViewModel: MessageViewModel) { + Storage.shared.writeAsync { [weak self] db in + guard + let threadId: String = self?.viewModel.threadData.threadId, + let interaction: Interaction = try? Interaction.fetchOne(db, id: cellViewModel.id), + let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) + else { return } + + try MessageSender.send( + db, + interaction: interaction, + in: thread, + isSyncMessage: (cellViewModel.state == .failedToSync) + ) + } + } func reply(_ cellViewModel: MessageViewModel) { let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending( diff --git a/Session/Conversations/ConversationViewModel.swift b/Session/Conversations/ConversationViewModel.swift index 92e55e45b..4f3a113cb 100644 --- a/Session/Conversations/ConversationViewModel.swift +++ b/Session/Conversations/ConversationViewModel.swift @@ -298,6 +298,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate { index == (sortedData.count - 1) && pageInfo.pageOffset == 0 ), + isLastOutgoing: ( + cellViewModel.id == sortedData + .filter { + $0.authorId == threadData.currentUserPublicKey || + $0.authorId == threadData.currentUserBlindedPublicKey + } + .last? + .id + ), currentUserBlindedPublicKey: threadData.currentUserBlindedPublicKey ) } diff --git a/Session/Conversations/Message Cells/VisibleMessageCell.swift b/Session/Conversations/Message Cells/VisibleMessageCell.swift index 6ac622f60..ce6898c5b 100644 --- a/Session/Conversations/Message Cells/VisibleMessageCell.swift +++ b/Session/Conversations/Message Cells/VisibleMessageCell.swift @@ -431,7 +431,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate { cellViewModel.variant == .infoCall || ( cellViewModel.state == .sent && - !cellViewModel.isLast + !cellViewModel.isLastOutgoing ) ) messageStatusLabelPaddingView.isHidden = ( diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index 43bd6dc68..d56724cdf 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -2,7 +2,7 @@ // Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -import Foundation +import UIKit import Reachability import SignalUtilitiesKit import PromiseKit diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 0dea671df..215fd222d 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -6,6 +6,8 @@ import AFNetworking import Foundation import PromiseKit import CoreServices +import SignalUtilitiesKit +import SessionUtilitiesKit // There's no UTI type for webp! enum GiphyFormat { diff --git a/Session/Meta/Translations/de.lproj/Localizable.strings b/Session/Meta/Translations/de.lproj/Localizable.strings index 49bb4222d..0fd9eca22 100644 --- a/Session/Meta/Translations/de.lproj/Localizable.strings +++ b/Session/Meta/Translations/de.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/en.lproj/Localizable.strings b/Session/Meta/Translations/en.lproj/Localizable.strings index 6035cb094..5c4a4bd38 100644 --- a/Session/Meta/Translations/en.lproj/Localizable.strings +++ b/Session/Meta/Translations/en.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/es.lproj/Localizable.strings b/Session/Meta/Translations/es.lproj/Localizable.strings index 66ac26c54..cccbb5424 100644 --- a/Session/Meta/Translations/es.lproj/Localizable.strings +++ b/Session/Meta/Translations/es.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/fa.lproj/Localizable.strings b/Session/Meta/Translations/fa.lproj/Localizable.strings index cd8ce41a3..9e693f8c6 100644 --- a/Session/Meta/Translations/fa.lproj/Localizable.strings +++ b/Session/Meta/Translations/fa.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/fi.lproj/Localizable.strings b/Session/Meta/Translations/fi.lproj/Localizable.strings index a25d2809a..830f10a5f 100644 --- a/Session/Meta/Translations/fi.lproj/Localizable.strings +++ b/Session/Meta/Translations/fi.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/fr.lproj/Localizable.strings b/Session/Meta/Translations/fr.lproj/Localizable.strings index f72ff557f..67bb04cd0 100644 --- a/Session/Meta/Translations/fr.lproj/Localizable.strings +++ b/Session/Meta/Translations/fr.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/hi.lproj/Localizable.strings b/Session/Meta/Translations/hi.lproj/Localizable.strings index ef84e37b3..e0235de22 100644 --- a/Session/Meta/Translations/hi.lproj/Localizable.strings +++ b/Session/Meta/Translations/hi.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/hr.lproj/Localizable.strings b/Session/Meta/Translations/hr.lproj/Localizable.strings index 13a38e591..1fd80d8bc 100644 --- a/Session/Meta/Translations/hr.lproj/Localizable.strings +++ b/Session/Meta/Translations/hr.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/id-ID.lproj/Localizable.strings b/Session/Meta/Translations/id-ID.lproj/Localizable.strings index 23d363a58..936e0b719 100644 --- a/Session/Meta/Translations/id-ID.lproj/Localizable.strings +++ b/Session/Meta/Translations/id-ID.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/it.lproj/Localizable.strings b/Session/Meta/Translations/it.lproj/Localizable.strings index 6c0cf9c3b..4f4ea0616 100644 --- a/Session/Meta/Translations/it.lproj/Localizable.strings +++ b/Session/Meta/Translations/it.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/ja.lproj/Localizable.strings b/Session/Meta/Translations/ja.lproj/Localizable.strings index 64b5f7da4..551c4a46d 100644 --- a/Session/Meta/Translations/ja.lproj/Localizable.strings +++ b/Session/Meta/Translations/ja.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/nl.lproj/Localizable.strings b/Session/Meta/Translations/nl.lproj/Localizable.strings index ad817674a..9d51f4cd0 100644 --- a/Session/Meta/Translations/nl.lproj/Localizable.strings +++ b/Session/Meta/Translations/nl.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/pl.lproj/Localizable.strings b/Session/Meta/Translations/pl.lproj/Localizable.strings index afc0cd8a5..c9fae8fe4 100644 --- a/Session/Meta/Translations/pl.lproj/Localizable.strings +++ b/Session/Meta/Translations/pl.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings index c289ab6d2..5f247944e 100644 --- a/Session/Meta/Translations/pt_BR.lproj/Localizable.strings +++ b/Session/Meta/Translations/pt_BR.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/ru.lproj/Localizable.strings b/Session/Meta/Translations/ru.lproj/Localizable.strings index fc3fb38c9..cbbb69f16 100644 --- a/Session/Meta/Translations/ru.lproj/Localizable.strings +++ b/Session/Meta/Translations/ru.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/si.lproj/Localizable.strings b/Session/Meta/Translations/si.lproj/Localizable.strings index 39fd64261..96b6549a3 100644 --- a/Session/Meta/Translations/si.lproj/Localizable.strings +++ b/Session/Meta/Translations/si.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/sk.lproj/Localizable.strings b/Session/Meta/Translations/sk.lproj/Localizable.strings index 56ac501a1..7aee26c94 100644 --- a/Session/Meta/Translations/sk.lproj/Localizable.strings +++ b/Session/Meta/Translations/sk.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/sv.lproj/Localizable.strings b/Session/Meta/Translations/sv.lproj/Localizable.strings index d2ac71190..b2bbd47b8 100644 --- a/Session/Meta/Translations/sv.lproj/Localizable.strings +++ b/Session/Meta/Translations/sv.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/th.lproj/Localizable.strings b/Session/Meta/Translations/th.lproj/Localizable.strings index ca9af2694..07ec32da5 100644 --- a/Session/Meta/Translations/th.lproj/Localizable.strings +++ b/Session/Meta/Translations/th.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings index 7b4ee536d..dba81630a 100644 --- a/Session/Meta/Translations/vi-VN.lproj/Localizable.strings +++ b/Session/Meta/Translations/vi-VN.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings index 9dae80736..31b93d847 100644 --- a/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh-Hant.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings index 90d6d85f6..e847a9bad 100644 --- a/Session/Meta/Translations/zh_CN.lproj/Localizable.strings +++ b/Session/Meta/Translations/zh_CN.lproj/Localizable.strings @@ -600,4 +600,10 @@ "MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; +"MESSAGE_DELIVERY_STATUS_FAILED_SYNC" = "Failed to sync"; +"MESSAGE_DELIVERY_STATUS_SYNCING" = "Syncing"; +"MESSAGE_DELIVERY_FAILED_TITLE" = "Failed to send message"; +"MESSAGE_DELIVERY_FAILED_SYNC_TITLE" = "Failed to sync message to your other devices"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices"; +"context_menu_resend" = "Resend"; +"context_menu_resync" = "Resync"; diff --git a/Session/Notifications/UserNotificationsAdaptee.swift b/Session/Notifications/UserNotificationsAdaptee.swift index c6da6323b..3de1be30a 100644 --- a/Session/Notifications/UserNotificationsAdaptee.swift +++ b/Session/Notifications/UserNotificationsAdaptee.swift @@ -5,6 +5,8 @@ import Foundation import UserNotifications import PromiseKit +import SignalCoreKit +import SignalUtilitiesKit import SessionMessagingKit class UserNotificationConfig { diff --git a/SessionMessagingKit/Database/Models/RecipientState.swift b/SessionMessagingKit/Database/Models/RecipientState.swift index 929a5cb8c..74a9cba22 100644 --- a/SessionMessagingKit/Database/Models/RecipientState.swift +++ b/SessionMessagingKit/Database/Models/RecipientState.swift @@ -40,6 +40,8 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe case failed case skipped case sent + case failedToSync // One-to-one Only + case syncing // One-to-one Only func message(hasAttachments: Bool, hasAtLeastOneReadReceipt: Bool) -> String { switch self { @@ -58,6 +60,9 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe } return "MESSAGE_STATUS_READ".localized() + + case .failedToSync: return "MESSAGE_DELIVERY_STATUS_FAILED_SYNC".localized() + case .syncing: return "MESSAGE_DELIVERY_STATUS_SYNCING".localized() default: owsFailDebug("Message has unexpected status: \(self).") @@ -96,6 +101,21 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe "MESSAGE_DELIVERY_STATUS_FAILED".localized(), .danger ) + + case (.failedToSync, _): + return ( + UIImage(systemName: "exclamationmark.triangle"), + "MESSAGE_DELIVERY_STATUS_FAILED_SYNC".localized(), + .warning + ) + + case (.syncing, _): + return ( + UIImage(systemName: "ellipsis.circle"), + "MESSAGE_DELIVERY_STATUS_SYNCING".localized(), + .warning + ) + } } } @@ -148,21 +168,3 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe self.mostRecentFailureText = mostRecentFailureText } } - -// MARK: - Mutation - -public extension RecipientState { - func with( - state: State? = nil, - readTimestampMs: Int64? = nil, - mostRecentFailureText: String? = nil - ) -> RecipientState { - return RecipientState( - interactionId: interactionId, - recipientId: recipientId, - state: (state ?? self.state), - readTimestampMs: (readTimestampMs ?? self.readTimestampMs), - mostRecentFailureText: (mostRecentFailureText ?? self.mostRecentFailureText) - ) - } -} diff --git a/SessionMessagingKit/Jobs/Types/FailedMessageSendsJob.swift b/SessionMessagingKit/Jobs/Types/FailedMessageSendsJob.swift index b83e2e31e..bdb53d53a 100644 --- a/SessionMessagingKit/Jobs/Types/FailedMessageSendsJob.swift +++ b/SessionMessagingKit/Jobs/Types/FailedMessageSendsJob.swift @@ -19,12 +19,16 @@ public enum FailedMessageSendsJob: JobExecutor { ) { // Update all 'sending' message states to 'failed' Storage.shared.write { db in - let changeCount: Int = try RecipientState + let sendChangeCount: Int = try RecipientState .filter(RecipientState.Columns.state == RecipientState.State.sending) .updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.failed)) + let syncChangeCount: Int = try RecipientState + .filter(RecipientState.Columns.state == RecipientState.State.syncing) + .updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.failedToSync)) let attachmentChangeCount: Int = try Attachment .filter(Attachment.Columns.state == Attachment.State.uploading) .updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedUpload)) + let changeCount: Int = (sendChangeCount + syncChangeCount) SNLog("Marked \(changeCount) message\(changeCount == 1 ? "" : "s") as failed (\(attachmentChangeCount) upload\(attachmentChangeCount == 1 ? "" : "s") cancelled)") } diff --git a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift index 582c5aae1..a9621fa83 100644 --- a/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageReceiveJob.swift @@ -88,7 +88,7 @@ public enum MessageReceiveJob: JobExecutor { failure(updatedJob, error, true) case .some(let error): - failure(updatedJob, error, false) // TODO: Confirm the 'noKeyPair' errors here aren't an issue + failure(updatedJob, error, false) case .none: success(updatedJob, false) diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index e8d3e0265..bae02f89c 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -167,7 +167,8 @@ public enum MessageSendJob: JobExecutor { message: details.message, to: details.destination .with(fileIds: messageFileIds), - interactionId: job.interactionId + interactionId: job.interactionId, + isSyncMessage: (details.isSyncMessage == true) ) } .done(on: queue) { _ in success(job, false) } @@ -213,21 +214,25 @@ extension MessageSendJob { private enum CodingKeys: String, CodingKey { case destination case message + case isSyncMessage case variant } public let destination: Message.Destination public let message: Message + public let isSyncMessage: Bool? public let variant: Message.Variant? // MARK: - Initialization public init( destination: Message.Destination, - message: Message + message: Message, + isSyncMessage: Bool? = nil ) { self.destination = destination self.message = message + self.isSyncMessage = isSyncMessage self.variant = Message.Variant(from: message) } @@ -243,7 +248,8 @@ extension MessageSendJob { self = Details( destination: try container.decode(Message.Destination.self, forKey: .destination), - message: try variant.decode(from: container, forKey: .message) + message: try variant.decode(from: container, forKey: .message), + isSyncMessage: try? container.decode(Bool.self, forKey: .isSyncMessage) ) } @@ -257,6 +263,7 @@ extension MessageSendJob { try container.encode(destination, forKey: .destination) try container.encode(message, forKey: .message) + try container.encodeIfPresent(isSyncMessage, forKey: .isSyncMessage) try container.encode(variant, forKey: .variant) } } diff --git a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift index 9d642e79d..f4b1c23d7 100644 --- a/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift +++ b/SessionMessagingKit/Jobs/Types/SendReadReceiptsJob.swift @@ -43,7 +43,8 @@ public enum SendReadReceiptsJob: JobExecutor { timestamps: details.timestampMsValues.map { UInt64($0) } ), to: details.destination, - interactionId: nil + interactionId: nil, + isSyncMessage: false ) } .done(on: queue) { diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 6719ac4ae..befca3712 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -177,10 +177,12 @@ public extension Message { } static func shouldSync(message: Message) -> Bool { - // For 'Note to Self' messages we always want to sync the message - guard message.sender != message.recipient else { return true } - switch message { + case is VisibleMessage: return true + case is ExpirationTimerUpdate: return true + case is ConfigurationMessage: return true + case is UnsendRequest: return true + case let controlMessage as ClosedGroupControlMessage: switch controlMessage.kind { case .new: return true @@ -192,9 +194,7 @@ public extension Message { case .answer, .endCall: return true default: return false } - - case is ConfigurationMessage: return true - case is UnsendRequest: return true + default: return false } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 2cfb8f433..cb3e55df7 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -139,13 +139,6 @@ extension MessageReceiver { return recipientParts[2] }() ).inserted(db) - - // If the message was an outgoing message then immediately update the recipient state to 'sent' - if variant == .standardOutgoing, let interactionId: Int64 = interaction.id { - _ = try? RecipientState - .filter(RecipientState.Columns.interactionId == interactionId) - .updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.sent)) - } } catch { switch error { @@ -375,6 +368,12 @@ extension MessageReceiver { ) throws { guard variant == .standardOutgoing else { return } + // Immediately update any existing outgoing message 'RecipientState' records to be 'sent' + _ = try? RecipientState + .filter(RecipientState.Columns.interactionId == interactionId) + .updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.sent)) + + // Create any addiitonal 'RecipientState' records as needed switch thread.variant { case .contact: if let syncTarget: String = syncTarget { diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index 4f95ea77c..895366363 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -9,7 +9,7 @@ extension MessageSender { // MARK: - Durable - public static func send(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws { + public static func send(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread, isSyncMessage: Bool = false) throws { guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } try prep(db, signalAttachments: attachments, for: interactionId) @@ -18,11 +18,12 @@ extension MessageSender { message: VisibleMessage.from(db, interaction: interaction), threadId: thread.id, interactionId: interactionId, - to: try Message.Destination.from(db, thread: thread) + to: try Message.Destination.from(db, thread: thread), + isSyncMessage: isSyncMessage ) } - public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread) throws { + public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread, isSyncMessage: Bool = false) throws { // Only 'VisibleMessage' types can be sent via this method guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } @@ -32,21 +33,37 @@ extension MessageSender { message: VisibleMessage.from(db, interaction: interaction), threadId: thread.id, interactionId: interactionId, - to: try Message.Destination.from(db, thread: thread) + to: try Message.Destination.from(db, thread: thread), + isSyncMessage: isSyncMessage ) } - public static func send(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws { + public static func send(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread, isSyncMessage: Bool = false) throws { send( db, message: message, threadId: thread.id, interactionId: interactionId, - to: try Message.Destination.from(db, thread: thread) + to: try Message.Destination.from(db, thread: thread), + isSyncMessage: isSyncMessage ) } - public static func send(_ db: Database, message: Message, threadId: String?, interactionId: Int64?, to destination: Message.Destination) { + public static func send(_ db: Database, message: Message, threadId: String?, interactionId: Int64?, to destination: Message.Destination, isSyncMessage: Bool = false) { + // If it's a sync message then we need to make some slight tweaks before sending so use the proper + // sync message sending process instead of the standard process + guard !isSyncMessage else { + scheduleSyncMessageIfNeeded( + db, + message: message, + destination: destination, + threadId: threadId, + interactionId: interactionId, + isAlreadySyncMessage: false + ) + return + } + JobRunner.add( db, job: Job( @@ -55,7 +72,8 @@ extension MessageSender { interactionId: interactionId, details: MessageSendJob.Details( destination: destination, - message: message + message: message, + isSyncMessage: isSyncMessage ) ) ) @@ -179,7 +197,8 @@ extension MessageSender { message: message, to: destination .with(fileIds: fileIds), - interactionId: interactionId + interactionId: interactionId, + isSyncMessage: false ) } } @@ -200,7 +219,7 @@ extension MessageSender { if forceSyncNow { try MessageSender - .sendImmediate(db, message: configurationMessage, to: destination, interactionId: nil) + .sendImmediate(db, message: configurationMessage, to: destination, interactionId: nil, isSyncMessage: false) .done { seal.fulfill(()) } .catch { _ in seal.reject(StorageError.generic) } .retainUntilComplete() diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index ed2aceaac..188818755 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -42,10 +42,16 @@ public final class MessageSender { // MARK: - Convenience - public static func sendImmediate(_ db: Database, message: Message, to destination: Message.Destination, interactionId: Int64?) throws -> Promise { + public static func sendImmediate( + _ db: Database, + message: Message, + to destination: Message.Destination, + interactionId: Int64?, + isSyncMessage: Bool + ) throws -> Promise { switch destination { case .contact, .closedGroup: - return try sendToSnodeDestination(db, message: message, to: destination, interactionId: interactionId) + return try sendToSnodeDestination(db, message: message, to: destination, interactionId: interactionId, isSyncMessage: isSyncMessage) case .openGroup: return sendToOpenGroupDestination(db, message: message, to: destination, interactionId: interactionId) @@ -65,7 +71,7 @@ public final class MessageSender { isSyncMessage: Bool = false ) throws -> Promise { let (promise, seal) = Promise.pending() - let userPublicKey: String = getUserHexEncodedPublicKey(db) + let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() // Set the timestamp, sender and recipient @@ -73,7 +79,7 @@ public final class MessageSender { message.sentTimestamp ?? // Visible messages will already have their sent timestamp set UInt64(messageSendTimestamp) ) - message.sender = userPublicKey + message.sender = currentUserPublicKey message.recipient = { switch destination { case .contact(let publicKey): return publicKey @@ -84,7 +90,7 @@ public final class MessageSender { // Set the failure handler (need it here already for precondition failure handling) func handleFailure(_ db: Database, with error: MessageSenderError) { - MessageSender.handleFailedMessageSend(db, message: message, with: error, interactionId: interactionId) + MessageSender.handleFailedMessageSend(db, message: message, with: error, interactionId: interactionId, isSyncMessage: isSyncMessage) seal.reject(error) } @@ -94,21 +100,8 @@ public final class MessageSender { return promise } - // Stop here if this is a self-send, unless we should sync the message - let isSelfSend: Bool = (message.recipient == userPublicKey) - - guard - !isSelfSend || - isSyncMessage || - Message.shouldSync(message: message) - else { - try MessageSender.handleSuccessfulMessageSend(db, message: message, to: destination, interactionId: interactionId) - seal.fulfill(()) - return promise - } - // Attach the user's profile if needed - if var messageWithProfile: MessageWithProfile = message as? MessageWithProfile { + if !isSyncMessage, var messageWithProfile: MessageWithProfile = message as? MessageWithProfile { let profile: Profile = Profile.fetchOrCreateCurrentUser(db) if let profileKey: Data = profile.profileEncryptionKey?.keyData, let profilePictureUrl: String = profile.profilePictureUrl { @@ -123,6 +116,9 @@ public final class MessageSender { } } + // Perform any pre-send actions + handleMessageWillSend(db, message: message, interactionId: interactionId, isSyncMessage: isSyncMessage) + // Convert it to protobuf guard let proto = message.toProto(db) else { handleFailure(db, with: .protoConversionFailed) @@ -233,6 +229,9 @@ public final class MessageSender { ) let shouldNotify: Bool = { + // Don't send a notification when sending messages in 'Note to Self' + guard message.recipient != currentUserPublicKey else { return false } + switch message { case is VisibleMessage, is UnsendRequest: return !isSyncMessage case let callMessage as CallMessage: @@ -402,6 +401,9 @@ public final class MessageSender { return promise } + // Perform any pre-send actions + handleMessageWillSend(db, message: message, interactionId: interactionId) + // Convert it to protobuf guard let proto = message.toProto(db) else { handleFailure(db, with: .protoConversionFailed) @@ -465,7 +467,7 @@ public final class MessageSender { dependencies: SMKDependencies = SMKDependencies() ) -> Promise { let (promise, seal) = Promise.pending() - let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) + let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies) guard case .openGroupInbox(let server, let openGroupPublicKey, let recipientBlindedPublicKey) = destination else { preconditionFailure() @@ -476,7 +478,7 @@ public final class MessageSender { message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) } - message.sender = userPublicKey + message.sender = currentUserPublicKey message.recipient = recipientBlindedPublicKey // Set the failure handler (need it here already for precondition failure handling) @@ -501,6 +503,9 @@ public final class MessageSender { } } + // Perform any pre-send actions + handleMessageWillSend(db, message: message, interactionId: interactionId) + // Convert it to protobuf guard let proto = message.toProto(db) else { handleFailure(db, with: .protoConversionFailed) @@ -569,6 +574,32 @@ public final class MessageSender { // MARK: Success & Failure Handling + public static func handleMessageWillSend( + _ db: Database, + message: Message, + interactionId: Int64?, + isSyncMessage: Bool = false + ) { + // If the message was a reaction then we don't want to do anything to the original + // interaction (which the 'interactionId' is pointing to + guard (message as? VisibleMessage)?.reaction == nil else { return } + + // Mark messages as "sending"/"syncing" if needed (this is for retries) + _ = try? RecipientState + .filter(RecipientState.Columns.interactionId == interactionId) + .filter(isSyncMessage ? + RecipientState.Columns.state == RecipientState.State.failedToSync : + RecipientState.Columns.state == RecipientState.State.failed + ) + .updateAll( + db, + RecipientState.Columns.state.set(to: isSyncMessage ? + RecipientState.State.syncing : + RecipientState.State.sending + ) + ) + } + private static func handleSuccessfulMessageSend( _ db: Database, message: Message, @@ -578,7 +609,7 @@ public final class MessageSender { isSyncMessage: Bool = false ) throws { // If the message was a reaction then we want to update the reaction instead of the original - // interaciton (which the 'interactionId' is pointing to + // interaction (which the 'interactionId' is pointing to if let visibleMessage: VisibleMessage = message as? VisibleMessage, let reaction: VisibleMessage.VMReaction = visibleMessage.reaction { try Reaction .filter(Reaction.Columns.interactionId == interactionId) @@ -624,18 +655,20 @@ public final class MessageSender { } } + let threadId: String = { + switch destination { + case .contact(let publicKey): return publicKey + case .closedGroup(let groupPublicKey): return groupPublicKey + case .openGroup(let roomToken, let server, _, _, _): + return OpenGroup.idFor(roomToken: roomToken, server: server) + + case .openGroupInbox(_, _, let blindedPublicKey): return blindedPublicKey + } + }() + // Prevent ControlMessages from being handled multiple times if not supported try? ControlMessageProcessRecord( - threadId: { - switch destination { - case .contact(let publicKey): return publicKey - case .closedGroup(let groupPublicKey): return groupPublicKey - case .openGroup(let roomToken, let server, _, _, _): - return OpenGroup.idFor(roomToken: roomToken, server: server) - - case .openGroupInbox(_, _, let blindedPublicKey): return blindedPublicKey - } - }(), + threadId: threadId, message: message, serverExpirationTimestamp: ( (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + @@ -643,36 +676,27 @@ public final class MessageSender { ) )?.insert(db) - // Sync the message if: - // • it's a visible message or an expiration timer update - // • the destination was a contact - // • we didn't sync it already - // • it wasn't set to 'Note to Self' - let userPublicKey = getUserHexEncodedPublicKey(db) - if case .contact(let publicKey) = destination, !isSyncMessage, publicKey != userPublicKey { - if let message = message as? VisibleMessage { message.syncTarget = publicKey } - if let message = message as? ExpirationTimerUpdate { message.syncTarget = publicKey } - - // FIXME: Make this a job - try sendToSnodeDestination( - db, - message: message, - to: .contact(publicKey: userPublicKey), - interactionId: interactionId, - isSyncMessage: true - ).retainUntilComplete() - } + // Sync the message if needed + scheduleSyncMessageIfNeeded( + db, + message: message, + destination: destination, + threadId: threadId, + interactionId: interactionId, + isAlreadySyncMessage: isSyncMessage + ) } public static func handleFailedMessageSend( _ db: Database, message: Message, with error: MessageSenderError, - interactionId: Int64? + interactionId: Int64?, + isSyncMessage: Bool = false ) { // TODO: Revert the local database change // If the message was a reaction then we don't want to do anything to the original - // interaciton (which the 'interactionId' is pointing to + // interaction (which the 'interactionId' is pointing to guard (message as? VisibleMessage)?.reaction == nil else { return } // Check if we need to mark any "sending" recipients as "failed" @@ -683,7 +707,12 @@ public final class MessageSender { let rowIds: [Int64] = (try? RecipientState .select(Column.rowID) .filter(RecipientState.Columns.interactionId == interactionId) - .filter(RecipientState.Columns.state == RecipientState.State.sending) + .filter(!isSyncMessage ? + RecipientState.Columns.state == RecipientState.State.sending : ( + RecipientState.Columns.state == RecipientState.State.syncing || + RecipientState.Columns.state == RecipientState.State.sent + ) + ) .asRequest(of: Int64.self) .fetchAll(db)) .defaulting(to: []) @@ -698,7 +727,9 @@ public final class MessageSender { .filter(rowIds.contains(Column.rowID)) .updateAll( db, - RecipientState.Columns.state.set(to: RecipientState.State.failed), + RecipientState.Columns.state.set( + to: (isSyncMessage ? RecipientState.State.failedToSync : RecipientState.State.failed) + ), RecipientState.Columns.mostRecentFailureText.set(to: error.localizedDescription) ) } @@ -720,6 +751,43 @@ public final class MessageSender { return nil } + + public static func scheduleSyncMessageIfNeeded( + _ db: Database, + message: Message, + destination: Message.Destination, + threadId: String?, + interactionId: Int64?, + isAlreadySyncMessage: Bool + ) { + // Sync the message if it's not a sync message, wasn't already sent to the current user and + // it's a message type which should be synced + let currentUserPublicKey = getUserHexEncodedPublicKey(db) + + if + case .contact(let publicKey) = destination, + !isAlreadySyncMessage, + publicKey != currentUserPublicKey, + Message.shouldSync(message: message) + { + if let message = message as? VisibleMessage { message.syncTarget = publicKey } + if let message = message as? ExpirationTimerUpdate { message.syncTarget = publicKey } + + JobRunner.add( + db, + job: Job( + variant: .messageSend, + threadId: threadId, + interactionId: interactionId, + details: MessageSendJob.Details( + destination: .contact(publicKey: currentUserPublicKey), + message: message, + isSyncMessage: true + ) + ) + ) + } + } } // MARK: - Objective-C Support diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 81c452039..ece71bca3 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -39,6 +39,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) + public static let isLastOutgoingKey: SQL = SQL(stringLiteral: CodingKeys.isLastOutgoing.stringValue) public static let profileString: String = CodingKeys.profile.stringValue public static let quoteString: String = CodingKeys.quote.stringValue @@ -140,6 +141,8 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, /// This value indicates whether this is the last message in the thread public let isLast: Bool + public let isLastOutgoing: Bool + /// This is the users blinded key (will only be set for messages within open groups) public let currentUserBlindedPublicKey: String? @@ -191,6 +194,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, positionInCluster: self.positionInCluster, isOnlyMessageInCluster: self.isOnlyMessageInCluster, isLast: self.isLast, + isLastOutgoing: self.isLastOutgoing, currentUserBlindedPublicKey: self.currentUserBlindedPublicKey ) } @@ -199,6 +203,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, prevModel: MessageViewModel?, nextModel: MessageViewModel?, isLast: Bool, + isLastOutgoing: Bool, currentUserBlindedPublicKey: String? ) -> MessageViewModel { let cellType: CellType = { @@ -403,6 +408,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, positionInCluster: positionInCluster, isOnlyMessageInCluster: isOnlyMessageInCluster, isLast: isLast, + isLastOutgoing: isLastOutgoing, currentUserBlindedPublicKey: currentUserBlindedPublicKey ) } @@ -498,7 +504,8 @@ public extension MessageViewModel { quote: Quote? = nil, cellType: CellType = .typingIndicator, isTypingIndicator: Bool? = nil, - isLast: Bool = true + isLast: Bool = true, + isLastOutgoing: Bool = false ) { self.threadId = "INVALID_THREAD_ID" self.threadVariant = .contact @@ -554,6 +561,7 @@ public extension MessageViewModel { self.positionInCluster = .middle self.isOnlyMessageInCluster = true self.isLast = isLast + self.isLastOutgoing = isLastOutgoing self.currentUserBlindedPublicKey = nil } } @@ -700,7 +708,8 @@ public extension MessageViewModel { false AS \(ViewModel.shouldShowDateHeaderKey), \(Position.middle) AS \(ViewModel.positionInClusterKey), false AS \(ViewModel.isOnlyMessageInClusterKey), - false AS \(ViewModel.isLastKey) + false AS \(ViewModel.isLastKey), + false AS \(ViewModel.isLastOutgoingKey) FROM \(Interaction.self) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift index 99ef6bd21..61ead815f 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicDark.swift @@ -10,6 +10,7 @@ internal enum Theme_ClassicDark: ThemeColors { .clear: .clear, .primary: .primary, .defaultPrimary: Theme.PrimaryColor.green.color, + .warning: .warning, .danger: .dangerDark, .disabled: .disabledDark, .backgroundPrimary: .classicDark0, diff --git a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift index 6e7e7dc32..645ec9631 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+ClassicLight.swift @@ -10,6 +10,7 @@ internal enum Theme_ClassicLight: ThemeColors { .clear: .clear, .primary: .primary, .defaultPrimary: Theme.PrimaryColor.green.color, + .warning: .warning, .danger: .dangerLight, .disabled: .disabledLight, .backgroundPrimary: .classicLight6, diff --git a/SessionUIKit/Style Guide/Themes/Theme+Colors.swift b/SessionUIKit/Style Guide/Themes/Theme+Colors.swift index 5a56070a6..057157bfd 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+Colors.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+Colors.swift @@ -41,6 +41,7 @@ public extension Theme { // MARK: - Standard Theme Colors internal extension UIColor { + static let warning: UIColor = #colorLiteral(red: 0.9882352941, green: 0.6941176471, blue: 0.3490196078, alpha: 1) // #FCB159 static let dangerDark: UIColor = #colorLiteral(red: 1, green: 0.2274509804, blue: 0.2274509804, alpha: 1) // #FF3A3A static let dangerLight: UIColor = #colorLiteral(red: 0.8823529412, green: 0.1764705882, blue: 0.09803921569, alpha: 1) // #E12D19 static let disabledDark: UIColor = #colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1) // #A1A2A1 diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift index e01656c8f..4b4b33efe 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanDark.swift @@ -10,6 +10,7 @@ internal enum Theme_OceanDark: ThemeColors { .clear: .clear, .primary: .primary, .defaultPrimary: Theme.PrimaryColor.blue.color, + .warning: .warning, .danger: .dangerDark, .disabled: .disabledDark, .backgroundPrimary: .oceanDark2, diff --git a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift index 649028a1e..f752f21de 100644 --- a/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift +++ b/SessionUIKit/Style Guide/Themes/Theme+OceanLight.swift @@ -10,6 +10,7 @@ internal enum Theme_OceanLight: ThemeColors { .clear: .clear, .primary: .primary, .defaultPrimary: Theme.PrimaryColor.blue.color, + .warning: .warning, .danger: .dangerLight, .disabled: .disabledLight, .backgroundPrimary: .oceanLight7, diff --git a/SessionUIKit/Style Guide/Themes/Theme.swift b/SessionUIKit/Style Guide/Themes/Theme.swift index ca0a620d9..57a5fb200 100644 --- a/SessionUIKit/Style Guide/Themes/Theme.swift +++ b/SessionUIKit/Style Guide/Themes/Theme.swift @@ -98,6 +98,7 @@ public indirect enum ThemeValue: Hashable { case clear case primary case defaultPrimary + case warning case danger case disabled case backgroundPrimary