Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Messages/Interactions/OutgoingDeleteMessage.swift
1 views
//
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
public import LibSignalClient

@objc(TSOutgoingDeleteMessage)
public final class OutgoingDeleteMessage: TransientOutgoingMessage {
    let messageTimestamp: UInt64
    let messageUniqueId: String?
    let isDeletingStoryMessage: Bool

    public init(
        thread: TSThread,
        message: TSOutgoingMessage,
        tx: DBReadTransaction,
    ) {
        owsAssertDebug(thread.uniqueId == message.uniqueThreadId)

        self.messageTimestamp = message.timestamp
        self.messageUniqueId = message.uniqueId
        self.isDeletingStoryMessage = false

        super.init(
            outgoingMessageWith: TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread),
            additionalRecipients: [],
            explicitRecipients: [],
            skippedRecipients: [],
            transaction: tx,
        )
    }

    override public class var supportsSecureCoding: Bool { true }

    override public func encode(with coder: NSCoder) {
        super.encode(with: coder)
        coder.encode(NSNumber(value: self.isDeletingStoryMessage), forKey: "isDeletingStoryMessage")
        coder.encode(NSNumber(value: self.messageTimestamp), forKey: "messageTimestamp")
        if let messageUniqueId {
            coder.encode(messageUniqueId, forKey: "messageUniqueId")
        }
    }

    public required init?(coder: NSCoder) {
        self.isDeletingStoryMessage = coder.decodeObject(of: NSNumber.self, forKey: "isDeletingStoryMessage")?.boolValue ?? false
        guard let messageTimestamp = coder.decodeObject(of: NSNumber.self, forKey: "messageTimestamp") else {
            return nil
        }
        self.messageTimestamp = messageTimestamp.uint64Value
        self.messageUniqueId = coder.decodeObject(of: NSString.self, forKey: "messageUniqueId") as String?
        super.init(coder: coder)
    }

    override public var hash: Int {
        var hasher = Hasher()
        hasher.combine(super.hash)
        hasher.combine(self.isDeletingStoryMessage)
        hasher.combine(self.messageTimestamp)
        hasher.combine(self.messageUniqueId)
        return hasher.finalize()
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? Self else { return false }
        guard super.isEqual(object) else { return false }
        guard self.isDeletingStoryMessage == object.isDeletingStoryMessage else { return false }
        guard self.messageTimestamp == object.messageTimestamp else { return false }
        guard self.messageUniqueId == object.messageUniqueId else { return false }
        return true
    }

    public init(
        thread: TSThread,
        storyMessage: StoryMessage,
        skippedRecipients: some Sequence<ServiceId>,
        tx: DBReadTransaction,
    ) {
        self.messageTimestamp = storyMessage.timestamp
        self.messageUniqueId = storyMessage.uniqueId
        self.isDeletingStoryMessage = true

        super.init(
            outgoingMessageWith: TSOutgoingMessageBuilder.outgoingMessageBuilder(thread: thread),
            additionalRecipients: [],
            explicitRecipients: [],
            skippedRecipients: skippedRecipients.map(ServiceIdObjC.wrapValue(_:)),
            transaction: tx,
        )
    }

    override public var isStorySend: Bool {
        return self.isDeletingStoryMessage
    }

    override public func dataMessageBuilder(with thread: TSThread, transaction: DBReadTransaction) -> SSKProtoDataMessageBuilder? {
        let deleteBuilder = SSKProtoDataMessageDelete.builder(targetSentTimestamp: self.messageTimestamp)
        let deleteProto: SSKProtoDataMessageDelete
        do {
            deleteProto = try deleteBuilder.build()
        } catch {
            owsFailDebug("could not build protobuf: \(error)")
            return nil
        }

        let builder = super.dataMessageBuilder(with: thread, transaction: transaction)
        builder?.setTimestamp(self.timestamp)
        builder?.setDelete(deleteProto)
        return builder
    }

    override public func anyUpdateOutgoingMessage(transaction: DBWriteTransaction, block: (TSOutgoingMessage) -> Void) {
        super.anyUpdateOutgoingMessage(transaction: transaction, block: block)

        // Some older outgoing delete messages didn't store the deleted message's
        // unique id. We want to mirror our sending state onto the original
        // message, so it shows up within the conversation.
        if let messageUniqueId {
            let deletedMessage = TSOutgoingMessage.fetchOutgoingMessageViaCache(uniqueId: messageUniqueId, transaction: transaction)
            deletedMessage?.updateWithRecipientAddressStates(self.recipientAddressStates, tx: transaction)
        }
    }

    override public var relatedUniqueIds: Set<String> {
        return super.relatedUniqueIds.union([self.messageUniqueId].compacted())
    }
}