mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Add forwarding to multiple people simultaneously
This commit is contained in:
@@ -21,6 +21,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
private let filter: ChatListNodePeersFilter
|
||||
private let hasGlobalSearch: Bool
|
||||
|
||||
private var presentationInterfaceState: ChatPresentationInterfaceState
|
||||
private var interfaceInteraction: ChatPanelInterfaceInteraction?
|
||||
|
||||
var inProgress: Bool = false {
|
||||
didSet {
|
||||
|
||||
@@ -33,6 +36,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
private let toolbarSeparatorNode: ASDisplayNode?
|
||||
private let segmentedControlNode: SegmentedControlNode?
|
||||
|
||||
private var textInputPanelNode: PeerSelectionTextInputPanelNode?
|
||||
|
||||
var contactListNode: ContactListNode?
|
||||
let chatListNode: ChatListNode
|
||||
|
||||
@@ -51,6 +56,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
var requestOpenDisabledPeer: ((Peer) -> Void)?
|
||||
var requestOpenPeerFromSearch: ((Peer) -> Void)?
|
||||
var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)?
|
||||
var requestSend: (([Peer], NSAttributedString) -> Void)?
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var presentationDataDisposable: Disposable?
|
||||
@@ -70,6 +76,8 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
self.presentationData = presentationData
|
||||
|
||||
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(PeerId(0)), subject: nil, peerNearbyData: nil, greetingData: nil, pendingUnpinnedAllMessages: false, activeGroupCallInfo: nil, hasActiveGroupCall: false, importState: nil)
|
||||
|
||||
if hasChatListSelector && hasContactSelector {
|
||||
self.toolbarBackgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.navigationBar.blurredBackgroundColor)
|
||||
|
||||
@@ -107,6 +115,9 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
|
||||
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).0
|
||||
}
|
||||
@@ -163,6 +174,112 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
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
|
||||
}, 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: {
|
||||
}, reportPeerIrrelevantGeoLocation: {
|
||||
}, displaySlowmodeTooltip: { _, _ in
|
||||
}, displaySendMessageOptions: { _, _ in
|
||||
}, openScheduledMessages: {
|
||||
}, openPeersNearby: {
|
||||
}, displaySearchResultsTooltip: { _, _ in
|
||||
}, unarchivePeer: {
|
||||
}, scrollToTop: {
|
||||
}, viewReplies: { _, _ in
|
||||
}, activatePinnedListPreview: { _, _ in
|
||||
}, joinGroupCall: { _ in
|
||||
}, presentInviteMembers: {
|
||||
}, presentGigagroupHelp: {
|
||||
}, editMessageMedia: { _, _ in
|
||||
}, updateShowCommands: { _ in }, statuses: nil)
|
||||
|
||||
self.readyValue.set(self.chatListNode.ready)
|
||||
}
|
||||
@@ -171,6 +288,89 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.presentationDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
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 textInputPanelNode = PeerSelectionTextInputPanelNode(presentationInterfaceState: self.presentationInterfaceState, presentController: { [weak self] c in self?.present(c, nil) })
|
||||
textInputPanelNode.interfaceInteraction = self.interfaceInteraction
|
||||
textInputPanelNode.sendMessage = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
if strongSelf.contactListActive {
|
||||
strongSelf.contactListNode?.multipleSelection = true
|
||||
let selectedContactPeers = strongSelf.contactListNode?.selectedPeers ?? []
|
||||
let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText
|
||||
var selectedPeers: [Peer] = []
|
||||
for contactPeer in selectedContactPeers {
|
||||
if case let .peer(peer, _, _) = contactPeer {
|
||||
selectedPeers.append(peer)
|
||||
}
|
||||
}
|
||||
if !selectedPeers.isEmpty {
|
||||
strongSelf.requestSend?(selectedPeers, effectiveInputText)
|
||||
}
|
||||
} else {
|
||||
var selectedPeerIds: [PeerId] = []
|
||||
var selectedPeerMap: [PeerId: Peer] = [:]
|
||||
strongSelf.chatListNode.updateState { state in
|
||||
selectedPeerIds = Array(state.selectedPeerIds)
|
||||
selectedPeerMap = state.selectedPeerMap
|
||||
return state
|
||||
}
|
||||
if !selectedPeerIds.isEmpty {
|
||||
let effectiveInputText = strongSelf.presentationInterfaceState.interfaceState.composeInputState.inputText
|
||||
var selectedPeers: [Peer] = []
|
||||
for peerId in selectedPeerIds {
|
||||
if let peer = selectedPeerMap[peerId] {
|
||||
selectedPeers.append(peer)
|
||||
}
|
||||
}
|
||||
strongSelf.requestSend?(selectedPeers, effectiveInputText)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -185,20 +385,48 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
self.containerLayout = (layout, navigationBarHeight, actualNavigationBarHeight)
|
||||
|
||||
let cleanInsets = layout.insets(options: [])
|
||||
var insets = layout.insets(options: [.input])
|
||||
|
||||
var toolbarHeight: CGFloat = cleanInsets.bottom
|
||||
|
||||
var textPanelHeight: CGFloat?
|
||||
|
||||
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, additionalSideInsets: UIEdgeInsets(), maxHeight: layout.size.height / 2.0, isSecondary: false, transition: panelTransition, interfaceState: self.presentationInterfaceState, metrics: layout.metrics)
|
||||
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
|
||||
textInputPanelNode.frame = initialPanelFrame
|
||||
}
|
||||
transition.updateFrame(node: textInputPanelNode, frame: panelFrame)
|
||||
}
|
||||
|
||||
if let segmentedControlNode = self.segmentedControlNode, let toolbarBackgroundNode = self.toolbarBackgroundNode, let toolbarSeparatorNode = self.toolbarSeparatorNode {
|
||||
toolbarHeight += 44.0
|
||||
if let textPanelHeight = textPanelHeight {
|
||||
toolbarHeight = textPanelHeight
|
||||
} 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)
|
||||
transition.updateFrame(node: segmentedControlNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: layout.size.height - toolbarHeight + floor((44.0 - controlSize.height) / 2.0)), size: controlSize))
|
||||
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))
|
||||
}
|
||||
|
||||
var insets = layout.insets(options: [.input])
|
||||
|
||||
insets.top += navigationBarHeight
|
||||
insets.bottom = max(insets.bottom, cleanInsets.bottom + 44.0)
|
||||
insets.left += layout.safeInsets.left
|
||||
@@ -239,7 +467,45 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
|
||||
if self.chatListNode.supernode != nil {
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ChatListSearchContainerNode(context: self.context, filter: self.filter, groupId: .root, displaySearchFilters: false, openPeer: { [weak self] peer, _ in
|
||||
if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch {
|
||||
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
|
||||
var exists = false
|
||||
for foundPeer in foundPeers {
|
||||
if peer.id == foundPeer.id {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
foundPeers.insert(peer, 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)
|
||||
}
|
||||
}, openDisabledPeer: { [weak self] peer in
|
||||
@@ -276,17 +542,48 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}
|
||||
self.searchDisplayController = SearchDisplayController(presentationData: self.presentationData, contentNode: ContactsSearchContainerNode(context: self.context, onlyWriteable: true, categories: categories, addContact: nil, openPeer: { [weak self] peer in
|
||||
if let strongSelf = self {
|
||||
switch peer {
|
||||
case let .peer(peer, _, _):
|
||||
let _ = (strongSelf.context.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peer.id)
|
||||
} |> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self, let peer = peer {
|
||||
strongSelf.requestOpenPeerFromSearch?(peer)
|
||||
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
|
||||
}
|
||||
})
|
||||
case .deviceContact:
|
||||
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.account.postbox.transaction { transaction -> Peer? in
|
||||
return transaction.getPeer(peer.id)
|
||||
} |> deliverOnMainQueue).start(next: { peer in
|
||||
if let strongSelf = self, let peer = peer {
|
||||
strongSelf.requestOpenPeerFromSearch?(peer)
|
||||
}
|
||||
})
|
||||
case .deviceContact:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}, contextAction: nil), cancel: { [weak self] in
|
||||
@@ -342,6 +639,11 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
let contactListNode = ContactListNode(context: context, 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?()
|
||||
}
|
||||
@@ -394,7 +696,7 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
} else if let contactListNode = self.contactListNode {
|
||||
contactListNode.enableUpdates = false
|
||||
|
||||
self.insertSubnode(chatListNode, aboveSubnode: contactListNode)
|
||||
self.insertSubnode(self.chatListNode, aboveSubnode: contactListNode)
|
||||
contactListNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user