Bot chat exceptions

This commit is contained in:
Isaac 2024-03-26 19:43:56 +04:00
parent 38bc517122
commit b280856c9e
9 changed files with 955 additions and 292 deletions

View file

@ -890,8 +890,12 @@ public func accountBackupData(postbox: Postbox) -> Signal<AccountBackupData?, No
}
public enum NetworkSpeedLimitedEvent {
public enum DownloadSubject {
case message(MessageId)
}
case upload
case download
case download(DownloadSubject)
}
public class Account {

View file

@ -731,8 +731,16 @@ private final class MultipartFetchManager {
private func processFloodWaitError(error: String) {
if error.hasPrefix("FLOOD_PREMIUM_WAIT") {
self.network.addNetworkSpeedLimitedEvent(event: .download)
var networkSpeedLimitSubject: NetworkSpeedLimitedEvent.DownloadSubject?
if let location = self.parameters?.location {
if let messageId = location.messageId {
networkSpeedLimitSubject = .message(messageId)
}
}
if let subject = networkSpeedLimitSubject {
if error.hasPrefix("FLOOD_PREMIUM_WAIT") {
self.network.addNetworkSpeedLimitedEvent(event: .download(subject))
}
}
}

View file

@ -747,12 +747,16 @@ private final class NetworkSpeedLimitedEventState {
let timestamp = CFAbsoluteTimeGetCurrent()
if self.lastNotifyTimestamp + self.notifyInterval < timestamp {
self.lastNotifyTimestamp = timestamp
return true
} else {
return false
}
}
func markNotifyTimestamp() {
let timestamp = CFAbsoluteTimeGetCurrent()
self.lastNotifyTimestamp = timestamp
}
}
public final class Network: NSObject, MTRequestMessageServiceDelegate {
@ -1161,6 +1165,12 @@ public final class Network: NSObject, MTRequestMessageServiceDelegate {
self.networkSpeedLimitedEventPipe.putNext(event)
}
}
public func markNetworkSpeedLimitDisplayed() {
self.networkSpeedLimitedEventState.with { state in
return state.markNotifyTimestamp()
}
}
}
public func retryRequest<T>(signal: Signal<T, MTRpcError>) -> Signal<T, NoError> {

View file

@ -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)

View file

@ -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))

View file

@ -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<Category>
var peers: [Peer]
init(categories: Set<Category>, 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<Empty>()
private let excludedSection = ComponentView<Empty>()
private let clearSection = ComponentView<Empty>()
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<Int>()
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<EnvironmentType>, 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<Empty>] = []
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<EnvironmentType>, 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)
}
}

View file

@ -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<Int>()
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<EnvironmentType>, 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<Empty>] = []
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(

View file

@ -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

View file

@ -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)