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
pull/784/head
Morgan Pretty 2 years ago
parent d020a7a05f
commit 3344e58716

@ -34,6 +34,17 @@ extension ContextMenuVC {
} }
// MARK: - Actions // 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 { static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
return Action( return Action(
@ -126,6 +137,14 @@ extension ContextMenuVC {
case .standardOutgoing, .standardIncoming: break case .standardOutgoing, .standardIncoming: break
} }
let canRetry: Bool = (
cellViewModel.variant == .standardOutgoing && (
cellViewModel.state == .failed || (
cellViewModel.threadVariant == .contact &&
cellViewModel.state == .failedToSync
)
)
)
let canReply: Bool = ( let canReply: Bool = (
cellViewModel.variant != .standardOutgoing || ( cellViewModel.variant != .standardOutgoing || (
cellViewModel.state != .failed && cellViewModel.state != .failed &&
@ -180,6 +199,7 @@ extension ContextMenuVC {
}() }()
let generatedActions: [Action] = [ let generatedActions: [Action] = [
(canRetry ? Action.retry(cellViewModel, delegate) : nil),
(canReply ? Action.reply(cellViewModel, delegate) : nil), (canReply ? Action.reply(cellViewModel, delegate) : nil),
(canCopy ? Action.copy(cellViewModel, delegate) : nil), (canCopy ? Action.copy(cellViewModel, delegate) : nil),
(canSave ? Action.save(cellViewModel, delegate) : nil), (canSave ? Action.save(cellViewModel, delegate) : nil),
@ -201,6 +221,7 @@ extension ContextMenuVC {
// MARK: - Delegate // MARK: - Delegate
protocol ContextMenuActionDelegate { protocol ContextMenuActionDelegate {
func retry(_ cellViewModel: MessageViewModel)
func reply(_ cellViewModel: MessageViewModel) func reply(_ cellViewModel: MessageViewModel)
func copy(_ cellViewModel: MessageViewModel) func copy(_ cellViewModel: MessageViewModel)
func copySessionID(_ cellViewModel: MessageViewModel) func copySessionID(_ cellViewModel: MessageViewModel)

@ -7,6 +7,7 @@ import PhotosUI
import Sodium import Sodium
import PromiseKit import PromiseKit
import GRDB import GRDB
import SessionUIKit
import SessionMessagingKit import SessionMessagingKit
import SessionUtilitiesKit import SessionUtilitiesKit
import SignalUtilitiesKit import SignalUtilitiesKit
@ -828,7 +829,7 @@ extension ConversationVC:
} }
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) { 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 // Show the failed message sheet
showFailedMessageSheet(for: cellViewModel) showFailedMessageSheet(for: cellViewModel)
return return
@ -1450,30 +1451,34 @@ extension ConversationVC:
// MARK: --action handling // MARK: --action handling
func showFailedMessageSheet(for cellViewModel: MessageViewModel) { func showFailedMessageSheet(for cellViewModel: MessageViewModel) {
let sheet = UIAlertController(title: cellViewModel.mostRecentFailureText, message: nil, preferredStyle: .actionSheet) let sheet = UIAlertController(
sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) title: (cellViewModel.state == .failedToSync ?
sheet.addAction(UIAlertAction(title: "Delete", style: .destructive, handler: { _ in "MESSAGE_DELIVERY_FAILED_SYNC_TITLE".localized() :
Storage.shared.writeAsync { db in "MESSAGE_DELIVERY_FAILED_TITLE".localized()
try Interaction ),
.filter(id: cellViewModel.id) message: cellViewModel.mostRecentFailureText,
.deleteAll(db) preferredStyle: .actionSheet
} )
})) sheet.addAction(UIAlertAction(title: "TXT_CANCEL_TITLE".localized(), style: .cancel, handler: nil))
sheet.addAction(UIAlertAction(title: "Resend", style: .default, handler: { _ in
Storage.shared.writeAsync { [weak self] db in if cellViewModel.state != .failedToSync {
guard sheet.addAction(UIAlertAction(title: "TXT_DELETE_TITLE".localized(), style: .destructive, handler: { _ in
let threadId: String = self?.viewModel.threadData.threadId, Storage.shared.writeAsync { db in
let interaction: Interaction = try? Interaction.fetchOne(db, id: cellViewModel.id), try Interaction
let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) .filter(id: cellViewModel.id)
else { return } .deleteAll(db)
}
try MessageSender.send( }))
db, }
interaction: interaction,
in: thread 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 // HACK: Extracting this info from the error string is pretty dodgy
let prefix: String = "HTTP request failed at destination (Service node " let prefix: String = "HTTP request failed at destination (Service node "
@ -1557,6 +1562,23 @@ extension ConversationVC:
} }
// MARK: - ContextMenuActionDelegate // 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) { func reply(_ cellViewModel: MessageViewModel) {
let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending( let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending(

@ -298,6 +298,15 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
index == (sortedData.count - 1) && index == (sortedData.count - 1) &&
pageInfo.pageOffset == 0 pageInfo.pageOffset == 0
), ),
isLastOutgoing: (
cellViewModel.id == sortedData
.filter {
$0.authorId == threadData.currentUserPublicKey ||
$0.authorId == threadData.currentUserBlindedPublicKey
}
.last?
.id
),
currentUserBlindedPublicKey: threadData.currentUserBlindedPublicKey currentUserBlindedPublicKey: threadData.currentUserBlindedPublicKey
) )
} }

@ -431,7 +431,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
cellViewModel.variant == .infoCall || cellViewModel.variant == .infoCall ||
( (
cellViewModel.state == .sent && cellViewModel.state == .sent &&
!cellViewModel.isLast !cellViewModel.isLastOutgoing
) )
) )
messageStatusLabelPaddingView.isHidden = ( messageStatusLabelPaddingView.isHidden = (

@ -2,7 +2,7 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // Copyright (c) 2019 Open Whisper Systems. All rights reserved.
// //
import Foundation import UIKit
import Reachability import Reachability
import SignalUtilitiesKit import SignalUtilitiesKit
import PromiseKit import PromiseKit

@ -6,6 +6,8 @@ import AFNetworking
import Foundation import Foundation
import PromiseKit import PromiseKit
import CoreServices import CoreServices
import SignalUtilitiesKit
import SessionUtilitiesKit
// There's no UTI type for webp! // There's no UTI type for webp!
enum GiphyFormat { enum GiphyFormat {

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -600,4 +600,10 @@
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent"; "MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
"MESSAGE_DELIVERY_STATUS_READ" = "Read"; "MESSAGE_DELIVERY_STATUS_READ" = "Read";
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send"; "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"; "delete_message_for_me_and_my_devices" = "Delete from all of my devices";
"context_menu_resend" = "Resend";
"context_menu_resync" = "Resync";

@ -5,6 +5,8 @@
import Foundation import Foundation
import UserNotifications import UserNotifications
import PromiseKit import PromiseKit
import SignalCoreKit
import SignalUtilitiesKit
import SessionMessagingKit import SessionMessagingKit
class UserNotificationConfig { class UserNotificationConfig {

@ -40,6 +40,8 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
case failed case failed
case skipped case skipped
case sent case sent
case failedToSync // One-to-one Only
case syncing // One-to-one Only
func message(hasAttachments: Bool, hasAtLeastOneReadReceipt: Bool) -> String { func message(hasAttachments: Bool, hasAtLeastOneReadReceipt: Bool) -> String {
switch self { switch self {
@ -58,6 +60,9 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
} }
return "MESSAGE_STATUS_READ".localized() return "MESSAGE_STATUS_READ".localized()
case .failedToSync: return "MESSAGE_DELIVERY_STATUS_FAILED_SYNC".localized()
case .syncing: return "MESSAGE_DELIVERY_STATUS_SYNCING".localized()
default: default:
owsFailDebug("Message has unexpected status: \(self).") owsFailDebug("Message has unexpected status: \(self).")
@ -96,6 +101,21 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
"MESSAGE_DELIVERY_STATUS_FAILED".localized(), "MESSAGE_DELIVERY_STATUS_FAILED".localized(),
.danger .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 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)
)
}
}

@ -19,12 +19,16 @@ public enum FailedMessageSendsJob: JobExecutor {
) { ) {
// Update all 'sending' message states to 'failed' // Update all 'sending' message states to 'failed'
Storage.shared.write { db in Storage.shared.write { db in
let changeCount: Int = try RecipientState let sendChangeCount: Int = try RecipientState
.filter(RecipientState.Columns.state == RecipientState.State.sending) .filter(RecipientState.Columns.state == RecipientState.State.sending)
.updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.failed)) .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 let attachmentChangeCount: Int = try Attachment
.filter(Attachment.Columns.state == Attachment.State.uploading) .filter(Attachment.Columns.state == Attachment.State.uploading)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedUpload)) .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)") SNLog("Marked \(changeCount) message\(changeCount == 1 ? "" : "s") as failed (\(attachmentChangeCount) upload\(attachmentChangeCount == 1 ? "" : "s") cancelled)")
} }

@ -88,7 +88,7 @@ public enum MessageReceiveJob: JobExecutor {
failure(updatedJob, error, true) failure(updatedJob, error, true)
case .some(let error): case .some(let error):
failure(updatedJob, error, false) // TODO: Confirm the 'noKeyPair' errors here aren't an issue failure(updatedJob, error, false)
case .none: case .none:
success(updatedJob, false) success(updatedJob, false)

@ -167,7 +167,8 @@ public enum MessageSendJob: JobExecutor {
message: details.message, message: details.message,
to: details.destination to: details.destination
.with(fileIds: messageFileIds), .with(fileIds: messageFileIds),
interactionId: job.interactionId interactionId: job.interactionId,
isSyncMessage: (details.isSyncMessage == true)
) )
} }
.done(on: queue) { _ in success(job, false) } .done(on: queue) { _ in success(job, false) }
@ -213,21 +214,25 @@ extension MessageSendJob {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case destination case destination
case message case message
case isSyncMessage
case variant case variant
} }
public let destination: Message.Destination public let destination: Message.Destination
public let message: Message public let message: Message
public let isSyncMessage: Bool?
public let variant: Message.Variant? public let variant: Message.Variant?
// MARK: - Initialization // MARK: - Initialization
public init( public init(
destination: Message.Destination, destination: Message.Destination,
message: Message message: Message,
isSyncMessage: Bool? = nil
) { ) {
self.destination = destination self.destination = destination
self.message = message self.message = message
self.isSyncMessage = isSyncMessage
self.variant = Message.Variant(from: message) self.variant = Message.Variant(from: message)
} }
@ -243,7 +248,8 @@ extension MessageSendJob {
self = Details( self = Details(
destination: try container.decode(Message.Destination.self, forKey: .destination), 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(destination, forKey: .destination)
try container.encode(message, forKey: .message) try container.encode(message, forKey: .message)
try container.encodeIfPresent(isSyncMessage, forKey: .isSyncMessage)
try container.encode(variant, forKey: .variant) try container.encode(variant, forKey: .variant)
} }
} }

