// // RelationshipViewModel.swift // // // Created by MainasuK on 2022-4-14. // import UIKit import Combine import MastodonAsset import MastodonLocalization import CoreDataStack public enum RelationshipAction: Int, CaseIterable { case isMyself case followingBy case blockingBy case none // set hide from UI case follow case request case pending case following case muting case blocked case blocking case suspended case edit case editing case updating public var option: RelationshipActionOptionSet { return RelationshipActionOptionSet(rawValue: 1 << rawValue) } } // construct option set on the enum for safe iterator public struct RelationshipActionOptionSet: OptionSet { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } public static let isMyself = RelationshipAction.isMyself.option public static let followingBy = RelationshipAction.followingBy.option public static let blockingBy = RelationshipAction.blockingBy.option public static let none = RelationshipAction.none.option public static let follow = RelationshipAction.follow.option public static let request = RelationshipAction.request.option public static let pending = RelationshipAction.pending.option public static let following = RelationshipAction.following.option public static let muting = RelationshipAction.muting.option public static let blocked = RelationshipAction.blocked.option public static let blocking = RelationshipAction.blocking.option public static let suspended = RelationshipAction.suspended.option public static let edit = RelationshipAction.edit.option public static let editing = RelationshipAction.editing.option public static let updating = RelationshipAction.updating.option public static let editOptions: RelationshipActionOptionSet = [.edit, .editing, .updating] public func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? { let set = subtracting(except) for action in RelationshipAction.allCases.reversed() where set.contains(action.option) { return action } return nil } public var title: String { guard let highPriorityAction = self.highPriorityAction(except: []) else { assertionFailure() return " " } switch highPriorityAction { case .isMyself: return "" case .followingBy: return " " case .blockingBy: return " " case .none: return " " case .follow: return L10n.Common.Controls.Friendship.follow case .request: return L10n.Common.Controls.Friendship.request case .pending: return L10n.Common.Controls.Friendship.pending case .following: return L10n.Common.Controls.Friendship.following case .muting: return L10n.Common.Controls.Friendship.muted case .blocked: return L10n.Common.Controls.Friendship.follow // blocked by user case .blocking: return L10n.Common.Controls.Friendship.blocked case .suspended: return L10n.Common.Controls.Friendship.follow case .edit: return L10n.Common.Controls.Friendship.editInfo case .editing: return L10n.Common.Controls.Actions.done case .updating: return " " } } } public final class RelationshipViewModel { var disposeBag = Set() public var userObserver: AnyCancellable? public var meObserver: AnyCancellable? // input @Published public var user: MastodonUser? @Published public var me: MastodonUser? public let relationshipUpdatePublisher = CurrentValueSubject(Void()) // needs initial event // output @Published public var isMyself = false @Published public var optionSet: RelationshipActionOptionSet? @Published public var isFollowing = false @Published public var isFollowingBy = false @Published public var isMuting = false @Published public var isBlocking = false @Published public var isBlockingBy = false public init() { Publishers.CombineLatest3( $user, $me, relationshipUpdatePublisher ) .sink { [weak self] user, me, _ in guard let self = self else { return } self.update(user: user, me: me) guard let user = user, let me = me else { self.userObserver = nil self.meObserver = nil return } // do not modify object to prevent infinity loop self.userObserver = RelationshipViewModel.createObjectChangePublisher(user: user) .sink { [weak self] _ in guard let self = self else { return } self.relationshipUpdatePublisher.send() } self.meObserver = RelationshipViewModel.createObjectChangePublisher(user: me) .sink { [weak self] _ in guard let self = self else { return } self.relationshipUpdatePublisher.send() } } .store(in: &disposeBag) } } extension RelationshipViewModel { public static func createObjectChangePublisher(user: MastodonUser) -> AnyPublisher { return ManagedObjectObserver .observe(object: user) .map { _ in Void() } .catch { error in return Just(Void()) } .eraseToAnyPublisher() } } extension RelationshipViewModel { private func update(user: MastodonUser?, me: MastodonUser?) { guard let user = user, let me = me else { reset() return } let optionSet = RelationshipViewModel.optionSet(user: user, me: me) self.isMyself = optionSet.contains(.isMyself) self.isFollowingBy = optionSet.contains(.followingBy) self.isFollowing = optionSet.contains(.following) self.isMuting = optionSet.contains(.muting) self.isBlockingBy = optionSet.contains(.blockingBy) self.isBlocking = optionSet.contains(.blocking) self.optionSet = optionSet } private func reset() { isMyself = false isFollowingBy = false isFollowing = false isMuting = false isBlockingBy = false isBlocking = false optionSet = nil } } extension RelationshipViewModel { public static func optionSet(user: MastodonUser, me: MastodonUser) -> RelationshipActionOptionSet { let isMyself = user.id == me.id && user.domain == me.domain guard !isMyself else { return [.isMyself] } let isProtected = user.locked let isFollowingBy = me.followingBy.contains(user) let isFollowing = user.followingBy.contains(me) let isPending = user.followRequestedBy.contains(me) let isMuting = user.mutingBy.contains(me) let isBlockingBy = me.blockingBy.contains(user) let isBlocking = user.blockingBy.contains(me) var optionSet: RelationshipActionOptionSet = [.follow] if isMyself { optionSet.insert(.isMyself) } if isProtected { optionSet.insert(.request) } if isFollowingBy { optionSet.insert(.followingBy) } if isFollowing { optionSet.insert(.following) } if isPending { optionSet.insert(.pending) } if isMuting { optionSet.insert(.muting) } if isBlockingBy { optionSet.insert(.blockingBy) } if isBlocking { optionSet.insert(.blocking) } return optionSet } }