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

public import Contacts

@objc(OWSContactAddress)
public final class OWSContactAddress: NSObject, NSSecureCoding, OWSContactField {
    public static var supportsSecureCoding: Bool { true }

    public init?(coder: NSCoder) {
        self.type = (coder.decodeObject(of: NSNumber.self, forKey: "addressType")?.intValue).flatMap(`Type`.init(rawValue:)) ?? .home
        self.city = coder.decodeObject(of: NSString.self, forKey: "city") as String?
        self.country = coder.decodeObject(of: NSString.self, forKey: "country") as String?
        self.label = coder.decodeObject(of: NSString.self, forKey: "label") as String?
        self.neighborhood = coder.decodeObject(of: NSString.self, forKey: "neighborhood") as String?
        self.pobox = coder.decodeObject(of: NSString.self, forKey: "pobox") as String?
        self.postcode = coder.decodeObject(of: NSString.self, forKey: "postcode") as String?
        self.region = coder.decodeObject(of: NSString.self, forKey: "region") as String?
        self.street = coder.decodeObject(of: NSString.self, forKey: "street") as String?
    }

    public func encode(with coder: NSCoder) {
        coder.encode(NSNumber(value: self.type.rawValue), forKey: "addressType")
        if let city {
            coder.encode(city, forKey: "city")
        }
        if let country {
            coder.encode(country, forKey: "country")
        }
        if let label {
            coder.encode(label, forKey: "label")
        }
        if let neighborhood {
            coder.encode(neighborhood, forKey: "neighborhood")
        }
        if let pobox {
            coder.encode(pobox, forKey: "pobox")
        }
        if let postcode {
            coder.encode(postcode, forKey: "postcode")
        }
        if let region {
            coder.encode(region, forKey: "region")
        }
        if let street {
            coder.encode(street, forKey: "street")
        }
    }

    override public var hash: Int {
        var hasher = Hasher()
        hasher.combine(type)
        hasher.combine(city)
        hasher.combine(country)
        hasher.combine(label)
        hasher.combine(neighborhood)
        hasher.combine(pobox)
        hasher.combine(postcode)
        hasher.combine(region)
        hasher.combine(street)
        return hasher.finalize()
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? Self else { return false }
        guard Swift.type(of: self) == Swift.type(of: object) else { return false }
        guard self.type == object.type else { return false }
        guard self.city == object.city else { return false }
        guard self.country == object.country else { return false }
        guard self.label == object.label else { return false }
        guard self.neighborhood == object.neighborhood else { return false }
        guard self.pobox == object.pobox else { return false }
        guard self.postcode == object.postcode else { return false }
        guard self.region == object.region else { return false }
        guard self.street == object.street else { return false }
        return true
    }

    @objc(OWSContactAddressType)
    public enum `Type`: Int, CustomStringConvertible {
        case home = 1
        case work
        case custom

        public var description: String {
            switch self {
            case .home: return "Home"
            case .work: return "Work"
            case .custom: return "Custom"
            }
        }
    }

    public let type: `Type`

    // Applies in the Type.custom case.
    public let label: String?
    public let street: String?
    public let pobox: String?
    public let neighborhood: String?
    public let city: String?
    public let region: String?
    public let postcode: String?
    public let country: String?

    public init(
        type: `Type`,
        label: String? = nil,
        street: String? = nil,
        pobox: String? = nil,
        neighborhood: String? = nil,
        city: String? = nil,
        region: String? = nil,
        postcode: String? = nil,
        country: String? = nil,
    ) {
        self.type = type
        self.label = label
        self.street = street
        self.pobox = pobox
        self.neighborhood = neighborhood
        self.city = city
        self.region = region
        self.postcode = postcode
        self.country = country
        super.init()
    }

    // MARK: OWSContactField

    public var isValid: Bool {
        let fields: [String?] = [street, pobox, neighborhood, city, region, postcode, country]
        for field in fields {
            if field?.strippedOrNil != nil {
                return true
            }
        }
        Logger.warn("Invalid address: empty")
        return false
    }

    public var localizedLabel: String {
        switch type {
        case .home:
            return CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: CNLabelHome)

        case .work:
            return CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: CNLabelWork)

        case .custom:
            guard let label = label?.strippedOrNil else {
                return OWSLocalizedString("CONTACT_ADDRESS", comment: "Label for a contact's postal address.")
            }
            return label
        }
    }
}

