From b280856c9e8002fc2d6fe264b7c12142b02a7934 Mon Sep 17 00:00:00 2001 From: Isaac <> Date: Tue, 26 Mar 2024 19:43:56 +0400 Subject: [PATCH] Bot chat exceptions --- .../Sources/Account/Account.swift | 6 +- .../Sources/Network/MultipartFetch.swift | 12 +- .../Sources/Network/Network.swift | 12 +- .../ChatEmptyNode/Sources/ChatEmptyNode.swift | 15 +- .../Sources/PeerInfoScreen.swift | 6 +- .../Sources/BusinessRecipientListScreen.swift | 709 ++++++++++++++++++ .../Sources/ChatbotSetupScreen.swift | 462 +++++------- .../TelegramUI/Sources/ChatController.swift | 24 + .../Sources/ChatControllerNode.swift | 1 + 9 files changed, 955 insertions(+), 292 deletions(-) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 1114aaa8bc..cae367b7cd 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -890,8 +890,12 @@ public func accountBackupData(postbox: Postbox) -> Signal(signal: Signal) -> Signal { diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index bc9ad9d698..5f443fd4d7 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1432,10 +1432,11 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { self.textNode = ImmediateTextNode() self.textNode.textAlignment = .center self.textNode.maximumNumberOfLines = 0 + self.textNode.lineSpacing = 0.2 self.textMaskNode = LinkHighlightingNode(color: .white) self.textMaskNode.inset = 0.0 - self.textMaskNode.useModernPathCalculation = true + self.textMaskNode.useModernPathCalculation = false self.badgeTextNode = ImmediateTextNode() self.badgeBackgroundView = UIImageView() @@ -1567,11 +1568,11 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { labelRects[i].size.height += 2.0 } if i == labelRects.count - 1 { - labelRects[i].size.height += 2.0 + labelRects[i].size.height += 3.0 } else { - let deltaY = labelRects[i + 1].minY - labelRects[i].maxY - let topDelta = deltaY * 0.5 + 2.0 - let bottomDelta = deltaY * 0.5 - 2.0 + let deltaYHalf = ceil((labelRects[i + 1].minY - labelRects[i].maxY) * 0.5) + let topDelta = deltaYHalf + 0.0 + let bottomDelta = deltaYHalf - 0.0 labelRects[i].size.height += topDelta labelRects[i + 1].origin.y -= bottomDelta labelRects[i + 1].size.height += bottomDelta @@ -1582,7 +1583,7 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { labelRects[i].origin.y -= 12.0 } if !labelRects.isEmpty { - self.textMaskNode.innerRadius = labelRects[0].height * 0.5 + self.textMaskNode.innerRadius = labelRects[0].height * 0.25 self.textMaskNode.outerRadius = labelRects[0].height * 0.5 } self.textMaskNode.updateRects(labelRects) @@ -1595,7 +1596,7 @@ private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { self.badgeTextNode.attributedText = NSAttributedString(string: "how?", font: Font.regular(11.0), textColor: serviceColor.primaryText) let badgeTextSize = self.badgeTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) if let lastLineFrame = labelRects.last { - let badgeTextFrame = CGRect(origin: CGPoint(x: lastLineFrame.maxX - badgeTextSize.width - 3.0, y: textFrame.maxY - badgeTextSize.height), size: badgeTextSize) + let badgeTextFrame = CGRect(origin: CGPoint(x: lastLineFrame.maxX - badgeTextSize.width - 3.0, y: textFrame.maxY - badgeTextSize.height - 3.0 - UIScreenPixel), size: badgeTextSize) self.badgeTextNode.frame = badgeTextFrame let badgeBackgroundFrame = badgeTextFrame.insetBy(dx: -4.0, dy: -1.0) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index c6d8a17fdb..fa3fe29af7 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -10425,8 +10425,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro } let sectionWidth = layout.size.width - insets.left - insets.right - if isFirstSection && sectionItems.first is PeerInfoScreenHeaderItem { - contentHeight -= 16.0 + if isFirstSection && sectionItems.first is PeerInfoScreenHeaderItem && !self.state.isEditing { + if self.data?.peer?.profileColor == nil { + contentHeight -= 16.0 + } } let sectionHeight = sectionNode.update(width: sectionWidth, safeInsets: UIEdgeInsets(), hasCorners: !insets.left.isZero, presentationData: self.presentationData, items: sectionItems, transition: transition) let sectionFrame = CGRect(origin: CGPoint(x: insets.left, y: contentHeight), size: CGSize(width: sectionWidth, height: sectionHeight)) diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift index cd24106bfd..c1bc990d7b 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -7,4 +7,713 @@ import TelegramPresentationData import TelegramCore import AccountContext import ListSectionComponent +import ListActionItemComponent +import PeerListItemComponent +import ViewControllerComponent +import BundleIconComponent +import AvatarNode +import SwiftSignalKit +final class BusinessRecipientListScreenComponent: Component { + typealias EnvironmentType = ViewControllerComponentContainer.Environment + + let context: AccountContext + let initialData: BusinessRecipientListScreen.InitialData + let mode: BusinessRecipientListScreen.Mode + let update: (BusinessRecipientListScreenComponent.PeerList) -> Void + + init( + context: AccountContext, + initialData: BusinessRecipientListScreen.InitialData, + mode: BusinessRecipientListScreen.Mode, + update: @escaping (BusinessRecipientListScreenComponent.PeerList) -> Void + ) { + self.context = context + self.initialData = initialData + self.mode = mode + self.update = update + } + + static func ==(lhs: BusinessRecipientListScreenComponent, rhs: BusinessRecipientListScreenComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + + return true + } + + private final class ScrollView: UIScrollView { + override func touchesShouldCancel(in view: UIView) -> Bool { + return true + } + } + + struct PeerList { + enum Category: Int { + case newChats = 0 + case existingChats = 1 + case contacts = 2 + case nonContacts = 3 + } + + struct Peer { + var peer: EnginePeer + var isContact: Bool + + init(peer: EnginePeer, isContact: Bool) { + self.peer = peer + self.isContact = isContact + } + } + + var categories: Set + var peers: [Peer] + + init(categories: Set, peers: [Peer]) { + self.categories = categories + self.peers = peers + } + } + + final class View: UIView, UIScrollViewDelegate { + private let topOverscrollLayer = SimpleLayer() + private let scrollView: ScrollView + + private let navigationTitle = ComponentView() + private let excludedSection = ComponentView() + private let clearSection = ComponentView() + + private var isUpdating: Bool = false + + private var component: BusinessRecipientListScreenComponent? + private(set) weak var state: EmptyComponentState? + private var environment: EnvironmentType? + + private var peerList = PeerList( + categories: Set(), + peers: [] + ) + + override init(frame: CGRect) { + self.scrollView = ScrollView() + self.scrollView.showsVerticalScrollIndicator = true + self.scrollView.showsHorizontalScrollIndicator = false + self.scrollView.scrollsToTop = false + self.scrollView.delaysContentTouches = false + self.scrollView.canCancelContentTouches = true + self.scrollView.contentInsetAdjustmentBehavior = .never + if #available(iOS 13.0, *) { + self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false + } + self.scrollView.alwaysBounceVertical = true + + super.init(frame: frame) + + self.scrollView.delegate = self + self.addSubview(self.scrollView) + + self.scrollView.layer.addSublayer(self.topOverscrollLayer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + func scrollToTop() { + self.scrollView.setContentOffset(CGPoint(), animated: true) + } + + func attemptNavigation(complete: @escaping () -> Void) -> Bool { + guard let component = self.component else { + return true + } + + component.update(self.peerList) + + return true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.updateScrolling(transition: .immediate) + } + + var scrolledUp = true + private func updateScrolling(transition: Transition) { + let navigationRevealOffsetY: CGFloat = 0.0 + + let navigationAlphaDistance: CGFloat = 16.0 + let navigationAlpha: CGFloat = max(0.0, min(1.0, (self.scrollView.contentOffset.y - navigationRevealOffsetY) / navigationAlphaDistance)) + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + transition.setAlpha(layer: navigationBar.backgroundNode.layer, alpha: navigationAlpha) + transition.setAlpha(layer: navigationBar.stripeNode.layer, alpha: navigationAlpha) + } + + var scrolledUp = false + if navigationAlpha < 0.5 { + scrolledUp = true + } else if navigationAlpha > 0.5 { + scrolledUp = false + } + + if self.scrolledUp != scrolledUp { + self.scrolledUp = scrolledUp + if !self.isUpdating { + self.state?.updated() + } + } + + if let navigationTitleView = self.navigationTitle.view { + transition.setAlpha(view: navigationTitleView, alpha: 1.0) + } + } + + static func makePeerListSetupScreen(context: AccountContext, mode: BusinessRecipientListScreen.Mode, initialPeerList: BusinessRecipientListScreenComponent.PeerList, completion: @escaping (BusinessRecipientListScreenComponent.PeerList) -> Void) -> ViewController { + let presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + enum AdditionalCategoryId: Int { + case existingChats + case newChats + case contacts + case nonContacts + } + + let hasAccessToAllChatsByDefault: Bool + let isExclude: Bool + switch mode { + case .excludeExceptions: + hasAccessToAllChatsByDefault = true + isExclude = false + case .includeExceptions: + hasAccessToAllChatsByDefault = false + isExclude = false + case .excludeUsers: + hasAccessToAllChatsByDefault = false + isExclude = true + } + + let additionalCategories: [ChatListNodeAdditionalCategory] + var selectedCategories = Set() + if isExclude { + additionalCategories = [] + } else { + additionalCategories = [ + ChatListNodeAdditionalCategory( + id: hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple), + title: hasAccessToAllChatsByDefault ? presentationData.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : presentationData.strings.BusinessMessageSetup_Recipients_CategoryNewChats + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.contacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), + title: presentationData.strings.BusinessMessageSetup_Recipients_CategoryContacts + ), + ChatListNodeAdditionalCategory( + id: AdditionalCategoryId.nonContacts.rawValue, + icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow), + smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow), + title: presentationData.strings.BusinessMessageSetup_Recipients_CategoryNonContacts + ) + ] + } + if !isExclude { + for category in initialPeerList.categories { + switch category { + case .existingChats: + selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue) + case .newChats: + selectedCategories.insert(AdditionalCategoryId.newChats.rawValue) + case .contacts: + selectedCategories.insert(AdditionalCategoryId.contacts.rawValue) + case .nonContacts: + selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue) + } + } + } + + //TODO:localize + let controller = context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( + title: "Add Exception", + searchPlaceholder: presentationData.strings.ChatListFilter_AddChatsSearchPlaceholder, + selectedChats: Set(initialPeerList.peers.map(\.peer.id)), + additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), + chatListFilters: nil, + onlyUsers: true + )), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in + })) + controller.navigationPresentation = .modal + + let _ = (controller.result + |> take(1) + |> deliverOnMainQueue).startStandalone(next: { [weak controller] result in + guard case let .result(rawPeerIds, additionalCategoryIds) = result else { + controller?.dismiss() + return + } + + let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in + switch id { + case let .peer(id): + return id + case .deviceContact: + return nil + } + } + + let _ = (context.engine.data.get( + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)) + ), + EngineDataMap( + peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:)) + ) + ) + |> deliverOnMainQueue).start(next: { peerMap, isContactMap in + var peerList = BusinessRecipientListScreenComponent.PeerList(categories: Set(), peers: []) + + if !isExclude { + let mappedCategories = additionalCategoryIds.compactMap { item -> PeerList.Category? in + switch item { + case AdditionalCategoryId.existingChats.rawValue: + return .existingChats + case AdditionalCategoryId.newChats.rawValue: + return .newChats + case AdditionalCategoryId.contacts.rawValue: + return .contacts + case AdditionalCategoryId.nonContacts.rawValue: + return .nonContacts + default: + return nil + } + } + + peerList.categories = Set(mappedCategories) + + peerList.peers.removeAll() + for id in peerIds { + guard let maybePeer = peerMap[id], let peer = maybePeer else { + continue + } + peerList.peers.append(PeerList.Peer( + peer: peer, + isContact: isContactMap[id] ?? false + )) + } + peerList.peers.sort(by: { lhs, rhs in + return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle + }) + } else { + peerList.peers.removeAll() + for id in peerIds { + guard let maybePeer = peerMap[id], let peer = maybePeer else { + continue + } + peerList.peers.append(PeerList.Peer( + peer: peer, + isContact: isContactMap[id] ?? false + )) + } + peerList.peers.sort(by: { lhs, rhs in + return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle + }) + } + + controller?.dismiss() + completion(peerList) + }) + }) + + return controller + } + + private func openPeerListSetup() { + guard let component = self.component, let environment = self.environment else { + return + } + let controller = BusinessRecipientListScreenComponent.View.makePeerListSetupScreen( + context: component.context, + mode: component.mode, + initialPeerList: self.peerList, + completion: { [weak self] peerList in + guard let self else { + return + } + self.peerList = peerList + self.state?.updated(transition: .immediate) + } + ) + environment.controller()?.push(controller) + } + + func update(component: BusinessRecipientListScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.isUpdating = true + defer { + self.isUpdating = false + } + + if self.component == nil { + self.peerList = component.initialData.peerList + } + + let environment = environment[EnvironmentType.self].value + let themeUpdated = self.environment?.theme !== environment.theme + self.environment = environment + + self.component = component + self.state = state + + let alphaTransition = transition.animation.isImmediate ? transition : .easeInOut(duration: 0.25) + + if themeUpdated { + self.backgroundColor = environment.theme.list.blocksBackgroundColor + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + + let title: String + switch component.mode { + case .excludeExceptions, .excludeUsers: + title = "Excluded Chats" + case .includeExceptions: + title = "Included Chats" + } + + //TODO:localize + let navigationTitleSize = self.navigationTitle.update( + transition: transition, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 100.0) + ) + let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) / 2.0), y: environment.statusBarHeight + floor((environment.navigationHeight - environment.statusBarHeight - navigationTitleSize.height) / 2.0)), size: navigationTitleSize) + if let navigationTitleView = self.navigationTitle.view { + if navigationTitleView.superview == nil { + if let controller = self.environment?.controller(), let navigationBar = controller.navigationBar { + navigationBar.view.addSubview(navigationTitleView) + } + } + transition.setFrame(view: navigationTitleView, frame: navigationTitleFrame) + } + + let bottomContentInset: CGFloat = 24.0 + let sideInset: CGFloat = 16.0 + environment.safeInsets.left + let sectionSpacing: CGFloat = 32.0 + + var contentHeight: CGFloat = 0.0 + + contentHeight += environment.navigationHeight + contentHeight += 16.0 + + var excludedSectionItems: [AnyComponentWithIdentity] = [] + excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Add Users", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemAccentColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .left, spacing: 2.0)), + leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( + name: "Chat List/AddIcon", + tintColor: environment.theme.list.itemAccentColor + ))), + accessory: nil, + action: { [weak self] _ in + guard let self else { + return + } + self.openPeerListSetup() + } + )))) + for category in self.peerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) { + let title: String + let icon: String + let color: AvatarBackgroundColor + switch category { + case .newChats: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats + icon = "Chat List/Filters/NewChats" + color = .purple + case .existingChats: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats + icon = "Chat List/Filters/Chats" + color = .purple + case .contacts: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryContacts + icon = "Chat List/Filters/Contact" + color = .blue + case .nonContacts: + title = environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts + icon = "Chat List/Filters/User" + color = .yellow + } + excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: 0.0, + title: title, + avatar: PeerListItemComponent.Avatar( + icon: icon, + color: color, + clipStyle: .roundedRect + ), + peer: nil, + subtitle: nil, + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: false, + action: { peer, _, _ in + }, + inlineActions: PeerListItemComponent.InlineActionsState( + actions: [PeerListItemComponent.InlineAction( + id: AnyHashable(0), + title: environment.strings.Common_Delete, + color: .destructive, + action: { [weak self] in + guard let self else { + return + } + + self.peerList.categories.remove(category) + self.state?.updated(transition: .spring(duration: 0.4)) + + if self.peerList.categories.isEmpty && self.peerList.peers.isEmpty { + let _ = self.attemptNavigation(complete: {}) + self.environment?.controller()?.dismiss() + } + } + )] + ) + )))) + } + for peer in self.peerList.peers { + excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + style: .generic, + sideInset: 0.0, + title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer.peer, + subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, + subtitleAccessory: .none, + presence: nil, + selectionState: .none, + hasNext: false, + action: { peer, _, _ in + }, + inlineActions: PeerListItemComponent.InlineActionsState( + actions: [PeerListItemComponent.InlineAction( + id: AnyHashable(0), + title: environment.strings.Common_Delete, + color: .destructive, + action: { [weak self] in + guard let self else { + return + } + self.peerList.peers.removeAll(where: { $0.peer.id == peer.peer.id }) + self.state?.updated(transition: .spring(duration: 0.4)) + + if self.peerList.categories.isEmpty && self.peerList.peers.isEmpty { + let _ = self.attemptNavigation(complete: {}) + self.environment?.controller()?.dismiss() + } + } + )] + ) + )))) + } + + let excludedSectionSize = self.excludedSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: excludedSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let excludedSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: excludedSectionSize) + if let excludedSectionView = self.excludedSection.view { + if excludedSectionView.superview == nil { + self.scrollView.addSubview(excludedSectionView) + } + transition.setFrame(view: excludedSectionView, frame: excludedSectionFrame) + } + contentHeight += excludedSectionSize.height + contentHeight += sectionSpacing + + let clearSectionSize = self.clearSection.update( + transition: transition, + component: AnyComponent(ListSectionComponent( + theme: environment.theme, + header: nil, + footer: nil, + items: [AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: "Remove All Exceptions", + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemDestructiveColor + )), + maximumNumberOfLines: 1 + ))), + ], alignment: .center, spacing: 2.0, fillWidth: true)), + leftIcon: nil, + icon: nil, + accessory: .none, + action: { [weak self] _ in + guard let self else { + return + } + self.peerList.categories.removeAll() + self.peerList.peers.removeAll() + + self.state?.updated(transition: .spring(duration: 0.4)) + + if self.peerList.categories.isEmpty && self.peerList.peers.isEmpty { + let _ = self.attemptNavigation(complete: {}) + self.environment?.controller()?.dismiss() + } + } + )))] + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let clearSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: clearSectionSize) + if let clearSectionView = self.clearSection.view { + if clearSectionView.superview == nil { + self.scrollView.addSubview(clearSectionView) + } + transition.setFrame(view: clearSectionView, frame: clearSectionFrame) + alphaTransition.setAlpha(view: clearSectionView, alpha: (self.peerList.categories.isEmpty && self.peerList.peers.isEmpty) ? 0.0 : 1.0) + } + if !self.peerList.categories.isEmpty || !self.peerList.peers.isEmpty { + contentHeight += clearSectionSize.height + } + + contentHeight += bottomContentInset + contentHeight += environment.safeInsets.bottom + + let previousBounds = self.scrollView.bounds + + let contentSize = CGSize(width: availableSize.width, height: contentHeight) + if self.scrollView.frame != CGRect(origin: CGPoint(), size: availableSize) { + self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) + } + if self.scrollView.contentSize != contentSize { + self.scrollView.contentSize = contentSize + } + let scrollInsets = UIEdgeInsets(top: environment.navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) + if self.scrollView.scrollIndicatorInsets != scrollInsets { + self.scrollView.scrollIndicatorInsets = scrollInsets + } + + if !previousBounds.isEmpty, !transition.animation.isImmediate { + let bounds = self.scrollView.bounds + if bounds.maxY != previousBounds.maxY { + let offsetY = previousBounds.maxY - bounds.maxY + transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: offsetY), to: CGPoint(), additive: true) + } + } + + self.topOverscrollLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -3000.0), size: CGSize(width: availableSize.width, height: 3000.0)) + + self.updateScrolling(transition: transition) + + return availableSize + } + } + + func makeView() -> View { + return View() + } + + 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) + } +} + +final class BusinessRecipientListScreen: ViewControllerComponentContainer { + final class InitialData { + fileprivate let peerList: BusinessRecipientListScreenComponent.PeerList + + fileprivate init( + peerList: BusinessRecipientListScreenComponent.PeerList + ) { + self.peerList = peerList + } + } + + enum Mode { + case includeExceptions + case excludeExceptions + case excludeUsers + } + + private let context: AccountContext + + init(context: AccountContext, peerList: BusinessRecipientListScreenComponent.PeerList, mode: Mode, update: @escaping (BusinessRecipientListScreenComponent.PeerList) -> Void) { + self.context = context + + super.init(context: context, component: BusinessRecipientListScreenComponent( + context: context, + initialData: InitialData(peerList: peerList), + mode: mode, + update: update + ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) + + let presentationData = context.sharedContext.currentPresentationData.with { $0 } + self.title = "" + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? BusinessRecipientListScreenComponent.View else { + return + } + componentView.scrollToTop() + } + + self.attemptNavigation = { [weak self] complete in + guard let self, let componentView = self.node.hostView.componentView as? BusinessRecipientListScreenComponent.View else { + return true + } + + return componentView.attemptNavigation(complete: complete) + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + } + + @objc private func cancelPressed() { + self.dismiss() + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 6846aefb26..9e804b1739 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -323,155 +323,157 @@ final class ChatbotSetupScreenComponent: Component { return } - enum AdditionalCategoryId: Int { - case existingChats - case newChats - case contacts - case nonContacts - } - - let additionalCategories: [ChatListNodeAdditionalCategory] - var selectedCategories = Set() - if isExclude { - additionalCategories = [] - } else { - additionalCategories = [ - ChatListNodeAdditionalCategory( - id: self.hasAccessToAllChatsByDefault ? AdditionalCategoryId.existingChats.rawValue : AdditionalCategoryId.newChats.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), cornerRadius: 12.0, color: .purple), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: self.hasAccessToAllChatsByDefault ? "Chat List/Filters/Chats" : "Chat List/Filters/NewChats"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .purple), - title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats : environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.contacts.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), cornerRadius: 12.0, color: .blue), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Contact"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue), - title: environment.strings.BusinessMessageSetup_Recipients_CategoryContacts - ), - ChatListNodeAdditionalCategory( - id: AdditionalCategoryId.nonContacts.rawValue, - icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), cornerRadius: 12.0, color: .yellow), - smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/User"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .yellow), - title: environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts - ) - ] - } + var mappedPeerList = BusinessRecipientListScreenComponent.PeerList(categories: Set(), peers: []) if !isExclude { for category in self.additionalPeerList.categories { switch category { case .existingChats: - selectedCategories.insert(AdditionalCategoryId.existingChats.rawValue) + mappedPeerList.categories.insert(.existingChats) case .newChats: - selectedCategories.insert(AdditionalCategoryId.newChats.rawValue) + mappedPeerList.categories.insert(.newChats) case .contacts: - selectedCategories.insert(AdditionalCategoryId.contacts.rawValue) + mappedPeerList.categories.insert(.contacts) case .nonContacts: - selectedCategories.insert(AdditionalCategoryId.nonContacts.rawValue) + mappedPeerList.categories.insert(.nonContacts) } } } - - let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( - title: (self.hasAccessToAllChatsByDefault || isExclude) ? environment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : environment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle, - searchPlaceholder: environment.strings.ChatListFilter_AddChatsSearchPlaceholder, - selectedChats: isExclude ? Set(self.additionalPeerList.excludePeers.map(\.peer.id)) : Set(self.additionalPeerList.peers.map(\.peer.id)), - additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: selectedCategories), - chatListFilters: nil, - onlyUsers: true - )), options: [], filters: [], alwaysEnabled: true, limit: 100, reachedLimit: { _ in - })) - controller.navigationPresentation = .modal - - let _ = (controller.result - |> take(1) - |> deliverOnMainQueue).startStandalone(next: { [weak self, weak controller] result in - guard let self, let component = self.component, case let .result(rawPeerIds, additionalCategoryIds) = result else { - controller?.dismiss() - return + if isExclude { + for peer in self.additionalPeerList.excludePeers { + mappedPeerList.peers.append(BusinessRecipientListScreenComponent.PeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + )) } - - let peerIds = rawPeerIds.compactMap { id -> EnginePeer.Id? in - switch id { - case let .peer(id): - return id - case .deviceContact: - return nil + } else { + for peer in self.additionalPeerList.peers { + mappedPeerList.peers.append(BusinessRecipientListScreenComponent.PeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + )) + } + } + + let mode: BusinessRecipientListScreen.Mode + if isExclude { + mode = .excludeUsers + } else { + if self.hasAccessToAllChatsByDefault { + mode = .excludeExceptions + } else { + mode = .includeExceptions + } + } + + if mappedPeerList.categories.isEmpty && mappedPeerList.peers.isEmpty { + let controller = BusinessRecipientListScreenComponent.View.makePeerListSetupScreen( + context: component.context, + mode: mode, + initialPeerList: mappedPeerList, + completion: { [weak self] peerList in + guard let self, let component = self.component, let environment = self.environment else { + return + } + + environment.controller()?.push(BusinessRecipientListScreen( + context: component.context, + peerList: peerList, + mode: mode, + update: { [weak self] updatedPeerList in + guard let self else { + return + } + + switch mode { + case .excludeExceptions, .includeExceptions: + self.additionalPeerList.peers.removeAll() + for peer in updatedPeerList.peers { + self.additionalPeerList.peers.append(AdditionalPeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + )) + + self.additionalPeerList.excludePeers.removeAll(where: { $0.peer.id == peer.peer.id }) + } + self.additionalPeerList.categories.removeAll() + for category in updatedPeerList.categories { + switch category { + case .existingChats: + self.additionalPeerList.categories.insert(.existingChats) + case .newChats: + self.additionalPeerList.categories.insert(.newChats) + case .contacts: + self.additionalPeerList.categories.insert(.contacts) + case .nonContacts: + self.additionalPeerList.categories.insert(.nonContacts) + } + } + case .excludeUsers: + for peer in updatedPeerList.peers { + self.additionalPeerList.excludePeers.append(AdditionalPeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + )) + + self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id }) + } + } + + self.state?.updated(transition: .immediate) + } + )) } - } - - let _ = (component.context.engine.data.get( - EngineDataMap( - peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init(id:)) - ), - EngineDataMap( - peerIds.map(TelegramEngine.EngineData.Item.Peer.IsContact.init(id:)) - ) ) - |> deliverOnMainQueue).start(next: { [weak self] peerMap, isContactMap in - guard let self else { - return + environment.controller()?.push(controller) + } else { + environment.controller()?.push(BusinessRecipientListScreen( + context: component.context, + peerList: mappedPeerList, + mode: mode, + update: { [weak self] updatedPeerList in + guard let self else { + return + } + + switch mode { + case .excludeExceptions, .includeExceptions: + self.additionalPeerList.peers.removeAll() + for peer in updatedPeerList.peers { + self.additionalPeerList.peers.append(AdditionalPeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + )) + + self.additionalPeerList.excludePeers.removeAll(where: { $0.peer.id == peer.peer.id }) + } + self.additionalPeerList.categories.removeAll() + for category in updatedPeerList.categories { + switch category { + case .existingChats: + self.additionalPeerList.categories.insert(.existingChats) + case .newChats: + self.additionalPeerList.categories.insert(.newChats) + case .contacts: + self.additionalPeerList.categories.insert(.contacts) + case .nonContacts: + self.additionalPeerList.categories.insert(.nonContacts) + } + } + case .excludeUsers: + for peer in updatedPeerList.peers { + self.additionalPeerList.excludePeers.append(AdditionalPeerList.Peer( + peer: peer.peer, + isContact: peer.isContact + )) + + self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id }) + } + } + + self.state?.updated(transition: .immediate) } - - if !isExclude { - let mappedCategories = additionalCategoryIds.compactMap { item -> AdditionalPeerList.Category? in - switch item { - case AdditionalCategoryId.existingChats.rawValue: - return .existingChats - case AdditionalCategoryId.newChats.rawValue: - return .newChats - case AdditionalCategoryId.contacts.rawValue: - return .contacts - case AdditionalCategoryId.nonContacts.rawValue: - return .nonContacts - default: - return nil - } - } - - self.additionalPeerList.categories = Set(mappedCategories) - - self.additionalPeerList.peers.removeAll() - for id in peerIds { - guard let maybePeer = peerMap[id], let peer = maybePeer else { - continue - } - self.additionalPeerList.peers.append(AdditionalPeerList.Peer( - peer: peer, - isContact: isContactMap[id] ?? false - )) - } - self.additionalPeerList.peers.sort(by: { lhs, rhs in - return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle - }) - - let includedIds = self.additionalPeerList.peers.map(\.peer.id) - self.additionalPeerList.excludePeers.removeAll(where: { includedIds.contains($0.peer.id) }) - } else { - self.additionalPeerList.excludePeers.removeAll() - for id in peerIds { - guard let maybePeer = peerMap[id], let peer = maybePeer else { - continue - } - self.additionalPeerList.excludePeers.append(AdditionalPeerList.Peer( - peer: peer, - isContact: isContactMap[id] ?? false - )) - } - self.additionalPeerList.excludePeers.sort(by: { lhs, rhs in - return lhs.peer.debugDisplayTitle < rhs.peer.debugDisplayTitle - }) - - let excludedIds = self.additionalPeerList.excludePeers.map(\.peer.id) - self.additionalPeerList.peers.removeAll(where: { excludedIds.contains($0.peer.id) }) - } - - self.state?.updated(transition: .immediate) - - controller?.dismiss() - }) - }) - - self.environment?.controller()?.push(controller) + )) + } } func update(component: ChatbotSetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -832,24 +834,50 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += accessSectionSize.height contentHeight += sectionSpacing + //TODO:localize + let categoriesAndUsersItemCount = self.additionalPeerList.categories.count + self.additionalPeerList.peers.count + let excludedSectionValue: String + if categoriesAndUsersItemCount == 0 { + excludedSectionValue = "Add" + } else if categoriesAndUsersItemCount == 1 { + excludedSectionValue = "1 item" + } else { + excludedSectionValue = "\(categoriesAndUsersItemCount) items" + } + + let excludedUsersItemCount = self.additionalPeerList.excludePeers.count + let excludedUsersValue: String + if excludedUsersItemCount == 0 { + excludedUsersValue = "Add" + } else if excludedUsersItemCount == 1 { + excludedUsersValue = "1 item" + } else { + excludedUsersValue = "\(excludedUsersItemCount) items" + } + var excludedSectionItems: [AnyComponentWithIdentity] = [] excludedSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( theme: environment.theme, title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_AddExclude : environment.strings.BusinessMessageSetup_Recipients_AddInclude, + string: self.hasAccessToAllChatsByDefault ? "Excluded Chats" : "Included Chats", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemAccentColor + textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( - name: "Chat List/AddIcon", - tintColor: environment.theme.list.itemAccentColor - ))), - accessory: nil, + leftIcon: nil, + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: excludedSectionValue, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )))), + accessory: .arrow, action: { [weak self] _ in guard let self else { return @@ -857,109 +885,12 @@ final class ChatbotSetupScreenComponent: Component { self.openAdditionalPeerListSetup(isExclude: false) } )))) - for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) { - let title: String - let icon: String - let color: AvatarBackgroundColor - switch category { - case .newChats: - title = environment.strings.BusinessMessageSetup_Recipients_CategoryNewChats - icon = "Chat List/Filters/NewChats" - color = .purple - case .existingChats: - title = environment.strings.BusinessMessageSetup_Recipients_CategoryExistingChats - icon = "Chat List/Filters/Chats" - color = .purple - case .contacts: - title = environment.strings.BusinessMessageSetup_Recipients_CategoryContacts - icon = "Chat List/Filters/Contact" - color = .blue - case .nonContacts: - title = environment.strings.BusinessMessageSetup_Recipients_CategoryNonContacts - icon = "Chat List/Filters/User" - color = .yellow - } - excludedSectionItems.append(AnyComponentWithIdentity(id: category, component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - style: .generic, - sideInset: 0.0, - title: title, - avatar: PeerListItemComponent.Avatar( - icon: icon, - color: color, - clipStyle: .roundedRect - ), - peer: nil, - subtitle: nil, - subtitleAccessory: .none, - presence: nil, - selectionState: .none, - hasNext: false, - action: { peer, _, _ in - }, - inlineActions: PeerListItemComponent.InlineActionsState( - actions: [PeerListItemComponent.InlineAction( - id: AnyHashable(0), - title: environment.strings.Common_Delete, - color: .destructive, - action: { [weak self] in - guard let self else { - return - } - self.additionalPeerList.categories.remove(category) - self.state?.updated(transition: .spring(duration: 0.4)) - } - )] - ) - )))) - } - for peer in self.additionalPeerList.peers { - excludedSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - style: .generic, - sideInset: 0.0, - title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer.peer, - subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, - subtitleAccessory: .none, - presence: nil, - selectionState: .none, - hasNext: false, - action: { peer, _, _ in - }, - inlineActions: PeerListItemComponent.InlineActionsState( - actions: [PeerListItemComponent.InlineAction( - id: AnyHashable(0), - title: environment.strings.Common_Delete, - color: .destructive, - action: { [weak self] in - guard let self else { - return - } - self.additionalPeerList.peers.removeAll(where: { $0.peer.id == peer.peer.id }) - self.state?.updated(transition: .spring(duration: 0.4)) - } - )] - ) - )))) - } let excludedSectionSize = self.excludedSection.update( transition: transition, component: AnyComponent(ListSectionComponent( theme: environment.theme, - header: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString( - string: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludedSectionHeader : environment.strings.BusinessMessageSetup_Recipients_IncludedSectionHeader, - font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), - textColor: environment.theme.list.freeTextColor - )), - maximumNumberOfLines: 0 - )), + header: nil, footer: AnyComponent(MultilineTextComponent( text: .markdown( text: self.hasAccessToAllChatsByDefault ? environment.strings.ChatbotSetup_Recipients_ExcludedSectionFooter : environment.strings.ChatbotSetup_Recipients_IncludedSectionFooter, @@ -997,18 +928,23 @@ final class ChatbotSetupScreenComponent: Component { title: AnyComponent(VStack([ AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( text: .plain(NSAttributedString( - string: environment.strings.BusinessMessageSetup_Recipients_AddExclude, + string: "Excluded Chats", font: Font.regular(presentationData.listsFontSize.baseDisplaySize), - textColor: environment.theme.list.itemAccentColor + textColor: environment.theme.list.itemPrimaryTextColor )), maximumNumberOfLines: 1 ))), ], alignment: .left, spacing: 2.0)), - leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent( - name: "Chat List/AddIcon", - tintColor: environment.theme.list.itemAccentColor - ))), - accessory: nil, + leftIcon: nil, + icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: excludedUsersValue, + font: Font.regular(presentationData.listsFontSize.baseDisplaySize), + textColor: environment.theme.list.itemSecondaryTextColor + )), + maximumNumberOfLines: 1 + )))), + accessory: .arrow, action: { [weak self] _ in guard let self else { return @@ -1016,38 +952,6 @@ final class ChatbotSetupScreenComponent: Component { self.openAdditionalPeerListSetup(isExclude: true) } )))) - for peer in self.additionalPeerList.excludePeers { - excludedUsersSectionItems.append(AnyComponentWithIdentity(id: peer.peer.id, component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - style: .generic, - sideInset: 0.0, - title: peer.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer.peer, - subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, - subtitleAccessory: .none, - presence: nil, - selectionState: .none, - hasNext: false, - action: { peer, _, _ in - }, - inlineActions: PeerListItemComponent.InlineActionsState( - actions: [PeerListItemComponent.InlineAction( - id: AnyHashable(0), - title: environment.strings.Common_Delete, - color: .destructive, - action: { [weak self] in - guard let self else { - return - } - self.additionalPeerList.excludePeers.removeAll(where: { $0.peer.id == peer.peer.id }) - self.state?.updated(transition: .spring(duration: 0.4)) - } - )] - ) - )))) - } let excludedUsersSectionSize = self.excludedUsersSection.update( transition: transition, component: AnyComponent(ListSectionComponent( diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 9a6770893c..ffb4adfa5c 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -11830,6 +11830,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G return } + switch event { + case let .download(subject): + if case let .message(messageId) = subject { + var isVisible = false + self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { + for (message, _) in item.content { + if message.id == messageId { + isVisible = true + } + } + } + } + + if !isVisible { + return + } + } + case .upload: + break + } + let timestamp = CFAbsoluteTimeGetCurrent() if lastEventTimestamp + 10.0 < timestamp { lastEventTimestamp = timestamp @@ -11858,6 +11880,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } let content: UndoOverlayContent = .universal(animation: "anim_speed_low", scale: 0.066, colors: [:], title: title, text: text, customUndoText: nil, timeout: 5.0) + self.context.account.network.markNetworkSpeedLimitDisplayed() + self.present(UndoOverlayController(presentationData: self.presentationData, content: content, elevatedLayout: false, position: .top, action: { [weak self] action in guard let self else { return false diff --git a/submodules/TelegramUI/Sources/ChatControllerNode.swift b/submodules/TelegramUI/Sources/ChatControllerNode.swift index 1e88539f5e..dd932f2c61 100644 --- a/submodules/TelegramUI/Sources/ChatControllerNode.swift +++ b/submodules/TelegramUI/Sources/ChatControllerNode.swift @@ -1005,6 +1005,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { mappedType = .botInfo } emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, subject: .emptyChat(mappedType), loadingNode: wasLoading && self.loadingNode.supernode != nil ? self.loadingNode : nil, backgroundNode: self.backgroundNode, size: size, insets: insets, transition: .immediate) + emptyNode.frame = CGRect(origin: CGPoint(), size: size) } if animated { emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)