@ -43,7 +43,8 @@ public enum SendReadReceiptsJob: JobExecutor {
timestamps: details.timestampMsValues.map { UInt64($0) } timestamps: details.timestampMsValues.map { UInt64($0) }
), ),
to: details.destination, to: details.destination,
interactionId: nil interactionId: nil,
isSyncMessage: false
) )
} }
.done(on: queue) { .done(on: queue) {

@ -177,10 +177,12 @@ public extension Message {
} }
static func shouldSync(message: Message) -> Bool { 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 { 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: case let controlMessage as ClosedGroupControlMessage:
switch controlMessage.kind { switch controlMessage.kind {
case .new: return true case .new: return true
@ -192,9 +194,7 @@ public extension Message {
case .answer, .endCall: return true case .answer, .endCall: return true
default: return false default: return false
} }
case is ConfigurationMessage: return true
case is UnsendRequest: return true
default: return false default: return false
} }
} }

@ -139,13 +139,6 @@ extension MessageReceiver {
return recipientParts[2] return recipientParts[2]
}() }()
).inserted(db) ).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 { catch {
switch error { switch error {
@ -375,6 +368,12 @@ extension MessageReceiver {
) throws { ) throws {
guard variant == .standardOutgoing else { return } 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 { switch thread.variant {
case .contact: case .contact:
if let syncTarget: String = syncTarget { if let syncTarget: String = syncTarget {

@ -9,7 +9,7 @@ extension MessageSender {
// MARK: - Durable // 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 } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
try prep(db, signalAttachments: attachments, for: interactionId) try prep(db, signalAttachments: attachments, for: interactionId)
@ -18,11 +18,12 @@ extension MessageSender {
message: VisibleMessage.from(db, interaction: interaction), message: VisibleMessage.from(db, interaction: interaction),
threadId: thread.id, threadId: thread.id,
interactionId: interactionId, 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 // Only 'VisibleMessage' types can be sent via this method
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved } guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
@ -32,21 +33,37 @@ extension MessageSender {
message: VisibleMessage.from(db, interaction: interaction), message: VisibleMessage.from(db, interaction: interaction),
threadId: thread.id, threadId: thread.id,
interactionId: interactionId, 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( send(
db, db,
message: message, message: message,
threadId: thread.id, threadId: thread.id,
interactionId: interactionId, 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( JobRunner.add(
db, db,
job: Job( job: Job(
@ -55,7 +72,8 @@ extension MessageSender {
interactionId: interactionId, interactionId: interactionId,
details: MessageSendJob.Details( details: MessageSendJob.Details(
destination: destination, destination: destination,
message: message message: message,
isSyncMessage: isSyncMessage
) )
) )
) )
@ -179,7 +197,8 @@ extension MessageSender {
message: message, message: message,
to: destination to: destination
.with(fileIds: fileIds), .with(fileIds: fileIds),
interactionId: interactionId interactionId: interactionId,
isSyncMessage: false
) )
} }
} }
@ -200,7 +219,7 @@ extension MessageSender {
if forceSyncNow { if forceSyncNow {
try MessageSender try MessageSender
.sendImmediate(db, message: configurationMessage, to: destination, interactionId: nil) .sendImmediate(db, message: configurationMessage, to: destination, interactionId: nil, isSyncMessage: false)
.done { seal.fulfill(()) } .done { seal.fulfill(()) }
.catch { _ in seal.reject(StorageError.generic) } .catch { _ in seal.reject(StorageError.generic) }
.retainUntilComplete() .retainUntilComplete()

@ -42,10 +42,16 @@ public final class MessageSender {
// MARK: - Convenience // MARK: - Convenience
public static func sendImmediate(_ db: Database, message: Message, to destination: Message.Destination, interactionId: Int64?) throws -> Promise<Void> { public static func sendImmediate(
_ db: Database,
message: Message,
to destination: Message.Destination,
interactionId: Int64?,
isSyncMessage: Bool
) throws -> Promise<Void> {
switch destination { switch destination {
case .contact, .closedGroup: 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: case .openGroup:
return sendToOpenGroupDestination(db, message: message, to: destination, interactionId: interactionId) return sendToOpenGroupDestination(db, message: message, to: destination, interactionId: interactionId)
@ -65,7 +71,7 @@ public final class MessageSender {
isSyncMessage: Bool = false isSyncMessage: Bool = false
) throws -> Promise<Void> { ) throws -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending() let (promise, seal) = Promise<Void>.pending()
let userPublicKey: String = getUserHexEncodedPublicKey(db) let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs() let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
// Set the timestamp, sender and recipient // 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 message.sentTimestamp ?? // Visible messages will already have their sent timestamp set
UInt64(messageSendTimestamp) UInt64(messageSendTimestamp)
) )
message.sender = userPublicKey message.sender = currentUserPublicKey
message.recipient = { message.recipient = {
switch destination { switch destination {
case .contact(let publicKey): return publicKey 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) // Set the failure handler (need it here already for precondition failure handling)
func handleFailure(_ db: Database, with error: MessageSenderError) { 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) seal.reject(error)
} }
@ -94,21 +100,8 @@ public final class MessageSender {
return promise 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 // 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) let profile: Profile = Profile.fetchOrCreateCurrentUser(db)
if let profileKey: Data = profile.profileEncryptionKey?.keyData, let profilePictureUrl: String = profile.profilePictureUrl { 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 // Convert it to protobuf
guard let proto = message.toProto(db) else { guard let proto = message.toProto(db) else {
handleFailure(db, with: .protoConversionFailed) handleFailure(db, with: .protoConversionFailed)
@ -233,6 +229,9 @@ public final class MessageSender {
) )
let shouldNotify: Bool = { let shouldNotify: Bool = {
// Don't send a notification when sending messages in 'Note to Self'
guard message.recipient != currentUserPublicKey else { return false }
switch message { switch message {
case is VisibleMessage, is UnsendRequest: return !isSyncMessage case is VisibleMessage, is UnsendRequest: return !isSyncMessage
case let callMessage as CallMessage: case let callMessage as CallMessage:
@ -402,6 +401,9 @@ public final class MessageSender {
return promise return promise
} }
// Perform any pre-send actions
handleMessageWillSend(db, message: message, interactionId: interactionId)
// Convert it to protobuf // Convert it to protobuf
guard let proto = message.toProto(db) else { guard let proto = message.toProto(db) else {
handleFailure(db, with: .protoConversionFailed) handleFailure(db, with: .protoConversionFailed)
@ -465,7 +467,7 @@ public final class MessageSender {
dependencies: SMKDependencies = SMKDependencies() dependencies: SMKDependencies = SMKDependencies()
) -> Promise<Void> { ) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending() let (promise, seal) = Promise<Void>.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 { guard case .openGroupInbox(let server, let openGroupPublicKey, let recipientBlindedPublicKey) = destination else {
preconditionFailure() preconditionFailure()
@ -476,7 +478,7 @@ public final class MessageSender {
message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
} }
message.sender = userPublicKey message.sender = currentUserPublicKey
message.recipient = recipientBlindedPublicKey message.recipient = recipientBlindedPublicKey
// Set the failure handler (need it here already for precondition failure handling) // 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 // Convert it to protobuf
guard let proto = message.toProto(db) else { guard let proto = message.toProto(db) else {
handleFailure(db, with: .protoConversionFailed) handleFailure(db, with: .protoConversionFailed)
@ -569,6 +574,32 @@ public final class MessageSender {
// MARK: Success & Failure Handling // 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( private static func handleSuccessfulMessageSend(
_ db: Database, _ db: Database,
message: Message, message: Message,
@ -578,7 +609,7 @@ public final class MessageSender {
isSyncMessage: Bool = false isSyncMessage: Bool = false
) throws { ) throws {
// If the message was a reaction then we want to update the reaction instead of the original // 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 { if let visibleMessage: VisibleMessage = message as? VisibleMessage, let reaction: VisibleMessage.VMReaction = visibleMessage.reaction {
try Reaction try Reaction
.filter(Reaction.Columns.interactionId == interactionId) .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 // Prevent ControlMessages from being handled multiple times if not supported
try? ControlMessageProcessRecord( try? ControlMessageProcessRecord(
threadId: { threadId: 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
}
}(),
message: message, message: message,
serverExpirationTimestamp: ( serverExpirationTimestamp: (
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
@ -643,36 +676,27 @@ public final class MessageSender {
) )
)?.insert(db) )?.insert(db)
// Sync the message if: // Sync the message if needed
// it's a visible message or an expiration timer update scheduleSyncMessageIfNeeded(
// the destination was a contact db,
// we didn't sync it already message: message,
// it wasn't set to 'Note to Self' destination: destination,
let userPublicKey = getUserHexEncodedPublicKey(db) threadId: threadId,
if case .contact(let publicKey) = destination, !isSyncMessage, publicKey != userPublicKey { interactionId: interactionId,
if let message = message as? VisibleMessage { message.syncTarget = publicKey } isAlreadySyncMessage: isSyncMessage
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()
}
} }
public static func handleFailedMessageSend( public static func handleFailedMessageSend(
_ db: Database, _ db: Database,
message: Message, message: Message,
with error: MessageSenderError, with error: MessageSenderError,
interactionId: Int64? interactionId: Int64?,
isSyncMessage: Bool = false
) { ) {
// TODO: Revert the local database change // TODO: Revert the local database change
// If the message was a reaction then we don't want to do anything to the original // 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 } guard (message as? VisibleMessage)?.reaction == nil else { return }
// Check if we need to mark any "sending" recipients as "failed" // Check if we need to mark any "sending" recipients as "failed"
@ -683,7 +707,12 @@ public final class MessageSender {
let rowIds: [Int64] = (try? RecipientState let rowIds: [Int64] = (try? RecipientState
.select(Column.rowID) .select(Column.rowID)
.filter(RecipientState.Columns.interactionId == interactionId) .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) .asRequest(of: Int64.self)
.fetchAll(db)) .fetchAll(db))
.defaulting(to: []) .defaulting(to: [])
@ -698,7 +727,9 @@ public final class MessageSender {
.filter(rowIds.contains(Column.rowID)) .filter(rowIds.contains(Column.rowID))
.updateAll( .updateAll(
db, 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) RecipientState.Columns.mostRecentFailureText.set(to: error.localizedDescription)
) )
} }
@ -720,6 +751,43 @@ public final class MessageSender {
return nil 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 // MARK: - Objective-C Support

@ -39,6 +39,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue)
public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue)
public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.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 profileString: String = CodingKeys.profile.stringValue
public static let quoteString: String = CodingKeys.quote.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 /// This value indicates whether this is the last message in the thread
public let isLast: Bool public let isLast: Bool
public let isLastOutgoing: Bool
/// This is the users blinded key (will only be set for messages within open groups) /// This is the users blinded key (will only be set for messages within open groups)
public let currentUserBlindedPublicKey: String? public let currentUserBlindedPublicKey: String?
@ -191,6 +194,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
positionInCluster: self.positionInCluster, positionInCluster: self.positionInCluster,
isOnlyMessageInCluster: self.isOnlyMessageInCluster, isOnlyMessageInCluster: self.isOnlyMessageInCluster,
isLast: self.isLast, isLast: self.isLast,
isLastOutgoing: self.isLastOutgoing,
currentUserBlindedPublicKey: self.currentUserBlindedPublicKey currentUserBlindedPublicKey: self.currentUserBlindedPublicKey
) )
} }
@ -199,6 +203,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
prevModel: MessageViewModel?, prevModel: MessageViewModel?,
nextModel: MessageViewModel?, nextModel: MessageViewModel?,
isLast: Bool, isLast: Bool,
isLastOutgoing: Bool,
currentUserBlindedPublicKey: String? currentUserBlindedPublicKey: String?
) -> MessageViewModel { ) -> MessageViewModel {
let cellType: CellType = { let cellType: CellType = {
@ -403,6 +408,7 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
positionInCluster: positionInCluster, positionInCluster: positionInCluster,
isOnlyMessageInCluster: isOnlyMessageInCluster, isOnlyMessageInCluster: isOnlyMessageInCluster,
isLast: isLast, isLast: isLast,
isLastOutgoing: isLastOutgoing,
currentUserBlindedPublicKey: currentUserBlindedPublicKey currentUserBlindedPublicKey: currentUserBlindedPublicKey
) )
} }
@ -498,7 +504,8 @@ public extension MessageViewModel {
quote: Quote? = nil, quote: Quote? = nil,
cellType: CellType = .typingIndicator, cellType: CellType = .typingIndicator,
isTypingIndicator: Bool? = nil, isTypingIndicator: Bool? = nil,
isLast: Bool = true isLast: Bool = true,
isLastOutgoing: Bool = false
) { ) {
self.threadId = "INVALID_THREAD_ID" self.threadId = "INVALID_THREAD_ID"
self.threadVariant = .contact self.threadVariant = .contact
@ -554,6 +561,7 @@ public extension MessageViewModel {
self.positionInCluster = .middle self.positionInCluster = .middle
self.isOnlyMessageInCluster = true self.isOnlyMessageInCluster = true
self.isLast = isLast self.isLast = isLast
self.isLastOutgoing = isLastOutgoing
self.currentUserBlindedPublicKey = nil self.currentUserBlindedPublicKey = nil
} }
} }
@ -700,7 +708,8 @@ public extension MessageViewModel {
false AS \(ViewModel.shouldShowDateHeaderKey), false AS \(ViewModel.shouldShowDateHeaderKey),
\(Position.middle) AS \(ViewModel.positionInClusterKey), \(Position.middle) AS \(ViewModel.positionInClusterKey),
false AS \(ViewModel.isOnlyMessageInClusterKey), false AS \(ViewModel.isOnlyMessageInClusterKey),
false AS \(ViewModel.isLastKey) false AS \(ViewModel.isLastKey),
false AS \(ViewModel.isLastOutgoingKey)
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId])

@ -10,6 +10,7 @@ internal enum Theme_ClassicDark: ThemeColors {
.clear: .clear, .clear: .clear,
.primary: .primary, .primary: .primary,
.defaultPrimary: Theme.PrimaryColor.green.color, .defaultPrimary: Theme.PrimaryColor.green.color,
.warning: .warning,
.danger: .dangerDark, .danger: .dangerDark,
.disabled: .disabledDark, .disabled: .disabledDark,
.backgroundPrimary: .classicDark0, .backgroundPrimary: .classicDark0,

@ -10,6 +10,7 @@ internal enum Theme_ClassicLight: ThemeColors {
.clear: .clear, .clear: .clear,
.primary: .primary, .primary: .primary,
.defaultPrimary: Theme.PrimaryColor.green.color, .defaultPrimary: Theme.PrimaryColor.green.color,
.warning: .warning,
.danger: .dangerLight, .danger: .dangerLight,
.disabled: .disabledLight, .disabled: .disabledLight,
.backgroundPrimary: .classicLight6, .backgroundPrimary: .classicLight6,

@ -41,6 +41,7 @@ public extension Theme {
// MARK: - Standard Theme Colors // MARK: - Standard Theme Colors
internal extension UIColor { 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 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 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 static let disabledDark: UIColor = #colorLiteral(red: 0.631372549, green: 0.6352941176, blue: 0.631372549, alpha: 1) // #A1A2A1

@ -10,6 +10,7 @@ internal enum Theme_OceanDark: ThemeColors {
.clear: .clear, .clear: .clear,
.primary: .primary, .primary: .primary,
.defaultPrimary: Theme.PrimaryColor.blue.color, .defaultPrimary: Theme.PrimaryColor.blue.color,
.warning: .warning,
.danger: .dangerDark, .danger: .dangerDark,
.disabled: .disabledDark, .disabled: .disabledDark,
.backgroundPrimary: .oceanDark2, .backgroundPrimary: .oceanDark2,

@ -10,6 +10,7 @@ internal enum Theme_OceanLight: ThemeColors {
.clear: .clear, .clear: .clear,
.primary: .primary, .primary: .primary,
.defaultPrimary: Theme.PrimaryColor.blue.color, .defaultPrimary: Theme.PrimaryColor.blue.color,
.warning: .warning,
.danger: .dangerLight, .danger: .dangerLight,
.disabled: .disabledLight, .disabled: .disabledLight,
.backgroundPrimary: .oceanLight7, .backgroundPrimary: .oceanLight7,

@ -98,6 +98,7 @@ public indirect enum ThemeValue: Hashable {
case clear case clear
case primary case primary
case defaultPrimary case defaultPrimary
case warning
case danger case danger
case disabled case disabled
case backgroundPrimary case backgroundPrimary

Loading…
Cancel
Save