diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 1249fbcf45..4f5f59f855 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -11362,7 +11362,7 @@ Sorry for the inconvenience."; "Business.QuickReplies" = "Quick Replies"; "Business.GreetingMessages" = "Greeting Messages"; "Business.AwayMessages" = "Away Messages"; -"Business.Chatbots" = "Chatbots"; +"Business.ChatbotsItem" = "Chatbots"; "Business.Intro" = "Intro"; "Business.LocationInfo" = "Display the location of your business on your account."; @@ -11409,6 +11409,7 @@ Sorry for the inconvenience."; "ChatList.ItemMenuEdit" = "Edit"; "ChatList.ItemMenuDelete" = "Delete"; "ChatList.PeerTypeNonContact" = "non-contact"; +"ChatList.PeerTypeNonContactUser" = "non-contact"; "ChatListFilter.TagLabelNoTag" = "NO TAG"; "ChatListFilter.TagLabelPremiumExpired" = "PREMIUM EXPIRED"; @@ -11575,7 +11576,7 @@ Sorry for the inconvenience."; "BusinessLocationSetup.AlertUnsavedChanges.Text" = "You have unsaved changes."; "BusinessLocationSetup.AlertUnsavedChanges.ResetAction" = "Revert"; -"ChatbotSetup.Title" = "Chatbots"; +"ChatbotSetup.TitleItem" = "Chatbots"; "ChatbotSetup.Text" = "Add a bot to your account to help you automatically process and respond to the messages you receive. [Learn More >]()"; "ChatbotSetup.TextLink" = "https://telegram.org"; diff --git a/submodules/ChatListUI/Sources/Node/ChatListNode.swift b/submodules/ChatListUI/Sources/Node/ChatListNode.swift index fb089b4510..4ea6c60c6f 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListNode.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListNode.swift @@ -4165,14 +4165,14 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres if isContact { return (strings.ChatList_PeerTypeContact, false, false, nil) } else { - return (strings.ChatList_PeerTypeNonContact, false, false, nil) + return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) } } } else if case .secretChat = peer { if isContact { return (strings.ChatList_PeerTypeContact, false, false, nil) } else { - return (strings.ChatList_PeerTypeNonContact, false, false, nil) + return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) } } else if case .legacyGroup = peer { return (strings.ChatList_PeerTypeGroup, false, false, nil) @@ -4183,7 +4183,7 @@ private func statusStringForPeerType(accountPeerId: EnginePeer.Id, strings: Pres return (strings.ChatList_PeerTypeChannel, false, false, nil) } } - return (strings.ChatList_PeerTypeNonContact, false, false, nil) + return (strings.ChatList_PeerTypeNonContactUser, false, false, nil) } public class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 999fd2ad77..c5520beba9 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -633,7 +633,7 @@ public enum PremiumPerk: CaseIterable { case .businessAwayMessage: return strings.Business_AwayMessages case .businessChatBots: - return strings.Business_Chatbots + return strings.Business_ChatbotsItem case .businessIntro: return strings.Business_Intro } diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index 4f7c3b6a0c..86568579be 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -971,7 +971,7 @@ public class PremiumLimitsListScreen: ViewController { videoFile: videos["business_bots"], decoration: .business )), - title: strings.Business_Chatbots, + title: strings.Business_ChatbotsItem, text: strings.Business_ChatbotsInfo, textColor: textColor ) diff --git a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift index e0a1e24c10..010283f7cb 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/StoreMessage_Telegram.swift @@ -600,7 +600,7 @@ func messageTextEntitiesFromApiEntities(_ entities: [Api.MessageEntity]) -> [Mes extension StoreMessage { convenience init?(apiMessage: Api.Message, accountPeerId: PeerId, peerIsForum: Bool, namespace: MessageId.Namespace = Namespaces.Message.Cloud) { switch apiMessage { - case let .message(flags, _, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId): + case let .message(flags, flags2, id, fromId, boosts, chatPeerId, savedPeerId, fwdFrom, viaBotId, viaBusinessBotId, replyTo, date, message, media, replyMarkup, entities, views, forwards, replies, editDate, postAuthor, groupingId, reactions, restrictionReason, ttlPeriod, quickReplyShortcutId): let resolvedFromId = fromId?.peerId ?? chatPeerId.peerId var namespace = namespace @@ -906,7 +906,7 @@ extension StoreMessage { storeFlags.insert(.IsForumTopic) } - if (flags & (1 << 4)) != 0 || (flags & (1 << 13)) != 0 { + if (flags & (1 << 4)) != 0 || (flags & (1 << 13)) != 0 || (flags2 & (1 << 1)) != 0 { var notificationFlags: NotificationInfoMessageAttributeFlags = [] if (flags & (1 << 4)) != 0 { notificationFlags.insert(.personal) @@ -916,6 +916,9 @@ extension StoreMessage { if (flags & (1 << 13)) != 0 { notificationFlags.insert(.muted) } + if (flags2 & (1 << 1)) != 0 { + notificationFlags.insert(.automaticMessage) + } attributes.append(NotificationInfoMessageAttribute(flags: notificationFlags)) } diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index 7dfad231f5..5b6ffebfd7 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -3452,7 +3452,17 @@ func replayFinalState( if message.flags.contains(.Incoming) { addedOperationIncomingMessageIds.append(id) if let authorId = message.authorId { - recordPeerActivityTimestamp(peerId: authorId, timestamp: message.timestamp, into: &peerActivityTimestamps) + var isAutomatic = false + for attribute in message.attributes { + if let attribute = attribute as? NotificationInfoMessageAttribute { + if attribute.flags.contains(.automaticMessage) { + isAutomatic = true + } + } + } + if !isAutomatic { + recordPeerActivityTimestamp(peerId: authorId, timestamp: message.timestamp, into: &peerActivityTimestamps) + } } } if message.flags.contains(.WasScheduled) { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NotificationInfoMessageAttribute.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NotificationInfoMessageAttribute.swift index 95c7dff56f..39f51dc526 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_NotificationInfoMessageAttribute.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_NotificationInfoMessageAttribute.swift @@ -12,9 +12,9 @@ public struct NotificationInfoMessageAttributeFlags: OptionSet { self.rawValue = 0 } - public static let muted = NotificationInfoMessageAttributeFlags(rawValue: 1) - public static let personal = NotificationInfoMessageAttributeFlags(rawValue: 2) - + public static let muted = NotificationInfoMessageAttributeFlags(rawValue: 1 << 0) + public static let personal = NotificationInfoMessageAttributeFlags(rawValue: 1 << 1) + public static let automaticMessage = NotificationInfoMessageAttributeFlags(rawValue: 1 << 2) } public class NotificationInfoMessageAttribute: MessageAttribute { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift index d122db451b..ca973fca3f 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Data/PeersData.swift @@ -1613,34 +1613,6 @@ public extension TelegramEngine.EngineData.Item { } } - public struct BusinessIntro: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { - public typealias Result = CachedTelegramBusinessIntro? - - fileprivate var id: EnginePeer.Id - public var mapKey: EnginePeer.Id { - return self.id - } - - public init(id: EnginePeer.Id) { - self.id = id - } - - var key: PostboxViewKey { - return .cachedPeerData(peerId: self.id) - } - - func extract(view: PostboxView) -> Result { - guard let view = view as? CachedPeerDataView else { - preconditionFailure() - } - if let cachedData = view.cachedPeerData as? CachedUserData { - return cachedData.businessIntro - } else { - return nil - } - } - } - public struct ChatManagingBot: TelegramEngineDataItem, TelegramEngineMapKeyDataItem, PostboxViewDataItem { public typealias Result = PeerStatusSettings.ManagingBot? diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift index 7b02fe98a0..cd762c591d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/BotWebView.swift @@ -371,6 +371,9 @@ func _internal_toggleChatManagingBotIsPaused(account: Account, chatId: EnginePee if let managingBot = peerStatusSettings.managingBot { isPaused = !managingBot.isPaused peerStatusSettings.managingBot?.isPaused = isPaused + if !isPaused { + peerStatusSettings.managingBot?.canReply = true + } } return current.withUpdatedPeerStatusSettings(peerStatusSettings) @@ -412,6 +415,35 @@ func _internal_removeChatManagingBot(account: Account, chatId: EnginePeer.Id) -> return current } }) + transaction.updatePeerCachedData(peerIds: Set([account.peerId]), update: { _, current in + guard let current = current as? CachedUserData else { + return current + } + + if let connectedBot = current.connectedBot { + var additionalPeers = connectedBot.recipients.additionalPeers + var excludePeers = connectedBot.recipients.excludePeers + if connectedBot.recipients.exclude { + additionalPeers.insert(chatId) + } else { + additionalPeers.remove(chatId) + excludePeers.insert(chatId) + } + + return current.withUpdatedConnectedBot(TelegramAccountConnectedBot( + id: connectedBot.id, + recipients: TelegramBusinessRecipients( + categories: connectedBot.recipients.categories, + additionalPeers: additionalPeers, + excludePeers: excludePeers, + exclude: connectedBot.recipients.exclude + ), + canReply: connectedBot.canReply + )) + } else { + return current + } + }) } |> mapToSignal { _ -> Signal in return account.postbox.transaction { transaction -> Api.InputPeer? in diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift index 0734c25558..cfa4f55a35 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/QuickReplyMessages.swift @@ -811,7 +811,7 @@ extension TelegramBusinessRecipients { self.init( categories: categories, additionalPeers: Set((users ?? []).map( { PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value($0)) })), - excludePeers: Set((excludeUsers ?? []).map(PeerId.init)), + excludePeers: Set((excludeUsers ?? []).map( { PeerId(namespace: Namespaces.Peer.CloudUser, id: ._internalFromInt64Value($0)) })), exclude: (flags & (1 << 5)) != 0 ) } diff --git a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift index 87e8929892..8fceb7a633 100644 --- a/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatEmptyNode/Sources/ChatEmptyNode.swift @@ -1175,6 +1175,250 @@ private enum ChatEmptyNodeContentType: Equatable { case premiumRequired } +private final class EmptyAttachedDescriptionNode: HighlightTrackingButtonNode { + private struct Params: Equatable { + var theme: PresentationTheme + var strings: PresentationStrings + var chatWallpaper: TelegramWallpaper + var peer: EnginePeer + var constrainedSize: CGSize + + init(theme: PresentationTheme, strings: PresentationStrings, chatWallpaper: TelegramWallpaper, peer: EnginePeer, constrainedSize: CGSize) { + self.theme = theme + self.strings = strings + self.chatWallpaper = chatWallpaper + self.peer = peer + self.constrainedSize = constrainedSize + } + + static func ==(lhs: Params, rhs: Params) -> Bool { + if lhs.theme !== rhs.theme { + return false + } + if lhs.strings !== rhs.strings { + return false + } + if lhs.chatWallpaper != rhs.chatWallpaper { + return false + } + if lhs.constrainedSize != rhs.constrainedSize { + return false + } + return true + } + } + + private struct Layout { + var params: Params + var size: CGSize + + init(params: Params, size: CGSize) { + self.params = params + self.size = size + } + } + + private let textNode: ImmediateTextNode + private var backgroundContent: WallpaperBubbleBackgroundNode? + private let textMaskNode: LinkHighlightingNode + + private let badgeTextNode: ImmediateTextNode + private let badgeBackgroundView: UIImageView + + private var currentLayout: Layout? + + var action: (() -> Void)? + + override init(pointerStyle: PointerStyle? = nil) { + self.textNode = ImmediateTextNode() + self.textNode.textAlignment = .center + self.textNode.maximumNumberOfLines = 0 + + self.textMaskNode = LinkHighlightingNode(color: .white) + self.textMaskNode.innerRadius = 5.0 + self.textMaskNode.outerRadius = 10.0 + self.textMaskNode.inset = 0.0 + + self.badgeTextNode = ImmediateTextNode() + self.badgeBackgroundView = UIImageView() + + super.init(pointerStyle: pointerStyle) + + self.addSubnode(self.textNode) + + self.view.addSubview(self.badgeBackgroundView) + self.addSubnode(self.badgeTextNode) + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + + self.highligthedChanged = { [weak self] highlighted in + if let self, self.bounds.width > 0.0 { + let animateScale = true + + let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width + let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width + + if highlighted { + self.layer.removeAnimation(forKey: "transform.scale") + + if animateScale { + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + transition.setScale(layer: self.layer, scale: topScale) + } + } else { + if animateScale { + let transition = Transition(animation: .none) + transition.setScale(layer: self.layer, scale: 1.0) + + self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in + guard let self else { + return + } + + self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue) + }) + } + } + } + } + } + + @objc private func pressed() { + self.action?() + } + + func update( + theme: PresentationTheme, + strings: PresentationStrings, + chatWallpaper: TelegramWallpaper, + peer: EnginePeer, + wallpaperBackgroundNode: WallpaperBackgroundNode?, + constrainedSize: CGSize + ) -> CGSize { + let params = Params( + theme: theme, + strings: strings, + chatWallpaper: chatWallpaper, + peer: peer, + constrainedSize: constrainedSize + ) + if let currentLayout = self.currentLayout, currentLayout.params == params { + return currentLayout.size + } else { + let size = self.updateInternal(params: params, wallpaperBackgroundNode: wallpaperBackgroundNode) + self.currentLayout = Layout(params: params, size: size) + return size + } + } + + private func updateInternal(params: Params, wallpaperBackgroundNode: WallpaperBackgroundNode?) -> CGSize { + let serviceColor = serviceMessageColorComponents(theme: params.theme, wallpaper: params.chatWallpaper) + + //TODO:localize + let textString = NSMutableAttributedString() + textString.append(NSAttributedString(string: "\(params.peer.compactDisplayTitle) added the message above for all empty chats", font: Font.regular(13.0), textColor: serviceColor.primaryText)) + textString.append(NSAttributedString(string: " .how?", font: Font.regular(11.0), textColor: .clear)) + self.textNode.attributedText = textString + + let maxTextSize = CGSize(width: min(300.0, params.constrainedSize.width - 8.0 * 2.0), height: params.constrainedSize.height - 8.0 * 2.0) + + var bestSize: (availableWidth: CGFloat, info: TextNodeLayout) + let info = self.textNode.updateLayoutFullInfo(maxTextSize) + bestSize = (maxTextSize.width, info) + if info.numberOfLines > 1 { + let measureIncrement = 8.0 + var measureWidth = info.size.width + measureWidth -= measureIncrement + while measureWidth > 0.0 { + let otherInfo = self.textNode.updateLayoutFullInfo(CGSize(width: measureWidth, height: maxTextSize.height)) + if otherInfo.numberOfLines > bestSize.info.numberOfLines { + break + } + if (otherInfo.size.width - otherInfo.trailingLineWidth) < (bestSize.info.size.width - bestSize.info.trailingLineWidth) { + bestSize = (measureWidth, otherInfo) + } + + measureWidth -= measureIncrement + } + + let bestInfo = self.textNode.updateLayoutFullInfo(CGSize(width: bestSize.availableWidth, height: maxTextSize.height)) + bestSize = (maxTextSize.width, bestInfo) + } + + let textLayout = bestSize.info + + var labelRects = textLayout.linesRects() + if labelRects.count > 1 { + let sortedIndices = (0 ..< labelRects.count).sorted(by: { labelRects[$0].width > labelRects[$1].width }) + for i in 0 ..< sortedIndices.count { + let index = sortedIndices[i] + for j in -1 ... 1 { + if j != 0 && index + j >= 0 && index + j < sortedIndices.count { + if abs(labelRects[index + j].width - labelRects[index].width) < 16.0 { + labelRects[index + j].size.width = max(labelRects[index + j].width, labelRects[index].width) + labelRects[index].size.width = labelRects[index + j].size.width + } + } + } + } + } + for i in 0 ..< labelRects.count { + labelRects[i] = labelRects[i].insetBy(dx: -6.0, dy: floor((labelRects[i].height - 20.0) / 2.0)) + labelRects[i].size.height = 20.0 + labelRects[i].origin.x = floor((textLayout.size.width - labelRects[i].width) / 2.0) + } + self.textMaskNode.updateRects(labelRects) + + let size = CGSize(width: textLayout.size.width + 4.0 * 2.0, height: textLayout.size.height + 4.0 * 2.0) + let textFrame = CGRect(origin: CGPoint(x: 4.0, y: 4.0), size: textLayout.size) + self.textNode.frame = textFrame + + //TODO:localize + 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 - 2.0, y: textFrame.maxY - badgeTextSize.height), size: badgeTextSize) + self.badgeTextNode.frame = badgeTextFrame + + let badgeBackgroundFrame = badgeTextFrame.insetBy(dx: -4.0, dy: -1.0) + if badgeBackgroundFrame.height != self.badgeBackgroundView.image?.size.height { + self.badgeBackgroundView.image = generateStretchableFilledCircleImage(diameter: badgeBackgroundFrame.height, color: serviceColor.primaryText.withMultipliedAlpha(0.1)) + } + self.badgeBackgroundView.frame = badgeBackgroundFrame + } + + self.textMaskNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - self.textMaskNode.inset + 4.0, y: textFrame.minY - self.textMaskNode.inset - 11.0), size: CGSize()) + + if let wallpaperBackgroundNode { + if self.backgroundContent == nil, let backgroundContent = wallpaperBackgroundNode.makeBubbleBackground(for: .free) { + + self.backgroundContent = backgroundContent + backgroundContent.view.mask = self.textMaskNode.view + self.insertSubnode(backgroundContent, at: 0) + } + + if let backgroundContent = self.backgroundContent { + backgroundContent.frame = CGRect(origin: CGPoint(x: -4.0, y: 0.0), size: CGSize(width: size.width + 4.0 * 2.0, height: size.height)) + } + } else if let backgroundContent = self.backgroundContent { + self.backgroundContent = nil + backgroundContent.removeFromSupernode() + } + + return size + } + + func updateAbsolutePosition(rect: CGRect, containerSize: CGSize, transition: ContainedViewLayoutTransition) { + guard let backgroundContent = self.backgroundContent else { + return + } + var backgroundFrame = backgroundContent.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) + } +} + public final class ChatEmptyNode: ASDisplayNode { public enum Subject { public enum EmptyType: Equatable { @@ -1203,6 +1447,7 @@ public final class ChatEmptyNode: ASDisplayNode { private var currentStrings: PresentationStrings? private var content: (ChatEmptyNodeContentType, ASDisplayNode & ChatEmptyNodeContent)? + private var attachedDescriptionNode: EmptyAttachedDescriptionNode? public init(context: AccountContext, interaction: ChatPanelInterfaceInteraction?) { self.context = context @@ -1247,6 +1492,12 @@ public final class ChatEmptyNode: ASDisplayNode { backgroundContent.cornerRadius = initialFrame.size.width / 2.0 transition.updateCornerRadius(layer: backgroundContent.layer, cornerRadius: targetCornerRadius) } + + if let attachedDescriptionNode = self.attachedDescriptionNode { + attachedDescriptionNode.layer.animatePosition(from: initialFrame.center, to: attachedDescriptionNode.position, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + attachedDescriptionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + attachedDescriptionNode.layer.animateScale(from: 0.001, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } } public func updateLayout(interfaceState: ChatPresentationInterfaceState, subject: Subject, loadingNode: ChatLoadingNode?, backgroundNode: WallpaperBackgroundNode?, size: CGSize, insets: UIEdgeInsets, transition: ContainedViewLayoutTransition) { @@ -1265,6 +1516,7 @@ public final class ChatEmptyNode: ASDisplayNode { } let contentType: ChatEmptyNodeContentType + var displayAttachedDescription = false switch subject { case .detailsPlaceholder: contentType = .regular @@ -1298,6 +1550,9 @@ public final class ChatEmptyNode: ASDisplayNode { contentType = .regular } else { contentType = .greeting + if interfaceState.businessIntro != nil { + displayAttachedDescription = true + } } } } else { @@ -1368,6 +1623,46 @@ public final class ChatEmptyNode: ASDisplayNode { transition.updateFrame(node: self.backgroundNode, frame: contentFrame) self.backgroundNode.update(size: self.backgroundNode.bounds.size, cornerRadius: min(20.0, self.backgroundNode.bounds.height / 2.0), transition: transition) + if displayAttachedDescription, let peer = interfaceState.renderedPeer?.chatMainPeer { + let attachedDescriptionNode: EmptyAttachedDescriptionNode + if let current = self.attachedDescriptionNode { + attachedDescriptionNode = current + } else { + attachedDescriptionNode = EmptyAttachedDescriptionNode() + self.attachedDescriptionNode = attachedDescriptionNode + self.addSubnode(attachedDescriptionNode) + + attachedDescriptionNode.action = { [weak self] in + guard let self else { + return + } + let controller = self.context.sharedContext.makePremiumIntroController(context: self.context, source: .settings, forceDark: false, dismissed: nil) + self.interaction?.chatController()?.push(controller) + } + } + + let attachedDescriptionSize = attachedDescriptionNode.update( + theme: interfaceState.theme, + strings: interfaceState.strings, + chatWallpaper: interfaceState.chatWallpaper, + peer: EnginePeer(peer), + wallpaperBackgroundNode: backgroundNode, + constrainedSize: CGSize(width: size.width - insets.left - insets.right, height: 200.0) + ) + let attachedDescriptionFrame = CGRect(origin: CGPoint(x: floor((size.width - attachedDescriptionSize.width) * 0.5), y: contentFrame.maxY + 4.0), size: attachedDescriptionSize) + transition.updateFrame(node: attachedDescriptionNode, frame: attachedDescriptionFrame) + + if let (rect, containerSize) = self.absolutePosition { + var backgroundFrame = attachedDescriptionNode.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + attachedDescriptionNode.updateAbsolutePosition(rect: backgroundFrame, containerSize: containerSize, transition: .immediate) + } + } else if let attachedDescriptionNode = self.attachedDescriptionNode { + self.attachedDescriptionNode = nil + attachedDescriptionNode.removeFromSupernode() + } + if backgroundNode?.hasExtraBubbleBackground() == true { if self.backgroundContent == nil, let backgroundContent = backgroundNode?.makeBubbleBackground(for: .free) { backgroundContent.clipsToBounds = true @@ -1409,5 +1704,12 @@ public final class ChatEmptyNode: ASDisplayNode { backgroundFrame.origin.y += rect.minY backgroundContent.update(rect: backgroundFrame, within: containerSize, transition: transition) } + + if let attachedDescriptionNode = self.attachedDescriptionNode { + var backgroundFrame = attachedDescriptionNode.frame + backgroundFrame.origin.x += rect.minX + backgroundFrame.origin.y += rect.minY + attachedDescriptionNode.updateAbsolutePosition(rect: backgroundFrame, containerSize: containerSize, transition: transition) + } } } diff --git a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift index ddf56c35de..ec54736a9d 100644 --- a/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift +++ b/submodules/TelegramUI/Components/SendInviteLinkScreen/Sources/SendInviteLinkScreen.swift @@ -91,10 +91,11 @@ private final class SendInviteLinkScreenComponent: Component { private var premiumSeparatorRight: SimpleLayer? private var premiumSeparatorText: ComponentView? - private let title = ComponentView() private let leftButton = ComponentView() - private let descriptionText = ComponentView() - private let actionButton = ComponentView() + + private var title: ComponentView? + private var descriptionText: ComponentView? + private var actionButton: ComponentView? private let itemContainerView: UIView private var items: [AnyHashable: ComponentView] = [:] @@ -242,7 +243,7 @@ private final class SendInviteLinkScreenComponent: Component { self.scrollContentClippingView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.backgroundLayer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - if let actionButtonView = self.actionButton.view { + if let actionButtonView = self.actionButton?.view { actionButtonView.layer.animatePosition(from: CGPoint(x: 0.0, y: animateOffset), to: CGPoint(), duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } } @@ -256,7 +257,7 @@ private final class SendInviteLinkScreenComponent: Component { }) self.backgroundLayer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) self.navigationBarContainer.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) - if let actionButtonView = self.actionButton.view { + if let actionButtonView = self.actionButton?.view { actionButtonView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: animateOffset), duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, additive: true) } } @@ -280,6 +281,7 @@ private final class SendInviteLinkScreenComponent: Component { self.environment = environment let hasPremiumRestrictedUsers = "".isEmpty + let hasInviteLink = "".isEmpty if themeUpdated { self.dimView.backgroundColor = UIColor(white: 0.0, alpha: 0.5) @@ -373,35 +375,6 @@ private final class SendInviteLinkScreenComponent: Component { self.premiumButton = premiumButton } - let premiumSeparatorText: ComponentView - if let current = self.premiumSeparatorText { - premiumSeparatorText = current - } else { - premiumSeparatorText = ComponentView() - self.premiumSeparatorText = premiumSeparatorText - } - - let premiumSeparatorLeft: SimpleLayer - if let current = self.premiumSeparatorLeft { - premiumSeparatorLeft = current - } else { - premiumSeparatorLeft = SimpleLayer() - self.premiumSeparatorLeft = premiumSeparatorLeft - self.scrollContentView.layer.addSublayer(premiumSeparatorLeft) - } - - let premiumSeparatorRight: SimpleLayer - if let current = self.premiumSeparatorRight { - premiumSeparatorRight = current - } else { - premiumSeparatorRight = SimpleLayer() - self.premiumSeparatorRight = premiumSeparatorRight - self.scrollContentView.layer.addSublayer(premiumSeparatorRight) - } - - premiumSeparatorLeft.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor - premiumSeparatorRight.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor - //TODO:localize let premiumTitleSize = premiumTitle.update( transition: .immediate, @@ -531,31 +504,78 @@ private final class SendInviteLinkScreenComponent: Component { transition.setFrame(view: premiumButtonView, frame: premiumButtonFrame) } contentHeight += premiumButtonSize.height - contentHeight += 19.0 - let premiumSeparatorTextSize = premiumSeparatorText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: "or", font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) - ) - let premiumSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumSeparatorTextSize.width) * 0.5), y: contentHeight), size: premiumSeparatorTextSize) - if let premiumSeparatorTextView = premiumSeparatorText.view { - if premiumSeparatorTextView.superview == nil { - self.scrollContentView.addSubview(premiumSeparatorTextView) + if hasInviteLink { + let premiumSeparatorText: ComponentView + if let current = self.premiumSeparatorText { + premiumSeparatorText = current + } else { + premiumSeparatorText = ComponentView() + self.premiumSeparatorText = premiumSeparatorText } - transition.setFrame(view: premiumSeparatorTextView, frame: premiumSeparatorTextFrame) + + let premiumSeparatorLeft: SimpleLayer + if let current = self.premiumSeparatorLeft { + premiumSeparatorLeft = current + } else { + premiumSeparatorLeft = SimpleLayer() + self.premiumSeparatorLeft = premiumSeparatorLeft + self.scrollContentView.layer.addSublayer(premiumSeparatorLeft) + } + + let premiumSeparatorRight: SimpleLayer + if let current = self.premiumSeparatorRight { + premiumSeparatorRight = current + } else { + premiumSeparatorRight = SimpleLayer() + self.premiumSeparatorRight = premiumSeparatorRight + self.scrollContentView.layer.addSublayer(premiumSeparatorRight) + } + + premiumSeparatorLeft.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + premiumSeparatorRight.backgroundColor = environment.theme.list.itemPlainSeparatorColor.cgColor + + contentHeight += 19.0 + + let premiumSeparatorTextSize = premiumSeparatorText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: "or", font: Font.regular(15.0), textColor: environment.theme.list.itemSecondaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let premiumSeparatorTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - premiumSeparatorTextSize.width) * 0.5), y: contentHeight), size: premiumSeparatorTextSize) + if let premiumSeparatorTextView = premiumSeparatorText.view { + if premiumSeparatorTextView.superview == nil { + self.scrollContentView.addSubview(premiumSeparatorTextView) + } + transition.setFrame(view: premiumSeparatorTextView, frame: premiumSeparatorTextFrame) + } + + let separatorWidth: CGFloat = 72.0 + let separatorSpacing: CGFloat = 10.0 + + transition.setFrame(layer: premiumSeparatorLeft, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) + transition.setFrame(layer: premiumSeparatorRight, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.maxX + separatorSpacing, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) + + contentHeight += 31.0 + } else { + if let premiumSeparatorLeft = self.premiumSeparatorLeft { + self.premiumSeparatorLeft = nil + premiumSeparatorLeft.removeFromSuperlayer() + } + if let premiumSeparatorRight = self.premiumSeparatorRight { + self.premiumSeparatorRight = nil + premiumSeparatorRight.removeFromSuperlayer() + } + if let premiumSeparatorText = self.premiumSeparatorText { + self.premiumSeparatorText = nil + premiumSeparatorText.view?.removeFromSuperview() + } + + contentHeight += 14.0 } - - let separatorWidth: CGFloat = 72.0 - let separatorSpacing: CGFloat = 10.0 - - transition.setFrame(layer: premiumSeparatorLeft, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.minX - separatorSpacing - separatorWidth, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) - transition.setFrame(layer: premiumSeparatorRight, frame: CGRect(origin: CGPoint(x: premiumSeparatorTextFrame.maxX + separatorSpacing, y: premiumSeparatorTextFrame.midY + 1.0), size: CGSize(width: separatorWidth, height: UIScreenPixel))) - - contentHeight += 31.0 } else { if let premiumTitle = self.premiumTitle { self.premiumTitle = nil @@ -569,241 +589,278 @@ private final class SendInviteLinkScreenComponent: Component { self.premiumButton = nil premiumButton.view?.removeFromSuperview() } - if let premiumSeparatorLeft = self.premiumSeparatorLeft { - self.premiumSeparatorLeft = nil - premiumSeparatorLeft.removeFromSuperlayer() - } - if let premiumSeparatorRight = self.premiumSeparatorRight { - self.premiumSeparatorRight = nil - premiumSeparatorRight.removeFromSuperlayer() - } - if let premiumSeparatorText = self.premiumSeparatorText { - self.premiumSeparatorText = nil - premiumSeparatorText.view?.removeFromSuperview() - } } - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.link != nil ? environment.strings.SendInviteLink_InviteTitle : environment.strings.SendInviteLink_LinkUnavailableTitle, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)) - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) - ) - let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) - if let titleView = self.title.view { - if titleView.superview == nil { - self.scrollContentView.addSubview(titleView) - } - transition.setFrame(view: titleView, frame: titleFrame) - } - - contentHeight += titleSize.height - contentHeight += 8.0 - - let text: String - if hasPremiumRestrictedUsers { - if component.link != nil { - //TODO:localize - text = "You can try to send an invite link instead." - } else { - if component.peers.count == 1 { - text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string - } else { - text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) - } - } - } else { - if component.link != nil { - if component.peers.count == 1 { - text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string - } else { - text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count)) - } - } else { - if component.peers.count == 1 { - text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string - } else { - text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) - } - } - } - - let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) - let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) - - let descriptionTextSize = self.descriptionText.update( - transition: .immediate, - component: AnyComponent(MultilineTextComponent( - text: .markdown(text: text, attributes: MarkdownAttributes( - body: body, - bold: bold, - link: body, - linkAttribute: { _ in nil } - )), - horizontalAlignment: .center, - maximumNumberOfLines: 0 - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) - ) - let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) - if let descriptionTextView = self.descriptionText.view { - if descriptionTextView.superview == nil { - self.scrollContentView.addSubview(descriptionTextView) - } - transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) - } - - contentHeight += descriptionTextFrame.height - contentHeight += 22.0 - - var singleItemHeight: CGFloat = 0.0 - - var itemsHeight: CGFloat = 0.0 - var validIds: [AnyHashable] = [] - for i in 0 ..< component.peers.count { - let peer = component.peers[i] - - for _ in 0 ..< 1 { - //let id: AnyHashable = AnyHashable("\(peer.id)_\(j)") - let id = AnyHashable(peer.id) - validIds.append(id) - - let item: ComponentView - var itemTransition = transition - if let current = self.items[id] { - item = current - } else { - itemTransition = .immediate - item = ComponentView() - self.items[id] = item - } - - let itemSize = item.update( - transition: itemTransition, - component: AnyComponent(PeerListItemComponent( - context: component.context, - theme: environment.theme, - strings: environment.strings, - sideInset: 0.0, - title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), - peer: peer, - presence: component.peerPresences[peer.id], - selectionState: component.link == nil ? .none : .editing(isSelected: self.selectedItems.contains(peer.id)), - hasNext: i != component.peers.count - 1, - action: { [weak self] peer in - guard let self else { - return - } - if self.selectedItems.contains(peer.id) { - self.selectedItems.remove(peer.id) - } else { - self.selectedItems.insert(peer.id) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) - ) - let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) - - if let itemView = item.view { - if itemView.superview == nil { - self.itemContainerView.addSubview(itemView) - } - itemTransition.setFrame(view: itemView, frame: itemFrame) - } - - itemsHeight += itemSize.height - singleItemHeight = itemSize.height - } - } - var removeIds: [AnyHashable] = [] - for (id, item) in self.items { - if !validIds.contains(id) { - removeIds.append(id) - item.view?.removeFromSuperview() - } - } - for id in removeIds { - self.items.removeValue(forKey: id) - } - transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) - - var initialContentHeight = contentHeight - initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5)) - - contentHeight += itemsHeight - contentHeight += 24.0 - initialContentHeight += 24.0 - - let actionButtonTitle: String - if component.link != nil { - actionButtonTitle = self.selectedItems.isEmpty ? environment.strings.SendInviteLink_ActionSkip : environment.strings.SendInviteLink_ActionInvite - } else { - actionButtonTitle = environment.strings.SendInviteLink_ActionClose - } - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(SolidRoundedButtonComponent( - title: actionButtonTitle, - badge: (self.selectedItems.isEmpty || component.link == nil) ? nil : "\(self.selectedItems.count)", - theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), - font: .bold, - fontSize: 17.0, - height: 50.0, - cornerRadius: 11.0, - gloss: false, - animationName: nil, - iconPosition: .right, - iconSpacing: 4.0, - action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - if self.selectedItems.isEmpty { - controller.dismiss() - } else if let link = component.link { - let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) } - - let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() - let text: String - if selectedPeers.count == 1 { - text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else if selectedPeers.count == 2 { - text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string - } else { - text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string - } - - let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) - - controller.dismiss() - } else { - controller.dismiss() - } - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) - ) - let bottomPanelHeight = 15.0 + environment.safeInsets.bottom + actionButtonSize.height - let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) - } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) - } - - contentHeight += bottomPanelHeight - initialContentHeight += bottomPanelHeight - let containerInset: CGFloat = environment.statusBarHeight + 10.0 + + var initialContentHeight = contentHeight + let clippingY: CGFloat + + if hasInviteLink { + let title: ComponentView + if let current = self.title { + title = current + } else { + title = ComponentView() + self.title = title + } + + let descriptionText: ComponentView + if let current = self.descriptionText { + descriptionText = current + } else { + descriptionText = ComponentView() + self.descriptionText = descriptionText + } + + let actionButton: ComponentView + if let current = self.actionButton { + actionButton = current + } else { + actionButton = ComponentView() + self.actionButton = actionButton + } + + let titleSize = title.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: component.link != nil ? environment.strings.SendInviteLink_InviteTitle : environment.strings.SendInviteLink_LinkUnavailableTitle, font: Font.semibold(24.0), textColor: environment.theme.list.itemPrimaryTextColor)) + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftButtonFrame.maxX * 2.0, height: 100.0) + ) + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: contentHeight), size: titleSize) + if let titleView = title.view { + if titleView.superview == nil { + self.scrollContentView.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + contentHeight += titleSize.height + contentHeight += 8.0 + + let text: String + if hasPremiumRestrictedUsers { + if component.link != nil { + //TODO:localize + text = "You can try to send an invite link instead." + } else { + if component.peers.count == 1 { + text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string + } else { + text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) + } + } + } else { + if component.link != nil { + if component.peers.count == 1 { + text = environment.strings.SendInviteLink_TextAvailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string + } else { + text = environment.strings.SendInviteLink_TextAvailableMultipleUsers(Int32(component.peers.count)) + } + } else { + if component.peers.count == 1 { + text = environment.strings.SendInviteLink_TextUnavailableSingleUser(component.peers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast)).string + } else { + text = environment.strings.SendInviteLink_TextUnavailableMultipleUsers(Int32(component.peers.count)) + } + } + } + + let body = MarkdownAttributeSet(font: Font.regular(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + let bold = MarkdownAttributeSet(font: Font.semibold(15.0), textColor: environment.theme.list.itemPrimaryTextColor) + + let descriptionTextSize = descriptionText.update( + transition: .immediate, + component: AnyComponent(MultilineTextComponent( + text: .markdown(text: text, attributes: MarkdownAttributes( + body: body, + bold: bold, + link: body, + linkAttribute: { _ in nil } + )), + horizontalAlignment: .center, + maximumNumberOfLines: 0 + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 16.0 * 2.0, height: 1000.0) + ) + let descriptionTextFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - descriptionTextSize.width) * 0.5), y: contentHeight), size: descriptionTextSize) + if let descriptionTextView = descriptionText.view { + if descriptionTextView.superview == nil { + self.scrollContentView.addSubview(descriptionTextView) + } + transition.setFrame(view: descriptionTextView, frame: descriptionTextFrame) + } + + contentHeight += descriptionTextFrame.height + contentHeight += 22.0 + + var singleItemHeight: CGFloat = 0.0 + + var itemsHeight: CGFloat = 0.0 + var validIds: [AnyHashable] = [] + for i in 0 ..< component.peers.count { + let peer = component.peers[i] + + for _ in 0 ..< 1 { + //let id: AnyHashable = AnyHashable("\(peer.id)_\(j)") + let id = AnyHashable(peer.id) + validIds.append(id) + + let item: ComponentView + var itemTransition = transition + if let current = self.items[id] { + item = current + } else { + itemTransition = .immediate + item = ComponentView() + self.items[id] = item + } + + let itemSize = item.update( + transition: itemTransition, + component: AnyComponent(PeerListItemComponent( + context: component.context, + theme: environment.theme, + strings: environment.strings, + sideInset: 0.0, + title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), + peer: peer, + presence: component.peerPresences[peer.id], + selectionState: component.link == nil ? .none : .editing(isSelected: self.selectedItems.contains(peer.id)), + hasNext: i != component.peers.count - 1, + action: { [weak self] peer in + guard let self else { + return + } + if self.selectedItems.contains(peer.id) { + self.selectedItems.remove(peer.id) + } else { + self.selectedItems.insert(peer.id) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .easeInOut))) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 1000.0) + ) + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: itemsHeight), size: itemSize) + + if let itemView = item.view { + if itemView.superview == nil { + self.itemContainerView.addSubview(itemView) + } + itemTransition.setFrame(view: itemView, frame: itemFrame) + } + + itemsHeight += itemSize.height + singleItemHeight = itemSize.height + } + } + var removeIds: [AnyHashable] = [] + for (id, item) in self.items { + if !validIds.contains(id) { + removeIds.append(id) + item.view?.removeFromSuperview() + } + } + for id in removeIds { + self.items.removeValue(forKey: id) + } + transition.setFrame(view: self.itemContainerView, frame: CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: CGSize(width: availableSize.width - sideInset * 2.0, height: itemsHeight))) + + initialContentHeight += singleItemHeight + 16.0 + initialContentHeight += min(itemsHeight, floor(singleItemHeight * 2.5)) + + contentHeight += itemsHeight + contentHeight += 24.0 + initialContentHeight += 24.0 + + let actionButtonTitle: String + if component.link != nil { + actionButtonTitle = self.selectedItems.isEmpty ? environment.strings.SendInviteLink_ActionSkip : environment.strings.SendInviteLink_ActionInvite + } else { + actionButtonTitle = environment.strings.SendInviteLink_ActionClose + } + let actionButtonSize = actionButton.update( + transition: transition, + component: AnyComponent(SolidRoundedButtonComponent( + title: actionButtonTitle, + badge: (self.selectedItems.isEmpty || component.link == nil) ? nil : "\(self.selectedItems.count)", + theme: SolidRoundedButtonComponent.Theme(theme: environment.theme), + font: .bold, + fontSize: 17.0, + height: 50.0, + cornerRadius: 11.0, + gloss: false, + animationName: nil, + iconPosition: .right, + iconSpacing: 4.0, + action: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + if self.selectedItems.isEmpty { + controller.dismiss() + } else if let link = component.link { + let selectedPeers = component.peers.filter { self.selectedItems.contains($0.id) } + + let _ = enqueueMessagesToMultiplePeers(account: component.context.account, peerIds: Array(self.selectedItems), threadIds: [:], messages: [.message(text: link, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).start() + let text: String + if selectedPeers.count == 1 { + text = environment.strings.Conversation_ShareLinkTooltip_Chat_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } else if selectedPeers.count == 2 { + text = environment.strings.Conversation_ShareLinkTooltip_TwoChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), selectedPeers[1].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: "")).string + } else { + text = environment.strings.Conversation_ShareLinkTooltip_ManyChats_One(selectedPeers[0].displayTitle(strings: environment.strings, displayOrder: .firstLast).replacingOccurrences(of: "*", with: ""), "\(selectedPeers.count - 1)").string + } + + let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } + controller.present(UndoOverlayController(presentationData: presentationData, content: .forward(savedMessages: false, text: text), elevatedLayout: false, action: { _ in return false }), in: .window(.root)) + + controller.dismiss() + } else { + controller.dismiss() + } + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 50.0) + ) + let bottomPanelHeight = 15.0 + environment.safeInsets.bottom + actionButtonSize.height + let actionButtonFrame = CGRect(origin: CGPoint(x: sideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + contentHeight += bottomPanelHeight + initialContentHeight += bottomPanelHeight + + clippingY = actionButtonFrame.minY - 24.0 + } else { + if let title = self.title { + self.title = nil + title.view?.removeFromSuperview() + } + if let descriptionText = self.descriptionText { + self.descriptionText = nil + descriptionText.view?.removeFromSuperview() + } + if let actionButton = self.actionButton { + self.actionButton = nil + actionButton.view?.removeFromSuperview() + } + + initialContentHeight += environment.safeInsets.bottom + + clippingY = availableSize.height + } + let topInset: CGFloat = max(0.0, availableSize.height - containerInset - initialContentHeight) let scrollContentHeight = max(topInset + contentHeight, availableSize.height - containerInset) @@ -817,12 +874,12 @@ private final class SendInviteLinkScreenComponent: Component { transition.setPosition(layer: self.backgroundLayer, position: CGPoint(x: availableSize.width / 2.0, y: availableSize.height / 2.0)) transition.setBounds(layer: self.backgroundLayer, bounds: CGRect(origin: CGPoint(), size: availableSize)) - let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: actionButtonFrame.minY - 24.0 - (containerInset))) + let scrollClippingFrame = CGRect(origin: CGPoint(x: sideInset, y: containerInset), size: CGSize(width: availableSize.width - sideInset * 2.0, height: clippingY - containerInset)) transition.setPosition(view: self.scrollContentClippingView, position: scrollClippingFrame.center) transition.setBounds(view: self.scrollContentClippingView, bounds: CGRect(origin: CGPoint(x: scrollClippingFrame.minX, y: scrollClippingFrame.minY), size: scrollClippingFrame.size)) self.ignoreScrolling = true - transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height - containerInset))) + transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: availableSize.height))) let contentSize = CGSize(width: availableSize.width, height: scrollContentHeight) if contentSize != self.scrollView.contentSize { self.scrollView.contentSize = contentSize diff --git a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift index ffe416beb0..c8c3624129 100644 --- a/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/AutomaticBusinessMessageSetupScreen/Sources/AutomaticBusinessMessageSetupScreen.swift @@ -1356,7 +1356,7 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component { 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_PeerTypeNonContact, + subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, subtitleAccessory: .none, presence: nil, selectionState: .none, diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift new file mode 100644 index 0000000000..cd24106bfd --- /dev/null +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/BusinessRecipientListScreen.swift @@ -0,0 +1,10 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import MultilineTextComponent +import TelegramPresentationData +import TelegramCore +import AccountContext +import ListSectionComponent + diff --git a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift index 4956501fdb..f48a0a066d 100644 --- a/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift +++ b/submodules/TelegramUI/Components/Settings/ChatbotSetupScreen/Sources/ChatbotSetupScreen.swift @@ -120,6 +120,7 @@ final class ChatbotSetupScreenComponent: Component { private let nameSection = ComponentView() private let accessSection = ComponentView() private let excludedSection = ComponentView() + private let excludedUsersSection = ComponentView() private let permissionsSection = ComponentView() private var isUpdating: Bool = false @@ -299,7 +300,7 @@ final class ChatbotSetupScreenComponent: Component { } } - private func openAdditionalPeerListSetup() { + private func openAdditionalPeerListSetup(isExclude: Bool) { guard let component = self.component, let environment = self.environment else { return } @@ -311,44 +312,51 @@ final class ChatbotSetupScreenComponent: Component { case nonContacts } - let additionalCategories: [ChatListNodeAdditionalCategory] = [ - 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 - ) - ] + let additionalCategories: [ChatListNodeAdditionalCategory] var selectedCategories = Set() - for category in self.additionalPeerList.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) + 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 + ) + ] + } + if !isExclude { + for category in self.additionalPeerList.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) + } } } let controller = component.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: component.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection( - title: self.hasAccessToAllChatsByDefault ? environment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : environment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle, + title: (self.hasAccessToAllChatsByDefault || isExclude) ? environment.strings.BusinessMessageSetup_Recipients_ExcludeSearchTitle : environment.strings.BusinessMessageSetup_Recipients_IncludeSearchTitle, searchPlaceholder: environment.strings.ChatListFilter_AddChatsSearchPlaceholder, - selectedChats: Set(self.additionalPeerList.peers.map(\.peer.id)), + 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 @@ -386,36 +394,59 @@ final class ChatbotSetupScreenComponent: Component { return } - 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 + 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.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 - }) self.state?.updated(transition: .immediate) controller?.dismiss() @@ -494,7 +525,7 @@ final class ChatbotSetupScreenComponent: Component { let navigationTitleSize = self.navigationTitle.update( transition: transition, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_Title, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: environment.strings.ChatbotSetup_TitleItem, font: Font.semibold(17.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center )), environment: {}, @@ -795,7 +826,7 @@ final class ChatbotSetupScreenComponent: Component { guard let self else { return } - self.openAdditionalPeerListSetup() + self.openAdditionalPeerListSetup(isExclude: false) } )))) for category in self.additionalPeerList.categories.sorted(by: { $0.rawValue < $1.rawValue }) { @@ -865,7 +896,7 @@ final class ChatbotSetupScreenComponent: Component { 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_PeerTypeNonContact, + subtitle: peer.isContact ? environment.strings.ChatList_PeerTypeContact : environment.strings.ChatList_PeerTypeNonContactUser, subtitleAccessory: .none, presence: nil, selectionState: .none, @@ -930,6 +961,103 @@ final class ChatbotSetupScreenComponent: Component { contentHeight += excludedSectionSize.height contentHeight += sectionSpacing + //TODO:localize + var excludedUsersContentHeight: CGFloat = 0.0 + var excludedUsersSectionItems: [AnyComponentWithIdentity] = [] + excludedUsersSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListActionItemComponent( + theme: environment.theme, + title: AnyComponent(VStack([ + AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString( + string: environment.strings.BusinessMessageSetup_Recipients_AddExclude, + 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.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( + theme: environment.theme, + header: nil, + footer: AnyComponent(MultilineTextComponent( + text: .markdown( + text: environment.strings.ChatbotSetup_Recipients_ExcludedSectionFooter, + attributes: MarkdownAttributes( + body: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + bold: MarkdownAttributeSet(font: Font.semibold(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.freeTextColor), + link: MarkdownAttributeSet(font: Font.regular(presentationData.listsFontSize.itemListBaseHeaderFontSize), textColor: environment.theme.list.itemAccentColor), + linkAttribute: { _ in + return nil + } + ) + ), + maximumNumberOfLines: 0 + )), + items: excludedUsersSectionItems + )), + environment: {}, + containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0) + ) + let excludedUsersSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight + excludedUsersContentHeight), size: excludedSectionSize) + if let excludedUsersSectionView = self.excludedUsersSection.view { + if excludedUsersSectionView.superview == nil { + self.scrollView.addSubview(excludedUsersSectionView) + } + transition.setFrame(view: excludedUsersSectionView, frame: excludedUsersSectionFrame) + transition.setAlpha(view: excludedUsersSectionView, alpha: !self.hasAccessToAllChatsByDefault ? 1.0 : 0.0) + } + excludedUsersContentHeight += excludedUsersSectionSize.height + excludedUsersContentHeight += sectionSpacing + if !self.hasAccessToAllChatsByDefault { + contentHeight += excludedUsersContentHeight + } + let permissionsSectionSize = self.permissionsSection.update( transition: transition, component: AnyComponent(ListSectionComponent( @@ -1107,6 +1235,7 @@ public final class ChatbotSetupScreen: ViewControllerComponentContainer { var additionalPeerIds = Set() additionalPeerIds.formUnion(connectedBot.recipients.additionalPeers) + additionalPeerIds.formUnion(connectedBot.recipients.excludePeers) return context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: connectedBot.id), diff --git a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift index bab56dccb2..2d2615604a 100644 --- a/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift +++ b/submodules/TelegramUI/Sources/ChatManagingBotTitlePanelNode.swift @@ -170,10 +170,16 @@ private final class ChatManagingBotTitlePanelComponent: Component { containerSize: CGSize(width: maxTextWidth, height: 100.0) ) //TODO:localize + let textValue: String + if component.isPaused { + textValue = "bot paused" + } else { + textValue = component.managesChat ? "bot manages this chat" : "bot has access to this chat" + } let textSize = self.text.update( transition: .immediate, component: AnyComponent(MultilineTextComponent( - text: .plain(NSAttributedString(string: component.managesChat ? "bot manages this chat" : "bot has access to this chat", font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)) + text: .plain(NSAttributedString(string: textValue, font: Font.regular(15.0), textColor: component.theme.rootController.navigationBar.secondaryTextColor)) )), environment: {}, containerSize: CGSize(width: maxTextWidth, height: 100.0) @@ -400,7 +406,7 @@ final class ChatManagingBotTitlePanelNode: ChatTitleAccessoryPanelNode { strings: interfaceState.strings, insets: UIEdgeInsets(top: 0.0, left: leftInset, bottom: 0.0, right: rightInset), peer: managingBot.bot, - managesChat: managingBot.canReply, + managesChat: managingBot.canReply || managingBot.isPaused, isPaused: managingBot.isPaused, toggleIsPaused: { [weak self] in guard let self else {