mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1249 lines
67 KiB
Swift
1249 lines
67 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
|
|
|
|
final class PeerSelectionControllerNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private weak var controller: PeerSelectionController?
|
|
private let present: (ViewController, Any?) -> Void
|
|
private let presentInGlobalOverlay: (ViewController, Any?) -> Void
|
|
private let dismiss: () -> Void
|
|
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 var interfaceInteraction: ChatPanelInterfaceInteraction?
|
|
|
|
var inProgress: Bool = false {
|
|
didSet {
|
|
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
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: ((Peer, Int64?) -> Void)?
|
|
var requestOpenDisabledPeer: ((Peer, Int64?) -> Void)?
|
|
var requestOpenPeerFromSearch: ((Peer, Int64?) -> Void)?
|
|
var requestOpenMessageFromSearch: ((Peer, Int64?, MessageId) -> Void)?
|
|
var requestSend: (([Peer], [PeerId: Peer], 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: PeerSelectionController, presentationData: PresentationData, filter: ChatListNodePeersFilter, forumPeerId: EnginePeer.Id?, hasChatListSelector: Bool, hasContactSelector: Bool, hasGlobalSearch: Bool, forwardedMessageIds: [EngineMessage.Id], hasTypeHeaders: Bool, requestPeerType: ReplyMarkupButtonRequestPeerType?, createNewGroup: (() -> Void)?, present: @escaping (ViewController, Any?) -> Void, presentInGlobalOverlay: @escaping (ViewController, Any?) -> Void, dismiss: @escaping () -> Void) {
|
|
self.context = context
|
|
self.controller = controller
|
|
self.present = present
|
|
self.presentInGlobalOverlay = presentInGlobalOverlay
|
|
self.dismiss = dismiss
|
|
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) }
|
|
|
|
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)
|
|
} else {
|
|
chatListMode = .peers(filter: filter, isSelecting: false, additionalCategories: chatListCategories, chatListFilters: nil, displayAutoremoveTimeout: false)
|
|
}
|
|
|
|
self.chatListNode = ChatListNode(context: context, location: chatListLocation, previewing: false, fillPreloadItems: false, mode: chatListMode, theme: self.presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, animationCache: self.animationCache, animationRenderer: self.animationRenderer, disableAnimations: true, isInlineMode: false)
|
|
|
|
super.init()
|
|
|
|
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.chatListNode.peerSelected = { [weak self] peer, threadId, _, _, _ in
|
|
self?.chatListNode.clearHighlightAnimated(true)
|
|
self?.requestOpenPeer?(peer._asPeer(), threadId)
|
|
}
|
|
|
|
self.chatListNode.disabledPeerSelected = { [weak self] peer, threadId in
|
|
self?.requestOpenDisabledPeer?(peer._asPeer(), threadId)
|
|
}
|
|
|
|
self.chatListNode.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)
|
|
}
|
|
}
|
|
|
|
self.addSubnode(self.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] value in
|
|
if let strongSelf = self {
|
|
strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardOptionsState($0.forwardOptionsState) }) })
|
|
}
|
|
}, presentForwardOptions: { _ 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
|
|
}, 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: String?
|
|
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)).string
|
|
}
|
|
inputMode = state.inputMode
|
|
return state
|
|
})
|
|
|
|
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text ?? "", link: nil, 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
|
|
}
|
|
let controller = ChatSendMessageActionSheetController(context: strongSelf.context, interfaceState: strongSelf.presentationInterfaceState, gesture: gesture, sourceSendButton: node, textInputNode: textInputNode, completion: {
|
|
}, sendMessage: { [weak textInputPanelNode] silently in
|
|
textInputPanelNode?.sendMessage(silently ? .silent : .generic)
|
|
}, 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: {
|
|
}, requestLayout: { _ in
|
|
}, chatController: {
|
|
return nil
|
|
}, statuses: nil)
|
|
|
|
self.readyValue.set(self.chatListNode.ready)
|
|
}
|
|
|
|
func updatePresentationData(_ presentationData: PresentationData) {
|
|
self.presentationData = presentationData
|
|
self.updateThemeAndStrings()
|
|
}
|
|
|
|
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
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
if strongSelf.contactListActive {
|
|
strongSelf.contactListNode?.multipleSelection = true
|
|
let selectedContactPeers = strongSelf.contactListNode?.selectedPeers ?? []
|
|
|
|
var selectedPeers: [Peer] = []
|
|
var selectedPeerMap: [PeerId: Peer] = [:]
|
|
for contactPeer in selectedContactPeers {
|
|
if case let .peer(peer, _, _) = contactPeer {
|
|
selectedPeers.append(peer)
|
|
selectedPeerMap[peer.id] = peer
|
|
}
|
|
}
|
|
if !selectedPeers.isEmpty {
|
|
strongSelf.requestSend?(selectedPeers, selectedPeerMap, effectiveInputText, mode, forwardOptionsState)
|
|
}
|
|
} else {
|
|
var selectedPeerIds: [PeerId] = []
|
|
var selectedPeerMap: [PeerId: Peer] = [:]
|
|
strongSelf.chatListNode.updateState { state in
|
|
selectedPeerIds = Array(state.selectedPeerIds)
|
|
selectedPeerMap = state.selectedPeerMap.mapValues({ $0._asPeer() })
|
|
return state
|
|
}
|
|
if !selectedPeerIds.isEmpty {
|
|
var selectedPeers: [Peer] = []
|
|
for peerId in selectedPeerIds {
|
|
if let peer = selectedPeerMap[peerId] {
|
|
selectedPeers.append(peer)
|
|
}
|
|
}
|
|
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?.updateSelectionState({ _ in
|
|
return ContactListNodeGroupSelectionState()
|
|
})
|
|
} else {
|
|
self.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, cleanInsets.bottom + 44.0)
|
|
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
|
|
|
|
self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
|
|
|
if let requestPeerType = self.requestPeerType {
|
|
if self.isEmpty {
|
|
self.chatListNode.isHidden = true
|
|
self.requirementsBackgroundNode?.isHidden = true
|
|
self.requirementsTextNode?.isHidden = true
|
|
self.requirementsSeparatorNode?.isHidden = true
|
|
self.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
|
|
|
|
var emptyTitle: String
|
|
var emptyText: String
|
|
var emptyButtonText: String
|
|
switch requestPeerType {
|
|
case let .user(user):
|
|
if let isBot = user.isBot, isBot {
|
|
emptyTitle = self.presentationData.strings.RequestPeer_BotsAllEmpty
|
|
emptyText = ""
|
|
} else {
|
|
emptyTitle = self.presentationData.strings.RequestPeer_UsersAllEmpty
|
|
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
|
|
emptyTitle = self.presentationData.strings.RequestPeer_UsersEmpty
|
|
emptyText = text
|
|
} else {
|
|
emptyText = ""
|
|
}
|
|
}
|
|
emptyButtonText = ""
|
|
case .group:
|
|
emptyTitle = self.presentationData.strings.RequestPeer_GroupsAllEmpty
|
|
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
|
|
emptyTitle = self.presentationData.strings.RequestPeer_GroupsEmpty
|
|
emptyText = text
|
|
} else {
|
|
emptyText = ""
|
|
}
|
|
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
|
|
case .channel:
|
|
emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty
|
|
if let text = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: false) {
|
|
emptyTitle = self.presentationData.strings.RequestPeer_ChannelsEmpty
|
|
emptyText = text
|
|
} else {
|
|
emptyText = ""
|
|
}
|
|
emptyButtonText = self.presentationData.strings.RequestPeer_CreateNewGroup
|
|
}
|
|
|
|
self.emptyTitleNode.attributedText = NSAttributedString(string: emptyTitle, font: Font.semibold(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: Font.regular(15.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
|
|
|
|
let padding: CGFloat = 44.0
|
|
let emptyTitleSize = self.emptyTitleNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
|
let emptyTextSize = self.emptyTextNode.updateLayout(CGSize(width: layout.size.width - insets.left * 2.0 - padding * 2.0, height: CGFloat.greatestFiniteMagnitude))
|
|
|
|
let emptyAnimationHeight = self.emptyAnimationSize.height
|
|
let emptyAnimationSpacing: CGFloat = 12.0
|
|
let emptyTextSpacing: CGFloat = 17.0
|
|
var emptyButtonSpacing: CGFloat = 15.0
|
|
var emptyButtonHeight: CGFloat = 50.0
|
|
if emptyButtonText.isEmpty {
|
|
emptyButtonSpacing = 0.0
|
|
emptyButtonHeight = 0.0
|
|
}
|
|
let emptyTotalHeight = emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing + emptyButtonSpacing + emptyButtonHeight
|
|
let emptyAnimationY = floorToScreenPixels((layout.size.height - emptyTotalHeight) / 2.0)
|
|
|
|
if !emptyButtonText.isEmpty {
|
|
let buttonPadding: CGFloat = 30.0
|
|
self.emptyButtonNode.title = emptyButtonText
|
|
self.emptyButtonNode.isHidden = false
|
|
let emptyButtonWidth = layout.size.width - insets.left - insets.right - buttonPadding * 2.0
|
|
let _ = self.emptyButtonNode.updateLayout(width: emptyButtonWidth, transition: transition)
|
|
transition.updateFrame(node: self.emptyButtonNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyButtonWidth) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing + emptyTextSize.height + emptyButtonSpacing), size: CGSize(width: emptyButtonWidth, height: emptyButtonHeight)))
|
|
} else {
|
|
self.emptyButtonNode.isHidden = true
|
|
}
|
|
|
|
let textTransition = ContainedViewLayoutTransition.immediate
|
|
textTransition.updateFrame(node: self.emptyAnimationNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - self.emptyAnimationSize.width) / 2.0), y: emptyAnimationY), size: self.emptyAnimationSize))
|
|
textTransition.updateFrame(node: self.emptyTitleNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTitleSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing), size: emptyTitleSize))
|
|
textTransition.updateFrame(node: self.emptyTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - emptyTextSize.width) / 2.0), y: emptyAnimationY + emptyAnimationHeight + emptyAnimationSpacing + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize))
|
|
self.emptyAnimationNode.updateLayout(size: self.emptyAnimationSize)
|
|
|
|
self.emptyAnimationNode.isHidden = false
|
|
self.emptyTitleNode.isHidden = false
|
|
self.emptyTextNode.isHidden = false
|
|
self.emptyAnimationNode.visibility = true
|
|
} else if let requirementsBackgroundNode = self.requirementsBackgroundNode, let requirementsSeparatorNode = self.requirementsSeparatorNode, let requirementsTextNode = self.requirementsTextNode, let requirementsText = stringForRequestPeerType(strings: self.presentationData.strings, peerType: requestPeerType, offset: true) {
|
|
let requirements = NSMutableAttributedString(string: self.presentationData.strings.RequestPeer_Requirements + "\n", font: Font.semibold(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor)
|
|
requirements.append(NSAttributedString(string: requirementsText, font: Font.regular(13.0), textColor: self.presentationData.theme.list.itemSecondaryTextColor))
|
|
|
|
requirementsTextNode.attributedText = requirements
|
|
let sideInset: CGFloat = 16.0
|
|
let verticalInset: CGFloat = 11.0
|
|
let requirementsSize = requirementsTextNode.updateLayout(CGSize(width: layout.size.width - insets.left - insets.right - sideInset * 2.0, height: .greatestFiniteMagnitude))
|
|
|
|
let requirementsBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: actualNavigationBarHeight), size: CGSize(width: layout.size.width, height: requirementsSize.height + verticalInset * 2.0))
|
|
insets.top += requirementsBackgroundFrame.height
|
|
|
|
requirementsBackgroundNode.update(size: requirementsBackgroundFrame.size, transition: transition)
|
|
transition.updateFrame(node: requirementsBackgroundNode, frame: requirementsBackgroundFrame)
|
|
|
|
transition.updateFrame(node: requirementsSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: requirementsBackgroundFrame.maxY - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
|
|
|
requirementsTextNode.frame = CGRect(origin: CGPoint(x: insets.left + sideInset, y: requirementsBackgroundFrame.minY + verticalInset), size: requirementsSize)
|
|
}
|
|
}
|
|
|
|
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
|
|
let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, headerInsets: headerInsets, duration: duration, curve: curve)
|
|
|
|
self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, visibleTopInset: updateSizeAndInsets.insets.top, originalTopInset: updateSizeAndInsets.insets.top, 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)
|
|
|
|
let contactsInsets = insets
|
|
|
|
contactListNode.containerLayoutUpdated(ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: contactsInsets, safeInsets: layout.safeInsets, additionalInsets: layout.additionalInsets, statusBarHeight: layout.statusBarHeight, inputHeight: layout.inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver), headerInsets: headerInsets, 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
|
|
}
|
|
|
|
if self.chatListNode.supernode != nil {
|
|
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
|
|
strongSelf.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._asPeer(), threadId)
|
|
}
|
|
},
|
|
openDisabledPeer: { [weak self] peer, threadId in
|
|
self?.requestOpenDisabledPeer?(peer._asPeer(), threadId)
|
|
},
|
|
openRecentPeerOptions: { _ in
|
|
},
|
|
openMessage: { [weak self] peer, threadId, messageId, _ in
|
|
if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch {
|
|
requestOpenMessageFromSearch(peer._asPeer(), threadId, messageId)
|
|
}
|
|
},
|
|
addContact: nil,
|
|
peerContextAction: nil,
|
|
present: { [weak self] c, a in
|
|
self?.present(c, a)
|
|
},
|
|
presentInGlobalOverlay: { _, _ in
|
|
},
|
|
navigationController: 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)
|
|
|
|
} else if let contactListNode = self.contactListNode, contactListNode.supernode != nil {
|
|
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._asPeer(), 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 {
|
|
searchDisplayController.deactivate(placeholder: placeholderNode)
|
|
self.searchDisplayController = nil
|
|
} else if let contactListNode = self.contactListNode, contactListNode.supernode != nil {
|
|
searchDisplayController.deactivate(placeholder: placeholderNode)
|
|
self.searchDisplayController = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func scrollToTop() {
|
|
if self.chatListNode.supernode != nil {
|
|
self.chatListNode.scrollToPosition(.top)
|
|
} 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.insertSubnode(contactListNode, aboveSubnode: self.chatListNode)
|
|
self.chatListNode.removeFromSupernode()
|
|
self.recursivelyEnsureDisplaySynchronously(true)
|
|
contactListNode.enableUpdates = true
|
|
} 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?(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 {
|
|
if let contactListNode = strongSelf.contactListNode {
|
|
strongSelf.insertSubnode(contactListNode, aboveSubnode: strongSelf.chatListNode)
|
|
}
|
|
strongSelf.chatListNode.removeFromSupernode()
|
|
strongSelf.recursivelyEnsureDisplaySynchronously(true)
|
|
}
|
|
})
|
|
} else {
|
|
if let contactListNode = self.contactListNode {
|
|
self.insertSubnode(contactListNode, aboveSubnode: self.chatListNode)
|
|
}
|
|
self.chatListNode.removeFromSupernode()
|
|
self.recursivelyEnsureDisplaySynchronously(true)
|
|
}
|
|
}
|
|
} else if let contactListNode = self.contactListNode {
|
|
contactListNode.enableUpdates = false
|
|
|
|
self.insertSubnode(self.chatListNode, aboveSubnode: contactListNode)
|
|
contactListNode.removeFromSupernode()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func stringForAdminRights(strings: PresentationStrings, adminRights: TelegramChatAdminRights) -> String {
|
|
var rights: [String] = []
|
|
func append(_ string: String) {
|
|
rights.append("• \(string)")
|
|
}
|
|
|
|
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 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(.canBanUsers) {
|
|
rights.append(strings.RequestPeer_Requirement_Channel_Rights_Ban)
|
|
}
|
|
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"))
|
|
}
|
|
}
|