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