diff --git a/submodules/ChatListUI/BUILD b/submodules/ChatListUI/BUILD index 13a6fd7410..89931e38b5 100644 --- a/submodules/ChatListUI/BUILD +++ b/submodules/ChatListUI/BUILD @@ -84,7 +84,8 @@ swift_library( "//submodules/TelegramUI/Components/NotificationPeerExceptionController", "//submodules/AnimationUI:AnimationUI", "//submodules/PeerInfoUI", - "//submodules/TelegramUI/Components/ChatListHeaderComponent:ChatListHeaderComponent", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", + "//submodules/TelegramUI/Components/ChatListTitleView", ], visibility = [ "//visibility:public", diff --git a/submodules/ChatListUI/Sources/ChatContextMenus.swift b/submodules/ChatListUI/Sources/ChatContextMenus.swift index 85133accc4..9cb7a38ad3 100644 --- a/submodules/ChatListUI/Sources/ChatContextMenus.swift +++ b/submodules/ChatListUI/Sources/ChatContextMenus.swift @@ -490,7 +490,7 @@ func chatContextMenuItems(context: AccountContext, peerId: PeerId, promoInfo: Ch } } -func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ChatListControllerImpl?, joined: Bool) -> Signal<[ContextMenuItem], NoError> { +func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: Int64, isPinned: Bool?, isClosed: Bool?, chatListController: ChatListControllerImpl?, joined: Bool, canSelect: Bool) -> Signal<[ContextMenuItem], NoError> { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) let strings = presentationData.strings @@ -763,11 +763,13 @@ func chatForumTopicMenuItems(context: AccountContext, peerId: PeerId, threadId: } } - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in - f(.default) - chatListController?.selectPeerThread(peerId: peerId, threadId: threadId) - }))) + if canSelect { + items.append(.separator) + items.append(.action(ContextMenuActionItem(text: strings.ChatList_Context_Select, textColor: .primary, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in + f(.default) + chatListController?.selectPeerThread(peerId: peerId, threadId: threadId) + }))) + } return .single(items) } diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 9ccbc00ca3..cf8fd86738 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -40,6 +40,9 @@ import ForumCreateTopicScreen import AnimationUI import ChatTitleView import PeerInfoUI +import ComponentDisplayAdapters +import ChatListHeaderComponent +import ChatListTitleView private func fixListNodeScrolling(_ listNode: ListView, searchNode: NavigationBarSearchContentNode) -> Bool { if listNode.scroller.isDragging { @@ -112,161 +115,6 @@ private final class ContextControllerContentSourceImpl: ContextControllerContent } } -public final class MoreHeaderButton: HighlightableButtonNode { - public enum Content { - case image(UIImage?) - case more(UIImage?) - } - - public let referenceNode: ContextReferenceContentNode - public let containerNode: ContextControllerSourceNode - private let iconNode: ASImageNode - private var animationNode: AnimationNode? - - public var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? - - private var color: UIColor - - public init(color: UIColor) { - self.color = color - - self.referenceNode = ContextReferenceContentNode() - self.containerNode = ContextControllerSourceNode() - self.containerNode.animateScale = false - self.iconNode = ASImageNode() - self.iconNode.displaysAsynchronously = false - self.iconNode.displayWithoutProcessing = true - self.iconNode.contentMode = .scaleToFill - - super.init() - - self.containerNode.addSubnode(self.referenceNode) - self.referenceNode.addSubnode(self.iconNode) - self.addSubnode(self.containerNode) - - self.containerNode.shouldBegin = { [weak self] location in - guard let strongSelf = self, let _ = strongSelf.contextAction else { - return false - } - return true - } - self.containerNode.activated = { [weak self] gesture, _ in - guard let strongSelf = self else { - return - } - strongSelf.contextAction?(strongSelf.containerNode, gesture) - } - - self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0)) - self.referenceNode.frame = self.containerNode.bounds - - self.iconNode.image = MoreHeaderButton.optionsCircleImage(color: color) - if let image = self.iconNode.image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) - } - - self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0) - } - - private var content: Content? - public func setContent(_ content: Content, animated: Bool = false) { - if case .more = content, self.animationNode == nil { - let iconColor = self.color - let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor, - "Point 3.Group 1.Fill 1": iconColor, - "Point 1.Group 1.Fill 1": iconColor], scale: 1.0) - let animationSize = CGSize(width: 22.0, height: 22.0) - animationNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - animationSize.width) / 2.0), y: floor((self.containerNode.bounds.height - animationSize.height) / 2.0)), size: animationSize) - self.addSubnode(animationNode) - self.animationNode = animationNode - } - if animated { - if let snapshotView = self.referenceNode.view.snapshotContentTree() { - snapshotView.frame = self.referenceNode.frame - self.view.addSubview(snapshotView) - - snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in - snapshotView?.removeFromSuperview() - }) - snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false) - - self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3) - - self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3) - } - - switch content { - case let .image(image): - if let image = image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) - } - - self.iconNode.image = image - self.iconNode.isHidden = false - self.animationNode?.isHidden = true - case let .more(image): - if let image = image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) - } - - self.iconNode.image = image - self.iconNode.isHidden = false - self.animationNode?.isHidden = false - } - } else { - self.content = content - switch content { - case let .image(image): - if let image = image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) - } - - self.iconNode.image = image - self.iconNode.isHidden = false - self.animationNode?.isHidden = true - case let .more(image): - if let image = image { - self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) - } - - self.iconNode.image = image - self.iconNode.isHidden = false - self.animationNode?.isHidden = false - } - } - } - - override public func didLoad() { - super.didLoad() - self.view.isOpaque = false - } - - override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { - return CGSize(width: 22.0, height: 44.0) - } - - public func onLayout() { - } - - public func play() { - self.animationNode?.playOnce() - } - - public static func optionsCircleImage(color: UIColor) -> UIImage? { - return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in - context.clear(CGRect(origin: CGPoint(), size: size)) - - context.setStrokeColor(color.cgColor) - let lineWidth: CGFloat = 1.3 - context.setLineWidth(lineWidth) - - context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth)) - }) - } -} - public class ChatListControllerImpl: TelegramBaseController, ChatListController { private var validLayout: ContainerViewLayout? @@ -286,15 +134,14 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return super.displayNode as! ChatListControllerNode } - private var titleView: ChatListTitleView? - private var chatTitleView: ChatTitleView? - private let infoReady = Promise() + private let headerContentView = ComponentView() - private var proxyUnavailableTooltipController: TooltipController? - private var didShowProxyUnavailableTooltipController = false + private var primaryContext: ChatListLocationContext? + private let primaryInfoReady = Promise() + + private var pendingSecondaryContext: ChatListLocationContext? + private var secondaryContext: ChatListLocationContext? - private var titleDisposable: Disposable? - private var chatTitleDisposable: Disposable? private var badgeDisposable: Disposable? private var badgeIconDisposable: Disposable? @@ -337,17 +184,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private weak var emojiStatusSelectionController: ViewController? - private let moreBarButton: MoreHeaderButton - private let moreBarButtonItem: UIBarButtonItem private var forumChannelTracker: ForumChannelTopics? - private var backNavigationItem: UIBarButtonItem? - private let selectAddMemberDisposable = MetaDisposable() private let addMemberDisposable = MetaDisposable() private let joinForumDisposable = MetaDisposable() private let actionDisposables = DisposableSet() + private var plainTitle: String = "" + public override func updateNavigationCustomData(_ data: Any?, progress: CGFloat, transition: ContainedViewLayoutTransition) { if self.isNodeLoaded { self.chatListDisplayNode.containerNode.updateSelectedChatLocation(data: data as? ChatLocation, progress: progress, transition: transition) @@ -371,31 +216,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let groupCallPanelSource: GroupCallPanelSource switch self.location { case .chatList: - self.titleView = ChatListTitleView( - context: context, - theme: self.presentationData.theme, - strings: self.presentationData.strings, - animationCache: self.animationCache, - animationRenderer: self.animationRenderer - ) groupCallPanelSource = .all case let .forum(peerId): - self.chatTitleView = ChatTitleView(context: self.context, theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, animationCache: self.context.animationCache, animationRenderer: self.context.animationRenderer) groupCallPanelSource = .peer(peerId) } - self.moreBarButton = MoreHeaderButton(color: self.presentationData.theme.rootController.navigationBar.buttonColor) - self.moreBarButton.isUserInteractionEnabled = true - self.moreBarButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: self.presentationData.theme.rootController.navigationBar.buttonColor))) - - self.moreBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)! - self.tabContainerNode = ChatListFilterTabContainerNode() super.init(context: context, navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData), mediaAccessoryPanelVisibility: .always, locationBroadcastPanelSource: .summary, groupCallPanelSource: groupCallPanelSource) - self.backNavigationItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.navigationBackPressed)) - self.tabBarItemContextActionType = .always self.automaticallyControlPresentationContextLayout = false @@ -409,21 +238,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { title = self.presentationData.strings.ChatList_ArchivedChatsTitle } + self.plainTitle = title case let .forum(peerId): title = "" self.forumChannelTracker = ForumChannelTopics(account: self.context.account, peerId: peerId) - self.moreBarButton.contextAction = { [weak self] sourceNode, gesture in - guard let self = self else { - return - } - guard case let .forum(peerId) = self.location else { - return - } - ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: self, isViewingAsTopics: true, sourceView: sourceNode.view, gesture: gesture) - } - self.moreBarButton.addTarget(self, action: #selector(self.moreButtonPressed), forControlEvents: .touchUpInside) - self.navigationBar?.userInfo = PeerInfoNavigationSourceTag(peerId: peerId) self.navigationBar?.allowsCustomTransition = { [weak self] in guard let strongSelf = self else { @@ -432,109 +251,38 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if strongSelf.navigationBar?.userInfo == nil { return false } + + //TODO:loc + if "".isEmpty { + return false + } return true } } switch self.location { case .chatList: - if let titleView = self.titleView { + /*if let titleView = self.titleView { titleView.title = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil) - self.navigationItem.titleView = titleView - - titleView.openStatusSetup = { [weak self] sourceView in - self?.openStatusSetup(sourceView: sourceView) - } - } - self.infoReady.set(.single(true)) - case let .forum(peerId): - if let chatTitleView = self.chatTitleView { - self.navigationItem.titleView = chatTitleView - - chatTitleView.pressed = { [weak self] in - guard let self = self else { - return - } - let _ = (self.context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) - ) - |> deliverOnMainQueue).start(next: { [weak self] peer in - guard let self = self, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { - return - } - (self.navigationController as? NavigationController)?.pushViewController(controller) - }) - } - self.chatTitleView?.longPressed = { [weak self] in - guard let self else { - return - } - self.activateSearch() - } - - let peerView = Promise() - peerView.set(context.account.viewTracker.peerView(peerId)) - - var onlineMemberCount: Signal = .single(nil) - - let recentOnlineSignal: Signal = peerView.get() - |> map { view -> Bool? in - if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { - if case .broadcast = peer.info { - return nil - } else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 { - return true - } else { - return false - } - } else { - return false - } - } - |> distinctUntilChanged - |> mapToSignal { isLarge -> Signal in - if let isLarge = isLarge { - if isLarge { - return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) - } else { - return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) - |> map(Optional.init) - } - } else { - return .single(nil) - } - } - onlineMemberCount = recentOnlineSignal - - self.chatTitleDisposable = (combineLatest(queue: Queue.mainQueue(), - peerView.get(), - onlineMemberCount, - self.chatListDisplayNode.containerNode.currentItemState - ) - |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, stateAndFilterId in - guard let strongSelf = self, let chatTitleView = strongSelf.chatTitleView else { - return - } - - if stateAndFilterId.state.editing && stateAndFilterId.state.selectedThreadIds.count > 0 { - chatTitleView.titleContent = .custom(strongSelf.presentationData.strings.ChatList_SelectedTopics(Int32(stateAndFilterId.state.selectedThreadIds.count)), nil, false) - } else { - chatTitleView.titleContent = .peer(peerView: peerView, customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil) - } - - strongSelf.infoReady.set(.single(true)) - - if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, !channel.flags.contains(.isForum) { - if let navigationController = strongSelf.navigationController as? NavigationController { - let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) - navigationController.replaceController(strongSelf, with: chatController, animated: true) - } - } - }) - } + //self.navigationItem.titleView = titleView + }*/ + //self.primaryInfoReady.set(.single(true)) + break + case .forum: + break } + let primaryContext = ChatListLocationContext( + context: context, + location: self.location, + parentController: self, + hideNetworkActivityStatus: self.hideNetworkActivityStatus, + chatListDisplayNode: self.chatListDisplayNode, + isReorderingTabs: self.isReorderingTabsValue.get() + ) + self.primaryContext = primaryContext + self.primaryInfoReady.set(primaryContext.ready.get()) + if !previewing { switch self.location { case let .chatList(groupId): @@ -555,24 +303,34 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.tabBarItem.animationOffset = CGPoint(x: 0.0, y: UIScreenPixel) } - let leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - leftBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Edit - self.navigationItem.leftBarButtonItem = leftBarButtonItem + self.primaryContext?.leftButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Edit, isBold: false), + pressed: { [weak self] _ in + self?.editPressed() + } + ))) - let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.VoiceOver_Navigation_Compose - self.navigationItem.rightBarButtonItem = rightBarButtonItem - let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) - backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back - self.navigationItem.backBarButtonItem = backBarButtonItem + self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "compose", component: AnyComponent(NavigationButtonComponent( + content: .icon(imageName: "Chat List/ComposeIcon"), + pressed: { [weak self] _ in + self?.composePressed() + } + ))) + + //let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.DialogList_Title, style: .plain, target: nil, action: nil) + //backBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Back + //self.navigationItem.backBarButtonItem = backBarButtonItem } else { switch self.location { case .chatList: - let rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.Common_Edit - self.navigationItem.rightBarButtonItem = rightBarButtonItem + self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Edit, isBold: false), + pressed: { [weak self] _ in + self?.editPressed() + } + ))) case .forum: - self.navigationItem.rightBarButtonItem = self.moreBarButtonItem + break } let backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) @@ -628,200 +386,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings]) - |> map { sharedData -> (Bool, Bool) in - if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) { - return (!settings.servers.isEmpty, settings.enabled) - } else { - return (false, false) - } - } - |> distinctUntilChanged(isEqual: { lhs, rhs in - return lhs == rhs - }) - - let passcode = context.sharedContext.accountManager.accessChallengeData() - |> map { view -> (Bool, Bool) in - let data = view.data - return (data.isLockable, false) - } - - let peerStatus: Signal - switch self.location { - case .chatList(.root): - peerStatus = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) - |> map { peer -> NetworkStatusTitle.Status? in - guard case let .user(user) = peer else { - return nil - } - if let emojiStatus = user.emojiStatus { - return .emoji(emojiStatus) - } else if user.isPremium { - return .premium - } else { - return nil - } - } - |> distinctUntilChanged - default: - peerStatus = .single(nil) - } - - let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil) - if !self.hideNetworkActivityStatus { - self.titleDisposable = combineLatest(queue: .mainQueue(), - context.account.networkState, - hasProxy, - passcode, - self.chatListDisplayNode.containerNode.currentItemState, - self.isReorderingTabsValue.get(), - peerStatus - ).start(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs, peerStatus in - if let strongSelf = self { - let defaultTitle: String - switch strongSelf.location { - case let .chatList(groupId): - if groupId == .root { - defaultTitle = strongSelf.presentationData.strings.DialogList_Title - } else { - defaultTitle = strongSelf.presentationData.strings.ChatList_ArchivedChatsTitle - } - case .forum: - defaultTitle = "" - } - let previousEditingAndNetworkState = previousEditingAndNetworkStateValue.swap((stateAndFilterId.state.editing, networkState)) - if stateAndFilterId.state.editing { - if case .chatList(.root) = strongSelf.location { - strongSelf.navigationItem.setRightBarButton(nil, animated: true) - } - let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? strongSelf.presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle - - var animated = false - if let (previousEditing, previousNetworkState) = previousEditingAndNetworkState { - if previousEditing != stateAndFilterId.state.editing, previousNetworkState == networkState, case .online = networkState { - animated = true - } - } - strongSelf.titleView?.setTitle(NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus), animated: animated) - } else if isReorderingTabs { - if case .chatList(.root) = strongSelf.location { - strongSelf.navigationItem.setRightBarButton(nil, animated: true) - } - let leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) - strongSelf.navigationItem.setLeftBarButton(leftBarButtonItem, animated: true) - - let (_, connectsViaProxy) = proxy - switch networkState { - case .waitingForNetwork: - strongSelf.titleView?.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) - case let .connecting(proxy): - var text = strongSelf.presentationData.strings.State_Connecting - if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { - text = strongSelf.presentationData.strings.State_ConnectingToProxy - } - strongSelf.titleView?.title = NetworkStatusTitle(text: text, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) - case .updating: - strongSelf.titleView?.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) - case .online: - strongSelf.titleView?.title = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) - } - } else { - var isRoot = false - if case .chatList(.root) = strongSelf.location { - isRoot = true - - if isReorderingTabs { - strongSelf.navigationItem.setRightBarButton(nil, animated: true) - } else { - let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(strongSelf.presentationData.theme), style: .plain, target: strongSelf, action: #selector(strongSelf.composePressed)) - rightBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.VoiceOver_Navigation_Compose - if strongSelf.navigationItem.rightBarButtonItem?.accessibilityLabel != rightBarButtonItem.accessibilityLabel { - strongSelf.navigationItem.setRightBarButton(rightBarButtonItem, animated: true) - } - } - - if isReorderingTabs { - let leftBarButtonItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: strongSelf, action: #selector(strongSelf.reorderingDonePressed)) - leftBarButtonItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Done - if strongSelf.navigationItem.leftBarButtonItem?.accessibilityLabel != leftBarButtonItem.accessibilityLabel { - strongSelf.navigationItem.setLeftBarButton(leftBarButtonItem, animated: true) - } - } else if strongSelf.chatListDisplayNode.inlineStackContainerTransitionFraction != 0.0 { - } else { - let editItem: UIBarButtonItem - if stateAndFilterId.state.editing { - editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(strongSelf.donePressed)) - editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Done - } else { - editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(strongSelf.editPressed)) - editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Edit - } - if strongSelf.navigationItem.leftBarButtonItem?.accessibilityLabel != editItem.accessibilityLabel { - strongSelf.navigationItem.setLeftBarButton(editItem, animated: true) - } - } - } else { - switch strongSelf.location { - case .chatList: - let editItem = UIBarButtonItem(title: strongSelf.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(strongSelf.editPressed)) - editItem.accessibilityLabel = strongSelf.presentationData.strings.Common_Edit - strongSelf.navigationItem.setRightBarButton(editItem, animated: true) - case .forum: - if strongSelf.navigationItem.rightBarButtonItem !== strongSelf.moreBarButtonItem { - strongSelf.navigationItem.setRightBarButton(strongSelf.moreBarButtonItem, animated: true) - } - } - } - - let (hasProxy, connectsViaProxy) = proxy - let (isPasscodeSet, isManuallyLocked) = passcode - var checkProxy = false - switch networkState { - case .waitingForNetwork: - strongSelf.titleView?.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) - case let .connecting(proxy): - var text = strongSelf.presentationData.strings.State_Connecting - if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { - text = strongSelf.presentationData.strings.State_ConnectingToProxy - } - if let proxy = proxy, proxy.hasConnectionIssues { - checkProxy = true - } - strongSelf.titleView?.title = NetworkStatusTitle(text: text, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) - case .updating: - strongSelf.titleView?.title = NetworkStatusTitle(text: strongSelf.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) - case .online: - strongSelf.titleView?.setTitle(NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus), animated: (previousEditingAndNetworkState?.0 ?? false) != stateAndFilterId.state.editing) - } - if case .chatList(.root) = location, checkProxy { - if strongSelf.proxyUnavailableTooltipController == nil && !strongSelf.didShowProxyUnavailableTooltipController && strongSelf.isNodeLoaded && strongSelf.displayNode.view.window != nil && strongSelf.navigationController?.topViewController === self { - strongSelf.didShowProxyUnavailableTooltipController = true - let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.Proxy_TooltipUnavailable), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, timeout: 60.0, dismissByTapOutside: true) - strongSelf.proxyUnavailableTooltipController = tooltipController - tooltipController.dismissed = { [weak tooltipController] _ in - if let strongSelf = self, let tooltipController = tooltipController, strongSelf.proxyUnavailableTooltipController === tooltipController { - strongSelf.proxyUnavailableTooltipController = nil - } - } - strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { - if let strongSelf = self, let titleView = strongSelf.titleView, let rect = titleView.proxyButtonFrame { - return (titleView, rect.insetBy(dx: 0.0, dy: -4.0)) - } - return nil - })) - } - } else { - strongSelf.didShowProxyUnavailableTooltipController = false - if let proxyUnavailableTooltipController = strongSelf.proxyUnavailableTooltipController { - strongSelf.proxyUnavailableTooltipController = nil - proxyUnavailableTooltipController.dismiss() - } - } - } - } - }) - } - self.badgeDisposable = (combineLatest(renderedTotalUnreadCount(accountManager: context.sharedContext.accountManager, engine: context.engine), self.presentationDataValue.get()) |> deliverOnMainQueue).start(next: { [weak self] count, presentationData in if let strongSelf = self { if count.0 == 0 { @@ -832,18 +396,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) - self.titleView?.toggleIsLocked = { [weak self] in - if let strongSelf = self { - strongSelf.context.sharedContext.appLockContext.lock() - } - } - - self.titleView?.openProxySettings = { [weak self] in - if let strongSelf = self { - (strongSelf.navigationController as? NavigationController)?.pushViewController(context.sharedContext.makeProxySettingsController(context: context)) - } - } - self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { @@ -1178,8 +730,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController deinit { self.openMessageFromSearchDisposable.dispose() - self.titleDisposable?.dispose() - self.chatTitleDisposable?.dispose() self.badgeDisposable?.dispose() self.badgeIconDisposable?.dispose() self.passcodeLockTooltipDisposable.dispose() @@ -1197,12 +747,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.actionDisposables.dispose() } + func findTitleView() -> ChatListTitleView? { + guard let componentView = self.headerContentView.view as? ChatListHeaderComponent.View else { + return nil + } + return componentView.findTitleView() + } + private func openStatusSetup(sourceView: UIView) { self.emojiStatusSelectionController?.dismiss() var selectedItems = Set() var topStatusTitle = self.presentationData.strings.PeerStatusSetup_NoTimerTitle var currentSelection: Int64? - if let peerStatus = self.titleView?.title.peerStatus, case let .emoji(emojiStatus) = peerStatus { + if let peerStatus = self.findTitleView()?.title.peerStatus, case let .emoji(emojiStatus) = peerStatus { selectedItems.insert(MediaId(namespace: Namespaces.Media.CloudFile, id: emojiStatus.fileId)) currentSelection = emojiStatus.fileId @@ -1266,35 +823,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.searchContentNode?.updateThemeAndPlaceholder(theme: self.presentationData.theme, placeholder: placeholder, compactPlaceholder: compactPlaceholder) - let editing = self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing - let editItem: UIBarButtonItem - if editing { - editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) - editItem.accessibilityLabel = self.presentationData.strings.Common_Done + /*let editing = self.chatListDisplayNode.containerNode.currentItemNode.currentState.editing + if case .chatList(.root) = self.location { + self.primaryContext?.leftButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Edit, isBold: false), + pressed: { [weak self] in + self?.editPressed() + } + ))) + self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "compose", component: AnyComponent(NavigationButtonComponent( + content: .icon(imageName: "Chat List/Compose Icon"), + pressed: { [weak self] in + self?.composePressed() + } + ))) } else { - switch self.location { - case .chatList: - editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - editItem.accessibilityLabel = self.presentationData.strings.Common_Edit - case .forum: - editItem = self.moreBarButtonItem - } - } - if self.chatListDisplayNode.inlineStackContainerTransitionFraction != 0.0 { - self.backNavigationItem?.title = self.presentationData.strings.Common_Back - } else if case .chatList(.root) = self.location { - self.navigationItem.leftBarButtonItem = editItem - let rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationComposeIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.composePressed)) - rightBarButtonItem.accessibilityLabel = self.presentationData.strings.VoiceOver_Navigation_Compose - self.navigationItem.rightBarButtonItem = rightBarButtonItem - } else { - self.navigationItem.rightBarButtonItem = editItem - } + self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Edit, isBold: false), + pressed: { [weak self] in + self?.editPressed() + } + ))) + }*/ - self.titleView?.theme = self.presentationData.theme - self.titleView?.strings = self.presentationData.strings - - self.chatTitleView?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, hasEmbeddedTitleContent: false) + self.primaryContext?.updatePresentationData(presentationData: self.presentationData) self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) @@ -1306,6 +858,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if self.isNodeLoaded { self.chatListDisplayNode.updatePresentationData(self.presentationData) } + + self.requestUpdateHeaderContent(transition: .immediate) } override public func loadDisplayNode() { @@ -1395,15 +949,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if case let .channel(channel) = peer, channel.flags.contains(.isForum), threadId == nil { strongSelf.chatListDisplayNode.clearHighlightAnimated(true) - if strongSelf.context.sharedContext.immediateExperimentalUISettings.inlineForums { - strongSelf.chatListDisplayNode.setInlineChatList(location: .forum(peerId: channel.id)) - - if strongSelf.navigationItem.leftBarButtonItem !== strongSelf.backNavigationItem { - strongSelf.navigationItem.setLeftBarButton(strongSelf.backNavigationItem, animated: true) - } - } else { - strongSelf.context.sharedContext.navigateToForumChannel(context: strongSelf.context, peerId: channel.id, navigationController: navigationController) - } + strongSelf.setInlineChatList(location: .forum(peerId: channel.id)) } else { if let threadId = threadId { let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: peer.id, threadId: threadId, messageId: nil, navigationController: navigationController, activateInput: nil, keepStack: .never).start() @@ -1692,7 +1238,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: nil, isClosed: nil, chatListController: strongSelf, joined: joined, canSelect: strongSelf.chatListDisplayNode.inlineStackContainerNode == nil) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } else { let chatListController = ChatListControllerImpl(context: strongSelf.context, location: .forum(peerId: channel.id), controlsHistoryPreload: false, hideNetworkActivityStatus: true, previewing: true, enableDebugActions: false) @@ -1728,7 +1274,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController chatController.canReadHistory.set(false) source = .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: node, navigationController: strongSelf.navigationController as? NavigationController)) - let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) + let contextController = ContextController(account: strongSelf.context.account, presentationData: strongSelf.presentationData, source: source, items: chatForumTopicMenuItems(context: strongSelf.context, peerId: peer.peerId, threadId: threadId, isPinned: isPinned, isClosed: threadInfo?.isClosed, chatListController: strongSelf, joined: joined, canSelect: strongSelf.chatListDisplayNode.inlineStackContainerNode == nil) |> map { ContextController.Items(content: .list($0)) }, gesture: gesture) strongSelf.presentInGlobalOverlay(contextController) } } @@ -2126,7 +1672,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } else { self.ready.set(combineLatest([ self.chatListDisplayNode.containerNode.ready, - self.infoReady.get() + self.primaryInfoReady.get() ]) |> map { values -> Bool in return !values.contains(where: { !$0 }) @@ -2204,7 +1750,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }) #endif - if let lockViewFrame = self.titleView?.lockViewFrame, !self.didShowPasscodeLockTooltipController { + if let lockViewFrame = self.findTitleView()?.lockViewFrame, !self.didShowPasscodeLockTooltipController { self.passcodeLockTooltipDisposable.set(combineLatest(queue: .mainQueue(), ApplicationSpecificNotice.getPasscodeLockTips(accountManager: self.context.sharedContext.accountManager), self.context.sharedContext.accountManager.accessChallengeData() |> take(1)).start(next: { [weak self] tooltipValue, passcodeView in if let strongSelf = self { if !tooltipValue { @@ -2214,7 +1760,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController let tooltipController = TooltipController(content: .text(strongSelf.presentationData.strings.DialogList_PasscodeLockHelp), baseFontSize: strongSelf.presentationData.listsFontSize.baseDisplaySize, dismissByTapOutside: true) strongSelf.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in - if let strongSelf = self, let titleView = strongSelf.titleView { + if let strongSelf = self, let titleView = strongSelf.findTitleView() { return (titleView, lockViewFrame.offsetBy(dx: 4.0, dy: 14.0)) } return nil @@ -2482,14 +2028,109 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true) } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - self.navigationBar?.secondaryContentNodeDisplayFraction = 1.0 - self.chatListDisplayNode.inlineStackContainerTransitionFraction + func requestUpdateHeaderContent(transition: ContainedViewLayoutTransition) { + if let validLayout = self.validLayout { + self.updateHeaderContent(layout: validLayout, transition: transition) + } + } + + private func updateHeaderContent(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + var primaryContent: ChatListHeaderComponent.Content? + if let primaryContext = self.primaryContext { + var backTitle: String? + if let previousItem = self.navigationBar?.previousItem { + switch previousItem { + case let .item(item): + backTitle = item.title ?? self.presentationData.strings.Common_Back + case .close: + backTitle = self.presentationData.strings.Common_Close + } + } + primaryContent = ChatListHeaderComponent.Content( + title: self.plainTitle, + titleComponent: primaryContext.chatTitleComponent.flatMap { AnyComponent($0) }, + chatListTitle: primaryContext.chatListTitle, + leftButton: primaryContext.leftButton, + rightButtons: primaryContext.rightButtons, + backTitle: backTitle, + backPressed: backTitle != nil ? { [weak self] in + guard let self else { + return + } + self.navigationBackPressed() + } : nil + ) + } + var secondaryContent: ChatListHeaderComponent.Content? + if let secondaryContext = self.secondaryContext { + secondaryContent = ChatListHeaderComponent.Content( + title: self.plainTitle, + titleComponent: secondaryContext.chatTitleComponent.flatMap { AnyComponent($0) }, + chatListTitle: secondaryContext.chatListTitle, + leftButton: secondaryContext.leftButton, + rightButtons: secondaryContext.rightButtons, + backTitle: nil, + backPressed: { [weak self] in + guard let self else { + return + } + self.setInlineChatList(location: nil) + } + ) + } + + let _ = self.headerContentView.update( + transition: Transition(transition), + component: AnyComponent(ChatListHeaderComponent( + sideInset: layout.safeInsets.left + 16.0, + primaryContent: primaryContent, + secondaryContent: secondaryContent, + secondaryTransition: self.chatListDisplayNode.inlineStackContainerTransitionFraction, + networkStatus: nil, + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + openStatusSetup: { [weak self] sourceView in + guard let self else { + return + } + self.openStatusSetup(sourceView: sourceView) + }, + toggleIsLocked: { [weak self] in + guard let self else { + return + } + self.context.sharedContext.appLockContext.lock() + } + )), + environment: {}, + containerSize: CGSize(width: layout.size.width, height: 44.0) + ) + if let componentView = self.headerContentView.view as? NavigationBarHeaderView { + if self.navigationBar?.customHeaderContentView !== componentView { + self.navigationBar?.customHeaderContentView = componentView + } + } + } + + override public func updateNavigationBarLayout(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + if self.chatListDisplayNode.searchDisplayController?.contentNode != nil { + self.navigationBar?.secondaryContentNodeDisplayFraction = 1.0 + } else { + self.navigationBar?.secondaryContentNodeDisplayFraction = 1.0 - self.chatListDisplayNode.inlineStackContainerTransitionFraction + } + + self.updateHeaderContent(layout: layout, transition: transition) + + super.updateNavigationBarLayout(layout, transition: transition) if let inlineStackContainerNode = self.chatListDisplayNode.inlineStackContainerNode { let _ = inlineStackContainerNode } else { } - + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) let wasInVoiceOver = self.validLayout?.inVoiceOver ?? false @@ -2526,16 +2167,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController super.navigationStackConfigurationUpdated(next: next) } - @objc private func editPressed() { - let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) - editItem.accessibilityLabel = self.presentationData.strings.Common_Done + @objc fileprivate func editPressed() { if case .chatList(.root) = self.location { - self.navigationItem.setLeftBarButton(editItem, animated: true) + self.primaryContext?.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.donePressed() + } + ))) (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.details, transition: .animated(duration: 0.5, curve: .spring)) } else { - self.navigationItem.setRightBarButton(editItem, animated: true) + self.primaryContext?.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.donePressed() + } + ))) (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(.master, transition: .animated(duration: 0.5, curve: .spring)) } + + self.requestUpdateHeaderContent(transition: .animated(duration: 0.3, curve: .spring)) + self.searchContentNode?.setIsEnabled(false, animated: true) self.chatListDisplayNode.didBeginSelectingChatsWhileEditing = false @@ -2551,7 +2203,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - @objc private func donePressed() { + @objc fileprivate func donePressed() { self.reorderingDonePressed() (self.navigationController as? NavigationController)?.updateMasterDetailsBlackout(nil, transition: .animated(duration: 0.4, curve: .spring)) @@ -2571,7 +2223,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - @objc private func reorderingDonePressed() { + @objc fileprivate func reorderingDonePressed() { + if !self.chatListDisplayNode.isReorderingFilters { + return + } + var reorderedFilterIdsValue: [Int32]? if let reorderedFilterIds = self.tabContainerNode.reorderedFilterIds { reorderedFilterIdsValue = reorderedFilterIds @@ -2614,20 +2270,35 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - @objc private func moreButtonPressed() { - self.moreBarButton.play() - self.moreBarButton.contextAction?(self.moreBarButton.containerNode, nil) + func setInlineChatList(location: ChatListControllerLocation?) { + if let location { + let pendingSecondaryContext = ChatListLocationContext( + context: self.context, + location: location, + parentController: self, + hideNetworkActivityStatus: false, + chatListDisplayNode: self.chatListDisplayNode, + isReorderingTabs: .single(false) + ) + self.pendingSecondaryContext = pendingSecondaryContext + let _ = (pendingSecondaryContext.ready.get() + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self, weak pendingSecondaryContext] _ in + guard let self, let pendingSecondaryContext = pendingSecondaryContext, self.pendingSecondaryContext === pendingSecondaryContext else { + return + } + self.secondaryContext = pendingSecondaryContext + self.chatListDisplayNode.setInlineChatList(location: location) + }) + } else { + self.secondaryContext = nil + self.chatListDisplayNode.setInlineChatList(location: nil) + } } - @objc private func navigationBackPressed() { - self.chatListDisplayNode.setInlineChatList(location: nil) - - if self.navigationItem.leftBarButtonItem === self.backNavigationItem { - let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) - editItem.accessibilityLabel = self.presentationData.strings.Common_Edit - - self.navigationItem.setLeftBarButton(editItem, animated: true) - } + private func navigationBackPressed() { + self.dismiss() } public static func openMoreMenu(context: AccountContext, peerId: EnginePeer.Id, sourceController: ViewController, isViewingAsTopics: Bool, sourceView: UIView, gesture: ContextGesture?) { @@ -2653,8 +2324,27 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return } - let chatController = context.sharedContext.makeChatListController(context: context, location: .forum(peerId: peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) - navigationController.replaceController(sourceController, with: chatController, animated: false) + if let targetController = navigationController.viewControllers.first(where: { controller in + var checkController = controller + if let tabBarController = checkController as? TabBarController { + if let currentController = tabBarController.currentController { + checkController = currentController + } else { + return false + } + } + if let controller = checkController as? ChatListControllerImpl { + if controller.chatListDisplayNode.inlineStackContainerNode?.location == .forum(peerId: peerId) { + return true + } + } + return false + }) { + let _ = navigationController.popToViewController(targetController, animated: true) + } else { + let chatController = context.sharedContext.makeChatListController(context: context, location: .forum(peerId: peerId), controlsHistoryPreload: false, hideNetworkActivityStatus: false, previewing: false, enableDebugActions: false) + navigationController.replaceController(sourceController, with: chatController, animated: false) + } }))) items.append(.action(ContextMenuActionItem(text: strings.Chat_ContextViewAsMessages, icon: { theme in if isViewingAsTopics { @@ -2669,7 +2359,12 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) - navigationController.replaceController(sourceController, with: chatController, animated: false) + + if let sourceController = sourceController as? ChatListControllerImpl, case .forum(peerId) = sourceController.location { + navigationController.replaceController(sourceController, with: chatController, animated: false) + } else { + navigationController.pushViewController(chatController) + } }))) items.append(.separator) @@ -3147,7 +2842,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.composePressed() } - @objc private func composePressed() { + @objc fileprivate func composePressed() { guard let navigationController = self.navigationController as? NavigationController else { return } @@ -4230,7 +3925,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } public var lockViewFrame: CGRect? { - if let titleView = self.titleView, let lockViewFrame = titleView.lockViewFrame { + if let titleView = self.findTitleView(), let lockViewFrame = titleView.lockViewFrame { return titleView.convert(lockViewFrame, to: self.view) } else { return nil @@ -4457,3 +4152,490 @@ private final class HeaderContextReferenceContentSource: ContextReferenceContent return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds) } } + +private final class ChatListLocationContext { + let context: AccountContext + let location: ChatListControllerLocation + weak var parentController: ChatListControllerImpl? + var presentationData: PresentationData + + private var proxyUnavailableTooltipController: TooltipController? + private var didShowProxyUnavailableTooltipController = false + + private var titleDisposable: Disposable? + + private(set) var title: String = "" + private(set) var chatTitleComponent: ChatTitleComponent? + private(set) var chatListTitle: NetworkStatusTitle? + + var leftButton: AnyComponentWithIdentity? + var rightButton: AnyComponentWithIdentity? + var proxyButton: AnyComponentWithIdentity? + + var rightButtons: [AnyComponentWithIdentity] { + var result: [AnyComponentWithIdentity] = [] + if let rightButton = self.rightButton { + result.append(rightButton) + } + if let proxyButton = self.proxyButton { + result.append(proxyButton) + } + return result + } + + private let previousEditingAndNetworkStateValue = Atomic<(Bool, AccountNetworkState)?>(value: nil) + + private var didSetReady: Bool = false + let ready = Promise() + + init( + context: AccountContext, + location: ChatListControllerLocation, + parentController: ChatListControllerImpl, + hideNetworkActivityStatus: Bool, + chatListDisplayNode: ChatListControllerNode, + isReorderingTabs: Signal + ) { + self.context = context + self.location = location + self.parentController = parentController + self.presentationData = context.sharedContext.currentPresentationData.with { $0 } + + let hasProxy = context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.proxySettings]) + |> map { sharedData -> (Bool, Bool) in + if let settings = sharedData.entries[SharedDataKeys.proxySettings]?.get(ProxySettings.self) { + return (!settings.servers.isEmpty, settings.enabled) + } else { + return (false, false) + } + } + |> distinctUntilChanged(isEqual: { lhs, rhs in + return lhs == rhs + }) + + let passcode = context.sharedContext.accountManager.accessChallengeData() + |> map { view -> (Bool, Bool) in + let data = view.data + return (data.isLockable, false) + } + + let peerStatus: Signal + switch self.location { + case .chatList(.root): + peerStatus = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) + |> map { peer -> NetworkStatusTitle.Status? in + guard case let .user(user) = peer else { + return nil + } + if let emojiStatus = user.emojiStatus { + return .emoji(emojiStatus) + } else if user.isPremium { + return .premium + } else { + return nil + } + } + |> distinctUntilChanged + default: + peerStatus = .single(nil) + } + + switch location { + case .chatList: + if !hideNetworkActivityStatus { + self.titleDisposable = combineLatest(queue: .mainQueue(), + context.account.networkState, + hasProxy, + passcode, + chatListDisplayNode.containerNode.currentItemState, + isReorderingTabs, + peerStatus + ).start(next: { [weak self] networkState, proxy, passcode, stateAndFilterId, isReorderingTabs, peerStatus in + guard let self else { + return + } + self.updateChatList( + networkState: networkState, + proxy: proxy, + passcode: passcode, + stateAndFilterId: stateAndFilterId, + isReorderingTabs: isReorderingTabs, + peerStatus: peerStatus + ) + }) + } else { + self.didSetReady = true + self.ready.set(.single(true)) + } + case let .forum(peerId): + //self.navigationItem.titleView = chatTitleView + + /*chatTitleView.pressed = { [weak self] in + guard let self = self else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self = self, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + (self.navigationController as? NavigationController)?.pushViewController(controller) + }) + } + self.chatTitleView?.longPressed = { [weak self] in + guard let self else { + return + } + self.activateSearch() + }*/ + + let peerView = Promise() + peerView.set(context.account.viewTracker.peerView(peerId)) + + var onlineMemberCount: Signal = .single(nil) + + let recentOnlineSignal: Signal = peerView.get() + |> map { view -> Bool? in + if let cachedData = view.cachedData as? CachedChannelData, let peer = peerViewMainPeer(view) as? TelegramChannel { + if case .broadcast = peer.info { + return nil + } else if let memberCount = cachedData.participantsSummary.memberCount, memberCount > 50 { + return true + } else { + return false + } + } else { + return false + } + } + |> distinctUntilChanged + |> mapToSignal { isLarge -> Signal in + if let isLarge = isLarge { + if isLarge { + return context.peerChannelMemberCategoriesContextsManager.recentOnline(account: context.account, accountPeerId: context.account.peerId, peerId: peerId) + |> map(Optional.init) + } else { + return context.peerChannelMemberCategoriesContextsManager.recentOnlineSmall(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId) + |> map(Optional.init) + } + } else { + return .single(nil) + } + } + onlineMemberCount = recentOnlineSignal + + self.titleDisposable = (combineLatest(queue: Queue.mainQueue(), + peerView.get(), + onlineMemberCount, + chatListDisplayNode.containerNode.currentItemState + ) + |> deliverOnMainQueue).start(next: { [weak self] peerView, onlineMemberCount, stateAndFilterId in + guard let self else { + return + } + self.updateForum( + peerId: peerId, + peerView: peerView, + onlineMemberCount: onlineMemberCount, + stateAndFilterId: stateAndFilterId + ) + }) + } + } + + private func updateChatList( + networkState: AccountNetworkState, + proxy: (Bool, Bool), + passcode: (Bool, Bool), + stateAndFilterId: (state: ChatListNodeState, filterId: Int32?), + isReorderingTabs: Bool, + peerStatus: NetworkStatusTitle.Status? + ) { + let defaultTitle: String + switch location { + case let .chatList(groupId): + if groupId == .root { + defaultTitle = self.presentationData.strings.DialogList_Title + } else { + defaultTitle = self.presentationData.strings.ChatList_ArchivedChatsTitle + } + case .forum: + defaultTitle = "" + } + let previousEditingAndNetworkState = self.previousEditingAndNetworkStateValue.swap((stateAndFilterId.state.editing, networkState)) + + var titleContent: NetworkStatusTitle + + if stateAndFilterId.state.editing { + if case .chatList(.root) = self.location { + self.rightButton = nil + } + let title = !stateAndFilterId.state.selectedPeerIds.isEmpty ? self.presentationData.strings.ChatList_SelectedChats(Int32(stateAndFilterId.state.selectedPeerIds.count)) : defaultTitle + + var animated = false + if let (previousEditing, previousNetworkState) = previousEditingAndNetworkState { + if previousEditing != stateAndFilterId.state.editing, previousNetworkState == networkState, case .online = networkState { + animated = true + } + } + titleContent = NetworkStatusTitle(text: title, activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) + let _ = animated + } else if isReorderingTabs { + if case .chatList(.root) = self.location { + self.rightButton = nil + } + self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.parentController?.reorderingDonePressed() + } + ))) + + let (_, connectsViaProxy) = proxy + + switch networkState { + case .waitingForNetwork: + titleContent = NetworkStatusTitle(text: self.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) + case let .connecting(proxy): + let text = self.presentationData.strings.State_Connecting + let _ = proxy + /*if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 { + text = self.presentationData.strings.State_ConnectingToProxy + }*/ + titleContent = NetworkStatusTitle(text: text, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) + case .updating: + titleContent = NetworkStatusTitle(text: self.presentationData.strings.State_Updating, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) + case .online: + titleContent = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: false, isManuallyLocked: false, peerStatus: peerStatus) + } + } else { + var isRoot = false + if case .chatList(.root) = self.location { + isRoot = true + + if isReorderingTabs { + self.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.parentController?.editPressed() + } + ))) + } else { + self.rightButton = AnyComponentWithIdentity(id: "compose", component: AnyComponent(NavigationButtonComponent( + content: .icon(imageName: "Chat List/ComposeIcon"), + pressed: { [weak self] _ in + self?.parentController?.composePressed() + } + ))) + } + + if isReorderingTabs { + self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.parentController?.reorderingDonePressed() + } + ))) + } else { + if stateAndFilterId.state.editing { + self.leftButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.parentController?.donePressed() + } + ))) + } else { + self.leftButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Edit, isBold: false), + pressed: { [weak self] _ in + self?.parentController?.editPressed() + } + ))) + } + } + } else { + self.rightButton = AnyComponentWithIdentity(id: "edit", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Edit, isBold: false), + pressed: { [weak self] _ in + self?.parentController?.editPressed() + } + ))) + } + + let (hasProxy, connectsViaProxy) = proxy + let (isPasscodeSet, isManuallyLocked) = passcode + var checkProxy = false + switch networkState { + case .waitingForNetwork: + titleContent = NetworkStatusTitle(text: self.presentationData.strings.State_WaitingForNetwork, activity: true, hasProxy: false, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) + case let .connecting(proxy): + let text = self.presentationData.strings.State_Connecting + /*if let layout = strongSelf.validLayout, proxy != nil && layout.metrics.widthClass != .regular && layout.size.width > 320.0 {*/ + //text = self.presentationData.strings.State_ConnectingToProxy + //} + if let proxy = proxy, proxy.hasConnectionIssues { + checkProxy = true + } + titleContent = NetworkStatusTitle(text: text, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) + case .updating: + titleContent = NetworkStatusTitle(text: self.presentationData.strings.State_Updating, activity: true, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) + case .online: + titleContent = NetworkStatusTitle(text: defaultTitle, activity: false, hasProxy: isRoot && hasProxy, connectsViaProxy: connectsViaProxy, isPasscodeSet: isRoot && isPasscodeSet, isManuallyLocked: isRoot && isManuallyLocked, peerStatus: peerStatus) + } + + if titleContent.hasProxy { + let proxyStatus: ChatTitleProxyStatus + if titleContent.connectsViaProxy { + proxyStatus = titleContent.activity ? .connecting : .connected + } else { + proxyStatus = .available + } + + self.proxyButton = AnyComponentWithIdentity(id: "proxy", component: AnyComponent(NavigationButtonComponent( + content: .proxy(status: proxyStatus), + pressed: { [weak self] _ in + guard let self, let parentController = self.parentController else { + return + } + (parentController.navigationController as? NavigationController)?.pushViewController(self.context.sharedContext.makeProxySettingsController(context: self.context)) + } + ))) + + titleContent.hasProxy = false + titleContent.connectsViaProxy = false + } else { + self.proxyButton = nil + } + + self.chatListTitle = titleContent + + if case .chatList(.root) = self.location, checkProxy { + if self.proxyUnavailableTooltipController == nil, !self.didShowProxyUnavailableTooltipController, let parentController = self.parentController, parentController.isNodeLoaded, parentController.displayNode.view.window != nil, parentController.navigationController?.topViewController == nil { + self.didShowProxyUnavailableTooltipController = true + let tooltipController = TooltipController(content: .text(self.presentationData.strings.Proxy_TooltipUnavailable), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 60.0, dismissByTapOutside: true) + self.proxyUnavailableTooltipController = tooltipController + tooltipController.dismissed = { [weak self, weak tooltipController] _ in + if let strongSelf = self, let tooltipController = tooltipController, strongSelf.proxyUnavailableTooltipController === tooltipController { + strongSelf.proxyUnavailableTooltipController = nil + } + } + self.parentController?.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in + if let strongSelf = self, let titleView = strongSelf.parentController?.self.findTitleView(), let rect = titleView.proxyButtonFrame { + return (titleView, rect.insetBy(dx: 0.0, dy: -4.0)) + } + return nil + })) + } + } else { + self.didShowProxyUnavailableTooltipController = false + if let proxyUnavailableTooltipController = self.proxyUnavailableTooltipController { + self.proxyUnavailableTooltipController = nil + proxyUnavailableTooltipController.dismiss() + } + } + } + + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + + self.parentController?.requestUpdateHeaderContent(transition: .immediate) + } + + private func updateForum( + peerId: EnginePeer.Id, + peerView: PeerView, + onlineMemberCount: Int32?, + stateAndFilterId: (state: ChatListNodeState, filterId: Int32?) + ) { + if stateAndFilterId.state.editing && stateAndFilterId.state.selectedThreadIds.count > 0 { + self.chatTitleComponent = ChatTitleComponent( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameDisplayOrder: self.presentationData.nameDisplayOrder, + content: .custom(self.presentationData.strings.ChatList_SelectedTopics(Int32(stateAndFilterId.state.selectedThreadIds.count)), nil, false), + tapped: { + }, + longTapped: { + } + ) + self.rightButton = AnyComponentWithIdentity(id: "done", component: AnyComponent(NavigationButtonComponent( + content: .text(title: self.presentationData.strings.Common_Done, isBold: true), + pressed: { [weak self] _ in + self?.parentController?.donePressed() + } + ))) + } else { + self.chatTitleComponent = ChatTitleComponent( + context: self.context, + theme: self.presentationData.theme, + strings: self.presentationData.strings, + dateTimeFormat: self.presentationData.dateTimeFormat, + nameDisplayOrder: self.presentationData.nameDisplayOrder, + content: .peer(peerView: peerView, customTitle: nil, onlineMemberCount: onlineMemberCount, isScheduledMessages: false, isMuted: nil, customMessageCount: nil), + tapped: { [weak self] in + guard let self else { + return + } + let _ = (self.context.engine.data.get( + TelegramEngine.EngineData.Item.Peer.Peer(id: peerId) + ) + |> deliverOnMainQueue).start(next: { [weak self] peer in + guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else { + return + } + (self.parentController?.navigationController as? NavigationController)?.pushViewController(controller) + }) + }, + longTapped: { [weak self] in + guard let self else { + return + } + self.parentController?.activateSearch() + } + ) + + self.rightButton = AnyComponentWithIdentity(id: "more", component: AnyComponent(NavigationButtonComponent( + content: .more, + pressed: { [weak self] sourceView in + guard let self, let parentController = self.parentController else { + return + } + ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: parentController, isViewingAsTopics: true, sourceView: sourceView, gesture: nil) + }, + contextAction: { [weak self] sourceView, gesture in + guard let self, let parentController = self.parentController else { + return + } + ChatListControllerImpl.openMoreMenu(context: self.context, peerId: peerId, sourceController: parentController, isViewingAsTopics: true, sourceView: sourceView, gesture: gesture) + } + ))) + } + + if !self.didSetReady { + self.didSetReady = true + self.ready.set(.single(true)) + } + + if let channel = peerView.peers[peerView.peerId] as? TelegramChannel, !channel.flags.contains(.isForum) { + if let parentController = self.parentController, let navigationController = parentController.navigationController as? NavigationController { + let chatController = self.context.sharedContext.makeChatController(context: self.context, chatLocation: .peer(id: peerId), subject: nil, botStart: nil, mode: .standard(previewing: false)) + navigationController.replaceController(parentController, with: chatController, animated: true) + } + } else { + self.parentController?.requestUpdateHeaderContent(transition: .immediate) + } + } + + func updatePresentationData(presentationData: PresentationData) { + } + + deinit { + self.titleDisposable?.dispose() + } +} diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 3df33b9ec7..33931cb0ef 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1268,7 +1268,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { guard let strongSelf = self, strongSelf.inlineStackContainerNode != nil else { return [] } - let directions: InteractiveTransitionGestureRecognizerDirections = [.leftCenter, .rightCenter] + let directions: InteractiveTransitionGestureRecognizerDirections = [.rightCenter] return directions }, edgeWidth: .widthMultiplier(factor: 1.0 / 6.0, min: 22.0, max: 80.0)) inlineContentPanRecognizer.delegate = self @@ -1345,7 +1345,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } if let directionIsToRight = directionIsToRight, directionIsToRight { - self.setInlineChatList(location: nil) + self.controller?.setInlineChatList(location: nil) } else { self.inlineStackContainerTransitionFraction = 1.0 self.controller?.requestLayout(transition: .animated(duration: 0.4, curve: .spring)) @@ -1482,12 +1482,14 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return nil } + let effectiveLocation = self.inlineStackContainerNode?.location ?? self.location + var filter: ChatListNodePeersFilter = [] - if case .forum = self.location { + if case .forum = effectiveLocation { filter.insert(.excludeRecent) } - let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: location, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in + let contentNode = ChatListSearchContainerNode(context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filter: filter, location: effectiveLocation, displaySearchFilters: displaySearchFilters, hasDownloads: hasDownloads, initialFilter: initialFilter, openPeer: { [weak self] peer, _, threadId, dismissSearch in self?.requestOpenPeerFromSearch?(peer, threadId, dismissSearch) }, openDisabledPeer: { _, _ in }, openRecentPeerOptions: { [weak self] peer in diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index 9699c23d7c..2c0e41c32e 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -745,7 +745,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { index = .chatList( EngineChatList.Item.Index.ChatList(pinningIndex: nil, messageIndex: message.index)) } } - return ChatListItem(presentationData: presentationData, context: context, chatListLocation: .chatList(groupId: .root), filterData: nil, index: index, content: .peer(messages: [message], peer: peer, threadInfo: chatThreadInfo, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false, forumTopicData: nil, topForumTopicItems: []), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) + return ChatListItem(presentationData: presentationData, context: context, chatListLocation: location, filterData: nil, index: index, content: .peer(messages: [message], peer: peer, threadInfo: chatThreadInfo, combinedReadState: readState, isRemovedFromTotalUnreadCount: false, presence: nil, hasUnseenMentions: false, hasUnseenReactions: false, draftState: nil, inputActivities: nil, promoInfo: nil, ignoreUnreadBadge: true, displayAsMessage: false, hasFailedMessages: false, forumTopicData: nil, topForumTopicItems: []), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): return ContactsAddItem(theme: theme, strings: strings, phoneNumber: phoneNumber, header: ChatListSearchItemHeader(type: .phoneNumber, theme: theme, strings: strings, actionTitle: nil, action: nil), action: { diff --git a/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift b/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift index 0c12549640..c5246a2db1 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListBadgeNode.swift @@ -34,8 +34,8 @@ private func measureString(_ string: String) -> String { } final class ChatListBadgeNode: ASDisplayNode { - private let backgroundNode: ASImageNode - private let textNode: TextNode + let backgroundNode: ASImageNode + let textNode: TextNode private let measureTextNode: TextNode private var text: String? @@ -43,6 +43,8 @@ final class ChatListBadgeNode: ASDisplayNode { private var isHiddenInternal = false + var disableBounce: Bool = false + override init() { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -97,7 +99,7 @@ final class ChatListBadgeNode: ASDisplayNode { } let badgeWidth = max(imageWidth, badgeWidth) - let previousBadgeWidth = !strongSelf.backgroundNode.frame.width.isZero ? strongSelf.backgroundNode.frame.width : badgeWidth + let previousBadgeWidth = !strongSelf.backgroundNode.bounds.width.isZero ? strongSelf.backgroundNode.bounds.width : badgeWidth var animateTextNode = false if animated { @@ -116,14 +118,16 @@ final class ChatListBadgeNode: ASDisplayNode { if currentIsEmpty && !nextIsEmpty { strongSelf.isHiddenInternal = false - if bounce { - strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in - if let strongSelf = self { - strongSelf.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false) - } - }) - } else { - strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false) + if !strongSelf.disableBounce { + if bounce { + strongSelf.layer.animateScale(from: 0.0001, to: 1.2, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in + if let strongSelf = self { + strongSelf.layer.animateScale(from: 1.15, to: 1.0, duration: 0.12, removeOnCompletion: false) + } + }) + } else { + strongSelf.layer.animateScale(from: 0.0001, to: 1.0, duration: 0.2, removeOnCompletion: false) + } } } else if !currentIsEmpty && !nextIsEmpty && currentContent?.text != content.text { var animateScale = bounce @@ -134,7 +138,7 @@ final class ChatListBadgeNode: ASDisplayNode { } } - if animateScale { + if animateScale && !strongSelf.disableBounce { strongSelf.layer.animateScale(from: 1.0, to: 1.2, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in if let strongSelf = self { strongSelf.layer.animateScale(from: 1.2, to: 1.0, duration: 0.12, removeOnCompletion: false) @@ -157,12 +161,16 @@ final class ChatListBadgeNode: ASDisplayNode { animateTextNode = true } else if !currentIsEmpty && nextIsEmpty && !strongSelf.isHiddenInternal { strongSelf.isHiddenInternal = true - strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in - if let strongSelf = self { - strongSelf.isHidden = true - strongSelf.layer.removeAnimation(forKey: "transform.scale") - } - }) + if !strongSelf.disableBounce { + strongSelf.layer.animateScale(from: 1.0, to: 0.0001, duration: 0.12, removeOnCompletion: false, completion: { [weak self] finished in + if let strongSelf = self { + strongSelf.isHidden = true + strongSelf.layer.removeAnimation(forKey: "transform.scale") + } + }) + } else { + strongSelf.isHidden = true + } } } else { if case .none = content { @@ -183,14 +191,16 @@ final class ChatListBadgeNode: ASDisplayNode { let backgroundFrame = CGRect(x: 0.0, y: 0.0, width: badgeWidth, height: strongSelf.backgroundNode.image?.size.height ?? 0.0) if let (textLayout, _) = textLayoutAndApply { - let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - textLayout.size.width / 2.0, y: backgroundFrame.minY + 2.0), size: textLayout.size) - strongSelf.textNode.frame = badgeTextFrame + let badgeTextFrame = CGRect(origin: CGPoint(x: backgroundFrame.midX - textLayout.size.width / 2.0, y: backgroundFrame.minY + floorToScreenPixels((backgroundFrame.height - textLayout.size.height) / 2.0)), size: textLayout.size) + strongSelf.textNode.position = badgeTextFrame.center + strongSelf.textNode.bounds = CGRect(origin: CGPoint(), size: badgeTextFrame.size) if animateTextNode { strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) strongSelf.textNode.layer.animatePosition(from: CGPoint(x: (previousBadgeWidth - badgeWidth) / 2.0, y: 8.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } } - strongSelf.backgroundNode.frame = backgroundFrame + strongSelf.backgroundNode.position = backgroundFrame.center + strongSelf.backgroundNode.bounds = CGRect(origin: CGPoint(), size: backgroundFrame.size) if animated && badgeWidth != previousBadgeWidth { let previousBackgroundFrame = CGRect(x: 0.0, y: 0.0, width: previousBadgeWidth, height: backgroundFrame.height) diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 55ab0e5eb0..dc2282f50d 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -816,6 +816,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let statusNode: ChatListStatusNode let badgeNode: ChatListBadgeNode let mentionBadgeNode: ChatListBadgeNode + var avatarBadgeNode: ChatListBadgeNode? + var avatarBadgeBackground: ASImageNode? let onlineNode: PeerOnlineMarkerNode let pinnedIconNode: ASImageNode var secretIconNode: ASImageNode? @@ -1115,9 +1117,14 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { }) self.contextContainer.shouldBegin = { [weak self] location in - guard let strongSelf = self else { + guard let strongSelf = self, let item = strongSelf.item else { return false } + + if item.interaction.inlineNavigationLocation != nil { + return false + } + if let value = strongSelf.hitTest(location, with: nil), value === strongSelf.compoundTextButtonNode?.view { strongSelf.contextContainer.targetNodeForActivationProgress = strongSelf.compoundHighlightingNode } else { @@ -1373,6 +1380,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { let textFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)) let dateFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0)) let badgeFont = Font.with(size: floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0), design: .regular, weight: .regular, traits: [.monospacedNumbers]) + let avatarBadgeFont = Font.with(size: 16.0, design: .regular, weight: .regular, traits: [.monospacedNumbers]) let account = item.context.account var messages: [EngineMessage] @@ -1497,6 +1505,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { var statusState = ChatListStatusNodeState.none var currentBadgeBackgroundImage: UIImage? + var currentAvatarBadgeBackgroundImage: UIImage? var currentMentionBadgeImage: UIImage? var currentPinnedIconImage: UIImage? var currentMutedIconImage: UIImage? @@ -1547,6 +1556,9 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } let badgeDiameter = floor(item.presentationData.fontSize.baseDisplaySize * 20.0 / 17.0) + let avatarBadgeDiameter: CGFloat = 22.0 + + let currentAvatarBadgeCleanBackgroundImage: UIImage? = PresentationResourcesChatList.badgeBackgroundBorder(item.presentationData.theme, diameter: avatarBadgeDiameter + 4.0) let leftInset: CGFloat = params.leftInset + avatarLeftInset @@ -1935,17 +1947,21 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { if unreadCount.isProvisonal { badgeTextColor = theme.unreadBadgeInactiveBackgroundColor currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) } else { badgeTextColor = theme.unreadBadgeInactiveTextColor currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundInactive(item.presentationData.theme, diameter: avatarBadgeDiameter) } } else { if unreadCount.isProvisonal { badgeTextColor = theme.unreadBadgeActiveBackgroundColor currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActiveProvisional(item.presentationData.theme, diameter: avatarBadgeDiameter) } else { badgeTextColor = theme.unreadBadgeActiveTextColor currentBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: badgeDiameter) + currentAvatarBadgeBackgroundImage = PresentationResourcesChatList.badgeBackgroundActive(item.presentationData.theme, diameter: avatarBadgeDiameter) } } let unreadCountText = compactNumericCountString(Int(unreadCount.count), decimalSeparator: item.presentationData.dateTimeFormat.decimalSeparator) @@ -2490,6 +2506,61 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } } + if let inlineNavigationLocation = item.interaction.inlineNavigationLocation, badgeContent != .none { + var animateIn = false + + let avatarBadgeBackground: ASImageNode + if let current = strongSelf.avatarBadgeBackground { + avatarBadgeBackground = current + } else { + avatarBadgeBackground = ASImageNode() + strongSelf.avatarBadgeBackground = avatarBadgeBackground + strongSelf.avatarNode.addSubnode(avatarBadgeBackground) + } + + avatarBadgeBackground.image = currentAvatarBadgeCleanBackgroundImage + + let avatarBadgeNode: ChatListBadgeNode + if let current = strongSelf.avatarBadgeNode { + avatarBadgeNode = current + } else { + animateIn = true + avatarBadgeNode = ChatListBadgeNode() + avatarBadgeNode.disableBounce = true + strongSelf.avatarBadgeNode = avatarBadgeNode + strongSelf.avatarNode.addSubnode(avatarBadgeNode) + } + + let makeAvatarBadgeLayout = avatarBadgeNode.asyncLayout() + let (avatarBadgeLayout, avatarBadgeApply) = makeAvatarBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), avatarBadgeDiameter, avatarBadgeFont, currentAvatarBadgeBackgroundImage, badgeContent) + let _ = avatarBadgeApply(animateBadges, false) + let avatarBadgeFrame = CGRect(origin: CGPoint(x: avatarFrame.width - avatarBadgeLayout.width, y: avatarFrame.height - avatarBadgeLayout.height), size: avatarBadgeLayout) + avatarBadgeNode.position = avatarBadgeFrame.center + avatarBadgeNode.bounds = CGRect(origin: CGPoint(), size: avatarBadgeFrame.size) + + let avatarBadgeBackgroundFrame = avatarBadgeFrame.insetBy(dx: -2.0, dy: -2.0) + avatarBadgeBackground.position = avatarBadgeBackgroundFrame.center + avatarBadgeBackground.bounds = CGRect(origin: CGPoint(), size: avatarBadgeBackgroundFrame.size) + + if animateIn { + ContainedViewLayoutTransition.immediate.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.001) + ContainedViewLayoutTransition.immediate.updateTransformScale(layer: avatarBadgeBackground.layer, scale: 0.001) + } + transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: max(0.001, inlineNavigationLocation.progress)) + transition.updateTransformScale(layer: avatarBadgeBackground.layer, scale: max(0.001, inlineNavigationLocation.progress)) + } else if let avatarBadgeNode = strongSelf.avatarBadgeNode { + strongSelf.avatarBadgeNode = nil + transition.updateSublayerTransformScale(node: avatarBadgeNode, scale: 0.001, completion: { [weak avatarBadgeNode] _ in + avatarBadgeNode?.removeFromSupernode() + }) + if let avatarBadgeBackground = strongSelf.avatarBadgeBackground { + strongSelf.avatarBadgeBackground = nil + transition.updateTransformScale(layer: avatarBadgeBackground.layer, scale: 0.001, completion: { [weak avatarBadgeBackground] _ in + avatarBadgeBackground?.removeFromSupernode() + }) + } + } + if let threadInfo = threadInfo { let avatarIconView: ComponentHostView if let current = strongSelf.avatarIconView { diff --git a/submodules/Display/Source/NavigationBar.swift b/submodules/Display/Source/NavigationBar.swift index 1721ee5f84..e0e9e08d9b 100644 --- a/submodules/Display/Source/NavigationBar.swift +++ b/submodules/Display/Source/NavigationBar.swift @@ -110,24 +110,24 @@ public final class NavigationBarPresentationData { } } -enum NavigationPreviousAction: Equatable { +public enum NavigationPreviousAction: Equatable { case item(UINavigationItem) case close - static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool { + public static func ==(lhs: NavigationPreviousAction, rhs: NavigationPreviousAction) -> Bool { switch lhs { - case let .item(lhsItem): - if case let .item(rhsItem) = rhs, lhsItem === rhsItem { - return true - } else { - return false - } - case .close: - if case .close = rhs { - return true - } else { - return false - } + case let .item(lhsItem): + if case let .item(rhsItem) = rhs, lhsItem === rhsItem { + return true + } else { + return false + } + case .close: + if case .close = rhs { + return true + } else { + return false + } } } } @@ -439,7 +439,6 @@ open class BlurredBackgroundView: UIView { } public protocol NavigationBarHeaderView: UIView { - func update(size: CGSize, transition: ContainedViewLayoutTransition) } open class NavigationBar: ASDisplayNode { @@ -657,7 +656,12 @@ open class NavigationBar: ASDisplayNode { self.customHeaderContentView?.removeFromSuperview() if let customHeaderContentView = self.customHeaderContentView { - self.view.addSubview(customHeaderContentView) + self.buttonsContainerNode.view.addSubview(customHeaderContentView) + self.backButtonNode.isHidden = true + self.backButtonArrow.isHidden = true + } else { + self.backButtonNode.isHidden = false + self.backButtonArrow.isHidden = false } } } @@ -708,7 +712,7 @@ open class NavigationBar: ASDisplayNode { } var _previousItem: NavigationPreviousAction? - var previousItem: NavigationPreviousAction? { + public internal(set) var previousItem: NavigationPreviousAction? { get { return self._previousItem } set(value) { @@ -1373,7 +1377,7 @@ open class NavigationBar: ASDisplayNode { if let customHeaderContentView = self.customHeaderContentView { let headerSize = CGSize(width: size.width, height: nominalHeight) - customHeaderContentView.update(size: headerSize, transition: transition) + //customHeaderContentView.update(size: headerSize, transition: transition) transition.updateFrame(view: customHeaderContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOrigin), size: headerSize)) } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 33268f09cc..d585251543 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -310,6 +310,7 @@ public enum PresentationResourceParameterKey: Hashable { case badgeBackgroundInactiveReactions(CGFloat) case chatListBadgeBackgroundInactiveMention(CGFloat) case chatListBadgeBackgroundPinned(CGFloat) + case badgeBackgroundBorder(CGFloat) case chatBubbleMediaCorner(incoming: Bool, mainRadius: CGFloat, inset: CGFloat) diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift index 4c314fc4f5..b4e4fa2c95 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChatList.swift @@ -235,6 +235,12 @@ public struct PresentationResourcesChatList { }) } + public static func badgeBackgroundBorder(_ theme: PresentationTheme, diameter: CGFloat) -> UIImage? { + return theme.image(PresentationResourceParameterKey.badgeBackgroundBorder(diameter), { theme in + return generateStretchableFilledCircleImage(diameter: diameter, color: theme.chatList.pinnedItemBackgroundColor.blitOver(theme.chatList.backgroundColor, alpha: 1.0)) + }) + } + public static func mutedIcon(_ theme: PresentationTheme) -> UIImage? { return theme.image(PresentationResourceKey.chatListMutedIcon.rawValue, { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerMutedIcon"), color: theme.chatList.muteIconColor) diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index f022709509..87854ad68c 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -302,6 +302,7 @@ swift_library( "//submodules/TelegramUI/Components/ChatTitleView", "//submodules/InviteLinksUI:InviteLinksUI", "//submodules/TelegramUI/Components/NotificationPeerExceptionController", + "//submodules/TelegramUI/Components/ChatListHeaderComponent", "//submodules/MediaPasteboardUI:MediaPasteboardUI", ] + select({ "@build_bazel_rules_apple//apple:ios_armv7": [], diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD index 7467c13e9f..ab8fd7628c 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/BUILD @@ -10,9 +10,15 @@ swift_library( "-warnings-as-errors", ], deps = [ - "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", - "//submodules/Display:Display", - "//submodules/ComponentFlow:ComponentFlow", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/Display", + "//submodules/ComponentFlow", + "//submodules/TelegramPresentationData", + "//submodules/TelegramUI/Components/ChatListTitleView", + "//submodules/AccountContext", + "//submodules/AppBundle", + "//submodules/AsyncDisplayKit", + "//submodules/AnimationUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift index 561e32fd15..2fa2829f2f 100644 --- a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/ChatListHeaderComponent.swift @@ -2,14 +2,932 @@ import Foundation import UIKit import Display import ComponentFlow +import TelegramPresentationData +import AccountContext +import ChatListTitleView +import AppBundle -/*public final class ChatListHeaderComponent: Component { - public final class View: UIView, NavigationBarHeaderView { - public func update(size: CGSize, transition: ContainedViewLayoutTransition) { - +public final class HeaderNetworkStatusComponent: Component { + public enum Content: Equatable { + case connecting + case updating + } + + public let content: Content + public let theme: PresentationTheme + public let strings: PresentationStrings + + public init( + content: Content, + theme: PresentationTheme, + strings: PresentationStrings + ) { + self.content = content + self.theme = theme + self.strings = strings + } + + public static func ==(lhs: HeaderNetworkStatusComponent, rhs: HeaderNetworkStatusComponent) -> Bool { + if lhs.content != rhs.content { + return false } - + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + public final class View: UIView { + private var component: HeaderNetworkStatusComponent? + private weak var state: EmptyComponentState? + override init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: HeaderNetworkStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.state = state + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class ChatListHeaderComponent: Component { + public final class Content: Equatable { + public let title: String + public let titleComponent: AnyComponent? + public let chatListTitle: NetworkStatusTitle? + public let leftButton: AnyComponentWithIdentity? + public let rightButtons: [AnyComponentWithIdentity] + public let backTitle: String? + public let backPressed: (() -> Void)? + + public init( + title: String, + titleComponent: AnyComponent?, + chatListTitle: NetworkStatusTitle?, + leftButton: AnyComponentWithIdentity?, + rightButtons: [AnyComponentWithIdentity], + backTitle: String?, + backPressed: (() -> Void)? + ) { + self.title = title + self.titleComponent = titleComponent + self.chatListTitle = chatListTitle + self.leftButton = leftButton + self.rightButtons = rightButtons + self.backTitle = backTitle + self.backPressed = backPressed + } + + public static func ==(lhs: Content, rhs: Content) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleComponent != rhs.titleComponent { + return false + } + if lhs.chatListTitle != rhs.chatListTitle { + return false + } + if lhs.leftButton != rhs.leftButton { + return false + } + if lhs.rightButtons != rhs.rightButtons { + return false + } + if lhs.backTitle != rhs.backTitle { + return false + } + return true + } + } + + public let sideInset: CGFloat + public let primaryContent: Content? + public let secondaryContent: Content? + public let secondaryTransition: CGFloat + public let networkStatus: HeaderNetworkStatusComponent.Content? + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + + public let openStatusSetup: (UIView) -> Void + public let toggleIsLocked: () -> Void + + public init( + sideInset: CGFloat, + primaryContent: Content?, + secondaryContent: Content?, + secondaryTransition: CGFloat, + networkStatus: HeaderNetworkStatusComponent.Content?, + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + openStatusSetup: @escaping (UIView) -> Void, + toggleIsLocked: @escaping () -> Void + ) { + self.sideInset = sideInset + self.primaryContent = primaryContent + self.secondaryContent = secondaryContent + self.secondaryTransition = secondaryTransition + self.context = context + self.networkStatus = networkStatus + self.theme = theme + self.strings = strings + self.openStatusSetup = openStatusSetup + self.toggleIsLocked = toggleIsLocked + } + + public static func ==(lhs: ChatListHeaderComponent, rhs: ChatListHeaderComponent) -> Bool { + if lhs.sideInset != rhs.sideInset { + return false + } + if lhs.primaryContent != rhs.primaryContent { + return false + } + if lhs.secondaryContent != rhs.secondaryContent { + return false + } + if lhs.secondaryTransition != rhs.secondaryTransition { + return false + } + if lhs.networkStatus != rhs.networkStatus { + return false + } + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + return true + } + + private final class BackButtonView: HighlightableButton { + private let onPressed: () -> Void + + let arrowView: UIImageView + let titleOffsetContainer: UIView + let titleView: ImmediateTextView + + private var currentColor: UIColor? + + init(onPressed: @escaping () -> Void) { + self.onPressed = onPressed + + self.arrowView = UIImageView() + self.titleOffsetContainer = UIView() + self.titleView = ImmediateTextView() + + super.init(frame: CGRect()) + + self.addSubview(self.arrowView) + + self.addSubview(self.titleOffsetContainer) + self.titleOffsetContainer.addSubview(self.titleView) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.alpha = 0.6 + } else { + self.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.onPressed() + } + + func update(title: String, theme: PresentationTheme, availableSize: CGSize, transition: Transition) -> CGSize { + self.titleView.attributedText = NSAttributedString(string: title, font: Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + let titleSize = self.titleView.updateLayout(CGSize(width: 100.0, height: 44.0)) + + if self.currentColor != theme.rootController.navigationBar.accentTextColor { + self.currentColor = theme.rootController.navigationBar.accentTextColor + self.arrowView.image = NavigationBarTheme.generateBackArrowImage(color: theme.rootController.navigationBar.accentTextColor) + } + + let iconSpacing: CGFloat = 8.0 + let iconOffset: CGFloat = -7.0 + + let arrowSize = self.arrowView.image?.size ?? CGSize(width: 13.0, height: 22.0) + + let arrowFrame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - arrowSize.height) / 2.0)), size: arrowSize) + transition.setPosition(view: self.arrowView, position: arrowFrame.center) + transition.setBounds(view: self.arrowView, bounds: CGRect(origin: CGPoint(), size: arrowFrame.size)) + + transition.setFrame(view: self.titleView, frame: CGRect(origin: CGPoint(x: iconOffset + arrowSize.width + iconSpacing, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)) + + return CGSize(width: iconOffset + arrowSize.width + iconSpacing + titleSize.width, height: availableSize.height) + } + } + + private final class ContentView: UIView { + let backPressed: () -> Void + let openStatusSetup: (UIView) -> Void + let toggleIsLocked: () -> Void + + let leftButtonOffsetContainer: UIView + var leftButtonViews: [AnyHashable: ComponentView] = [:] + let rightButtonOffsetContainer: UIView + var rightButtonViews: [AnyHashable: ComponentView] = [:] + var backButtonView: BackButtonView? + + let titleOffsetContainer: UIView + let titleTextView: ImmediateTextView + var titleContentView: ComponentView? + var chatListTitleView: ChatListTitleView? + + init( + backPressed: @escaping () -> Void, + openStatusSetup: @escaping (UIView) -> Void, + toggleIsLocked: @escaping () -> Void + ) { + self.backPressed = backPressed + self.openStatusSetup = openStatusSetup + self.toggleIsLocked = toggleIsLocked + + self.leftButtonOffsetContainer = UIView() + self.rightButtonOffsetContainer = UIView() + self.titleOffsetContainer = UIView() + + self.titleTextView = ImmediateTextView() + + super.init(frame: CGRect()) + + self.addSubview(self.titleOffsetContainer) + self.addSubview(self.leftButtonOffsetContainer) + self.addSubview(self.rightButtonOffsetContainer) + + self.titleOffsetContainer.addSubview(self.titleTextView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let view = self.titleContentView?.view, let result = view.hitTest(self.convert(point, to: view), with: event) { + return result + } + if let view = self.chatListTitleView, let result = view.hitTest(self.convert(point, to: view), with: event) { + return result + } + if let backButtonView = self.backButtonView { + if let result = backButtonView.hitTest(self.convert(point, to: backButtonView), with: event) { + return result + } + } + for (_, buttonView) in self.leftButtonViews { + if let view = buttonView.view, let result = view.hitTest(self.convert(point, to: view), with: event) { + return result + } + } + for (_, buttonView) in self.rightButtonViews { + if let view = buttonView.view, let result = view.hitTest(self.convert(point, to: view), with: event) { + return result + } + } + return nil + } + + func updateNavigationTransitionAsPrevious(nextView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + transition.setBounds(view: self.leftButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: fraction * self.bounds.width * 0.5, y: 0.0), size: self.leftButtonOffsetContainer.bounds.size), completion: { _ in + completion() + }) + transition.setAlpha(view: self.rightButtonOffsetContainer, alpha: pow(1.0 - fraction, 2.0)) + + if let chatListTitleView = self.chatListTitleView, let nextBackButtonView = nextView.backButtonView { + let titleFrame = chatListTitleView.titleNode.view.convert(chatListTitleView.titleNode.bounds, to: self.titleOffsetContainer) + let backButtonTitleFrame = nextBackButtonView.convert(nextBackButtonView.titleView.frame, to: nextView) + + let totalOffset = titleFrame.midX - backButtonTitleFrame.midX + + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: totalOffset * fraction, y: 0.0), size: self.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: self.titleOffsetContainer, alpha: (1.0 - fraction)) + } + } + + func updateNavigationTransitionAsNext(previousView: ContentView, fraction: CGFloat, transition: Transition, completion: @escaping () -> Void) { + transition.setBounds(view: self.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.titleOffsetContainer.bounds.size), completion: { _ in + completion() + }) + transition.setBounds(view: self.rightButtonOffsetContainer, bounds: CGRect(origin: CGPoint(x: -(1.0 - fraction) * self.bounds.width, y: 0.0), size: self.rightButtonOffsetContainer.bounds.size)) + if let backButtonView = self.backButtonView { + transition.setScale(view: backButtonView.arrowView, scale: pow(max(0.001, fraction), 2.0)) + transition.setAlpha(view: backButtonView.arrowView, alpha: pow(fraction, 2.0)) + + if let previousChatListTitleView = previousView.chatListTitleView { + let previousTitleFrame = previousChatListTitleView.titleNode.view.convert(previousChatListTitleView.titleNode.bounds, to: previousView.titleOffsetContainer) + let backButtonTitleFrame = backButtonView.convert(backButtonView.titleView.frame, to: self) + + let totalOffset = previousTitleFrame.midX - backButtonTitleFrame.midX + + transition.setBounds(view: backButtonView.titleOffsetContainer, bounds: CGRect(origin: CGPoint(x: -totalOffset * (1.0 - fraction), y: 0.0), size: backButtonView.titleOffsetContainer.bounds.size)) + transition.setAlpha(view: backButtonView.titleOffsetContainer, alpha: fraction) + } + } + } + + func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content, backTitle: String?, sideInset: CGFloat, size: CGSize, transition: Transition) { + self.titleTextView.attributedText = NSAttributedString(string: content.title, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor) + + let buttonSpacing: CGFloat = 8.0 + + var leftOffset = sideInset + + if let backTitle = backTitle { + var backButtonTransition = transition + let backButtonView: BackButtonView + if let current = self.backButtonView { + backButtonView = current + } else { + backButtonTransition = .immediate + backButtonView = BackButtonView(onPressed: { [weak self] in + guard let self else { + return + } + self.backPressed() + }) + self.backButtonView = backButtonView + self.addSubview(backButtonView) + } + let backButtonSize = backButtonView.update(title: backTitle, theme: theme, availableSize: CGSize(width: 100.0, height: size.height), transition: backButtonTransition) + backButtonTransition.setFrame(view: backButtonView, frame: CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - backButtonSize.height) / 2.0)), size: backButtonSize)) + leftOffset += backButtonSize.width + buttonSpacing + } else if let backButtonView = self.backButtonView { + self.backButtonView = nil + backButtonView.removeFromSuperview() + } + + var validLeftButtons = Set() + if let leftButton = content.leftButton { + validLeftButtons.insert(leftButton.id) + + var buttonTransition = transition + var animateButtonIn = false + let buttonView: ComponentView + if let current = self.leftButtonViews[leftButton.id] { + buttonView = current + } else { + buttonTransition = .immediate + animateButtonIn = true + buttonView = ComponentView() + self.leftButtonViews[leftButton.id] = buttonView + } + let buttonSize = buttonView.update( + transition: buttonTransition, + component: leftButton.component, + environment: { + NavigationButtonComponentEnvironment(theme: theme) + }, + containerSize: CGSize(width: 100.0, height: size.height) + ) + let buttonFrame = CGRect(origin: CGPoint(x: leftOffset, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + if let buttonComponentView = buttonView.view { + if buttonComponentView.superview == nil { + self.leftButtonOffsetContainer.addSubview(buttonComponentView) + } + buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) + if animateButtonIn { + transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + } + } + leftOffset = buttonFrame.maxX + buttonSpacing + } + var removeLeftButtons: [AnyHashable] = [] + for (id, buttonView) in self.leftButtonViews { + if !validLeftButtons.contains(id) { + if let buttonComponentView = buttonView.view { + transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + buttonComponentView?.removeFromSuperview() + }) + } + removeLeftButtons.append(id) + } + } + for id in removeLeftButtons { + self.leftButtonViews.removeValue(forKey: id) + } + + var rightOffset = size.width - sideInset + var validRightButtons = Set() + for rightButton in content.rightButtons { + validRightButtons.insert(rightButton.id) + + var buttonTransition = transition + var animateButtonIn = false + let buttonView: ComponentView + if let current = self.rightButtonViews[rightButton.id] { + buttonView = current + } else { + buttonTransition = .immediate + animateButtonIn = true + buttonView = ComponentView() + self.rightButtonViews[rightButton.id] = buttonView + } + let buttonSize = buttonView.update( + transition: buttonTransition, + component: rightButton.component, + environment: { + NavigationButtonComponentEnvironment(theme: theme) + }, + containerSize: CGSize(width: 100.0, height: size.height) + ) + let buttonFrame = CGRect(origin: CGPoint(x: rightOffset - buttonSize.width, y: floor((size.height - buttonSize.height) / 2.0)), size: buttonSize) + if let buttonComponentView = buttonView.view { + if buttonComponentView.superview == nil { + self.rightButtonOffsetContainer.addSubview(buttonComponentView) + } + buttonTransition.setFrame(view: buttonComponentView, frame: buttonFrame) + if animateButtonIn { + transition.animateAlpha(view: buttonComponentView, from: 0.0, to: 1.0) + } + } + rightOffset = buttonFrame.minX - buttonSpacing + } + var removeRightButtons: [AnyHashable] = [] + for (id, buttonView) in self.rightButtonViews { + if !validRightButtons.contains(id) { + if let buttonComponentView = buttonView.view { + transition.setAlpha(view: buttonComponentView, alpha: 0.0, completion: { [weak buttonComponentView] _ in + buttonComponentView?.removeFromSuperview() + }) + } + removeRightButtons.append(id) + } + } + for id in removeRightButtons { + self.rightButtonViews.removeValue(forKey: id) + } + + let commonInset: CGFloat = max(leftOffset, size.width - rightOffset) + let remainingWidth = size.width - commonInset * 2.0 + + let titleTextSize = self.titleTextView.updateLayout(CGSize(width: remainingWidth, height: size.height)) + let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleTextSize.width) / 2.0), y: floor((size.height - titleTextSize.height) / 2.0)), size: titleTextSize) + transition.setFrame(view: self.titleTextView, frame: titleFrame) + + if let titleComponent = content.titleComponent { + var titleContentTransition = transition + let titleContentView: ComponentView + if let current = self.titleContentView { + titleContentView = current + } else { + titleContentTransition = .immediate + titleContentView = ComponentView() + self.titleContentView = titleContentView + } + let titleContentSize = titleContentView.update( + transition: titleContentTransition, + component: titleComponent, + environment: {}, + containerSize: CGSize(width: remainingWidth, height: size.height) + ) + if let titleContentComponentView = titleContentView.view { + if titleContentComponentView.superview == nil { + self.titleOffsetContainer.addSubview(titleContentComponentView) + } + titleContentTransition.setFrame(view: titleContentComponentView, frame: CGRect(origin: CGPoint(x: floor((size.width - titleContentSize.width) / 2.0), y: floor((size.height - titleContentSize.height) / 2.0)), size: titleContentSize)) + } + } else { + if let titleContentView = self.titleContentView { + self.titleContentView = nil + titleContentView.view?.removeFromSuperview() + } + } + + if let chatListTitle = content.chatListTitle { + var chatListTitleTransition = transition + let chatListTitleView: ChatListTitleView + if let current = self.chatListTitleView { + chatListTitleView = current + } else { + chatListTitleTransition = .immediate + chatListTitleView = ChatListTitleView(context: context, theme: theme, strings: strings, animationCache: context.animationCache, animationRenderer: context.animationRenderer) + chatListTitleView.manualLayout = true + self.chatListTitleView = chatListTitleView + self.titleOffsetContainer.addSubview(chatListTitleView) + } + + let chatListTitleContentSize = size + chatListTitleView.setTitle(chatListTitle, animated: false) + chatListTitleView.updateLayout(size: chatListTitleContentSize, clearBounds: CGRect(origin: CGPoint(), size: chatListTitleContentSize), transition: transition.containedViewLayoutTransition) + + chatListTitleView.openStatusSetup = { [weak self] sourceView in + guard let self else { + return + } + self.openStatusSetup(sourceView) + } + chatListTitleView.toggleIsLocked = { [weak self] in + guard let self else { + return + } + self.toggleIsLocked() + } + + chatListTitleTransition.setFrame(view: chatListTitleView, frame: CGRect(origin: CGPoint(x: floor((size.width - chatListTitleContentSize.width) / 2.0), y: floor((size.height - chatListTitleContentSize.height) / 2.0)), size: chatListTitleContentSize)) + } else { + if let chatListTitleView = self.chatListTitleView { + self.chatListTitleView = nil + chatListTitleView.removeFromSuperview() + } + } + + self.titleTextView.isHidden = self.chatListTitleView != nil || self.titleContentView != nil + } + } + + public final class View: UIView, NavigationBarHeaderView { + private var component: ChatListHeaderComponent? + private weak var state: EmptyComponentState? + + private var primaryContentView: ContentView? + private var secondaryContentView: ContentView? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ChatListHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.state = state + + let previousComponent = self.component + self.component = component + + if let primaryContent = component.primaryContent { + var primaryContentTransition = transition + let primaryContentView: ContentView + if let current = self.primaryContentView { + primaryContentView = current + } else { + primaryContentTransition = .immediate + primaryContentView = ContentView( + backPressed: { [weak self] in + guard let self, let component = self.component else { + return + } + component.primaryContent?.backPressed?() + }, + openStatusSetup: { [weak self] sourceView in + guard let self else { + return + } + self.component?.openStatusSetup(sourceView) + }, + toggleIsLocked: { [weak self] in + guard let self else { + return + } + self.component?.toggleIsLocked() + } + ) + self.primaryContentView = primaryContentView + self.addSubview(primaryContentView) + } + primaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: primaryContent, backTitle: primaryContent.backTitle, sideInset: component.sideInset, size: availableSize, transition: primaryContentTransition) + primaryContentTransition.setFrame(view: primaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + } else if let primaryContentView = self.primaryContentView { + self.primaryContentView = nil + primaryContentView.removeFromSuperview() + } + + if let secondaryContent = component.secondaryContent { + var secondaryContentTransition = transition + let secondaryContentView: ContentView + if let current = self.secondaryContentView { + secondaryContentView = current + } else { + secondaryContentTransition = .immediate + secondaryContentView = ContentView( + backPressed: { [weak self] in + guard let self, let component = self.component else { + return + } + component.secondaryContent?.backPressed?() + }, + openStatusSetup: { [weak self] sourceView in + guard let self else { + return + } + self.component?.openStatusSetup(sourceView) + }, + toggleIsLocked: { [weak self] in + guard let self else { + return + } + self.component?.toggleIsLocked() + } + ) + self.secondaryContentView = secondaryContentView + self.addSubview(secondaryContentView) + } + secondaryContentView.update(context: component.context, theme: component.theme, strings: component.strings, content: secondaryContent, backTitle: component.primaryContent?.title, sideInset: component.sideInset, size: availableSize, transition: secondaryContentTransition) + secondaryContentTransition.setFrame(view: secondaryContentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + if let primaryContentView = self.primaryContentView { + if let previousComponent = previousComponent, previousComponent.secondaryContent == nil { + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: .immediate, completion: {}) + } + + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: component.secondaryTransition, transition: transition, completion: {}) + } + } else if let secondaryContentView = self.secondaryContentView { + self.secondaryContentView = nil + + if let primaryContentView = self.primaryContentView { + primaryContentView.updateNavigationTransitionAsPrevious(nextView: secondaryContentView, fraction: 0.0, transition: transition, completion: {}) + secondaryContentView.updateNavigationTransitionAsNext(previousView: primaryContentView, fraction: 0.0, transition: transition, completion: { [weak secondaryContentView] in + secondaryContentView?.removeFromSuperview() + }) + } else { + secondaryContentView.removeFromSuperview() + } + } + + return availableSize + } + + public func findTitleView() -> ChatListTitleView? { + return self.primaryContentView?.chatListTitleView + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} + +public final class NavigationButtonComponentEnvironment: Equatable { + public let theme: PresentationTheme + + public init(theme: PresentationTheme) { + self.theme = theme + } + + public static func ==(lhs: NavigationButtonComponentEnvironment, rhs: NavigationButtonComponentEnvironment) -> Bool { + if lhs.theme != rhs.theme { + return false + } + return true + } +} + +public final class NavigationButtonComponent: Component { + public typealias EnvironmentType = NavigationButtonComponentEnvironment + + public enum Content: Equatable { + case text(title: String, isBold: Bool) + case more + case icon(imageName: String) + case proxy(status: ChatTitleProxyStatus) + } + + public let content: Content + public let pressed: (UIView) -> Void + public let contextAction: ((UIView, ContextGesture?) -> Void)? + + public init( + content: Content, + pressed: @escaping (UIView) -> Void, + contextAction: ((UIView, ContextGesture?) -> Void)? = nil + ) { + self.content = content + self.pressed = pressed + self.contextAction = contextAction + } + + public static func ==(lhs: NavigationButtonComponent, rhs: NavigationButtonComponent) -> Bool { + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: HighlightTrackingButton { + private var textView: ImmediateTextView? + + private var iconView: UIImageView? + private var iconImageName: String? + + private var proxyNode: ChatTitleProxyNode? + + private var moreButton: MoreHeaderButton? + + private var component: NavigationButtonComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.textView?.alpha = 0.6 + self.proxyNode?.alpha = 0.6 + self.iconView?.alpha = 0.6 + } else { + self.textView?.alpha = 1.0 + self.textView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.proxyNode?.alpha = 1.0 + self.proxyNode?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + + self.iconView?.alpha = 1.0 + self.iconView?.layer.animateAlpha(from: 0.6, to: 1.0, duration: 0.2) + } + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func pressed() { + self.component?.pressed(self) + } + + func update(component: NavigationButtonComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let theme = environment[NavigationButtonComponentEnvironment.self].value.theme + + let iconOffset: CGFloat = 4.0 + + var textString: NSAttributedString? + var imageName: String? + var proxyStatus: ChatTitleProxyStatus? + var isMore: Bool = false + + switch component.content { + case let .text(title, isBold): + textString = NSAttributedString(string: title, font: isBold ? Font.bold(17.0) : Font.regular(17.0), textColor: theme.rootController.navigationBar.accentTextColor) + case .more: + isMore = true + case let .icon(imageNameValue): + imageName = imageNameValue + case let .proxy(status): + proxyStatus = status + } + + var size = CGSize(width: 0.0, height: availableSize.height) + + if let textString = textString { + let textView: ImmediateTextView + if let current = self.textView { + textView = current + } else { + textView = ImmediateTextView() + textView.isUserInteractionEnabled = false + self.textView = textView + self.addSubview(textView) + } + + textView.attributedText = textString + let textSize = textView.updateLayout(availableSize) + size.width = textSize.width + + textView.frame = CGRect(origin: CGPoint(x: 0.0, y: floor((availableSize.height - textSize.height) / 2.0)), size: textSize) + } else if let textView = self.textView { + self.textView = nil + textView.removeFromSuperview() + } + + if let imageName = imageName { + let iconView: UIImageView + if let current = self.iconView { + iconView = current + } else { + iconView = UIImageView() + iconView.isUserInteractionEnabled = false + self.iconView = iconView + self.addSubview(iconView) + } + if self.iconImageName != imageName { + self.iconImageName = imageName + iconView.image = generateTintedImage(image: UIImage(bundleImageName: imageName), color: theme.rootController.navigationBar.accentTextColor) + } + + if let iconSize = iconView.image?.size { + size.width = iconSize.width + + iconView.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - iconSize.height) / 2.0)), size: iconSize) + } + } else if let iconView = self.iconView { + self.iconView = nil + iconView.removeFromSuperview() + self.iconImageName = nil + } + + if let proxyStatus = proxyStatus { + let proxyNode: ChatTitleProxyNode + if let current = self.proxyNode { + proxyNode = current + } else { + proxyNode = ChatTitleProxyNode(theme: theme) + proxyNode.isUserInteractionEnabled = false + self.proxyNode = proxyNode + self.addSubnode(proxyNode) + } + + let proxySize = CGSize(width: 30.0, height: 30.0) + size.width = proxySize.width + + proxyNode.theme = theme + proxyNode.status = proxyStatus + + proxyNode.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - proxySize.height) / 2.0)), size: proxySize) + } else if let proxyNode = self.proxyNode { + self.proxyNode = nil + proxyNode.removeFromSupernode() + } + + if isMore { + let moreButton: MoreHeaderButton + if let current = self.moreButton { + moreButton = current + } else { + moreButton = MoreHeaderButton(color: theme.rootController.navigationBar.buttonColor) + moreButton.isUserInteractionEnabled = true + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) + moreButton.onPressed = { [weak self] in + guard let self, let component = self.component else { + return + } + component.pressed(self) + } + moreButton.contextAction = { [weak self] sourceNode, gesture in + guard let self, let component = self.component else { + return + } + component.contextAction?(self, gesture) + } + self.addSubnode(moreButton) + } + + let buttonSize = CGSize(width: 26.0, height: 44.0) + size.width = buttonSize.width + + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: theme.rootController.navigationBar.buttonColor))) + + moreButton.frame = CGRect(origin: CGPoint(x: iconOffset, y: floor((availableSize.height - buttonSize.height) / 2.0)), size: buttonSize) + } else if let moreButton = self.moreButton { + self.moreButton = nil + moreButton.removeFromSupernode() + } + + return size + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } -*/ diff --git a/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/MoreHeaderButton.swift b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/MoreHeaderButton.swift new file mode 100644 index 0000000000..df8464588b --- /dev/null +++ b/submodules/TelegramUI/Components/ChatListHeaderComponent/Sources/MoreHeaderButton.swift @@ -0,0 +1,168 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import AnimationUI + +public final class MoreHeaderButton: HighlightableButtonNode { + public enum Content { + case image(UIImage?) + case more(UIImage?) + } + + public let referenceNode: ContextReferenceContentNode + public let containerNode: ContextControllerSourceNode + private let iconNode: ASImageNode + private var animationNode: AnimationNode? + + public var contextAction: ((ASDisplayNode, ContextGesture?) -> Void)? + + private var color: UIColor + + public var onPressed: (() -> Void)? + + public init(color: UIColor) { + self.color = color + + self.referenceNode = ContextReferenceContentNode() + self.containerNode = ContextControllerSourceNode() + self.containerNode.animateScale = false + self.iconNode = ASImageNode() + self.iconNode.displaysAsynchronously = false + self.iconNode.displayWithoutProcessing = true + self.iconNode.contentMode = .scaleToFill + + super.init() + + self.containerNode.addSubnode(self.referenceNode) + self.referenceNode.addSubnode(self.iconNode) + self.addSubnode(self.containerNode) + + self.containerNode.shouldBegin = { [weak self] location in + guard let strongSelf = self, let _ = strongSelf.contextAction else { + return false + } + return true + } + self.containerNode.activated = { [weak self] gesture, _ in + guard let strongSelf = self else { + return + } + strongSelf.contextAction?(strongSelf.containerNode, gesture) + } + + self.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 26.0, height: 44.0)) + self.referenceNode.frame = self.containerNode.bounds + + self.iconNode.image = MoreHeaderButton.optionsCircleImage(color: color) + if let image = self.iconNode.image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.hitTestSlop = UIEdgeInsets(top: 0.0, left: -4.0, bottom: 0.0, right: -4.0) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.onPressed?() + } + + private var content: Content? + public func setContent(_ content: Content, animated: Bool = false) { + if case .more = content, self.animationNode == nil { + let iconColor = self.color + let animationNode = AnimationNode(animation: "anim_profilemore", colors: ["Point 2.Group 1.Fill 1": iconColor, + "Point 3.Group 1.Fill 1": iconColor, + "Point 1.Group 1.Fill 1": iconColor], scale: 1.0) + let animationSize = CGSize(width: 22.0, height: 22.0) + animationNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - animationSize.width) / 2.0), y: floor((self.containerNode.bounds.height - animationSize.height) / 2.0)), size: animationSize) + self.addSubnode(animationNode) + self.animationNode = animationNode + } + if animated { + if let snapshotView = self.referenceNode.view.snapshotContentTree() { + snapshotView.frame = self.referenceNode.frame + self.view.addSubview(snapshotView) + + snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in + snapshotView?.removeFromSuperview() + }) + snapshotView.layer.animateScale(from: 1.0, to: 0.1, duration: 0.3, removeOnCompletion: false) + + self.iconNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.iconNode.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3) + + self.animationNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.animationNode?.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3) + } + + switch content { + case let .image(image): + if let image = image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = true + case let .more(image): + if let image = image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = false + } + } else { + self.content = content + switch content { + case let .image(image): + if let image = image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = true + case let .more(image): + if let image = image { + self.iconNode.frame = CGRect(origin: CGPoint(x: floor((self.containerNode.bounds.width - image.size.width) / 2.0), y: floor((self.containerNode.bounds.height - image.size.height) / 2.0)), size: image.size) + } + + self.iconNode.image = image + self.iconNode.isHidden = false + self.animationNode?.isHidden = false + } + } + } + + override public func didLoad() { + super.didLoad() + self.view.isOpaque = false + } + + override public func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: 22.0, height: 44.0) + } + + public func onLayout() { + } + + public func play() { + self.animationNode?.playOnce() + } + + public static func optionsCircleImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 22.0, height: 22.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + context.setStrokeColor(color.cgColor) + let lineWidth: CGFloat = 1.3 + context.setLineWidth(lineWidth) + + context.strokeEllipse(in: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth, dy: lineWidth)) + }) + } +} diff --git a/submodules/TelegramUI/Components/ChatListTitleView/BUILD b/submodules/TelegramUI/Components/ChatListTitleView/BUILD new file mode 100644 index 0000000000..e4441abff4 --- /dev/null +++ b/submodules/TelegramUI/Components/ChatListTitleView/BUILD @@ -0,0 +1,30 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "ChatListTitleView", + module_name = "ChatListTitleView", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit", + "//submodules/AsyncDisplayKit:AsyncDisplayKit", + "//submodules/Display:Display", + "//submodules/TelegramCore:TelegramCore", + "//submodules/TelegramPresentationData:TelegramPresentationData", + "//submodules/ActivityIndicator:ActivityIndicator", + "//submodules/AccountContext", + "//submodules/ComponentFlow", + "//submodules/AppBundle", + "//submodules/TelegramUI/Components/EmojiStatusComponent", + "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", + "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/ChatListUI/Sources/ChatListTitleLockView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleLockView.swift similarity index 100% rename from submodules/ChatListUI/Sources/ChatListTitleLockView.swift rename to submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleLockView.swift diff --git a/submodules/ChatListUI/Sources/ChatListTitleProxyNode.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift similarity index 94% rename from submodules/ChatListUI/Sources/ChatListTitleProxyNode.swift rename to submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift index 617d4fea7b..5ec474b50d 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleProxyNode.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleProxyNode.swift @@ -6,7 +6,7 @@ import TelegramPresentationData import ActivityIndicator import AppBundle -enum ChatTitleProxyStatus { +public enum ChatTitleProxyStatus { case connecting case connected case available @@ -35,11 +35,11 @@ private func generateIcon(color: UIColor, connected: Bool, off: Bool) -> UIImage }) } -final class ChatTitleProxyNode: ASDisplayNode { +public final class ChatTitleProxyNode: ASDisplayNode { private let iconNode: ASImageNode private let activityIndicator: ActivityIndicator - var theme: PresentationTheme { + public var theme: PresentationTheme { didSet { if self.theme !== oldValue { switch self.status { @@ -55,7 +55,7 @@ final class ChatTitleProxyNode: ASDisplayNode { } } - var status: ChatTitleProxyStatus = .connected { + public var status: ChatTitleProxyStatus = .connected { didSet { if self.status != oldValue { switch self.status { @@ -73,7 +73,7 @@ final class ChatTitleProxyNode: ASDisplayNode { } } - init(theme: PresentationTheme) { + public init(theme: PresentationTheme) { self.theme = theme self.iconNode = ASImageNode() diff --git a/submodules/ChatListUI/Sources/ChatListTitleView.swift b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift similarity index 89% rename from submodules/ChatListUI/Sources/ChatListTitleView.swift rename to submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift index 786946a4d4..3c03404c1e 100644 --- a/submodules/ChatListUI/Sources/ChatListTitleView.swift +++ b/submodules/TelegramUI/Components/ChatListTitleView/Sources/ChatListTitleView.swift @@ -14,24 +14,42 @@ import AccountContext private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) -struct NetworkStatusTitle: Equatable { - enum Status: Equatable { +public struct NetworkStatusTitle: Equatable { + public enum Status: Equatable { case premium case emoji(PeerEmojiStatus) } - let text: String - let activity: Bool - let hasProxy: Bool - let connectsViaProxy: Bool - let isPasscodeSet: Bool - let isManuallyLocked: Bool - let peerStatus: Status? + public var text: String + public var activity: Bool + public var hasProxy: Bool + public var connectsViaProxy: Bool + public var isPasscodeSet: Bool + public var isManuallyLocked: Bool + public var peerStatus: Status? + + public init( + text: String, + activity: Bool, + hasProxy: Bool, + connectsViaProxy: Bool, + isPasscodeSet: Bool, + isManuallyLocked: Bool, + peerStatus: Status? + ) { + self.text = text + self.activity = activity + self.hasProxy = hasProxy + self.connectsViaProxy = connectsViaProxy + self.isPasscodeSet = isPasscodeSet + self.isManuallyLocked = isManuallyLocked + self.peerStatus = peerStatus + } } -final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode { +public final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitleTransitionNode { private let context: AccountContext - private let titleNode: ImmediateTextNode + public let titleNode: ImmediateTextNode private let lockView: ChatListTitleLockView private weak var lockSnapshotView: UIView? private let activityIndicator: ActivityIndicator @@ -42,12 +60,14 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer - var openStatusSetup: ((UIView) -> Void)? + public var openStatusSetup: ((UIView) -> Void)? private var validLayout: (CGSize, CGRect)? + public var manualLayout: Bool = false + private var _title: NetworkStatusTitle = NetworkStatusTitle(text: "", activity: false, hasProxy: false, connectsViaProxy: false, isPasscodeSet: false, isManuallyLocked: false, peerStatus: nil) - var title: NetworkStatusTitle { + public var title: NetworkStatusTitle { get { return self._title } @@ -56,7 +76,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } } - func setTitle(_ title: NetworkStatusTitle, animated: Bool) { + public func setTitle(_ title: NetworkStatusTitle, animated: Bool) { let oldValue = self._title self._title = title @@ -170,17 +190,19 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } } - self.setNeedsLayout() + if !self.manualLayout { + self.setNeedsLayout() + } } } - var toggleIsLocked: (() -> Void)? - var openProxySettings: (() -> Void)? + public var toggleIsLocked: (() -> Void)? + public var openProxySettings: (() -> Void)? private var isPasscodeSet = false private var isManuallyLocked = false - var theme: PresentationTheme { + public var theme: PresentationTheme { didSet { self.titleNode.attributedText = NSAttributedString(string: self.title.text, font: titleFont, textColor: self.theme.rootController.navigationBar.primaryTextColor) @@ -191,13 +213,13 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } } - var strings: PresentationStrings { + public var strings: PresentationStrings { didSet { self.proxyButton.accessibilityLabel = self.strings.VoiceOver_Navigation_ProxySettings } } - init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { + public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer) { self.context = context self.theme = theme self.strings = strings @@ -283,19 +305,19 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl self.proxyButton.addTarget(self, action: #selector(self.proxyButtonPressed), for: .touchUpInside) } - required init?(coder aDecoder: NSCoder) { + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override func layoutSubviews() { + override public func layoutSubviews() { super.layoutSubviews() - if let (size, clearBounds) = self.validLayout { + if !self.manualLayout, let (size, clearBounds) = self.validLayout { self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate) } } - func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { + public func updateLayout(size: CGSize, clearBounds: CGRect, transition: ContainedViewLayoutTransition) { self.validLayout = (size, clearBounds) var indicatorPadding: CGFloat = 0.0 @@ -409,7 +431,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl self.openProxySettings?() } - func makeTransitionMirrorNode() -> ASDisplayNode { + public func makeTransitionMirrorNode() -> ASDisplayNode { let snapshotView = self.snapshotView(afterScreenUpdates: false) return ASDisplayNode(viewBlock: { @@ -417,17 +439,17 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl }, didLoad: nil) } - func animateLayoutTransition() { + public func animateLayoutTransition() { } - var proxyButtonFrame: CGRect? { + public var proxyButtonFrame: CGRect? { if !self.proxyNode.isHidden { return proxyNode.frame } return nil } - var lockViewFrame: CGRect? { + public var lockViewFrame: CGRect? { if !self.lockView.isHidden && !self.lockView.frame.isEmpty { return self.lockView.frame } else { @@ -435,7 +457,7 @@ final class ChatListTitleView: UIView, NavigationBarTitleView, NavigationBarTitl } } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let titleCredibilityIconView = self.titleCredibilityIconView, !titleCredibilityIconView.isHidden, titleCredibilityIconView.alpha != 0.0 { if titleCredibilityIconView.bounds.insetBy(dx: -8.0, dy: -8.0).contains(self.convert(point, to: titleCredibilityIconView)) { if let result = titleCredibilityIconView.hitTest(titleCredibilityIconView.bounds.center, with: event) { diff --git a/submodules/TelegramUI/Components/ChatTitleView/BUILD b/submodules/TelegramUI/Components/ChatTitleView/BUILD index 3ef632edef..26fb6712c8 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/BUILD +++ b/submodules/TelegramUI/Components/ChatTitleView/BUILD @@ -30,6 +30,7 @@ swift_library( "//submodules/TelegramUI/Components/EmojiStatusComponent", "//submodules/TelegramUI/Components/AnimationCache:AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer", + "//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift index 0017a5c95a..0e5049fed7 100644 --- a/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift +++ b/submodules/TelegramUI/Components/ChatTitleView/Sources/ChatTitleView.swift @@ -21,11 +21,12 @@ import ComponentFlow import EmojiStatusComponent import AnimationCache import MultiAnimationRenderer +import ComponentDisplayAdapters private let titleFont = Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]) private let subtitleFont = Font.regular(13.0) -public enum ChatTitleContent { +public enum ChatTitleContent: Equatable { public enum ReplyThreadType { case comments case replies @@ -34,6 +35,48 @@ public enum ChatTitleContent { case peer(peerView: PeerView, customTitle: String?, onlineMemberCount: Int32?, isScheduledMessages: Bool, isMuted: Bool?, customMessageCount: Int?) case replyThread(type: ReplyThreadType, count: Int) case custom(String, String?, Bool) + + public static func ==(lhs: ChatTitleContent, rhs: ChatTitleContent) -> Bool { + switch lhs { + case let .peer(peerView, customTitle, onlineMemberCount, isScheduledMessages, isMuted, customMessageCount): + if case let .peer(rhsPeerView, rhsCustomTitle, rhsOnlineMemberCount, rhsIsScheduledMessages, rhsIsMuted, rhsCustomMessageCount) = rhs { + if peerView !== rhsPeerView { + return false + } + if customTitle != rhsCustomTitle { + return false + } + if onlineMemberCount != rhsOnlineMemberCount { + return false + } + if isScheduledMessages != rhsIsScheduledMessages { + return false + } + if isMuted != rhsIsMuted { + return false + } + if customMessageCount != rhsCustomMessageCount { + return false + } + + return true + } else { + return false + } + case let .replyThread(type, count): + if case .replyThread(type, count) = rhs { + return true + } else { + return false + } + case let .custom(title, status, active): + if case .custom(title, status, active) = rhs { + return true + } else { + return false + } + } + } } private enum ChatTitleIcon { @@ -72,6 +115,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { private let button: HighlightTrackingButtonNode + var manualLayout: Bool = false private var validLayout: (CGSize, CGRect)? private var titleLeftIcon: ChatTitleIcon = .none @@ -89,7 +133,9 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } private func updateNetworkStatusNode(networkState: AccountNetworkState, layout: ContainerViewLayout?) { - self.setNeedsLayout() + if self.manualLayout { + self.setNeedsLayout() + } } public var networkState: AccountNetworkState = .online(proxy: nil) { @@ -306,7 +352,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { self.button.isUserInteractionEnabled = isEnabled if !self.updateStatus() { if updated { - if let (size, clearBounds) = self.validLayout { + if !self.manualLayout, let (size, clearBounds) = self.validLayout { self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.2, curve: .easeInOut)) } } @@ -559,7 +605,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { } if self.activityNode.transitionToState(state, animation: .slide) { - if let (size, clearBounds) = self.validLayout { + if !self.manualLayout, let (size, clearBounds) = self.validLayout { self.updateLayout(size: size, clearBounds: clearBounds, transition: .animated(duration: 0.3, curve: .spring)) } return true @@ -642,7 +688,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { override public func layoutSubviews() { super.layoutSubviews() - if let (size, clearBounds) = self.validLayout { + if !self.manualLayout, let (size, clearBounds) = self.validLayout { self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate) } } @@ -657,7 +703,7 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { self.titleContent = titleContent let _ = self.updateStatus() - if let (size, clearBounds) = self.validLayout { + if !self.manualLayout, let (size, clearBounds) = self.validLayout { self.updateLayout(size: size, clearBounds: clearBounds, transition: .immediate) } } @@ -861,3 +907,121 @@ public final class ChatTitleView: UIView, NavigationBarTitleView { snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) } } + +public final class ChatTitleComponent: Component { + public let context: AccountContext + public let theme: PresentationTheme + public let strings: PresentationStrings + public let dateTimeFormat: PresentationDateTimeFormat + public let nameDisplayOrder: PresentationPersonNameOrder + public let content: ChatTitleContent + public let tapped: () -> Void + public let longTapped: () -> Void + + public init( + context: AccountContext, + theme: PresentationTheme, + strings: PresentationStrings, + dateTimeFormat: PresentationDateTimeFormat, + nameDisplayOrder: PresentationPersonNameOrder, + content: ChatTitleContent, + tapped: @escaping () -> Void, + longTapped: @escaping () -> Void + ) { + self.context = context + self.theme = theme + self.strings = strings + self.dateTimeFormat = dateTimeFormat + self.nameDisplayOrder = nameDisplayOrder + self.content = content + self.tapped = tapped + self.longTapped = longTapped + } + + public static func ==(lhs: ChatTitleComponent, rhs: ChatTitleComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.dateTimeFormat != rhs.dateTimeFormat { + return false + } + if lhs.nameDisplayOrder != rhs.nameDisplayOrder { + return false + } + if lhs.content != rhs.content { + return false + } + return true + } + + public final class View: UIView { + private var contentView: ChatTitleView? + + private var component: ChatTitleComponent? + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: ChatTitleComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + + let contentView: ChatTitleView + if let current = self.contentView { + contentView = current + } else { + contentView = ChatTitleView( + context: component.context, + theme: component.theme, + strings: component.strings, + dateTimeFormat: component.dateTimeFormat, + nameDisplayOrder: component.nameDisplayOrder, + animationCache: component.context.animationCache, + animationRenderer: component.context.animationRenderer + ) + contentView.pressed = { [weak self] in + guard let self else { + return + } + self.component?.tapped() + } + contentView.longPressed = { [weak self] in + guard let self else { + return + } + self.component?.longTapped() + } + contentView.manualLayout = true + self.contentView = contentView + self.addSubview(contentView) + } + + if contentView.titleContent != component.content { + contentView.titleContent = component.content + } + + contentView.updateLayout(size: availableSize, clearBounds: CGRect(origin: CGPoint(), size: availableSize), transition: transition.containedViewLayoutTransition) + transition.setFrame(view: contentView, frame: CGRect(origin: CGPoint(), size: availableSize)) + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index c8f4c11eb7..7a8dd08ba1 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -85,6 +85,7 @@ import ChatTitleView import EmojiStatusComponent import ChatTimerScreen import MediaPasteboardUI +import ChatListHeaderComponent #if DEBUG import os.signpost