// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation @objc(OWSBlockListCacheDelegate) public protocol BlockListCacheDelegate: class { func blockListCacheDidUpdate(_ blocklistCache: BlockListCache) } /// A performant cache for which contacts/groups are blocked. /// /// The source of truth for which contacts and groups are blocked is the `blockingManager`, but because /// those accessors are made to be thread safe, they can be slow in tight loops, e.g. when rendering table /// view cells. /// /// Typically you'll want to create a Cache, update it to the latest state while simultaneously being informed /// of any future changes to block list state. /// /// class SomeViewController: BlockListCacheDelegate { /// let blockListCache = BlockListCache() /// func viewDidLoad() { /// super.viewDidLoad() /// blockListCache.startObservingAndSyncState(delegate: self) /// self.updateAnyViewsWhichDepondOnBlockListCache() /// } /// /// func blockListCacheDidUpdate(_ blocklistCache: BlockListCache) { /// self.updateAnyViewsWhichDepondOnBlockListCache() /// } /// /// ... /// } /// @objc(OWSBlockListCache) public class BlockListCache: NSObject { private var blockedRecipientIds: Set = Set() private var blockedGroupIds: Set = Set() private let serialQueue: DispatchQueue = DispatchQueue(label: "BlockListCache") weak var delegate: BlockListCacheDelegate? private var blockingManager: OWSBlockingManager { return OWSBlockingManager.shared() } /// Generally something which wants to use this cache wants to do 3 things /// 1. get the cache on the latest state /// 2. update the cache whenever the blockingManager's state changes /// 3. be notified when the cache updates /// This method does all three. @objc public func startObservingAndSyncState(delegate: BlockListCacheDelegate) { self.delegate = delegate NotificationCenter.default.addObserver(self, selector: #selector(blockListDidChange), name: NSNotification.Name(rawValue: kNSNotificationName_BlockListDidChange), object: nil) updateWithoutNotifyingDelegate() } // MARK: - @objc func blockListDidChange() { self.update() } @objc(isRecipientIdBlocked:) public func isBlocked(recipientId: String) -> Bool { return serialQueue.sync { blockedRecipientIds.contains(recipientId) } } @objc(isGroupIdBlocked:) public func isBlocked(groupId: Data) -> Bool { return serialQueue.sync { blockedGroupIds.contains(groupId) } } @objc(isThreadBlocked:) public func isBlocked(thread: TSThread) -> Bool { switch thread { case let contactThread as TSContactThread: return serialQueue.sync { blockedRecipientIds.contains(contactThread.contactIdentifier()) } case let groupThread as TSGroupThread: return serialQueue.sync { blockedGroupIds.contains(groupThread.groupModel.groupId) } default: owsFailDebug("\(self.logTag) in \(#function) unexpected thread type: \(type(of: thread))") return false } } // MARK: - public func update() { updateWithoutNotifyingDelegate() DispatchQueue.main.async { self.delegate?.blockListCacheDidUpdate(self) } } private func updateWithoutNotifyingDelegate() { let blockedRecipientIds = Set(blockingManager.blockedPhoneNumbers()) let blockedGroupIds = Set(blockingManager.blockedGroupIds) update(blockedRecipientIds: blockedRecipientIds, blockedGroupIds: blockedGroupIds) } private func update(blockedRecipientIds: Set, blockedGroupIds: Set) { serialQueue.sync { self.blockedRecipientIds = blockedRecipientIds self.blockedGroupIds = blockedGroupIds } } }