Bot peer request support

Launch screen icon
This commit is contained in:
Ilya Laktyushin
2023-01-13 22:38:04 +04:00
parent 58e641230a
commit 930e237bf1
37 changed files with 1242 additions and 125 deletions

View File

@@ -18,9 +18,13 @@ import ChatSendMessageActionUI
import ChatTextLinkEditUI
import AnimationCache
import MultiAnimationRenderer
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import SolidRoundedButtonNode
final class PeerSelectionControllerNode: ASDisplayNode {
private let context: AccountContext
private weak var controller: PeerSelectionController?
private let present: (ViewController, Any?) -> Void
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
private let dismiss: () -> Void
@@ -29,6 +33,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
private let hasGlobalSearch: Bool
private let forwardedMessageIds: [EngineMessage.Id]
private let hasTypeHeaders: Bool
private let requestPeerType: ReplyMarkupButtonRequestPeerType?
private var presentationInterfaceState: ChatPresentationInterfaceState
private var interfaceInteraction: ChatPanelInterfaceInteraction?
@@ -41,6 +46,16 @@ final class PeerSelectionControllerNode: ASDisplayNode {
var navigationBar: NavigationBar?
private let requirementsBackgroundNode: NavigationBackgroundNode?
private let requirementsSeparatorNode: ASDisplayNode?
private let requirementsTextNode: ImmediateTextNode?
private let emptyAnimationNode: AnimatedStickerNode
private var emptyAnimationSize = CGSize()
private let emptyTitleNode: ImmediateTextNode
private let emptyTextNode: ImmediateTextNode
private let emptyButtonNode: SolidRoundedButtonNode
private let toolbarBackgroundNode: NavigationBackgroundNode?
private let toolbarSeparatorNode: ASDisplayNode?
private let segmentedControlNode: SegmentedControlNode?
@@ -83,12 +98,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self.readyValue.get()
}
private var isEmpty = false
private var updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) {
return (self.presentationData, self.presentationDataPromise.get())
}
init(context: AccountContext, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
init(context: AccountContext, controller: PeerSelectionController, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: ReplyMarkupButtonRequestPeerType?, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
self.context = context
self.controller = controller
self.present = present
self.presentInGlobalOverlay = presentInGlobalOverlay
self.dismiss = dismiss
@@ -97,6 +115,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.hasGlobalSearch = hasGlobalSearch
self.forwardedMessageIds = forwardedMessageIds
self.hasTypeHeaders = hasTypeHeaders
self.requestPeerType = requestPeerType
self.presentationData = presentationData
@@ -107,6 +126,43 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) }
if let _ = self.requestPeerType {
self.requirementsBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
self.requirementsSeparatorNode = ASDisplayNode()
self.requirementsSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.requirementsTextNode = ImmediateTextNode()
self.requirementsTextNode?.maximumNumberOfLines = 0
self.requirementsTextNode?.lineSpacing = 0.1
} else {
self.requirementsBackgroundNode = nil
self.requirementsSeparatorNode = nil
self.requirementsTextNode = nil
}
self.emptyTitleNode = ImmediateTextNode()
self.emptyTitleNode.displaysAsynchronously = false
self.emptyTitleNode.maximumNumberOfLines = 0
self.emptyTitleNode.isHidden = true
self.emptyTitleNode.textAlignment = .center
self.emptyTitleNode.lineSpacing = 0.25
self.emptyTextNode = ImmediateTextNode()
self.emptyTextNode.displaysAsynchronously = false
self.emptyTextNode.maximumNumberOfLines = 0
self.emptyTextNode.isHidden = true
self.emptyTextNode.lineSpacing = 0.25
self.emptyAnimationNode = DefaultAnimatedStickerNodeImpl()
self.emptyAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ChatListNoResults"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.emptyAnimationNode.isHidden = true
self.emptyAnimationSize = CGSize(width: 120.0, height: 120.0)
self.emptyButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: self.presentationData.theme), cornerRadius: 11.0, gloss: true)
self.emptyButtonNode.isHidden = true
self.emptyButtonNode.pressed = {
createNewGroup?()
}
if hasChatListSelector && hasContactSelector {
self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
@@ -136,8 +192,15 @@ final class PeerSelectionControllerNode: ASDisplayNode {
} else {
chatListLocation = .chatList(groupId: .root)
}
let chatListMode: ChatListNodeMode
if let requestPeerType = self.requestPeerType {
chatListMode = .peerType(type: requestPeerType)
} else {
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false)
}
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false), theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
super.init()
@@ -184,6 +247,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
return self?.contentScrollingEnded?(listView) ?? false
}
self.chatListNode.isEmptyUpdated = { [weak self] state, _, _ in
guard let strongSelf = self else {
return
}
if case .empty(false, _) = state, let (layout, navigationBarHeight, actualNavigationBarHeight) = strongSelf.containerLayout {
strongSelf.isEmpty = true
strongSelf.controller?.navigationBar?.setContentNode(nil, animated: false)
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate)
}
}
self.addSubnode(self.chatListNode)
if hasChatListSelector && hasContactSelector {
@@ -196,6 +270,17 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.addSubnode(self.segmentedControlNode!)
}
if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode {
self.chatListNode.addSubnode(requirementsBackgroundNode)
self.chatListNode.addSubnode(requirementsSeparatorNode)
self.chatListNode.addSubnode(requirementsTextNode)
self.addSubnode(self.emptyAnimationNode)
self.addSubnode(self.emptyTitleNode)
self.addSubnode(self.emptyTextNode)
self.addSubnode(self.emptyButtonNode)
}
if !hasChatListSelector && hasContactSelector {
self.indexChanged(1)
}
@@ -484,6 +569,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.updateChatPresentationInterfaceState({ $0.updatedTheme(self.presentationData.theme) })
self.requirementsBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.toolbarBackgroundNode?.updateColor(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode?.updateTheme(SegmentedControlTheme(theme: self.presentationData.theme))
@@ -574,6 +660,113 @@ final class PeerSelectionControllerNode: ASDisplayNode {
self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
if let requestPeerType = self.requestPeerType {
if self.isEmpty {
self.chatListNode.isHidden = true
self.requirementsBackgroundNode?.isHidden = true
self.requirementsTextNode?.isHidden = true
self.requirementsSeparatorNode?.isHidden = true
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
var emptyTitle: String
var emptyText: String
var emptyButtonText: String
switch requestPeerType {
case let .user(user):
if let isBot = user.isBot, isBot {
emptyTitle = self.presentationData.strings.RequestPeer_BotsAllEmpty
emptyText = ""
} else {
emptyTitle = self.presentationData.strings.RequestPeer_UsersAllEmpty
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
emptyTitle = self.presentationData.strings.RequestPeer_UsersEmpty
emptyText = text
} else {
emptyText = ""
}
}
emptyButtonText = ""
case .group:
emptyTitle = self.presentationData.strings.RequestPeer_GroupsAllEmpty
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
emptyTitle = self.presentationData.strings.RequestPeer_GroupsEmpty
emptyText = text
} else {
emptyText = ""
}
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
case .channel:
emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty
emptyText = text
} else {
emptyText = ""
}
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
}
self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
let padding: CGFloat = 44.0
let emptyTitleSize = self.emptyTitleNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
let emptyAnimationHeight = self.emptyAnimationSize.height
let emptyAnimationSpacing: CGFloat = 12.0
let emptyTextSpacing: CGFloat = 17.0
var emptyButtonSpacing: CGFloat = 15.0
var emptyButtonHeight: CGFloat = 50.0
if emptyButtonText.isEmpty {
emptyButtonSpacing = 0.0
emptyButtonHeight = 0.0
}
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + emptyButtonSpacing + emptyButtonHeight
let emptyAnimationY = floorToScreenPixels((layout.size.height - emptyTotalHeight) / 2.0)
if !emptyButtonText.isEmpty {
let buttonPadding: CGFloat = 30.0
self.emptyButtonNode.title = emptyButtonText
self.emptyButtonNode.isHidden = false
let emptyButtonWidth = layout.size.width - insets.left - insets.right - buttonPadding * 2.0
let _ = self.emptyButtonNode.updateLayout(width: emptyButtonWidth, transition: transition)
transition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyButtonWidth) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing + emptyTextSize.height + emptyButtonSpacing), size: CGSize(width: emptyButtonWidth, height: emptyButtonHeight)))
} else {
self.emptyButtonNode.isHidden = true
}
let textTransition = ContainedViewLayoutTransition.immediate
textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - self.emptyAnimationSize.width) / 2.0), y: emptyAnimationY), size: self.emptyAnimationSize))
textTransition.updateFrame(node: self.emptyTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTitleSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize))
textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTextSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize)
self.emptyAnimationNode.isHidden = false
self.emptyTitleNode.isHidden = false
self.emptyTextNode.isHidden = false
self.emptyAnimationNode.visibility = true
} else if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode, let requirementsText = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: true) {
let requirements = NSMutableAttributedString(string: self.presentationData.strings.RequestPeer_Requirements + "\n", font: Font.semibold(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
requirements.append(NSAttributedString(string: requirementsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor))
requirementsTextNode.attributedText = requirements
let sideInset: CGFloat = 16.0
let verticalInset: CGFloat = 11.0
let requirementsSize = requirementsTextNode.updateLayout(CGSize(width: layout.size.width - insets.left - insets.right - sideInset * 2.0, height: .greatestFiniteMagnitude))
let requirementsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: actualNavigationBarHeight), size: CGSize(width: layout.size.width, height: requirementsSize.height + verticalInset * 2.0))
insets.top += requirementsBackgroundFrame.height
requirementsBackgroundNode.update(size: requirementsBackgroundFrame.size, transition: transition)
transition.updateFrame(node: requirementsBackgroundNode, frame: requirementsBackgroundFrame)
transition.updateFrame(node: requirementsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: requirementsBackgroundFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
requirementsTextNode.frame = CGRect(origin: CGPoint(x: insets.left + sideInset, y: requirementsBackgroundFrame.minY + verticalInset), size: requirementsSize)
}
}
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve)
@@ -614,6 +807,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
animationRenderer: self.animationRenderer,
updatedPresentationData: self.updatedPresentationData,
filter: self.filter,
requestPeerType: self.requestPeerType,
location: chatListLocation,
displaySearchFilters: false,
hasDownloads: false,
@@ -865,3 +1059,100 @@ final class PeerSelectionControllerNode: ASDisplayNode {
}
}
}
private func stringForRequestPeerType(strings: PresentationStrings, peerType: ReplyMarkupButtonRequestPeerType, offset: Bool) -> String? {
var lines: [String] = []
func append(_ string: String) {
if offset {
lines.append("\(string)")
} else {
lines.append("\(string)")
}
}
switch peerType {
case let .user(user):
if let isPremium = user.isPremium {
if isPremium {
append(strings.RequestPeer_Requirement_UserPremiumOn)
} else {
append(strings.RequestPeer_Requirement_UserPremiumOff)
}
}
case let .group(group):
if group.isCreator {
append(strings.RequestPeer_Requirement_Group_CreatorOn)
}
if let hasUsername = group.hasUsername {
if hasUsername {
append(strings.RequestPeer_Requirement_Group_HasUsernameOn)
} else {
append(strings.RequestPeer_Requirement_Group_HasUsernameOff)
}
}
if let isForum = group.isForum {
if isForum {
append(strings.RequestPeer_Requirement_Group_ForumOn)
} else {
append(strings.RequestPeer_Requirement_Group_ForumOff)
}
}
if let adminRights = group.userAdminRights {
var rights: [String] = []
if adminRights.rights.contains(.canChangeInfo) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Info)
}
if adminRights.rights.contains(.canPostMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Send)
}
if adminRights.rights.contains(.canDeleteMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Delete)
}
if adminRights.rights.contains(.canEditMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Edit)
}
if adminRights.rights.contains(.canBanUsers) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Ban)
}
if adminRights.rights.contains(.canInviteUsers) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Invite)
}
if adminRights.rights.contains(.canPinMessages) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Pin)
}
if adminRights.rights.contains(.canManageTopics) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Topics)
}
if adminRights.rights.contains(.canManageCalls) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_VideoChats)
}
if adminRights.rights.contains(.canBeAnonymous) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_Anonymous)
}
if adminRights.rights.contains(.canAddAdmins) {
rights.append(strings.RequestPeer_Requirement_Group_Rights_AddAdmins)
}
if !rights.isEmpty {
let rightsString = strings.RequestPeer_Requirement_Group_Rights(String(rights.joined(separator: ", "))).string
append(rightsString)
}
}
case let .channel(channel):
if channel.isCreator {
append(strings.RequestPeer_Requirement_Channel_CreatorOn)
}
if let hasUsername = channel.hasUsername {
if hasUsername {
append(strings.RequestPeer_Requirement_Channel_HasUsernameOn)
} else {
append(strings.RequestPeer_Requirement_Channel_HasUsernameOff)
}
}
}
if lines.isEmpty {
return nil
} else {
return String(lines.joined(separator: "\n"))
}
}