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
This commit is contained in:
Morgan Pretty 2023-02-14 13:41:24 +11:00
parent d020a7a05f
commit 3344e58716
45 changed files with 434 additions and 131 deletions

View File

@ -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)

View File

@ -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(

View File

@ -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
)
}

View File

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

View File

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

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

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

View File

@ -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)
)
}
}

View File

@ -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)")
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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()

View File

@ -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<Void> {
public static func sendImmediate(
_ db: Database,
message: Message,
to destination: Message.Destination,
interactionId: Int64?,
isSyncMessage: Bool
) throws -> Promise<Void> {
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<Void> {
let (promise, seal) = Promise<Void>.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<Void> {
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 {
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

View File

@ -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])

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

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