TM-SGNL-iOS/SignalServiceKit/Messages/Reactions/OWSReaction.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

261 lines
9.9 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public import GRDB
public import LibSignalClient
@objc(OWSReaction) // Named explicitly to preserve NSKeyedUnarchiving compatability
public final class OWSReaction: NSObject, SDSCodableModel, Decodable, NSSecureCoding {
public static let databaseTableName = "model_OWSReaction"
public static var recordType: UInt { SDSRecordType.reaction.rawValue }
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case id
case recordType
case uniqueId
case emoji
case reactorE164
case reactorUUID
// See context on ``sortOrder`` below.
case sortOrder = "receivedAtTimestamp"
case sentAtTimestamp
case uniqueMessageId
case read
}
public var id: Int64?
@objc
public let uniqueId: String
@objc
public let uniqueMessageId: String
public let emoji: String
public let reactorAci: Aci?
public let reactorPhoneNumber: String?
public let sentAtTimestamp: UInt64
/// Higher values mean more recent, i.e. should be rendered first in reaction lists.
///
/// Historically, this would be the received timestamp (on the local device). However, the timestamp
/// information is unavailable when restoring from a backup; that just has a sortOrder which makes
/// no guarantees except that it can be used for sorting reactions for display.
/// Locally, we can continue to write timestamps into here since they have the same sorting property
/// as any other sortOrder value, and put those timestamps into our own backups as the "sortOrder".
/// One edge case: restore a message with reactions and get a new reaction. Now we mix backup sortOrder
/// values with local timestamp values. This should be fine; nobody will put any value into sortOrder
/// larger than the current timestamp, anyway.
public let sortOrder: UInt64
public private(set) var read: Bool
public var reactor: SignalServiceAddress {
SignalServiceAddress.legacyAddress(serviceId: reactorAci, phoneNumber: reactorPhoneNumber)
}
/// Note that we initialize with a receivedAtTimestamp, but should make no assumptions
/// that the sortOrder is always a timestamp at read time. Backups use sortOrders that
/// may not be timestamps.
public convenience init(
uniqueMessageId: String,
emoji: String,
reactor: Aci,
sentAtTimestamp: UInt64,
receivedAtTimestamp: UInt64
) {
self.init(
uniqueMessageId: uniqueMessageId,
emoji: emoji,
reactorAci: reactor,
reactorPhoneNumber: nil,
sentAtTimestamp: sentAtTimestamp,
sortOrder: receivedAtTimestamp
)
}
private init(
uniqueMessageId: String,
emoji: String,
reactorAci: Aci?,
reactorPhoneNumber: String?,
sentAtTimestamp: UInt64,
sortOrder: UInt64
) {
self.uniqueId = UUID().uuidString
self.uniqueMessageId = uniqueMessageId
self.emoji = emoji
self.reactorAci = reactorAci
self.reactorPhoneNumber = reactorPhoneNumber
self.sentAtTimestamp = sentAtTimestamp
self.sortOrder = sortOrder
self.read = false
}
public static func fromRestoredBackup(
uniqueMessageId: String,
emoji: String,
reactorAci: Aci,
sentAtTimestamp: UInt64,
sortOrder: UInt64
) -> Self {
return Self.init(
uniqueMessageId: uniqueMessageId,
emoji: emoji,
reactorAci: reactorAci,
reactorPhoneNumber: nil,
sentAtTimestamp: sentAtTimestamp,
sortOrder: sortOrder
)
}
public static func fromRestoredBackup(
uniqueMessageId: String,
emoji: String,
reactorE164: E164,
sentAtTimestamp: UInt64,
sortOrder: UInt64
) -> OWSReaction {
return .init(
uniqueMessageId: uniqueMessageId,
emoji: emoji,
reactorAci: nil,
reactorPhoneNumber: reactorE164.stringValue,
sentAtTimestamp: sentAtTimestamp,
sortOrder: sortOrder
)
}
public func markAsRead(transaction: SDSAnyWriteTransaction) {
anyUpdate(transaction: transaction) { reaction in
reaction.read = true
}
SSKEnvironment.shared.notificationPresenterRef.cancelNotifications(reactionId: uniqueId)
}
@objc
public static func anyEnumerateObjc(
transaction: SDSAnyReadTransaction,
batched: Bool,
block: @escaping (OWSReaction, UnsafeMutablePointer<ObjCBool>) -> Void
) {
let batchingPreference: BatchingPreference = batched ? .batched() : .unbatched
anyEnumerate(transaction: transaction, batchingPreference: batchingPreference, block: block)
}
// MARK: - Codable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let decodedRecordType = try container.decode(Int.self, forKey: .recordType)
owsAssertDebug(decodedRecordType == Self.recordType, "Unexpectedly decoded record with wrong type.")
id = try container.decodeIfPresent(RowId.self, forKey: .id)
uniqueId = try container.decode(String.self, forKey: .uniqueId)
uniqueMessageId = try container.decode(String.self, forKey: .uniqueMessageId)
emoji = try container.decode(String.self, forKey: .emoji)
// If we have an ACI, ignore the phone number.
reactorAci = try container.decodeIfPresent(UUID.self, forKey: .reactorUUID).map { Aci(fromUUID: $0) }
reactorPhoneNumber = (reactorAci != nil) ? nil : try container.decodeIfPresent(String.self, forKey: .reactorE164)
sentAtTimestamp = try container.decode(UInt64.self, forKey: .sentAtTimestamp)
sortOrder = try container.decode(UInt64.self, forKey: .sortOrder)
read = try container.decode(Bool.self, forKey: .read)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try id.map { try container.encode($0, forKey: .id) }
try container.encode(Self.recordType, forKey: .recordType)
try container.encode(uniqueId, forKey: .uniqueId)
try container.encode(uniqueMessageId, forKey: .uniqueMessageId)
try container.encode(emoji, forKey: .emoji)
// If we have an ACI, ignore the phone number.
if let reactorAci {
try container.encode(reactorAci.rawUUID, forKey: .reactorUUID)
} else if let reactorPhoneNumber {
try container.encode(reactorPhoneNumber, forKey: .reactorE164)
}
try container.encode(sentAtTimestamp, forKey: .sentAtTimestamp)
try container.encode(sortOrder, forKey: .sortOrder)
try container.encode(read, forKey: .read)
}
// MARK: - NSSecureCoding
public static var supportsSecureCoding: Bool { true }
public func encode(with coder: NSCoder) {
id.map { coder.encode(NSNumber(value: $0), forKey: CodingKeys.id.rawValue) }
coder.encode(uniqueId, forKey: CodingKeys.uniqueId.rawValue)
coder.encode(uniqueMessageId, forKey: CodingKeys.uniqueMessageId.rawValue)
coder.encode(emoji, forKey: CodingKeys.emoji.rawValue)
// If we have an ACI, ignore the phone number.
if let reactorAci {
coder.encode(reactorAci.rawUUID, forKey: CodingKeys.reactorUUID.rawValue)
} else if let reactorPhoneNumber {
coder.encode(reactorPhoneNumber, forKey: CodingKeys.reactorE164.rawValue)
}
coder.encode(NSNumber(value: sentAtTimestamp), forKey: CodingKeys.sentAtTimestamp.rawValue)
coder.encode(NSNumber(value: sortOrder), forKey: CodingKeys.sortOrder.rawValue)
coder.encode(NSNumber(value: read), forKey: CodingKeys.read.rawValue)
}
public required init?(coder: NSCoder) {
self.id = coder.decodeObject(of: NSNumber.self, forKey: CodingKeys.id.rawValue)?.int64Value
guard let uniqueId = coder.decodeObject(of: NSString.self, forKey: CodingKeys.uniqueId.rawValue) as String? else {
owsFailDebug("Missing uniqueId")
return nil
}
self.uniqueId = uniqueId
guard let uniqueMessageId = coder.decodeObject(of: NSString.self, forKey: CodingKeys.uniqueMessageId.rawValue) as String? else {
owsFailDebug("Missing uniqueMessageId")
return nil
}
self.uniqueMessageId = uniqueMessageId
guard let emoji = coder.decodeObject(of: NSString.self, forKey: CodingKeys.emoji.rawValue) as String? else {
owsFailDebug("Missing emoji")
return nil
}
self.emoji = emoji
// If we have an ACI, ignore the phone number.
let reactorAciUuid = coder.decodeObject(of: NSUUID.self, forKey: CodingKeys.reactorUUID.rawValue)
reactorAci = reactorAciUuid.map { Aci(fromUUID: $0 as UUID) }
reactorPhoneNumber = (reactorAci != nil) ? nil : {
coder.decodeObject(of: NSString.self, forKey: CodingKeys.reactorE164.rawValue) as String?
}()
guard let sentAtTimestamp = coder.decodeObject(of: NSNumber.self, forKey: CodingKeys.sentAtTimestamp.rawValue)?.uint64Value else {
owsFailDebug("Missing sentAtTimestamp")
return nil
}
self.sentAtTimestamp = sentAtTimestamp
guard let sortOrder = coder.decodeObject(of: NSNumber.self, forKey: CodingKeys.sortOrder.rawValue)?.uint64Value else {
owsFailDebug("Missing sortOrder")
return nil
}
self.sortOrder = sortOrder
guard let read = coder.decodeObject(of: NSNumber.self, forKey: CodingKeys.read.rawValue)?.boolValue else {
owsFailDebug("Missing read")
return nil
}
self.read = read
}
}