import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import AccountContext import SearchBarNode import SearchUI import ContactListUI import ChatListUI import SegmentedControlNode import AttachmentTextInputPanelNode import ChatPresentationInterfaceState import ChatSendMessageActionUI import ChatTextLinkEditUI import AnimationCache import MultiAnimationRenderer import AnimatedStickerNode import TelegramAnimatedStickerNode import SolidRoundedButtonNode import ContextUI import TextFormat import ForwardAccessoryPanelNode final class PeerSelectionControllerNode: ASDisplayNode { private let context: AccountContext private weak var controller: PeerSelectionControllerImpl? private let present: (ViewController, Any?) -> Void private let presentInGlobalOverlay: (ViewController, Any?) -> Void private let dismiss: () -> Void private let filter: ChatListNodePeersFilter private let forumPeerId: EnginePeer.Id? private let hasGlobalSearch: Bool private let forwardedMessageIds: [EngineMessage.Id] private let hasTypeHeaders: Bool private let requestPeerType: [ReplyMarkupButtonRequestPeerType]? private var presentationInterfaceState: ChatPresentationInterfaceState private let presentationInterfaceStatePromise = ValuePromise() private var interfaceInteraction: ChatPanelInterfaceInteraction? var inProgress: Bool = false 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? private var textInputPanelNode: AttachmentTextInputPanelNode? private var forwardAccessoryPanelNode: ForwardAccessoryPanelNode? var contactListNode: ContactListNode? let chatListNode: ChatListNode? let mainContainerNode: ChatListContainerNode? private var contactListActive = false private var searchDisplayController: SearchDisplayController? private var containerLayout: (ContainerViewLayout, CGFloat, CGFloat)? var contentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? var contentScrollingEnded: ((ListView) -> Bool)? var requestActivateSearch: (() -> Void)? var requestDeactivateSearch: (() -> Void)? var requestOpenPeer: ((EnginePeer, Int64?) -> Void)? var requestOpenDisabledPeer: ((EnginePeer, Int64?, ChatListDisabledPeerReason) -> Void)? var requestOpenPeerFromSearch: ((EnginePeer, Int64?) -> Void)? var requestOpenMessageFromSearch: ((EnginePeer, Int64?, EngineMessage.Id) -> Void)? var requestSend: (([EnginePeer], [EnginePeer.Id: EnginePeer], NSAttributedString, AttachmentTextInputPanelSendMode, ChatInterfaceForwardOptionsState?) -> Void)? private var presentationData: PresentationData { didSet { self.presentationDataPromise.set(.single(self.presentationData)) } } private var presentationDataPromise = Promise() private let animationCache: AnimationCache private let animationRenderer: MultiAnimationRenderer private var readyValue = Promise() var ready: Signal { return self.readyValue.get() } private var isEmpty = false private var updatedPresentationData: (initial: PresentationData, signal: Signal) { return (self.presentationData, self.presentationDataPromise.get()) } init(context: AccountContext, controller: PeerSelectionControllerImpl, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasFilters: Bool, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: [ReplyMarkupButtonRequestPeerType]?, hasCreation: Bool, 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 self.filter = filter self.forumPeerId = forumPeerId self.hasGlobalSearch = hasGlobalSearch self.forwardedMessageIds = forwardedMessageIds self.hasTypeHeaders = hasTypeHeaders self.requestPeerType = requestPeerType self.presentationData = presentationData self.animationCache = context.animationCache self.animationRenderer = context.animationRenderer self.presentationInterfaceState = ChatPresentationInterfaceState(chatWallpaper: .builtin(WallpaperSettings()), theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameDisplayOrder: self.presentationData.nameDisplayOrder, limitsConfiguration: self.context.currentLimitsConfiguration.with { $0 }, fontSize: self.presentationData.chatFontSize, bubbleCorners: self.presentationData.chatBubbleCorners, accountPeerId: self.context.account.peerId, mode: .standard(.default), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: nil, replyMessage: nil, accountPeerColor: nil) self.presentationInterfaceState = self.presentationInterfaceState.updatedInterfaceState { $0.withUpdatedForwardMessageIds(forwardedMessageIds) } self.presentationInterfaceStatePromise.set(self.presentationInterfaceState) 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) self.toolbarSeparatorNode = ASDisplayNode() self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor let items = [ self.presentationData.strings.DialogList_TabTitle, self.presentationData.strings.Contacts_TabTitle ] self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: self.presentationData.theme), items: items.map { SegmentedControlItem(title: $0) }, selectedIndex: 0) } else { self.toolbarBackgroundNode = nil self.toolbarSeparatorNode = nil self.segmentedControlNode = nil } var chatListCategories: [ChatListNodeAdditionalCategory] = [] if let _ = createNewGroup { chatListCategories.append(ChatListNodeAdditionalCategory(id: 0, icon: PresentationResourcesItemList.createGroupIcon(self.presentationData.theme), smallIcon: nil, title: self.presentationData.strings.PeerSelection_ImportIntoNewGroup, appearance: .action)) } let chatListLocation: ChatListControllerLocation if let forumPeerId = self.forumPeerId { chatListLocation = .forum(peerId: forumPeerId) } else { chatListLocation = .chatList(groupId: .root) } let chatListMode: ChatListNodeMode if let requestPeerType = self.requestPeerType { chatListMode = .peerType(type: requestPeerType, hasCreate: hasCreation) } else { chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false, displayPresence: false) } if hasFilters { self.mainContainerNode = ChatListContainerNode(context: context, controller: nil, location: chatListLocation, chatListMode: chatListMode, previewing: false, controlsHistoryPreload: false, isInlineMode: false, presentationData: presentationData, animationCache: self.animationCache, animationRenderer: self.animationRenderer, filterBecameEmpty: { _ in }, filterEmptyAction: { _ in }, secondaryEmptyAction: { }, openArchiveSettings: { }) self.chatListNode = nil } else { self.mainContainerNode = nil 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, autoSetReady: true, isMainTab: false) } super.init() self.setViewBlock({ return UITracingLayerView() }) self.chatListNode?.additionalCategorySelected = { _ in createNewGroup?() } self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.chatListNode?.selectionCountChanged = { [weak self] count in self?.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) } self.chatListNode?.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).string } self.chatListNode?.activateSearch = { [weak self] in self?.requestActivateSearch?() } self.mainContainerNode?.activateSearch = { [weak self] in self?.requestActivateSearch?() } self.chatListNode?.peerSelected = { [weak self] peer, threadId, _, _, _ in self?.chatListNode?.clearHighlightAnimated(true) self?.requestOpenPeer?(peer, threadId) } self.mainContainerNode?.peerSelected = { [weak self] peer, threadId, _, _, _ in self?.chatListNode?.clearHighlightAnimated(true) self?.requestOpenPeer?(peer, threadId) } self.chatListNode?.disabledPeerSelected = { [weak self] peer, threadId, reason in self?.requestOpenDisabledPeer?(peer, threadId, reason) } self.chatListNode?.contentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return } if strongSelf.chatListNode?.supernode != nil { strongSelf.contentOffsetChanged?(offset) } } self.mainContainerNode?.contentOffsetChanged = { [weak self] offset, _ in guard let strongSelf = self else { return } if strongSelf.chatListNode?.supernode != nil { strongSelf.contentOffsetChanged?(offset) } } self.chatListNode?.contentScrollingEnded = { [weak self] listView in 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) } } if let mainContainerNode = self.mainContainerNode { mainContainerNode.displayFilterLimit = { [weak self] in guard let strongSelf = self else { return } var replaceImpl: ((ViewController) -> Void)? let controller = context.sharedContext.makePremiumLimitController(context: context, subject: .folders, count: strongSelf.controller?.tabContainerNode?.filtersCount ?? 0, forceDark: false, cancel: {}, action: { let controller = context.sharedContext.makePremiumIntroController(context: context, source: .folders, forceDark: false, dismissed: nil) replaceImpl?(controller) return true }) replaceImpl = { [weak controller] c in controller?.replace(with: c) } strongSelf.controller?.push(controller) } self.addSubnode(mainContainerNode) } if let chatListNode = self.chatListNode { self.addSubnode(chatListNode) } if hasChatListSelector && hasContactSelector { self.segmentedControlNode!.selectedIndexChanged = { [weak self] index in self?.indexChanged(index) } self.addSubnode(self.toolbarBackgroundNode!) self.addSubnode(self.toolbarSeparatorNode!) 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) } self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _, _ in }, setupEditMessage: { _, _ in }, beginMessageSelection: { _, _ in }, cancelMessageSelection: { _ in }, deleteSelectedMessages: { }, reportSelectedMessages: { }, reportMessages: { _, _ in }, blockMessageAuthor: { _, _ in }, deleteMessages: { _, _, f in f(.default) }, forwardSelectedMessages: { }, forwardCurrentForwardMessages: { }, forwardMessages: { _ in }, updateForwardOptionsState: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState(f($0.forwardOptionsState ?? ChatInterfaceForwardOptionsState(hideNames: false, hideCaptions: false, unhideNamesOnCaptionChange: false))) }) }) } }, presentForwardOptions: { [weak self] sourceNode in guard let strongSelf = self else { return } let presentationData = strongSelf.presentationData let peerIds = strongSelf.selectedPeers.0.map { $0.id } let forwardOptions: Signal forwardOptions = strongSelf.presentationInterfaceStatePromise.get() |> map { state -> ChatControllerSubject.ForwardOptions in return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false) } |> distinctUntilChanged let chatController = strongSelf.context.sharedContext.makeChatController( context: strongSelf.context, chatLocation: .peer(id: strongSelf.context.account.peerId), subject: .messageOptions(peerIds: peerIds, ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], info: .forward(ChatControllerSubject.MessageOptionsInfo.Forward(options: forwardOptions))), botStart: nil, mode: .standard(.previewing) ) chatController.canReadHistory.set(false) let messageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] let messagesCount: Signal if messageIds.count > 1 { messagesCount = .single(messageIds.count) |> then( chatController.presentationInterfaceStateSignal |> map { state -> Int in guard let state = state as? ChatPresentationInterfaceState else { return 1 } return state.interfaceState.selectionState?.selectedIds.count ?? 1 } ) } else { messagesCount = .single(1) } let accountPeerId = strongSelf.context.account.peerId let items = combineLatest(forwardOptions, strongSelf.context.account.postbox.messagesAtIds(messageIds), messagesCount) |> map { forwardOptions, messages, messagesCount -> [ContextMenuItem] in var items: [ContextMenuItem] = [] var hasCaptions = false var uniquePeerIds = Set() var hasOther = false var hasNotOwnMessages = false for message in messages { if let author = message.effectiveAuthor { if !uniquePeerIds.contains(author.id) { uniquePeerIds.insert(author.id) } if message.id.peerId == accountPeerId && message.forwardInfo == nil { } else { hasNotOwnMessages = true } } var isDice = false var isMusic = false for media in message.media { if let media = media as? TelegramMediaFile, media.isMusic { isMusic = true } else if media is TelegramMediaDice { isDice = true } else { if !message.text.isEmpty { if media is TelegramMediaImage || media is TelegramMediaFile { hasCaptions = true } } } } if !isDice && !isMusic { hasOther = true } } let canHideNames = hasNotOwnMessages && hasOther let hideNames = forwardOptions.hideNames let hideCaptions = forwardOptions.hideCaptions if !"".isEmpty { // check if seecret chat } else { if canHideNames { items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_ShowSendersName : presentationData.strings.Conversation_ForwardOptions_ShowSendersNames, icon: { theme in if hideNames { return nil } else { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) } }, action: { [weak self] _, f in self?.interfaceInteraction?.updateForwardOptionsState({ current in var updated = current updated.hideNames = false updated.hideCaptions = false updated.unhideNamesOnCaptionChange = false return updated }) }))) items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames, icon: { theme in if hideNames { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) } else { return nil } }, action: { _, f in self?.interfaceInteraction?.updateForwardOptionsState({ current in var updated = current updated.hideNames = true updated.unhideNamesOnCaptionChange = false return updated }) }))) items.append(.separator) } if hasCaptions { items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowCaption, icon: { theme in if hideCaptions { return nil } else { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) } }, action: { [weak self] _, f in self?.interfaceInteraction?.updateForwardOptionsState({ current in var updated = current updated.hideCaptions = false if updated.unhideNamesOnCaptionChange { updated.unhideNamesOnCaptionChange = false updated.hideNames = false } return updated }) }))) items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { theme in if hideCaptions { return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor) } else { return nil } }, action: { _, f in self?.interfaceInteraction?.updateForwardOptionsState({ current in var updated = current updated.hideCaptions = true if !updated.hideNames { updated.hideNames = true updated.unhideNamesOnCaptionChange = true } return updated }) }))) items.append(.separator) } } items.append(.action(ContextMenuActionItem(text: messagesCount == 1 ? presentationData.strings.Conversation_ForwardOptions_SendMessage : presentationData.strings.Conversation_ForwardOptions_SendMessages, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.contextMenu.primaryColor) }, action: { [weak self, weak chatController] c, f in guard let strongSelf = self else { return } if let selectedMessageIds = chatController?.selectedMessageIds { var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) } strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) }) } strongSelf.textInputPanelNode?.sendMessage(.generic) f(.default) }))) return items } let contextController = ContextController(presentationData: strongSelf.presentationData, source: .controller(ContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)), items: items |> map { ContextController.Items(content: .list($0)) }) contextController.dismissedForCancel = { [weak chatController] in if let selectedMessageIds = chatController?.selectedMessageIds { var forwardMessageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [] forwardMessageIds = forwardMessageIds.filter { selectedMessageIds.contains($0) } strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds) }) }) } } contextController.immediateItemsTransitionAnimation = true strongSelf.controller?.presentInGlobalOverlay(contextController) }, presentReplyOptions: { _ in }, presentLinkOptions: { _ in }, shareSelectedMessages: { }, updateTextInputStateAndMode: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in let (updatedState, updatedMode) = f(state.interfaceState.effectiveInputState, state.inputMode) return state.updatedInterfaceState { interfaceState in return interfaceState.withUpdatedEffectiveInputState(updatedState) }.updatedInputMode({ _ in updatedMode }) }) } }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, { let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in var value = value value.closedButtonKeyboardMessageId = updatedClosedButtonKeyboardMessageId return value }) }) }) } }, openStickers: { }, editMessage: { }, beginMessageSearch: { _, _ in }, dismissMessageSearch: { }, updateMessageSearch: { _ in }, openSearchResults: { }, navigateMessageSearch: { _ in }, openCalendarSearch: { }, toggleMembersSearch: { _ in }, navigateToMessage: { _, _, _, _ in }, navigateToChat: { _ in }, navigateToProfile: { _ in }, openPeerInfo: { }, togglePeerNotifications: { }, sendContextResult: { _, _, _, _ in return false }, sendBotCommand: { _, _ in }, sendBotStart: { _ in }, botSwitchChatWithPayload: { _, _ in }, beginMediaRecording: { _ in }, finishMediaRecording: { _ in }, stopMediaRecording: { }, lockMediaRecording: { }, resumeMediaRecording: { }, deleteRecordedMedia: { }, sendRecordedMedia: { _, _ in }, displayRestrictedInfo: { _, _ in }, displayVideoUnmuteTip: { _ in }, switchMediaRecordingMode: { }, setupMessageAutoremoveTimeout: { }, sendSticker: { _, _, _, _, _, _ in return false }, unblockPeer: { }, pinMessage: { _, _ in }, unpinMessage: { _, _, _ in }, unpinAllMessages: { }, openPinnedList: { _ in }, shareAccountContact: { }, reportPeer: { }, presentPeerContact: { }, dismissReportPeer: { }, deleteChat: { }, beginCall: { _ in }, toggleMessageStickerStarred: { _ in }, presentController: { _, _ in }, presentControllerInCurrent: { _, _ in }, getNavigationController: { return nil }, presentGlobalOverlayController: { _, _ in }, navigateFeed: { }, openGrouping: { }, toggleSilentPost: { }, requestUnvoteInMessage: { _ in }, requestStopPollInMessage: { _ in }, updateInputLanguage: { _ in }, unarchiveChat: { }, openLinkEditing: { [weak self] in if let strongSelf = self { var selectionRange: Range? var text: NSAttributedString? var inputMode: ChatInputMode? strongSelf.updateChatPresentationInterfaceState(animated: true, { state in selectionRange = state.interfaceState.effectiveInputState.selectionRange if let selectionRange = selectionRange { text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)) } inputMode = state.inputMode return state }) var link: String? if let text { text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute { link = linkAttribute.url } } } let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange { if let link = link { strongSelf.updateChatPresentationInterfaceState(animated: true, { state in return state.updatedInterfaceState({ $0.withUpdatedEffectiveInputState(chatTextInputAddLinkAttribute($0.effectiveInputState, selectionRange: selectionRange, url: link)) }) }) } if let textInputPanelNode = strongSelf.textInputPanelNode { textInputPanelNode.ensureFocused() } strongSelf.updateChatPresentationInterfaceState(animated: true, { state in return state.updatedInputMode({ _ in return inputMode }).updatedInterfaceState({ $0.withUpdatedEffectiveInputState(ChatTextInputState(inputText: $0.effectiveInputState.inputText, selectionRange: selectionRange.endIndex ..< selectionRange.endIndex)) }) }) } }) strongSelf.present(controller, nil) } }, reportPeerIrrelevantGeoLocation: { }, displaySlowmodeTooltip: { _, _ in }, displaySendMessageOptions: { [weak self] node, gesture in guard let strongSelf = self, let textInputPanelNode = strongSelf.textInputPanelNode else { return } textInputPanelNode.loadTextInputNodeIfNeeded() guard let textInputNode = textInputPanelNode.textInputNode else { return } var hasEntityKeyboard = false if case .media = strongSelf.presentationInterfaceState.inputMode { hasEntityKeyboard = true } let controller = ChatSendMessageActionSheetController(context: strongSelf.context, peerId: strongSelf.presentationInterfaceState.chatLocation.peerId, forwardMessageIds: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds, hasEntityKeyboard: hasEntityKeyboard, gesture: gesture, sourceSendButton: node, textInputView: textInputNode.textView, canSendWhenOnline: false, completion: { }, sendMessage: { [weak textInputPanelNode] mode in switch mode { case .generic: textInputPanelNode?.sendMessage(.generic) case .silently: textInputPanelNode?.sendMessage(.silent) case .whenOnline: textInputPanelNode?.sendMessage(.whenOnline) } }, schedule: { [weak textInputPanelNode] in textInputPanelNode?.sendMessage(.schedule) }) controller.emojiViewProvider = textInputPanelNode.emojiViewProvider strongSelf.presentInGlobalOverlay(controller, nil) }, openScheduledMessages: { }, openPeersNearby: { }, displaySearchResultsTooltip: { _, _ in }, unarchivePeer: { }, scrollToTop: { }, viewReplies: { _, _ in }, activatePinnedListPreview: { _, _ in }, joinGroupCall: { _ in }, presentInviteMembers: { }, presentGigagroupHelp: { }, editMessageMedia: { _, _ in }, updateShowCommands: { _ in }, updateShowSendAsPeers: { _ in }, openInviteRequests: { }, openSendAsPeer: { _, _ in }, presentChatRequestAdminInfo: { }, displayCopyProtectionTip: { _, _ in }, openWebView: { _, _, _, _ in }, updateShowWebView: { _ in }, insertText: { _ in }, backwardsDeleteText: { }, restartTopic: { }, toggleTranslation: { _ in }, changeTranslationLanguage: { _ in }, addDoNotTranslateLanguage: { _ in }, hideTranslationPanel: { }, openPremiumGift: { }, openPremiumRequiredForMessaging: { }, updateHistoryFilter: { _ in }, updateDisplayHistoryFilterAsList: { _ in }, requestLayout: { _ in }, chatController: { return nil }, statuses: nil) if let chatListNode = self.chatListNode { self.readyValue.set(chatListNode.ready) } } func updatePresentationData(_ presentationData: PresentationData) { self.presentationData = presentationData self.updateThemeAndStrings() self.mainContainerNode?.updatePresentationData(presentationData) } private func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { self.updateChatPresentationInterfaceState(transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate, f, completion: completion) } private func updateChatPresentationInterfaceState(transition: ContainedViewLayoutTransition, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState, completion externalCompletion: @escaping (ContainedViewLayoutTransition) -> Void = { _ in }) { let presentationInterfaceState = f(self.presentationInterfaceState) let updateInputTextState = self.presentationInterfaceState.interfaceState.effectiveInputState != presentationInterfaceState.interfaceState.effectiveInputState self.presentationInterfaceState = presentationInterfaceState self.presentationInterfaceStatePromise.set(presentationInterfaceState) if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { textInputPanelNode.updateInputTextState(presentationInterfaceState.interfaceState.effectiveInputState, animated: transition.isAnimated) } if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: transition) } } private var selectedPeers: ([EnginePeer], [EnginePeer.Id: EnginePeer]) { if self.contactListActive { let selectedContactPeers = self.contactListNode?.selectedPeers ?? [] var selectedPeers: [EnginePeer] = [] var selectedPeerMap: [EnginePeer.Id: EnginePeer] = [:] for contactPeer in selectedContactPeers { if case let .peer(peer, _, _) = contactPeer { selectedPeers.append(EnginePeer(peer)) selectedPeerMap[peer.id] = EnginePeer(peer) } } return (selectedPeers, selectedPeerMap) } else { var selectedPeerIds: [EnginePeer.Id] = [] var selectedPeerMap: [EnginePeer.Id: EnginePeer] = [:] if let mainContainerNode = self.mainContainerNode { mainContainerNode.currentItemNode.updateState { state in selectedPeerIds = Array(state.selectedPeerIds) selectedPeerMap = state.selectedPeerMap return state } } if let chatListNode = self.chatListNode { chatListNode.updateState { state in selectedPeerIds = Array(state.selectedPeerIds) selectedPeerMap = state.selectedPeerMap return state } } var selectedPeers: [EnginePeer] = [] for peerId in selectedPeerIds { if let peer = selectedPeerMap[peerId] { selectedPeers.append(peer) } } return (selectedPeers, selectedPeerMap) } } func beginSelection() { if let _ = self.textInputPanelNode { } else { let forwardAccessoryPanelNode = ForwardAccessoryPanelNode(context: self.context, messageIds: self.forwardedMessageIds, theme: self.presentationData.theme, strings: self.presentationData.strings, fontSize: self.presentationData.chatFontSize, nameDisplayOrder: self.presentationData.nameDisplayOrder, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState, animationCache: nil, animationRenderer: nil) forwardAccessoryPanelNode.interfaceInteraction = self.interfaceInteraction self.addSubnode(forwardAccessoryPanelNode) self.forwardAccessoryPanelNode = forwardAccessoryPanelNode let textInputPanelNode = AttachmentTextInputPanelNode(context: self.context, presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) }, makeEntityInputView: { return nil }) textInputPanelNode.interfaceInteraction = self.interfaceInteraction textInputPanelNode.sendMessage = { [weak self] mode in guard let strongSelf = self else { return } let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText let forwardOptionsState = strongSelf.presentationInterfaceState.interfaceState.forwardOptionsState let (selectedPeers, selectedPeerMap) = strongSelf.selectedPeers if !selectedPeers.isEmpty { strongSelf.requestSend?(selectedPeers, selectedPeerMap, effectiveInputText, mode, forwardOptionsState) } } self.addSubnode(textInputPanelNode) self.textInputPanelNode = textInputPanelNode if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .animated(duration: 0.3, curve: .spring)) } } if self.contactListActive { self.contactListNode?.multipleSelection = true self.contactListNode?.updateSelectionState({ _ in return ContactListNodeGroupSelectionState() }) } else { if let mainContainerNode = self.mainContainerNode { mainContainerNode.currentItemNode.selectionCountChanged = { [weak self] count in self?.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) } mainContainerNode.currentItemNode.updateState({ state in var state = state state.editing = true return state }) } else if let chatListNode = self.chatListNode { chatListNode.updateState { state in var state = state state.editing = true return state } } } } private func updateThemeAndStrings() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(self.presentationData) self.chatListNode?.updateThemeAndStrings(theme: self.presentationData.theme, fontSize: self.presentationData.listsFontSize, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: true) 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)) if let (layout, navigationBarHeight, actualNavigationBarHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, actualNavigationBarHeight: actualNavigationBarHeight, transition: .immediate) } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, actualNavigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight) let cleanInsets = layout.insets(options: []) var insets = layout.insets(options: [.input]) var toolbarHeight: CGFloat = cleanInsets.bottom var textPanelHeight: CGFloat? var accessoryHeight: CGFloat = 0.0 if let forwardAccessoryPanelNode = self.forwardAccessoryPanelNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) accessoryHeight = size.height } if let textInputPanelNode = self.textInputPanelNode { var panelTransition = transition if textInputPanelNode.frame.width.isZero { panelTransition = .immediate } var panelHeight = textInputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: layout.intrinsicInsets.bottom, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics, isMediaInputExpanded: false) if self.searchDisplayController == nil { panelHeight += insets.bottom } else { panelHeight += cleanInsets.bottom } textPanelHeight = panelHeight let panelFrame = CGRect(x: 0.0, y: layout.size.height - panelHeight, width: layout.size.width, height: panelHeight) if textInputPanelNode.frame.width.isZero { var initialPanelFrame = panelFrame initialPanelFrame.origin.y = layout.size.height + accessoryHeight textInputPanelNode.frame = initialPanelFrame } transition.updateFrame(node: textInputPanelNode, frame: panelFrame) } if let forwardAccessoryPanelNode = self.forwardAccessoryPanelNode { let size = forwardAccessoryPanelNode.calculateSizeThatFits(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right, height: layout.size.height)) forwardAccessoryPanelNode.updateState(size: size, inset: layout.safeInsets.left, interfaceState: self.presentationInterfaceState) forwardAccessoryPanelNode.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings, forwardOptionsState: self.presentationInterfaceState.interfaceState.forwardOptionsState) let panelFrame = CGRect(x: 0.0, y: layout.size.height - (textPanelHeight ?? 0.0) - size.height, width: size.width, height: size.height) accessoryHeight = size.height if forwardAccessoryPanelNode.frame.width.isZero { var initialPanelFrame = panelFrame initialPanelFrame.origin.y = layout.size.height forwardAccessoryPanelNode.frame = initialPanelFrame } transition.updateFrame(node: forwardAccessoryPanelNode, frame: panelFrame) } if let segmentedControlNode = self.segmentedControlNode, let toolbarBackgroundNode = self.toolbarBackgroundNode, let toolbarSeparatorNode = self.toolbarSeparatorNode { if let textPanelHeight = textPanelHeight { toolbarHeight = textPanelHeight + accessoryHeight } else { toolbarHeight += 44.0 } transition.updateFrame(node: toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) toolbarBackgroundNode.update(size: toolbarBackgroundNode.bounds.size, transition: transition) transition.updateFrame(node: toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) let controlSize = segmentedControlNode.updateLayout(.sizeToFit(maximumWidth: layout.size.width, minimumWidth: 200.0, height: 32.0), transition: transition) let controlOrigin = layout.size.height - (textPanelHeight == nil ? toolbarHeight : 0.0) + floor((44.0 - controlSize.height) / 2.0) transition.updateFrame(node: segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: controlOrigin), size: controlSize)) } insets.top += navigationBarHeight insets.bottom = max(insets.bottom, toolbarHeight) insets.left += layout.safeInsets.left insets.right += layout.safeInsets.right var headerInsets = layout.insets(options: [.input]) headerInsets.top += actualNavigationBarHeight headerInsets.bottom = max(headerInsets.bottom, cleanInsets.bottom) headerInsets.left += layout.safeInsets.left headerInsets.right += layout.safeInsets.right if let chatListNode = self.chatListNode { chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) } if let mainContainerNode = self.mainContainerNode { transition.updateFrame(node: mainContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) mainContainerNode.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: actualNavigationBarHeight, originalNavigationHeight: navigationBarHeight, cleanNavigationBarHeight: navigationBarHeight, insets: insets, isReorderingFilters: false, isEditing: false, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0, storiesInset: 0.0, transition: transition) } if let requestPeerTypes = self.requestPeerType, let requestPeerType = requestPeerTypes.first { 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) if let chatListNode = self.chatListNode { chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: updateSizeAndInsets.insets.top, originalTopInset: updateSizeAndInsets.insets.top, storiesInset: 0.0, inlineNavigationLocation: nil, inlineNavigationTransitionFraction: 0.0) } if let contactListNode = self.contactListNode { contactListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) contactListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: insets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, storiesInset: 0.0, transition: transition) } if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) } } func activateSearch(placeholderNode: SearchBarPlaceholderNode) { guard let (containerLayout, navigationBarHeight, _) = self.containerLayout, let navigationBar = self.navigationBar else { return } self.navigationBar?.setSecondaryContentNode(nil, animated: true) if self.chatListNode?.supernode != nil || self.mainContainerNode?.supernode != nil { self.chatListNode?.accessibilityElementsHidden = true self.mainContainerNode?.accessibilityElementsHidden = true let chatListLocation: ChatListControllerLocation if let forumPeerId = self.forumPeerId { chatListLocation = .forum(peerId: forumPeerId) } else { chatListLocation = .chatList(groupId: EngineChatList.Group(.root)) } self.searchDisplayController = SearchDisplayController( presentationData: self.presentationData, contentNode: ChatListSearchContainerNode( context: self.context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, updatedPresentationData: self.updatedPresentationData, filter: self.filter, requestPeerType: self.requestPeerType, location: chatListLocation, displaySearchFilters: false, hasDownloads: false, openPeer: { [weak self] peer, chatPeer, threadId, _ in guard let strongSelf = self else { return } var updated = false var count = 0 let chatListNode: ChatListNode? if let mainContainerNode = strongSelf.mainContainerNode { chatListNode = mainContainerNode.currentItemNode } else { chatListNode = strongSelf.chatListNode } chatListNode?.updateState { state in if state.editing { updated = true var state = state var foundPeers = state.foundPeers var selectedPeerMap = state.selectedPeerMap selectedPeerMap[peer.id] = peer if case .secretChat = peer, let chatPeer = chatPeer { selectedPeerMap[chatPeer.id] = chatPeer } var exists = false for foundPeer in foundPeers { if peer.id == foundPeer.0.id { exists = true break } } if !exists { foundPeers.insert((peer, chatPeer), at: 0) } if state.selectedPeerIds.contains(peer.id) { state.selectedPeerIds.remove(peer.id) } else { state.selectedPeerIds.insert(peer.id) } state.foundPeers = foundPeers state.selectedPeerMap = selectedPeerMap count = state.selectedPeerIds.count return state } else { return state } } if updated { strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) strongSelf.requestDeactivateSearch?() } else if let requestOpenPeerFromSearch = strongSelf.requestOpenPeerFromSearch { requestOpenPeerFromSearch(peer, threadId) } }, openDisabledPeer: { [weak self] peer, threadId, reason in self?.requestOpenDisabledPeer?(peer, threadId, reason) }, openRecentPeerOptions: { _ in }, openMessage: { [weak self] peer, threadId, messageId, _ in if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { requestOpenMessageFromSearch(peer, threadId, messageId) } }, addContact: nil, peerContextAction: nil, present: { [weak self] c, a in self?.present(c, a) }, presentInGlobalOverlay: { _, _ in }, navigationController: nil, parentController: { [weak self] in return self?.controller } ), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() } } ) self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { if isSearchBar { strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) } else { strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) } } }, placeholder: placeholderNode) } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.accessibilityElementsHidden = true var categories: ContactsSearchCategories = [.cloudContacts] if self.hasGlobalSearch { categories.insert(.global) } self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, updatedPresentationData: self.updatedPresentationData, onlyWriteable: true, categories: categories, addContact: nil, openPeer: { [weak self] peer in if let strongSelf = self { var updated = false var count = 0 strongSelf.contactListNode?.updateSelectionState { state -> ContactListNodeGroupSelectionState? in if let state = state { updated = true var foundPeers = state.foundPeers var selectedPeerMap = state.selectedPeerMap selectedPeerMap[peer.id] = peer var exists = false for foundPeer in foundPeers { if peer.id == foundPeer.id { exists = true break } } if !exists { foundPeers.insert(peer, at: 0) } let updatedState = state.withToggledPeerId(peer.id).withFoundPeers(foundPeers).withSelectedPeerMap(selectedPeerMap) count = updatedState.selectedPeerIndices.count return updatedState } else { return nil } } if updated { strongSelf.textInputPanelNode?.updateSendButtonEnabled(count > 0, animated: true) strongSelf.requestDeactivateSearch?() } else { switch peer { case let .peer(peer, _, _): let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id)) |> deliverOnMainQueue).start(next: { peer in if let strongSelf = self, let peer = peer { strongSelf.requestOpenPeerFromSearch?(peer, nil) } }) case .deviceContact: break } } } }, openDisabledPeer: { [weak self] peer, reason in guard let self else { return } self.requestOpenDisabledPeer?(peer, nil, reason) }, contextAction: nil), cancel: { [weak self] in if let requestDeactivateSearch = self?.requestDeactivateSearch { requestDeactivateSearch() } }) self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) self.searchDisplayController?.activate(insertSubnode: { [weak self, weak placeholderNode] subnode, isSearchBar in if let strongSelf = self, let strongPlaceholderNode = placeholderNode { if isSearchBar { strongPlaceholderNode.supernode?.insertSubnode(subnode, aboveSubnode: strongPlaceholderNode) } else { strongSelf.insertSubnode(subnode, belowSubnode: navigationBar) } } }, placeholder: placeholderNode) } } func deactivateSearch(placeholderNode: SearchBarPlaceholderNode) { if let searchDisplayController = self.searchDisplayController { if self.chatListNode?.supernode != nil || self.mainContainerNode?.supernode != nil { self.chatListNode?.accessibilityElementsHidden = false self.mainContainerNode?.accessibilityElementsHidden = false self.navigationBar?.setSecondaryContentNode(self.controller?.tabContainerNode, animated: true) self.controller?.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { contactListNode.accessibilityElementsHidden = false self.controller?.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) searchDisplayController.deactivate(placeholder: placeholderNode) self.searchDisplayController = nil } } } func scrollToTop() { if self.mainContainerNode?.supernode != nil { self.mainContainerNode?.scrollToTop(animated: true, adjustForTempInset: false) } else if self.chatListNode?.supernode != nil { self.chatListNode?.scrollToPosition(.top(adjustForTempInset: false)) } else if let contactListNode = self.contactListNode, contactListNode.supernode != nil { //contactListNode.scrollToTop() } } private func indexChanged(_ index: Int) { let contactListActive = index == 1 if contactListActive != self.contactListActive { self.contactListActive = contactListActive if contactListActive { if let contactListNode = self.contactListNode { self.navigationBar?.setSecondaryContentNode(nil, animated: false) if let chatListNode = self.chatListNode, chatListNode.supernode != nil { self.insertSubnode(contactListNode, aboveSubnode: chatListNode) chatListNode.removeFromSupernode() } else if let mainContainerNode = self.mainContainerNode, mainContainerNode.supernode != nil { self.insertSubnode(contactListNode, aboveSubnode: mainContainerNode) mainContainerNode.removeFromSupernode() } self.recursivelyEnsureDisplaySynchronously(true) contactListNode.enableUpdates = true if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } } else { let contactListNode = ContactListNode(context: self.context, updatedPresentationData: self.updatedPresentationData, presentation: .single(.natural(options: [], includeChatList: false, topPeers: false)), onlyWriteable: self.filter.contains(.onlyWriteable)) self.contactListNode = contactListNode contactListNode.enableUpdates = true contactListNode.selectionStateUpdated = { [weak self] selectionState in if let strongSelf = self { strongSelf.textInputPanelNode?.updateSendButtonEnabled((selectionState?.selectedPeerIndices.count ?? 0) > 0, animated: true) } } contactListNode.activateSearch = { [weak self] in self?.requestActivateSearch?() } contactListNode.openPeer = { [weak self] peer, _ in if case let .peer(peer, _, _) = peer { self?.contactListNode?.listNode.clearHighlightAnimated(true) self?.requestOpenPeer?(EnginePeer(peer), nil) } } contactListNode.openDisabledPeer = { [weak self] peer, reason in guard let self else { return } self.requestOpenDisabledPeer?(peer, nil, reason) } contactListNode.suppressPermissionWarning = { [weak self] in if let strongSelf = self { strongSelf.context.sharedContext.presentContactsWarningSuppression(context: strongSelf.context, present: { c, a in strongSelf.present(c, a) }) } } contactListNode.contentOffsetChanged = { [weak self] offset in guard let strongSelf = self else { return } if strongSelf.contactListNode?.supernode != nil { strongSelf.contentOffsetChanged?(offset) } } contactListNode.contentScrollingEnded = { [weak self] listView in return self?.contentScrollingEnded?(listView) ?? false } if let (layout, navigationHeight, actualNavigationHeight) = self.containerLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, actualNavigationBarHeight: actualNavigationHeight, transition: .immediate) let _ = (contactListNode.ready |> deliverOnMainQueue).start(next: { [weak self] _ in if let strongSelf = self { strongSelf.navigationBar?.setSecondaryContentNode(nil, animated: false) if let contactListNode = strongSelf.contactListNode { if let chatListNode = strongSelf.chatListNode, chatListNode.supernode != nil { strongSelf.insertSubnode(contactListNode, aboveSubnode: chatListNode) chatListNode.removeFromSupernode() } else if let mainContainerNode = strongSelf.mainContainerNode, mainContainerNode.supernode != nil { strongSelf.insertSubnode(contactListNode, aboveSubnode: mainContainerNode) mainContainerNode.removeFromSupernode() } } strongSelf.recursivelyEnsureDisplaySynchronously(true) if let (layout, _, _) = strongSelf.containerLayout { strongSelf.controller?.containerLayoutUpdated(layout, transition: .immediate) } } }) } else { self.navigationBar?.setSecondaryContentNode(nil, animated: false) if let chatListNode = self.chatListNode { self.insertSubnode(contactListNode, aboveSubnode: chatListNode) chatListNode.removeFromSupernode() } else if let mainContainerNode = self.mainContainerNode { self.insertSubnode(contactListNode, aboveSubnode: mainContainerNode) mainContainerNode.removeFromSupernode() } self.recursivelyEnsureDisplaySynchronously(true) if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } } } } else if let contactListNode = self.contactListNode { self.navigationBar?.setSecondaryContentNode(self.controller?.tabContainerNode, animated: false) contactListNode.enableUpdates = false if let mainContainerNode = self.mainContainerNode { self.insertSubnode(mainContainerNode, aboveSubnode: contactListNode) } if let chatListNode = self.chatListNode { self.insertSubnode(chatListNode, aboveSubnode: contactListNode) } contactListNode.removeFromSupernode() if let (layout, _, _) = self.containerLayout { self.controller?.containerLayoutUpdated(layout, transition: .immediate) } } } } } public func stringForAdminRights(strings: PresentationStrings, adminRights: TelegramChatAdminRights, isChannel: Bool) -> String { var rights: [String] = [] func append(_ string: String) { rights.append("• \(string)") } if isChannel { if adminRights.rights.contains(.canChangeInfo) { append(strings.RequestPeer_Requirement_Channel_Rights_Info) } if adminRights.rights.contains(.canPostMessages) { append(strings.RequestPeer_Requirement_Channel_Rights_Send) } if adminRights.rights.contains(.canDeleteMessages) { append(strings.RequestPeer_Requirement_Channel_Rights_Delete) } if adminRights.rights.contains(.canEditMessages) { append(strings.RequestPeer_Requirement_Channel_Rights_Edit) } if adminRights.rights.contains(.canInviteUsers) { append(strings.RequestPeer_Requirement_Channel_Rights_Invite) } if adminRights.rights.contains(.canPinMessages) { append(strings.RequestPeer_Requirement_Channel_Rights_Pin) } if adminRights.rights.contains(.canManageTopics) { append(strings.RequestPeer_Requirement_Channel_Rights_Topics) } if adminRights.rights.contains(.canManageCalls) { append(strings.RequestPeer_Requirement_Channel_Rights_VideoChats) } if adminRights.rights.contains(.canBeAnonymous) { append(strings.RequestPeer_Requirement_Channel_Rights_Anonymous) } if adminRights.rights.contains(.canAddAdmins) { append(strings.RequestPeer_Requirement_Channel_Rights_AddAdmins) } } else { if adminRights.rights.contains(.canChangeInfo) { append(strings.RequestPeer_Requirement_Group_Rights_Info) } if adminRights.rights.contains(.canPostMessages) { append(strings.RequestPeer_Requirement_Group_Rights_Send) } if adminRights.rights.contains(.canDeleteMessages) { append(strings.RequestPeer_Requirement_Group_Rights_Delete) } if adminRights.rights.contains(.canEditMessages) { append(strings.RequestPeer_Requirement_Group_Rights_Edit) } if adminRights.rights.contains(.canBanUsers) { append(strings.RequestPeer_Requirement_Group_Rights_Ban) } if adminRights.rights.contains(.canInviteUsers) { append(strings.RequestPeer_Requirement_Group_Rights_Invite) } if adminRights.rights.contains(.canPinMessages) { append(strings.RequestPeer_Requirement_Group_Rights_Pin) } if adminRights.rights.contains(.canManageTopics) { append(strings.RequestPeer_Requirement_Group_Rights_Topics) } if adminRights.rights.contains(.canManageCalls) { append(strings.RequestPeer_Requirement_Group_Rights_VideoChats) } if adminRights.rights.contains(.canBeAnonymous) { append(strings.RequestPeer_Requirement_Group_Rights_Anonymous) } if adminRights.rights.contains(.canAddAdmins) { append(strings.RequestPeer_Requirement_Group_Rights_AddAdmins) } } if !rights.isEmpty { return String(rights.joined(separator: "\n")) } else { return "" } } 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 group.botParticipant { append(strings.RequestPeer_Requirement_Group_ParticipantOn) } if let adminRights = group.userAdminRights, !group.isCreator { 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 let adminRights = channel.userAdminRights, !channel.isCreator { var rights: [String] = [] if adminRights.rights.contains(.canChangeInfo) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Info) } if adminRights.rights.contains(.canPostMessages) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Send) } if adminRights.rights.contains(.canDeleteMessages) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Delete) } if adminRights.rights.contains(.canEditMessages) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Edit) } if adminRights.rights.contains(.canInviteUsers) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Invite) } if adminRights.rights.contains(.canPinMessages) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Pin) } if adminRights.rights.contains(.canManageTopics) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Topics) } if adminRights.rights.contains(.canManageCalls) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_VideoChats) } if adminRights.rights.contains(.canBeAnonymous) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_Anonymous) } if adminRights.rights.contains(.canAddAdmins) { rights.append(strings.RequestPeer_Requirement_Channel_Rights_AddAdmins) } if !rights.isEmpty { let rightsString = strings.RequestPeer_Requirement_Group_Rights(String(rights.joined(separator: ", "))).string append(rightsString) } } } if lines.isEmpty { return nil } else { return String(lines.joined(separator: "\n")) } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController weak var sourceNode: ASDisplayNode? let sourceRect: CGRect? let navigationController: NavigationController? = nil let passthroughTouches: Bool init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) { self.controller = controller self.sourceNode = sourceNode self.sourceRect = sourceRect self.passthroughTouches = passthroughTouches } func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceNode = self.sourceNode let sourceRect = self.sourceRect return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { return (sourceNode.view, sourceRect ?? sourceNode.bounds) } else { return nil } }) } func animatedIn() { } }