// MARK: CNContact Conversion

extension OWSContactAddress {

    public convenience init(cnLabeledValue: CNLabeledValue<CNPostalAddress>) {
        let customLabel: String?
        let type: `Type`
        switch cnLabeledValue.label {
        case CNLabelHome:
            type = .home
            customLabel = nil

        case CNLabelWork:
            type = .work
            customLabel = nil

        default:
            type = .custom
            customLabel = SystemContact.localizedString(
                forCNLabel: cnLabeledValue.label,
                labeledValueType: CNLabeledValue<CNPostalAddress>.self,
            )
        }
        let cnPostalAddress = cnLabeledValue.value
        self.init(
            type: type,
            label: customLabel,
            street: cnPostalAddress.street,
            pobox: nil,
            neighborhood: nil,
            city: cnPostalAddress.city,
            region: cnPostalAddress.state,
            postcode: cnPostalAddress.postalCode,
            country: cnPostalAddress.isoCountryCode,
        )
    }

    public func cnLabeledValue() -> CNLabeledValue<CNPostalAddress>? {
        guard isValid else { return nil }

        let cnPostalAddress = CNMutablePostalAddress()
        cnPostalAddress.street = street ?? ""
        // TODO: Is this the correct mapping?
        // cnPostalAddress.subLocality = address.neighborhood;
        cnPostalAddress.city = city ?? ""
        // TODO: Is this the correct mapping?
        // cnPostalAddress.subAdministrativeArea = address.region;
        cnPostalAddress.state = region ?? ""
        cnPostalAddress.postalCode = postcode ?? ""
        // TODO: Should we be using 2-letter codes, 3-letter codes or names?
        if let country {
            cnPostalAddress.isoCountryCode = country
            cnPostalAddress.country = PhoneNumberUtil.countryName(fromCountryCode: country)
        }

        let cnLabel: String? = {
            switch type {
            case .home:
                return CNLabelHome
            case .work:
                return CNLabelWork
            case .custom:
                return label
            }
        }()
        return CNLabeledValue(label: cnLabel, value: cnPostalAddress)
    }
}

// MARK: - Protobuf

extension OWSContactAddress {

    public convenience init?(proto: SSKProtoDataMessageContactPostalAddress) {
        let type: `Type`
        if proto.hasType {
            switch proto.unwrappedType {
            case .home:
                type = .home

            case .work:
                type = .work

            default:
                type = .custom
            }
        } else {
            type = .custom
        }

        let label: String?
        if proto.hasLabel {
            label = proto.label?.strippedOrNil
        } else {
            label = nil
        }

        self.init(
            type: type,
            label: label,
            street: proto.hasStreet ? proto.street?.strippedOrNil : nil,
            pobox: proto.hasPobox ? proto.pobox?.strippedOrNil : nil,
            neighborhood: proto.hasNeighborhood ? proto.neighborhood?.strippedOrNil : nil,
            city: proto.hasCity ? proto.city?.strippedOrNil : nil,
            region: proto.hasRegion ? proto.region?.strippedOrNil : nil,
            postcode: proto.hasPostcode ? proto.postcode?.strippedOrNil : nil,
            country: proto.hasCountry ? proto.country?.strippedOrNil : nil,
        )

        guard isValid else { return nil }
    }

    public func proto() -> SSKProtoDataMessageContactPostalAddress? {
        guard isValid else { return nil }

        let builder = SSKProtoDataMessageContactPostalAddress.builder()

        if let label = label?.strippedOrNil {
            builder.setLabel(label)
        }

        let type: SSKProtoDataMessageContactPostalAddressType = {
            switch self.type {
            case .home: return .home
            case .work: return .work
            case .custom: return .custom
            }
        }()
        builder.setType(type)

        if let value = street?.strippedOrNil {
            builder.setStreet(value)
        }
        if let value = pobox?.strippedOrNil {
            builder.setPobox(value)
        }
        if let value = neighborhood?.strippedOrNil {
            builder.setNeighborhood(value)
        }
        if let value = city?.strippedOrNil {
            builder.setCity(value)
        }
        if let value = region?.strippedOrNil {
            builder.setRegion(value)
        }
        if let value = postcode?.strippedOrNil {
            builder.setPostcode(value)
        }
        if let value = country?.strippedOrNil {
            builder.setCountry(value)
        }

        return builder.buildInfallibly()
    }
}