import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import ItemListUI import PresentationDataUtils import AccountContext import ContactsPeerItem import SearchUI import SolidRoundedButtonNode func localizedOldChannelDate(peer: InactiveChannel, strings: PresentationStrings) -> String { let timestamp = peer.lastActivityDate let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) var t: time_t = time_t(TimeInterval(timestamp)) var timeinfo: tm = tm() localtime_r(&t, &timeinfo) var now: time_t = time_t(nowTimestamp) var timeinfoNow: tm = tm() localtime_r(&now, &timeinfoNow) var string: String if timeinfoNow.tm_year == timeinfo.tm_year && timeinfoNow.tm_mon == timeinfo.tm_mon { //weeks let dif = Int(roundf(Float(timeinfoNow.tm_mday - timeinfo.tm_mday) / 7)) string = strings.OldChannels_InactiveWeek(Int32(dif)) } else if timeinfoNow.tm_year == timeinfo.tm_year { //month let dif = Int(timeinfoNow.tm_mon - timeinfo.tm_mon) string = strings.OldChannels_InactiveMonth(Int32(dif)) } else { //year var dif = Int(timeinfoNow.tm_year - timeinfo.tm_year) if Int(timeinfoNow.tm_mon - timeinfo.tm_mon) > 6 { dif += 1 } string = strings.OldChannels_InactiveYear(Int32(dif)) } if let channel = peer.peer as? TelegramChannel, case .group = channel.info { if let participantsCount = peer.participantsCount, participantsCount != 0 { string = strings.OldChannels_GroupFormat(participantsCount) + string } else { string = strings.OldChannels_GroupEmptyFormat + string } } else { string = strings.OldChannels_ChannelFormat + string } return string } private final class OldChannelsItemArguments { let context: AccountContext let togglePeer: (PeerId, Bool) -> Void init( context: AccountContext, togglePeer: @escaping (PeerId, Bool) -> Void ) { self.context = context self.togglePeer = togglePeer } } private enum OldChannelsSection: Int32 { case info case peers } private enum OldChannelsEntryId: Hashable { case info case peersHeader case peer(PeerId) } private enum OldChannelsEntry: ItemListNodeEntry { case info(String, String) case peersHeader(String) case peer(Int, InactiveChannel, Bool) var section: ItemListSectionId { switch self { case .info: return OldChannelsSection.info.rawValue case .peersHeader, .peer: return OldChannelsSection.peers.rawValue } } var stableId: OldChannelsEntryId { switch self { case .info: return .info case .peersHeader: return .peersHeader case let .peer(_, peer, _): return .peer(peer.peer.id) } } static func ==(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool { switch lhs { case let .info(title, text): if case .info(title, text) = rhs { return true } else { return false } case let .peersHeader(title): if case .peersHeader(title) = rhs { return true } else { return false } case let .peer(lhsIndex, lhsPeer, lhsSelected): if case let .peer(rhsIndex, rhsPeer, rhsSelected) = rhs { if lhsIndex != rhsIndex { return false } if lhsPeer != rhsPeer { return false } if lhsSelected != rhsSelected { return false } return true } else { return false } } } static func <(lhs: OldChannelsEntry, rhs: OldChannelsEntry) -> Bool { switch lhs { case .info: if case .info = rhs { return false } else { return true } case .peersHeader: switch rhs { case .info, .peersHeader: return false case .peer: return true } case let .peer(lhsIndex, _, _): switch rhs { case .info, .peersHeader: return false case let .peer(rhsIndex, _, _): return lhsIndex < rhsIndex } } } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! OldChannelsItemArguments switch self { case let .info(title, text): return ItemListInfoItem(presentationData: presentationData, title: title, text: .plain(text), style: .blocks, sectionId: self.section, closeAction: nil) case let .peersHeader(title): return ItemListSectionHeaderItem(presentationData: presentationData, text: title, sectionId: self.section) case let .peer(_, peer, selected): return ContactsPeerItem(presentationData: presentationData, style: .blocks, sectionId: self.section, sortOrder: .firstLast, displayOrder: .firstLast, context: arguments.context, peerMode: .peer, peer: .peer(peer: peer.peer, chatPeer: peer.peer), status: .custom(localizedOldChannelDate(peer: peer, strings: presentationData.strings)), badge: nil, enabled: true, selection: ContactsPeerItemSelection.selectable(selected: selected), editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), options: [], actionIcon: .none, index: nil, header: nil, action: { _ in arguments.togglePeer(peer.peer.id, true) }, setPeerIdWithRevealedOptions: nil, deletePeer: nil, itemHighlighting: nil, contextAction: nil) } } } private struct OldChannelsState: Equatable { var selectedPeers: Set = Set() var isSearching: Bool = false } private func oldChannelsEntries(presentationData: PresentationData, state: OldChannelsState, peers: [InactiveChannel]?, intent: OldChannelsControllerIntent) -> [OldChannelsEntry] { var entries: [OldChannelsEntry] = [] let noticeText: String switch intent { case .join: noticeText = presentationData.strings.OldChannels_NoticeText case .create: noticeText = presentationData.strings.OldChannels_NoticeCreateText case .upgrade: noticeText = presentationData.strings.OldChannels_NoticeUpgradeText } entries.append(.info(presentationData.strings.OldChannels_NoticeTitle, noticeText)) if let peers = peers, !peers.isEmpty { entries.append(.peersHeader(presentationData.strings.OldChannels_ChannelsHeader)) for peer in peers { entries.append(.peer(entries.count, peer, state.selectedPeers.contains(peer.peer.id))) } } return entries } private final class OldChannelsActionPanelNode: ASDisplayNode { private let separatorNode: ASDisplayNode let buttonNode: SolidRoundedButtonNode init(presentationData: ItemListPresentationData, leaveAction: @escaping () -> Void) { self.separatorNode = ASDisplayNode() self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor self.buttonNode = SolidRoundedButtonNode(title: "", icon: nil, theme: SolidRoundedButtonTheme(theme: presentationData.theme), height: 50.0, cornerRadius: 10.0, gloss: false) super.init() self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor self.addSubnode(self.separatorNode) self.addSubnode(self.buttonNode) self.buttonNode.pressed = { leaveAction() } } func updatePresentationData(_ presentationData: ItemListPresentationData) { self.separatorNode.backgroundColor = presentationData.theme.rootController.navigationBar.separatorColor self.backgroundColor = presentationData.theme.rootController.navigationBar.backgroundColor } func updateLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) -> CGFloat { let sideInset: CGFloat = 16.0 let verticalInset: CGFloat = 16.0 let buttonHeight: CGFloat = 50.0 let insets = layout.insets(options: [.input]) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: UIScreenPixel))) self.buttonNode.updateLayout(width: layout.size.width - sideInset * 2.0, transition: transition) transition.updateFrame(node: self.buttonNode, frame: CGRect(origin: CGPoint(x: sideInset, y: verticalInset), size: CGSize(width: layout.size.width, height: buttonHeight))) return buttonHeight + verticalInset * 2.0 + insets.bottom } } private final class OldChannelsControllerImpl: ItemListController { private let panelNode: OldChannelsActionPanelNode private var displayPanel: Bool = false private var validLayout: ContainerViewLayout? private var presentationData: ItemListPresentationData private var presentationDataDisposable: Disposable? var leaveAction: (() -> Void)? override init(presentationData: ItemListPresentationData, updatedPresentationData: Signal, state: Signal<(ItemListControllerState, (ItemListNodeState, ItemGenerationArguments)), NoError>, tabBarItem: Signal?) { self.presentationData = presentationData var leaveActionImpl: (() -> Void)? self.panelNode = OldChannelsActionPanelNode(presentationData: presentationData, leaveAction: { leaveActionImpl?() }) super.init(presentationData: presentationData, updatedPresentationData: updatedPresentationData, state: state, tabBarItem: tabBarItem) self.presentationDataDisposable = (updatedPresentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in guard let strongSelf = self else { return } strongSelf.presentationData = presentationData strongSelf.panelNode.updatePresentationData(presentationData) }) leaveActionImpl = { [weak self] in self?.leaveAction?() } } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.presentationDataDisposable?.dispose() } override var navigationBarRequiresEntireLayoutUpdate: Bool { return false } override func loadDisplayNode() { super.loadDisplayNode() self.displayNode.addSubnode(self.panelNode) } override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout let panelHeight = self.panelNode.updateLayout(layout, transition: transition) var additionalInsets = UIEdgeInsets() additionalInsets.bottom = max(layout.intrinsicInsets.bottom, panelHeight) self.additionalInsets = additionalInsets super.containerLayoutUpdated(layout, transition: transition) transition.updateFrame(node: self.panelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.displayPanel ? (layout.size.height - panelHeight) : layout.size.height), size: CGSize(width: layout.size.width, height: panelHeight)), beginWithCurrentState: true) } func updatePanelPeerCount(_ value: Int) { self.panelNode.buttonNode.title = self.presentationData.strings.OldChannels_Leave(Int32(value)) if self.displayPanel != (value != 0) { self.displayPanel = (value != 0) if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: .animated(duration: 0.3, curve: .spring)) } } } } public enum OldChannelsControllerIntent { case join case create case upgrade } public func oldChannelsController(context: AccountContext, intent: OldChannelsControllerIntent, completed: @escaping (Bool) -> Void = { _ in }) -> ViewController { let initialState = OldChannelsState() let statePromise = ValuePromise(initialState, ignoreRepeated: true) let stateValue = Atomic(value: initialState) let updateState: ((OldChannelsState) -> OldChannelsState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var updateSelectedPeersImpl: ((Int) -> Void)? var dismissImpl: (() -> Void)? var setDisplayNavigationBarImpl: ((Bool) -> Void)? var ensurePeerVisibleImpl: ((PeerId) -> Void)? let actionsDisposable = DisposableSet() let arguments = OldChannelsItemArguments( context: context, togglePeer: { peerId, ensureVisible in var selectedPeerCount = 0 var didSelect = false updateState { state in var state = state if state.selectedPeers.contains(peerId) { state.selectedPeers.remove(peerId) } else { state.selectedPeers.insert(peerId) didSelect = true } selectedPeerCount = state.selectedPeers.count return state } updateSelectedPeersImpl?(selectedPeerCount) if didSelect && ensureVisible { ensurePeerVisibleImpl?(peerId) } } ) let selectedPeerIds = statePromise.get() |> map { $0.selectedPeers } |> distinctUntilChanged let peersSignal: Signal<[InactiveChannel]?, NoError> = .single(nil) |> then( inactiveChannelList(network: context.account.network) |> map { peers -> [InactiveChannel]? in return peers.sorted(by: { lhs, rhs in return lhs.lastActivityDate < rhs.lastActivityDate }) } ) let peersPromise = Promise<[InactiveChannel]?>() peersPromise.set(peersSignal) var previousPeersWereEmpty = true let signal = combineLatest( queue: Queue.mainQueue(), context.sharedContext.presentationData, statePromise.get(), peersPromise.get() ) |> map { presentationData, state, peers -> (ItemListControllerState, (ItemListNodeState, Any)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.OldChannels_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) var searchItem: OldChannelsSearchItem? searchItem = OldChannelsSearchItem(context: context, theme: presentationData.theme, placeholder: presentationData.strings.Common_Search, activated: state.isSearching, updateActivated: { value in if !value { setDisplayNavigationBarImpl?(true) } updateState { state in var state = state state.isSearching = value return state } if value { setDisplayNavigationBarImpl?(false) } }, peers: peersPromise.get() |> map { $0 ?? [] }, selectedPeerIds: selectedPeerIds, togglePeer: { peerId in arguments.togglePeer(peerId, false) }) let peersAreEmpty = peers == nil let peersAreEmptyUpdated = previousPeersWereEmpty != peersAreEmpty previousPeersWereEmpty = peersAreEmpty var emptyStateItem: ItemListControllerEmptyStateItem? if peersAreEmpty { emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme) } let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: oldChannelsEntries(presentationData: presentationData, state: state, peers: peers, intent: intent), style: .blocks, emptyStateItem: emptyStateItem, searchItem: searchItem, initialScrollToItem: ListViewScrollToItem(index: 0, position: .top(-navigationBarSearchContentHeight), animated: false, curve: .Default(duration: 0.0), directionHint: .Up), crossfadeState: peersAreEmptyUpdated, animateChanges: false) return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() } let controller = OldChannelsControllerImpl(context: context, state: signal) controller.navigationPresentation = .modal updateSelectedPeersImpl = { [weak controller] value in controller?.updatePanelPeerCount(value) } controller.leaveAction = { let state = stateValue.with { $0 } let _ = (peersPromise.get() |> take(1) |> mapToSignal { peers in return context.account.postbox.transaction { transaction -> Void in if let peers = peers { for peer in peers { if state.selectedPeers.contains(peer.peer.id) { if transaction.getPeer(peer.peer.id) == nil { updatePeers(transaction: transaction, peers: [peer.peer], update: { _, updated in return updated }) } removePeerChat(account: context.account, transaction: transaction, mediaBox: context.account.postbox.mediaBox, peerId: peer.peer.id, reportChatSpam: false, deleteGloballyIfPossible: false) } } } } } |> deliverOnMainQueue).start(completed: { completed(true) dismissImpl?() }) } dismissImpl = { [weak controller] in controller?.dismiss() } setDisplayNavigationBarImpl = { [weak controller] display in controller?.setDisplayNavigationBar(display, transition: .animated(duration: 0.5, curve: .spring)) } ensurePeerVisibleImpl = { [weak controller] peerId in guard let controller = controller else { return } controller.forEachItemNode { itemNode in if let itemNode = itemNode as? ContactsPeerItemNode, let peer = itemNode.chatPeer, peer.id == peerId { controller.ensureItemNodeVisible(itemNode, curve: .Spring(duration: 0.3)) } } } return controller }