2023-10-11 00:40:43 +04:00

1681 lines
90 KiB
Swift

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<ChatPresentationInterfaceState>()
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?) -> 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<PresentationData>()
private let animationCache: AnimationCache
private let animationRenderer: MultiAnimationRenderer
private var readyValue = Promise<Bool>()
var ready: Signal<Bool, NoError> {
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, 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(previewing: false), chatLocation: .peer(id: PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil, threadData: nil, isGeneralThreadClosed: 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 in
self?.requestOpenDisabledPeer?(peer, threadId)
}
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
}, 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<ChatControllerSubject.ForwardOptions, NoError>
forwardOptions = strongSelf.presentationInterfaceStatePromise.get()
|> map { state -> ChatControllerSubject.ForwardOptions in
return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false, replyOptions: nil)
}
|> 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: ChatControllerSubject.MessageOptionsInfo(kind: .forward), options: forwardOptions),
botStart: nil,
mode: .standard(previewing: true)
)
chatController.canReadHistory.set(false)
let messageIds = strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? []
let messagesCount: Signal<Int, NoError>
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<PeerId>()
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
}, 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: {
}, 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<Int>?
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: {
}, 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 in
self?.requestOpenDisabledPeer?(peer, threadId)
},
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
}
}
}
}, 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)))
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.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() {
}
}