import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import TelegramUIPreferences import ItemListUI import PresentationDataUtils import AccountContext import ItemListPeerItem import ItemListPeerActionItem private final class Arguments { let context: AccountContext let updateUseHints: (Bool) -> Void let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void let removePeer: (PeerId) -> Void let addPeer: () -> Void let openPeer: (PeerId) -> Void init(context: AccountContext, updateUseHints: @escaping (Bool) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, addPeer: @escaping () -> Void, openPeer: @escaping (PeerId) -> Void) { self.context = context self.updateUseHints = updateUseHints self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions self.removePeer = removePeer self.addPeer = addPeer self.openPeer = openPeer } } private enum WidgetSetupScreenEntry: ItemListNodeEntry { enum Section: Int32 { case mode case peers } enum StableId: Hashable { case useHints case peersHeaderItem case add case peer(PeerId) } case useHints(String, Bool) case peersHeaderItem(String) case peerItem(Int32, PresentationDateTimeFormat, PresentationPersonNameOrder, SelectivePrivacyPeer, ItemListPeerItemEditing, Bool) case addItem(String, Bool) var section: ItemListSectionId { switch self { case .useHints: return Section.mode.rawValue case .peersHeaderItem, .peerItem: return Section.peers.rawValue case .addItem: return Section.peers.rawValue } } var stableId: StableId { switch self { case .useHints: return .useHints case .peersHeaderItem: return .peersHeaderItem case let .peerItem(_, _, _, peer, _, _): return .peer(peer.peer.id) case .addItem: return .add } } var sortIndex: Int32 { switch self { case .useHints: return 0 case .peersHeaderItem: return 1 case .addItem: return 2 case let .peerItem(index, _, _, _, _, _): return 10 + index } } static func ==(lhs: WidgetSetupScreenEntry, rhs: WidgetSetupScreenEntry) -> Bool { switch lhs { case let .useHints(text, value): if case .useHints(text, value) = rhs { return true } else { return false } case let .peersHeaderItem(text): if case .peersHeaderItem(text) = rhs { return true } else { return false } case let .peerItem(lhsIndex, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsEditing, lhsEnabled): if case let .peerItem(rhsIndex, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } if lhsDateTimeFormat != rhsDateTimeFormat { return false } if lhsNameOrder != rhsNameOrder { return false } if lhsPeer != rhsPeer { return false } if lhsEditing != rhsEditing { return false } if lhsEnabled != rhsEnabled { return false } return true } else { return false } case let .addItem(lhsText, lhsEditing): if case let .addItem(rhsText, rhsEditing) = rhs, lhsText == rhsText, lhsEditing == rhsEditing { return true } else { return false } } } static func <(lhs: WidgetSetupScreenEntry, rhs: WidgetSetupScreenEntry) -> Bool { return lhs.sortIndex < rhs.sortIndex } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! Arguments switch self { case let .useHints(text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in arguments.updateUseHints(value) }) case let .peersHeaderItem(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) case let .peerItem(_, dateTimeFormat, nameOrder, peer, editing, enabled): var text: ItemListPeerItemText = .none if let group = peer.peer as? TelegramGroup { text = .text(presentationData.strings.Conversation_StatusMembers(Int32(group.participantCount))) } else if let channel = peer.peer as? TelegramChannel { if let participantCount = peer.participantCount { text = .text(presentationData.strings.Conversation_StatusMembers(Int32(participantCount))) } else { switch channel.info { case .group: text = .text(presentationData.strings.Group_Status) case .broadcast: text = .text(presentationData.strings.Channel_Status) } } } return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, context: arguments.context, peer: peer.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: { arguments.openPeer(peer.peer.id) }, setPeerIdWithRevealedOptions: { previousId, id in arguments.setPeerIdWithRevealedOptions(previousId, id) }, removePeer: { peerId in arguments.removePeer(peerId) }) case let .addItem(text, editing): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, editing: editing, action: { arguments.addPeer() }) } } } private struct WidgetSetupScreenControllerState: Equatable { var editing: Bool = false var peerIdWithRevealedOptions: PeerId? = nil } private func selectivePrivacyPeersControllerEntries(presentationData: PresentationData, state: WidgetSetupScreenControllerState, useHints: Bool, peers: [SelectivePrivacyPeer]) -> [WidgetSetupScreenEntry] { var entries: [WidgetSetupScreenEntry] = [] entries.append(.useHints("Show Recent Chats", useHints)) if !useHints { entries.append(.peersHeaderItem(presentationData.strings.Privacy_ChatsTitle)) entries.append(.addItem(presentationData.strings.Privacy_AddNewPeer, state.editing)) var index: Int32 = 0 for peer in peers { entries.append(.peerItem(index, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer, ItemListPeerItemEditing(editable: true, editing: state.editing, canBeReordered: state.editing, revealed: peer.peer.id == state.peerIdWithRevealedOptions), true)) index += 1 } } return entries } public func widgetSetupScreen(context: AccountContext) -> ViewController { let statePromise = ValuePromise(WidgetSetupScreenControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: WidgetSetupScreenControllerState()) let updateState: ((WidgetSetupScreenControllerState) -> WidgetSetupScreenControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var dismissImpl: (() -> Void)? var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? let actionsDisposable = DisposableSet() let addPeerDisposable = MetaDisposable() actionsDisposable.add(addPeerDisposable) let arguments = Arguments(context: context, updateUseHints: { value in let _ = (updateWidgetSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings settings.useHints = value return settings }) |> deliverOnMainQueue).start() }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in updateState { state in var state = state if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) { state.peerIdWithRevealedOptions = peerId } return state } }, removePeer: { memberId in }, addPeer: { let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .peerSelection(searchChatList: true, searchGroups: true, searchChannels: false), options: [])) addPeerDisposable.set((controller.result |> take(1) |> deliverOnMainQueue).start(next: { [weak controller] result in var peerIds: [ContactListPeerId] = [] if case let .result(peerIdsValue, _) = result { peerIds = peerIdsValue } let _ = (updateWidgetSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings for peerId in peerIds { switch peerId { case let .peer(peerId): settings.peers.removeAll(where: { $0 == peerId }) settings.peers.insert(peerId, at: 0) case .deviceContact: break } } return settings }) |> deliverOnMainQueue).start(completed: { controller?.dismiss() }) })) presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, openPeer: { peerId in let _ = (context.account.postbox.transaction { transaction -> Peer? in return transaction.getPeer(peerId) } |> deliverOnMainQueue).start(next: { peer in guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false) else { return } pushControllerImpl?(controller) }) }) var previousPeers: [SelectivePrivacyPeer]? var previousState: WidgetSetupScreenControllerState? struct InputData { var settings: WidgetSettings var peers: [SelectivePrivacyPeer] } let preferencesKey: PostboxViewKey = .preferences(keys: Set([ ApplicationSpecificPreferencesKeys.widgetSettings ])) let inputData: Signal = context.account.postbox.combinedView(keys: [ preferencesKey ]) |> mapToSignal { views -> Signal in let widgetSettings: WidgetSettings if let view = views.views[preferencesKey] as? PreferencesView, let value = view.values[ApplicationSpecificPreferencesKeys.widgetSettings] as? WidgetSettings { widgetSettings = value } else { widgetSettings = .default } return context.account.postbox.transaction { transaction -> InputData in return InputData( settings: widgetSettings, peers: widgetSettings.peers.compactMap { peerId -> SelectivePrivacyPeer? in guard let peer = transaction.getPeer(peerId) else { return nil } return SelectivePrivacyPeer(peer: peer, participantCount: nil) } ) } } let signal = combineLatest(context.sharedContext.presentationData, statePromise.get(), inputData) |> deliverOnMainQueue |> map { presentationData, state, inputData -> (ItemListControllerState, (ItemListNodeState, Any)) in var rightNavigationButton: ItemListNavigationButton? if !inputData.peers.isEmpty { if state.editing { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { updateState { state in var state = state state.editing = false return state } }) } else { rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { state in var state = state state.editing = true return state } }) } } let previous = previousPeers previousPeers = inputData.peers let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text("Widget"), leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) var animated = true if let previous = previous { if previous.count <= inputData.peers.count { if Set(previous.map { $0.peer.id }) == Set(inputData.peers.map { $0.peer.id }) && previous.map({ $0.peer.id }) != inputData.peers.map({ $0.peer.id }) { } else { animated = false } } } else { animated = false } if let previousState = previousState { if previousState.editing != state.editing { animated = true } } else { animated = false } previousState = state let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: selectivePrivacyPeersControllerEntries(presentationData: presentationData, state: state, useHints: inputData.settings.useHints, peers: inputData.peers), style: .blocks, emptyStateItem: nil, animateChanges: animated) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = ItemListController(context: context, state: signal) dismissImpl = { [weak controller] in if let controller = controller, let navigationController = controller.navigationController as? NavigationController { navigationController.filterController(controller, animated: true) } } presentControllerImpl = { [weak controller] c, p in if let controller = controller { controller.present(c, in: .window(.root), with: p) } } pushControllerImpl = { [weak controller] c in if let navigationController = controller?.navigationController as? NavigationController { navigationController.pushViewController(c) } } controller.setReorderEntry({ (fromIndex: Int, toIndex: Int, entries: [WidgetSetupScreenEntry]) -> Signal in let fromEntry = entries[fromIndex] guard case let .peerItem(_, _, _, fromPeer, _, _) = fromEntry else { return .single(false) } var referencePeerId: PeerId? var beforeAll = false var afterAll = false if toIndex < entries.count { switch entries[toIndex] { case let .peerItem(_, _, _, peer, _, _): referencePeerId = peer.peer.id default: if entries[toIndex] < fromEntry { beforeAll = true } else { afterAll = true } } } else { afterAll = true } return context.account.postbox.transaction { transaction -> Bool in var updatedOrder = false updateWidgetSettingsInteractively(transaction: transaction, { settings in let initialPeers = settings.peers var settings = settings if let index = settings.peers.firstIndex(of: fromPeer.peer.id) { settings.peers.remove(at: index) } if let referencePeerId = referencePeerId { var inserted = false for i in 0 ..< settings.peers.count { if settings.peers[i] == referencePeerId { if fromIndex < toIndex { settings.peers.insert(fromPeer.peer.id, at: i + 1) } else { settings.peers.insert(fromPeer.peer.id, at: i) } inserted = true break } } if !inserted { settings.peers.append(fromPeer.peer.id) } } else if beforeAll { settings.peers.insert(fromPeer.peer.id, at: 0) } else if afterAll { settings.peers.append(fromPeer.peer.id) } if initialPeers != settings.peers { updatedOrder = true } return settings }) return updatedOrder } }) return controller }