219 lines
8.8 KiB
Swift
219 lines
8.8 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
extension TSInteraction {
|
|
|
|
@objc
|
|
public func fillInMissingSortIdForJustInsertedInteraction(transaction: SDSAnyReadTransaction) {
|
|
switch transaction.readTransaction {
|
|
case .grdbRead(let grdbRead):
|
|
fillInMissingSortIdForJustInsertedInteraction(transaction: grdbRead)
|
|
}
|
|
}
|
|
|
|
private func fillInMissingSortIdForJustInsertedInteraction(transaction: GRDBReadTransaction) {
|
|
guard self.sortId == 0 else {
|
|
owsFailDebug("Unexpected sortId: \(sortId).")
|
|
return
|
|
}
|
|
guard let sortId = BaseModel.grdbIdByUniqueId(tableMetadata: TSInteractionSerializer.table,
|
|
uniqueIdColumnName: InteractionRecord.columnName(.uniqueId),
|
|
uniqueIdColumnValue: self.uniqueId,
|
|
transaction: transaction) else {
|
|
owsFailDebug("Missing sortId.")
|
|
return
|
|
}
|
|
guard sortId > 0, sortId <= UInt64.max else {
|
|
owsFailDebug("Invalid sortId: \(sortId).")
|
|
return
|
|
}
|
|
self.replaceSortId(UInt64(sortId))
|
|
owsAssertDebug(self.sortId > 0)
|
|
}
|
|
|
|
/// Returns `true` if the receiver was inserted into the database by updating the placeholder
|
|
/// Returns `false` if the receiver needs to be inserted into the database.
|
|
private func updatePlaceholder(
|
|
from sender: SignalServiceAddress,
|
|
transaction: SDSAnyWriteTransaction
|
|
) -> Bool {
|
|
let placeholders: [TSInteraction]
|
|
do {
|
|
placeholders = try InteractionFinder.interactions(
|
|
withTimestamp: timestamp,
|
|
filter: { candidate in
|
|
guard let placeholder = candidate as? OWSRecoverableDecryptionPlaceholder else { return false }
|
|
return placeholder.sender == sender && placeholder.timestamp == self.timestamp
|
|
},
|
|
transaction: transaction
|
|
)
|
|
} catch {
|
|
owsFailDebug("Failed to fetch placeholder interaction: \(error)")
|
|
return false
|
|
}
|
|
|
|
guard !placeholders.isEmpty else {
|
|
return false
|
|
}
|
|
|
|
Logger.info("Fetched placeholder with timestamp: \(timestamp) from sender: \(sender). Performing replacement...")
|
|
guard let placeholder = (placeholders.first as? OWSRecoverableDecryptionPlaceholder) else {
|
|
owsFailDebug("Unexpected interaction type")
|
|
return false
|
|
}
|
|
|
|
if placeholder.supportsReplacement {
|
|
placeholder.replaceWithInteraction(self, writeTx: transaction)
|
|
return true
|
|
} else {
|
|
Logger.info("Placeholder not eligible for replacement, deleting.")
|
|
DependenciesBridge.shared.interactionDeleteManager
|
|
.delete(placeholder, sideEffects: .default(), tx: transaction.asV2Write)
|
|
return false
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func insertOrReplacePlaceholder(from sender: SignalServiceAddress, transaction: SDSAnyWriteTransaction) {
|
|
if updatePlaceholder(from: sender, transaction: transaction) {
|
|
Logger.info("Successfully replaced placeholder with interaction: \(timestamp)")
|
|
} else {
|
|
anyInsert(transaction: transaction)
|
|
|
|
// Replaced interactions will inherit the existing sortId
|
|
// Inserted interactions will be assigned a sortId from SQLite, but
|
|
// we need to fetch from the database.
|
|
owsAssertDebug(sortId == 0)
|
|
fillInMissingSortIdForJustInsertedInteraction(transaction: transaction)
|
|
owsAssertDebug(sortId > 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - shouldAppearInInbox
|
|
|
|
extension TSInteraction {
|
|
|
|
/// Returns whether the given interaction should pull a conversation to the top of the list and
|
|
/// marked unread.
|
|
///
|
|
/// This operation necessarily happens after the interaction has been pulled out of the
|
|
/// database. If possible, they should also be filtered as part of the database queries in the
|
|
/// `mostRecentInteractionForInbox(transaction:)` implementations in InteractionFinder.swift.
|
|
@objc
|
|
public func shouldAppearInInbox(transaction: SDSAnyReadTransaction) -> Bool {
|
|
return shouldAppearInInbox(groupUpdateItemsBuilder: { infoMessage in
|
|
guard
|
|
let localIdentifiers = DependenciesBridge.shared.tsAccountManager
|
|
.localIdentifiers(tx: transaction.asV2Read),
|
|
let updates = infoMessage.computedGroupUpdateItems(
|
|
localIdentifiers: localIdentifiers,
|
|
tx: transaction
|
|
)
|
|
else {
|
|
return nil
|
|
}
|
|
return updates
|
|
})
|
|
}
|
|
|
|
/// Returns whether the given interaction should pull a conversation to the top of the list and
|
|
/// marked unread.
|
|
///
|
|
/// - parameter groupUpdateItemsBuilder: If the message is a group update info message,
|
|
/// a block that builds the PersistableGroupUpdateItems for the message, which is run synchronously
|
|
/// and may make use of a transaction if needed.
|
|
public func shouldAppearInInbox(
|
|
groupUpdateItemsBuilder: (TSInfoMessage) -> [TSInfoMessage.PersistableGroupUpdateItem]?
|
|
) -> Bool {
|
|
if !shouldBeSaved || isDynamicInteraction || self is OWSOutgoingSyncMessage {
|
|
owsFailDebug("Unexpected interaction type: \(type(of: self))")
|
|
return false
|
|
}
|
|
|
|
switch self {
|
|
case let errorMessage as TSErrorMessage:
|
|
return Self.shouldErrorMessageAppearInInbox(errorMessage)
|
|
case let infoMessage as TSInfoMessage:
|
|
return Self.shouldInfoMessageAppearInInbox(
|
|
infoMessage,
|
|
groupUpdateItemsBuilder: groupUpdateItemsBuilder
|
|
)
|
|
case let message as TSMessage:
|
|
return Self.shouldMessageAppearInInbox(message)
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
private static func shouldErrorMessageAppearInInbox(_ message: TSErrorMessage) -> Bool {
|
|
switch message.errorType {
|
|
case .nonBlockingIdentityChange:
|
|
// Otherwise all group threads with the recipient will percolate to the top of the inbox, even though
|
|
// there was no meaningful interaction.
|
|
return false
|
|
case .decryptionFailure:
|
|
if message is OWSRecoverableDecryptionPlaceholder {
|
|
// Replaceable interactions should never be shown to the user
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
private static func shouldMessageAppearInInbox(_ message: TSMessage) -> Bool {
|
|
owsPrecondition(!(message is TSErrorMessage))
|
|
owsPrecondition(!(message is TSInfoMessage))
|
|
// skip considering this message if it's a group story reply, or a past edit revision
|
|
return !message.isGroupStoryReply && !message.isPastEditRevision()
|
|
}
|
|
|
|
private static func shouldInfoMessageAppearInInbox(
|
|
_ message: TSInfoMessage,
|
|
groupUpdateItemsBuilder: (TSInfoMessage) -> [TSInfoMessage.PersistableGroupUpdateItem]?
|
|
) -> Bool {
|
|
switch message.messageType {
|
|
case .verificationStateChange: return false
|
|
case .profileUpdate: return false
|
|
case .phoneNumberChange: return false
|
|
case .recipientHidden: return false
|
|
case .threadMerge: return false
|
|
case .sessionSwitchover: return false
|
|
case .reportedSpam: return false
|
|
case .learnedProfileName: return false
|
|
case .acceptedMessageRequest: return false
|
|
case .typeGroupUpdate:
|
|
guard
|
|
let updates = groupUpdateItemsBuilder(message)
|
|
else {
|
|
return true
|
|
}
|
|
return updates.contains { $0.shouldAppearInInbox }
|
|
case .typeLocalUserEndedSession: return true
|
|
case .typeRemoteUserEndedSession: return true
|
|
case .userNotRegistered: return true
|
|
case .typeUnsupportedMessage: return true
|
|
case .typeGroupQuit: return true
|
|
case .typeDisappearingMessagesUpdate: return true
|
|
case .addToContactsOffer: return true
|
|
case .addUserToProfileWhitelistOffer: return true
|
|
case .addGroupToProfileWhitelistOffer: return true
|
|
case .unknownProtocolVersion: return true
|
|
case .userJoinedSignal: return true
|
|
case .syncedThread: return true
|
|
case .paymentsActivationRequest: return true
|
|
case .paymentsActivated: return true
|
|
case .blockedOtherUser: return true
|
|
case .blockedGroup: return true
|
|
case .unblockedOtherUser: return true
|
|
case .unblockedGroup: return true
|
|
}
|
|
}
|
|
}
|