diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 7e47d91100..559fa3f670 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -130,6 +130,9 @@ D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; }; D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; }; D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; }; + D0C932361E0988C60074F044 /* ChatButtonKeyboardInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */; }; + D0C932381E09E0EA0074F044 /* ChatBotInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */; }; + D0C9323C1E0B4AE90074F044 /* DataAndStorageSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */; }; D0D03AE31DECACB700220C46 /* ManagedAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */; }; D0D03AE51DECAE8900220C46 /* ManagedAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */; }; D0D03B081DECB0FE00220C46 /* diag_range.c in Sources */ = {isa = PBXBuildFile; fileRef = D0D03AE81DECB0FE00220C46 /* diag_range.c */; }; @@ -466,6 +469,9 @@ D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = ""; }; D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = ""; }; D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = ""; }; + D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatButtonKeyboardInputNode.swift; sourceTree = ""; }; + D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatBotInfoItem.swift; sourceTree = ""; }; + D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataAndStorageSettingsController.swift; sourceTree = ""; }; D0D03AE21DECACB700220C46 /* ManagedAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioSession.swift; sourceTree = ""; }; D0D03AE41DECAE8900220C46 /* ManagedAudioRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedAudioRecorder.swift; sourceTree = ""; }; D0D03AE81DECB0FE00220C46 /* diag_range.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = diag_range.c; sourceTree = ""; }; @@ -723,6 +729,7 @@ children = ( D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */, D021E0E31DB55CDB00C6B04F /* Media */, + D0C932341E0988AD0074F044 /* Button Keyboard */, ); name = "Input Nodes"; sourceTree = ""; @@ -943,6 +950,31 @@ name = "Input Panels"; sourceTree = ""; }; + D0C932341E0988AD0074F044 /* Button Keyboard */ = { + isa = PBXGroup; + children = ( + D0C932351E0988C60074F044 /* ChatButtonKeyboardInputNode.swift */, + ); + name = "Button Keyboard"; + sourceTree = ""; + }; + D0C932391E0B4AC60074F044 /* Settings */ = { + isa = PBXGroup; + children = ( + D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */, + D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */, + ); + name = Settings; + sourceTree = ""; + }; + D0C9323A1E0B4AD40074F044 /* Data and Storage */ = { + isa = PBXGroup; + children = ( + D0C9323B1E0B4AE90074F044 /* DataAndStorageSettingsController.swift */, + ); + name = "Data and Storage"; + sourceTree = ""; + }; D0D03AE61DECB0D200220C46 /* Audio Recorder */ = { isa = PBXGroup; children = ( @@ -1385,6 +1417,7 @@ D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */, D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */, D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */, + D0C932371E09E0EA0074F044 /* ChatBotInfoItem.swift */, ); name = Items; sourceTree = ""; @@ -1481,8 +1514,8 @@ D0F69E791D6B8C3B0046BCD6 /* Settings */ = { isa = PBXGroup; children = ( - D0F69E7A1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift */, - D0F69E7B1D6B8C470046BCD6 /* SettingsController.swift */, + D0C932391E0B4AC60074F044 /* Settings */, + D0C9323A1E0B4AD40074F044 /* Data and Storage */, ); name = Settings; sourceTree = ""; @@ -1744,6 +1777,7 @@ D0D2686C1D788F8200C422DA /* ChatTitleAccessoryPanelNode.swift in Sources */, D0215D541E043018001A0B1E /* InstantPageController.swift in Sources */, D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */, + D0C9323C1E0B4AE90074F044 /* DataAndStorageSettingsController.swift in Sources */, D01F66131DE8903300345CBE /* ChatTextInputAudioRecordingButton.swift in Sources */, D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */, D0736F251DF4D0E500F2C02A /* TelegramController.swift in Sources */, @@ -1764,6 +1798,7 @@ D0215D401E0410D9001A0B1E /* InstantPageLinkSelectionView.swift in Sources */, D04B66B81DD672D00049C3D2 /* GeoLocation.swift in Sources */, D02383791DDF1A4D004018B6 /* ChatRequestInProgressTitlePanelNode.swift in Sources */, + D0C932381E09E0EA0074F044 /* ChatBotInfoItem.swift in Sources */, D0DE77291D932923002B8809 /* GridMessageSelectionNode.swift in Sources */, D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */, D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */, @@ -1819,6 +1854,7 @@ D039EB0A1DEC7A8700886EBC /* ChatTextInputAudioRecordingCancelIndicator.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0215D3E1E041048001A0B1E /* InstantPageMedia.swift in Sources */, + D0C932361E0988C60074F044 /* ChatButtonKeyboardInputNode.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */, diff --git a/TelegramUI/ActionSheetRollImageItem.swift b/TelegramUI/ActionSheetRollImageItem.swift index 73a1d79b51..35df20523b 100644 --- a/TelegramUI/ActionSheetRollImageItem.swift +++ b/TelegramUI/ActionSheetRollImageItem.swift @@ -2,6 +2,7 @@ import Foundation import Display import AsyncDisplayKit import Photos +import SwiftSignalKit private let testBackground = generateStretchableFilledCircleImage(radius: 8.0, color: UIColor.lightGray) @@ -18,18 +19,19 @@ final class ActionSheetRollImageItem: ListViewItem { self.asset = asset } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ActionSheetRollImageItemNode() node.contentSize = CGSize(width: 84.0, height: 84.0) node.insets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) node.updateAsset(asset: self.asset) completion(node, { + return (nil, {}) }) } } - func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: (ListViewItemNodeLayout, () -> Void) -> Void) { + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { }) } diff --git a/TelegramUI/AuthorizationController.swift b/TelegramUI/AuthorizationController.swift index 8a3f71cda0..ddf4d0f675 100644 --- a/TelegramUI/AuthorizationController.swift +++ b/TelegramUI/AuthorizationController.swift @@ -60,7 +60,7 @@ public class AuthorizationController: NavigationController { modifier.setState(state) return state } |> map { state -> Account in - return Account(id: account.id, postbox: account.postbox, network: account.network, peerId: user.id) + return Account(id: account.id, basePath: account.basePath, postbox: account.postbox, network: account.network, peerId: user.id) } } } else { diff --git a/TelegramUI/AvatarNode.swift b/TelegramUI/AvatarNode.swift index 181698d65e..5011e2091e 100644 --- a/TelegramUI/AvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -4,6 +4,7 @@ import Postbox import UIKit import Display import TelegramCore +import SwiftSignalKit private class AvatarNodeParameters: NSObject { let account: Account @@ -65,9 +66,21 @@ public final class AvatarNode: ASDisplayNode { private var state: AvatarNodeState = .Empty + private let imageReady = Promise(false) + public var ready: Signal { + let imageReady = self.imageReady + return Signal { subscriber in + return imageReady.get().start(next: { next in + if next { + subscriber.putCompletion() + } + }) + } + } + public init(font: UIFont) { self.font = font - self.imageNode = ImageNode() + self.imageNode = ImageNode(enableHasImage: true) super.init() @@ -102,8 +115,10 @@ public final class AvatarNode: ASDisplayNode { self.contents = nil if let signal = peerAvatarImage(account: account, peer: peer) { + self.imageReady.set(self.imageNode.ready) self.imageNode.setSignal(signal) } else { + self.imageReady.set(.single(true)) self.displaySuspended = false } if self.parameters == nil || self.parameters != parameters { @@ -171,4 +186,32 @@ public final class AvatarNode: ASDisplayNode { context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) } } + + static func asyncLayout(_ node: AvatarNode?) -> (_ account: Account, _ peer: Peer, _ font: UIFont) -> () -> AvatarNode? { + let currentState = node?.state + let createNode = node == nil + return { [weak node] account, peer, font in + let state: AvatarNodeState = .PeerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage) + if currentState != state { + + } + var createdNode: AvatarNode? + if createNode { + createdNode = AvatarNode(font: font) + } + return { + let updatedNode: AvatarNode? + if let createdNode = createdNode { + updatedNode = createdNode + } else { + updatedNode = node + } + if let updatedNode = updatedNode { + return updatedNode + } else { + return nil + } + } + } + } } diff --git a/TelegramUI/ChatBotInfoItem.swift b/TelegramUI/ChatBotInfoItem.swift new file mode 100644 index 0000000000..babcb552e1 --- /dev/null +++ b/TelegramUI/ChatBotInfoItem.swift @@ -0,0 +1,72 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit + +final class ChatBotInfoItem: ListViewItem { + fileprivate let controllerInteraction: ChatControllerInteraction + + init(controllerInteraction: ChatControllerInteraction) { + self.controllerInteraction = controllerInteraction + } + + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { + let configure = { + let node = ChatBotInfoItemNode() + + let nodeLayout = node.asyncLayout() + let (layout, apply) = nodeLayout(self, width) + + node.contentSize = layout.contentSize + node.insets = layout.insets + + completion(node, { + return (nil, { apply(.None) }) + }) + } + if Thread.isMainThread { + async { + configure() + } + } else { + configure() + } + } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + if let node = node as? ChatBotInfoItemNode { + Queue.mainQueue().async { + let nodeLayout = node.asyncLayout() + + async { + let (layout, apply) = nodeLayout(self, width) + Queue.mainQueue().async { + completion(layout, { + apply(animation) + }) + } + } + } + } + } +} + +final class ChatBotInfoItemNode: ListViewItemNode { + var controllerInteraction: ChatControllerInteraction? + + init() { + super.init(layerBacked: false, dynamicBounce: true, rotated: true) + + self.backgroundColor = .blue + } + + func asyncLayout() -> (_ item: ChatBotInfoItem, _ width: CGFloat) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { [weak self] item, width in + return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 128.0), insets: UIEdgeInsets()), { _ in + if let strongSelf = self { + + } + }) + } + } +} diff --git a/TelegramUI/ChatButtonKeyboardInputNode.swift b/TelegramUI/ChatButtonKeyboardInputNode.swift new file mode 100644 index 0000000000..987019457d --- /dev/null +++ b/TelegramUI/ChatButtonKeyboardInputNode.swift @@ -0,0 +1,189 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + +private let defaultPortraitPanelHeight: CGFloat = UIScreenScale.isEqual(to: 3.0) ? 271.0 : 258.0 + +private func generateButtonBackgroundImage(color: UIColor) -> UIImage? { + let radius: CGFloat = 5.0 + let shadowSize: CGFloat = 1.0 + return generateImage(CGSize(width: radius * 2.0, height: radius * 2.0 + shadowSize), contextGenerator: { size, context in + context.setFillColor(UIColor(0xc3c7c9).cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: 0.0, width: radius * 2.0, height: radius * 2.0)) + context.setFillColor(color.cgColor) + context.fillEllipse(in: CGRect(x: 0.0, y: shadowSize, width: radius * 2.0, height: radius * 2.0)) + })?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)) +} +private let buttonBackgroundImage = generateButtonBackgroundImage(color: .white) +private let buttonHighlightedBackgroundImage = generateButtonBackgroundImage(color: UIColor(0xa8b3c0)) + +private final class ChatButtonKeyboardInputButtonNode: ASButtonNode { + var button: ReplyMarkupButton? + + override init() { + super.init() + + self.setBackgroundImage(buttonBackgroundImage, for: []) + self.setBackgroundImage(buttonHighlightedBackgroundImage, for: [.highlighted]) + } +} + +final class ChatButtonKeyboardInputNode: ChatInputNode { + private let account: Account + private let controllerInteraction: ChatControllerInteraction + + private let separatorNode: ASDisplayNode + private let scrollNode: ASScrollNode + + private var buttonNodes: [ChatButtonKeyboardInputButtonNode] = [] + private var message: Message? + + init(account: Account, controllerInteraction: ChatControllerInteraction) { + self.account = account + self.controllerInteraction = controllerInteraction + + self.scrollNode = ASScrollNode() + + self.separatorNode = ASDisplayNode() + self.separatorNode.isLayerBacked = true + self.separatorNode.backgroundColor = UIColor(0xBEC2C6) + + super.init() + + self.backgroundColor = UIColor(0xE8EBF0) + self.addSubnode(self.scrollNode) + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.canCancelContentTouches = true + + self.addSubnode(self.separatorNode) + } + + private func heightForWidth(width: CGFloat) -> CGFloat { + return defaultPortraitPanelHeight + } + + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: UIScreenPixel))) + + var validatedMarkup: ReplyMarkupMessageAttribute? + if let message = interfaceState.keyboardButtonsMessage { + for attribute in message.attributes { + if let replyMarkup = attribute as? ReplyMarkupMessageAttribute { + if !replyMarkup.rows.isEmpty { + validatedMarkup = replyMarkup + } + break + } + } + } + + self.message = interfaceState.keyboardButtonsMessage + + if let markup = validatedMarkup { + let verticalInset: CGFloat = 10.0 + let sideInset: CGFloat = 6.0 + var buttonHeight: CGFloat = 43.0 + let columnSpacing: CGFloat = 6.0 + let rowSpacing: CGFloat = 5.0 + + var panelHeight = self.heightForWidth(width: width) + + var rowsHeight = verticalInset + CGFloat(markup.rows.count) * buttonHeight + CGFloat(max(0, markup.rows.count - 1)) * rowSpacing + verticalInset + if !markup.flags.contains(.fit) && rowsHeight < panelHeight { + buttonHeight = floor((panelHeight - verticalInset * 2.0 - CGFloat(max(0, markup.rows.count - 1)) * rowSpacing) / CGFloat(markup.rows.count)) + } + + var verticalOffset = verticalInset + var buttonIndex = 0 + for row in markup.rows { + let buttonWidth = floor(((width - sideInset - sideInset) + columnSpacing - CGFloat(row.buttons.count) * columnSpacing) / CGFloat(row.buttons.count)) + + var columnIndex = 0 + for button in row.buttons { + let buttonNode: ChatButtonKeyboardInputButtonNode + if buttonIndex < self.buttonNodes.count { + buttonNode = self.buttonNodes[buttonIndex] + } else { + buttonNode = ChatButtonKeyboardInputButtonNode() + buttonNode.addTarget(self, action: #selector(self.buttonPressed(_:)), forControlEvents: [.touchUpInside]) + self.scrollNode.addSubnode(buttonNode) + self.buttonNodes.append(buttonNode) + } + buttonIndex += 1 + if buttonNode.button != button { + buttonNode.button = button + buttonNode.setTitle(button.title, with: Font.regular(16.0), with: .black, for: []) + } + buttonNode.frame = CGRect(origin: CGPoint(x: sideInset + CGFloat(columnIndex) * (buttonWidth + columnSpacing), y: verticalOffset), size: CGSize(width: buttonWidth, height: buttonHeight)) + columnIndex += 1 + } + verticalOffset += buttonHeight + rowSpacing + } + + for i in (buttonIndex ..< self.buttonNodes.count).reversed() { + self.buttonNodes[i].removeFromSupernode() + self.buttonNodes.remove(at: i) + } + + if markup.flags.contains(.fit) { + panelHeight = min(panelHeight, rowsHeight) + } + + transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))) + self.scrollNode.view.contentSize = CGSize(width: width, height: rowsHeight) + + return panelHeight + } else { + return 0.0 + } + } + + @objc func buttonPressed(_ button: ASButtonNode) { + if let button = button as? ChatButtonKeyboardInputButtonNode, let markupButton = button.button { + switch markupButton.action { + case .text: + controllerInteraction.sendMessage(markupButton.title) + case let .url(url): + controllerInteraction.openUrl(url) + case .requestMap: + controllerInteraction.shareCurrentLocation() + case .requestPhone: + controllerInteraction.shareAccountContact() + case .openWebApp: + if let message = self.message { + controllerInteraction.requestMessageActionCallback(message.id, nil) + } + case let .callback(data): + if let message = self.message { + controllerInteraction.requestMessageActionCallback(message.id, data) + } + case let .switchInline(samePeer, query): + if let message = message { + var botPeer: Peer? + + var found = false + for attribute in message.attributes { + if let attribute = attribute as? InlineBotMessageAttribute { + botPeer = message.peers[attribute.peerId] + found = true + } + } + if !found { + botPeer = message.author + } + + var peerId: PeerId? + if samePeer { + peerId = message.id.peerId + } + if let botPeer = botPeer, let addressName = botPeer.addressName { + controllerInteraction.openPeer(peerId, .chat(textInputState: ChatTextInputState(inputText: "@\(addressName) \(query)"))) + } + } + } + } + } +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 0b207de012..c8d3eb666a 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -53,6 +53,8 @@ public class ChatController: TelegramController { private var audioRecorder = Promise() private var audioRecorderDisposable: Disposable? + private var buttonKeyboardMessageDisposable: Disposable? + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId @@ -296,13 +298,25 @@ public class ChatController: TelegramController { } }, sendMessage: { [weak self] text in if let strongSelf = self { - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) - enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, attributes: [], media: nil, replyToMessageId: nil)]).start() + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, attributes: [], media: nil, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() } }, sendSticker: { [weak self] file in if let strongSelf = self { - strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) - enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: nil)]).start() + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", attributes: [], media: file, replyToMessageId: strongSelf.presentationInterfaceState.interfaceState.replyMessageId)]).start() } }, requestMessageActionCallback: { [weak self] messageId, data in if let strongSelf = self { @@ -385,6 +399,13 @@ public class ChatController: TelegramController { if !command.contains("@") && (strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel || strongSelf.peerId.namespace == Namespaces.Peer.CloudGroup) { postAsReply = true } + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: "")) } + }) + } + }) enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, attributes: [], media: nil, replyToMessageId: postAsReply ? messageId : nil)]).start() } }, openInstantPage: { [weak self] messageId in @@ -571,6 +592,7 @@ public class ChatController: TelegramController { self.botCallbackAlertMessageDisposable?.dispose() self.contextQueryState?.1.dispose() self.audioRecorderDisposable?.dispose() + self.buttonKeyboardMessageDisposable?.dispose() } var chatDisplayNode: ChatControllerNode { @@ -584,14 +606,30 @@ public class ChatController: TelegramController { let initialData = self.chatDisplayNode.historyNode.initialData |> take(1) - |> beforeNext { [weak self] initialData in - if let strongSelf = self, let initialData = initialData { - if let interfaceState = initialData.chatInterfaceState as? ChatInterfaceState { - strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }) }) + |> beforeNext { [weak self] combinedInitialData in + if let strongSelf = self, let combinedInitialData = combinedInitialData { + if let interfaceState = combinedInitialData.initialData?.chatInterfaceState as? ChatInterfaceState { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { $0.updatedInterfaceState({ _ in return interfaceState }).updatedKeyboardButtonsMessage(combinedInitialData.buttonKeyboardMessage) }) } } } + self.buttonKeyboardMessageDisposable = self.chatDisplayNode.historyNode.buttonKeyboardMessage.start(next: { [weak self] message in + if let strongSelf = self { + var buttonKeyboardMessageUpdated = false + if let currentButtonKeyboardMessage = strongSelf.presentationInterfaceState.keyboardButtonsMessage, let message = message { + if currentButtonKeyboardMessage.id != message.id || currentButtonKeyboardMessage.stableVersion != message.stableVersion { + buttonKeyboardMessageUpdated = true + } + } else if (strongSelf.presentationInterfaceState.keyboardButtonsMessage != nil) != (message != nil) { + buttonKeyboardMessageUpdated = true + } + if buttonKeyboardMessageUpdated { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, { $0.updatedKeyboardButtonsMessage(message) }) + } + } + }) + self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyReady.get(), self._peerReady.get(), initialData) |> map { historyReady, peerReady, _ in return historyReady && peerReady }) @@ -658,7 +696,7 @@ public class ChatController: TelegramController { stationaryItemRange = (maxInsertedItem + 1, Int.max) } - mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData), updateSizeAndInsets) + mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { @@ -851,9 +889,12 @@ public class ChatController: TelegramController { if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEffectiveInputState(f($0.effectiveInputState)) } }) } - }, updateInputMode: { [weak self] f in + }, updateInputModeAndDismissedButtonKeyboardMessageId: { [weak self] f in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputMode(f) }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { + let (updatedInputMode, updatedClosedButtonKeyboardMessageId) = f($0) + return $0.updatedInputMode({ _ in return updatedInputMode }).updatedInterfaceState({ $0.withUpdatedMessageActionsState({ $0.withUpdatedClosedButtonKeyboardMessageId(updatedClosedButtonKeyboardMessageId) }) }) + }) } }, editMessage: { [weak self] messageId, text in if let strongSelf = self { @@ -974,7 +1015,38 @@ public class ChatController: TelegramController { } func updateChatPresentationInterfaceState(animated: Bool = true, interactive: Bool, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { - let temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) + var temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) + + if self.presentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup != temporaryChatPresentationInterfaceState.keyboardButtonsMessage?.visibleButtonKeyboardMarkup { + if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, let _ = keyboardButtonsMessage.visibleButtonKeyboardMarkup { + if self.presentationInterfaceState.interfaceState.editMessage == nil && self.presentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty && keyboardButtonsMessage.id != temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.closedButtonKeyboardMessageId { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ _ in + return .inputButtons + }) + } + + if self.peerId.namespace == Namespaces.Peer.CloudChannel || self.peerId.namespace == Namespaces.Peer.CloudGroup { + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) }) + } + } + } else { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputMode({ mode in + if case .inputButtons = mode { + return .text + } else { + return mode + } + }) + } + } + + if let keyboardButtonsMessage = temporaryChatPresentationInterfaceState.keyboardButtonsMessage, keyboardButtonsMessage.requestsSetupReply { + if temporaryChatPresentationInterfaceState.interfaceState.replyMessageId == nil && temporaryChatPresentationInterfaceState.interfaceState.messageActionsState.processedSetupReplyMessageId != keyboardButtonsMessage.id { + temporaryChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInterfaceState({ $0.withUpdatedReplyMessageId(keyboardButtonsMessage.id).withUpdatedMessageActionsState({ $0.withUpdatedProcessedSetupReplyMessageId(keyboardButtonsMessage.id) }) }) + } + } + let inputTextPanelState = inputTextPanelStateForChatPresentationInterfaceState(temporaryChatPresentationInterfaceState, account: self.account) var updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputTextPanelState({ _ in return inputTextPanelState }) diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 5b0837dc30..7ea12d82bc 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -157,7 +157,6 @@ class ChatControllerNode: ASDisplayNode { } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets) -> Void) { - //print("update \(self.scheduledLayoutTransitionRequest)") self.scheduledLayoutTransitionRequest = nil var previousInputHeight: CGFloat = 0.0 if let (previousLayout, _) = self.containerLayoutAndNavigationBarHeight { @@ -552,9 +551,27 @@ class ChatControllerNode: ASDisplayNode { } else { if !self.ignoreUpdateHeight { if interactive { - self.scheduleLayoutTransitionRequest(layoutTransition) + if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { + switch scheduledLayoutTransitionRequest.1 { + case .immediate: + self.scheduleLayoutTransitionRequest(layoutTransition) + default: + break + } + } else { + self.scheduleLayoutTransitionRequest(layoutTransition) + } } else { - self.requestLayout(layoutTransition) + if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { + switch scheduledLayoutTransitionRequest.1 { + case .immediate: + self.requestLayout(layoutTransition) + case .animated: + self.scheduleLayoutTransitionRequest(scheduledLayoutTransitionRequest.1) + } + } else { + self.requestLayout(layoutTransition) + } } } } @@ -578,7 +595,9 @@ class ChatControllerNode: ASDisplayNode { case .none: break default: - self.interfaceInteraction?.updateInputMode({ _ in .none }) + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.none, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + }) } } diff --git a/TelegramUI/ChatHistoryEntriesForView.swift b/TelegramUI/ChatHistoryEntriesForView.swift index 40640ca1c3..36366f4afe 100644 --- a/TelegramUI/ChatHistoryEntriesForView.swift +++ b/TelegramUI/ChatHistoryEntriesForView.swift @@ -31,5 +31,9 @@ func chatHistoryEntriesForView(_ view: MessageHistoryView, includeUnreadEntry: B } } + if view.earlierId == nil { + entries.insert(.ChatInfoEntry, at: 0) + } + return entries } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index dd530aba76..0de2bf7f5d 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -5,6 +5,7 @@ enum ChatHistoryEntry: Identifiable, Comparable { case HoleEntry(MessageHistoryHole) case MessageEntry(Message, Bool) case UnreadEntry(MessageIndex) + case ChatInfoEntry var stableId: UInt64 { switch self { @@ -14,6 +15,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { return UInt64(message.stableId) | ((UInt64(2) << 40)) case .UnreadEntry: return UInt64(3) << 40 + case .ChatInfoEntry: + return UInt64(4) << 40 } } @@ -25,6 +28,8 @@ enum ChatHistoryEntry: Identifiable, Comparable { return MessageIndex(message) case let .UnreadEntry(index): return index + case .ChatInfoEntry: + return MessageIndex.absoluteLowerBound() } } } @@ -33,10 +38,10 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { switch lhs { case let .HoleEntry(lhsHole): switch rhs { - case let .HoleEntry(rhsHole) where lhsHole == rhsHole: - return true - default: - return false + case let .HoleEntry(rhsHole) where lhsHole == rhsHole: + return true + default: + return false } case let .MessageEntry(lhsMessage, lhsRead): switch rhs { @@ -70,12 +75,18 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { } case let .UnreadEntry(lhsIndex): switch rhs { - case let .UnreadEntry(rhsIndex) where lhsIndex == rhsIndex: + case let .UnreadEntry(rhsIndex) where lhsIndex == rhsIndex: + return true + default: + return false + } + case .ChatInfoEntry: + if case .ChatInfoEntry = rhs { return true - default: + } else { return false } - } + } } func <(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index 97295460c1..f351dd5762 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -24,6 +24,9 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt case .UnreadEntry: assertionFailure() return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) + case .ChatInfoEntry: + assertionFailure() + return GridNodeInsertItem(index: entry.index, item: GridHoleItem(), previousIndex: entry.previousIndex) } } } @@ -38,6 +41,9 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt case .UnreadEntry: assertionFailure() return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) + case .ChatInfoEntry: + assertionFailure() + return GridNodeUpdateItem(index: entry.index, item: GridHoleItem()) } } } @@ -172,7 +178,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: false)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: nil, keyboardButtonsMessage: nil) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index df72be0fc7..e4e2fb4e31 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -20,9 +20,14 @@ enum ChatHistoryViewUpdateType { case Generic(type: ViewUpdateType) } +public struct ChatHistoryCombinedInitialData { + let initialData: InitialMessageHistoryData? + let buttonKeyboardMessage: Message? +} + enum ChatHistoryViewUpdate { case Loading(initialData: InitialMessageHistoryData?) - case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?) + case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: ChatHistoryCombinedInitialData) } struct ChatHistoryView { @@ -60,6 +65,7 @@ struct ChatHistoryViewTransition { let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? let initialData: InitialMessageHistoryData? + let keyboardButtonsMessage: Message? } struct ChatHistoryListViewTransition { @@ -71,6 +77,7 @@ struct ChatHistoryListViewTransition { let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? let initialData: InitialMessageHistoryData? + let keyboardButtonsMessage: Message? } private func maxIncomingMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageIndex? { @@ -105,6 +112,8 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case .UnreadEntry: return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index), directionHint: entry.directionHint) + case .ChatInfoEntry: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(controllerInteraction: controllerInteraction), directionHint: entry.directionHint) } } } @@ -132,12 +141,14 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case .UnreadEntry: return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index), directionHint: entry.directionHint) + case .ChatInfoEntry: + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatBotInfoItem(controllerInteraction: controllerInteraction), directionHint: entry.directionHint) } } } private func mappedChatHistoryViewListTransition(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, mode: ChatHistoryListMode, transition: ChatHistoryViewTransition) -> ChatHistoryListViewTransition { - return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData) + return ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, initialData: transition.initialData, keyboardButtonsMessage: transition.keyboardButtonsMessage) } private final class ChatHistoryTransactionOpaqueState { @@ -169,12 +180,18 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public let historyReady = Promise() private var didSetHistoryReady = false - private let _initialData = Promise() + private let _initialData = Promise() private var didSetInitialData = false - public var initialData: Signal { + public var initialData: Signal { return self._initialData.get() } + private var _buttonKeyboardMessage = Promise(nil) + private var currentButtonKeyboardMessage: Message? + public var buttonKeyboardMessage: Signal { + return self._buttonKeyboardMessage.get() + } + private let maxVisibleIncomingMessageIndex = ValuePromise(ignoreRepeated: true) let canReadHistory = ValuePromise() @@ -226,15 +243,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let previousView = Atomic(value: nil) let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal in - let initialData: InitialMessageHistoryData? + let initialData: ChatHistoryCombinedInitialData? switch update { case let .Loading(data): - initialData = data + let combinedInitialData = ChatHistoryCombinedInitialData(initialData: data, buttonKeyboardMessage: nil) + initialData = combinedInitialData Queue.mainQueue().async { [weak self] in if let strongSelf = self { if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(data)) + strongSelf._initialData.set(.single(combinedInitialData)) } if !strongSelf.didSetHistoryReady { strongSelf.didSetHistoryReady = true @@ -267,7 +285,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let processedView = ChatHistoryView(originalView: view, filteredEntries: chatHistoryEntriesForView(view, includeUnreadEntry: mode == .bubbles)) let previous = previousView.swap(processedView) - return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) + return preparedChatHistoryViewTransition(from: previous, to: processedView, reason: reason, account: account, peerId: peerId, controllerInteraction: controllerInteraction, scrollPosition: scrollPosition, initialData: initialData?.initialData, keyboardButtonsMessage: view.topTaggedMessages.first) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -379,7 +397,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } else { if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(transition.initialData)) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage))) } if !strongSelf.didSetHistoryReady { strongSelf.didSetHistoryReady = true @@ -413,13 +431,26 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } if !strongSelf.didSetInitialData { strongSelf.didSetInitialData = true - strongSelf._initialData.set(.single(transition.initialData)) + strongSelf._initialData.set(.single(ChatHistoryCombinedInitialData(initialData: transition.initialData, buttonKeyboardMessage: transition.keyboardButtonsMessage))) } if !strongSelf.didSetHistoryReady { strongSelf.didSetHistoryReady = true strongSelf.historyReady.set(.single(true)) } + var buttonKeyboardMessageUpdated = false + if let currentButtonKeyboardMessage = strongSelf.currentButtonKeyboardMessage, let buttonKeyboardMessage = transition.keyboardButtonsMessage { + if currentButtonKeyboardMessage.id != buttonKeyboardMessage.id || currentButtonKeyboardMessage.stableVersion != buttonKeyboardMessage.stableVersion { + buttonKeyboardMessageUpdated = true + } + } else if (strongSelf.currentButtonKeyboardMessage != nil) != (transition.keyboardButtonsMessage != nil) { + buttonKeyboardMessageUpdated = true + } + if buttonKeyboardMessageUpdated { + strongSelf.currentButtonKeyboardMessage = transition.keyboardButtonsMessage + strongSelf._buttonKeyboardMessage.set(.single(transition.keyboardButtonsMessage)) + } + completion() } } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index cdb87e08fe..c041c74dd1 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -17,7 +17,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) } else { var scrollPosition: ChatHistoryViewScrollPosition? @@ -58,7 +58,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: initialData) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) } } case let .InitialSearch(messageId, count): @@ -66,7 +66,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun var fadeIn = false return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) } else { let anchorIndex = view.anchorIndex @@ -90,7 +90,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun preloaded = true //case Index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: initialData) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) } } case let .Navigation(index, anchorIndex): @@ -103,7 +103,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: initialData) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up @@ -118,7 +118,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: initialData) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first)) } } } diff --git a/TelegramUI/ChatHoleItem.swift b/TelegramUI/ChatHoleItem.swift index 5b1669dd18..cd8f82a9a3 100644 --- a/TelegramUI/ChatHoleItem.swift +++ b/TelegramUI/ChatHoleItem.swift @@ -3,6 +3,7 @@ import UIKit import Postbox import AsyncDisplayKit import Display +import SwiftSignalKit private func backgroundImage(color: UIColor) -> UIImage? { return generateImage(CGSize(width: 20.0, height: 20.0), contextGenerator: { size, context -> Void in @@ -23,14 +24,20 @@ class ChatHoleItem: ListViewItem { self.header = ChatMessageDateHeader(timestamp: index.timestamp) } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { - + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatHoleItemNode() node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - completion(node, {}) + completion(node, { + return (nil, {}) + }) } } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + }) + } } class ChatHoleItemNode: ListViewItemNode { diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 7413d59ce8..e29a0675b2 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -159,14 +159,19 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte } } switch chatPresentationInterfaceState.inputMode { - case .media: + case .media, .inputButtons: return ChatTextInputPanelState(accessoryItems: [.keyboard], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) case .none, .text: if let _ = chatPresentationInterfaceState.interfaceState.editMessage { return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) } else { if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty { - return ChatTextInputPanelState(accessoryItems: [.stickers], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) + var accessoryItems: [ChatTextInputAccessoryItem] = [] + accessoryItems.append(.stickers) + if let message = chatPresentationInterfaceState.keyboardButtonsMessage, let _ = message.visibleButtonKeyboardMarkup { + accessoryItems.append(.inputButtons) + } + return ChatTextInputPanelState(accessoryItems: accessoryItems, contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) } else { return ChatTextInputPanelState(accessoryItems: [], contextPlaceholder: contextPlaceholder, audioRecordingState: chatPresentationInterfaceState.inputTextPanelState.audioRecordingState) } diff --git a/TelegramUI/ChatInterfaceInputNodes.swift b/TelegramUI/ChatInterfaceInputNodes.swift index 43d3945d90..dab4ce43ad 100644 --- a/TelegramUI/ChatInterfaceInputNodes.swift +++ b/TelegramUI/ChatInterfaceInputNodes.swift @@ -14,6 +14,14 @@ func inputNodeForChatPresentationIntefaceState(_ chatPresentationInterfaceState: inputNode.interfaceInteraction = interfaceInteraction return inputNode } + case .inputButtons: + if let currentNode = currentNode as? ChatButtonKeyboardInputNode { + return currentNode + } else { + let inputNode = ChatButtonKeyboardInputNode(account: account, controllerInteraction: controllerInteraction) + inputNode.interfaceInteraction = interfaceInteraction + return inputNode + } case .none, .text: return nil } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index 5957254c3e..14b48c30af 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -125,6 +125,73 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { } } +struct ChatInterfaceMessageActionsState: Coding, Equatable { + let closedButtonKeyboardMessageId: MessageId? + let processedSetupReplyMessageId: MessageId? + + var isEmpty: Bool { + return self.closedButtonKeyboardMessageId == nil && self.processedSetupReplyMessageId == nil + } + + init() { + self.closedButtonKeyboardMessageId = nil + self.processedSetupReplyMessageId = nil + } + + init(closedButtonKeyboardMessageId: MessageId?, processedSetupReplyMessageId: MessageId?) { + self.closedButtonKeyboardMessageId = closedButtonKeyboardMessageId + self.processedSetupReplyMessageId = processedSetupReplyMessageId + } + + init(decoder: Decoder) { + if let closedMessageIdPeerId = (decoder.decodeInt64ForKey("cb.p") as Int64?), let closedMessageIdNamespace = (decoder.decodeInt32ForKey("cb.n") as Int32?), let closedMessageIdId = (decoder.decodeInt32ForKey("cb.i") as Int32?) { + self.closedButtonKeyboardMessageId = MessageId(peerId: PeerId(closedMessageIdPeerId), namespace: closedMessageIdNamespace, id: closedMessageIdId) + } else { + self.closedButtonKeyboardMessageId = nil + } + + if let processedMessageIdPeerId = (decoder.decodeInt64ForKey("pb.p") as Int64?), let processedMessageIdNamespace = (decoder.decodeInt32ForKey("pb.n") as Int32?), let processedMessageIdId = (decoder.decodeInt32ForKey("pb.i") as Int32?) { + self.processedSetupReplyMessageId = MessageId(peerId: PeerId(processedMessageIdPeerId), namespace: processedMessageIdNamespace, id: processedMessageIdId) + } else { + self.processedSetupReplyMessageId = nil + } + } + + func encode(_ encoder: Encoder) { + if let closedButtonKeyboardMessageId = self.closedButtonKeyboardMessageId { + encoder.encodeInt64(closedButtonKeyboardMessageId.peerId.toInt64(), forKey: "cb.p") + encoder.encodeInt32(closedButtonKeyboardMessageId.namespace, forKey: "cb.n") + encoder.encodeInt32(closedButtonKeyboardMessageId.id, forKey: "cb.i") + } else { + encoder.encodeNil(forKey: "cb.p") + encoder.encodeNil(forKey: "cb.n") + encoder.encodeNil(forKey: "cb.i") + } + + if let processedSetupReplyMessageId = self.processedSetupReplyMessageId { + encoder.encodeInt64(processedSetupReplyMessageId.peerId.toInt64(), forKey: "pb.p") + encoder.encodeInt32(processedSetupReplyMessageId.namespace, forKey: "pb.n") + encoder.encodeInt32(processedSetupReplyMessageId.id, forKey: "pb.i") + } else { + encoder.encodeNil(forKey: "pb.p") + encoder.encodeNil(forKey: "pb.n") + encoder.encodeNil(forKey: "pb.i") + } + } + + static func ==(lhs: ChatInterfaceMessageActionsState, rhs: ChatInterfaceMessageActionsState) -> Bool { + return lhs.closedButtonKeyboardMessageId == rhs.closedButtonKeyboardMessageId && lhs.processedSetupReplyMessageId == rhs.processedSetupReplyMessageId + } + + func withUpdatedClosedButtonKeyboardMessageId(_ closedButtonKeyboardMessageId: MessageId?) -> ChatInterfaceMessageActionsState { + return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: closedButtonKeyboardMessageId, processedSetupReplyMessageId: self.processedSetupReplyMessageId) + } + + func withUpdatedProcessedSetupReplyMessageId(_ processedSetupReplyMessageId: MessageId?) -> ChatInterfaceMessageActionsState { + return ChatInterfaceMessageActionsState(closedButtonKeyboardMessageId: self.closedButtonKeyboardMessageId, processedSetupReplyMessageId: processedSetupReplyMessageId) + } +} + final class ChatInterfaceState: PeerChatInterfaceState, Equatable { let timestamp: Int32 let composeInputState: ChatTextInputState @@ -132,6 +199,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { let forwardMessageIds: [MessageId]? let editMessage: ChatEditMessageState? let selectionState: ChatInterfaceSelectionState? + let messageActionsState: ChatInterfaceMessageActionsState var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { if !self.composeInputState.inputText.isEmpty && self.timestamp != 0 { @@ -156,15 +224,17 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { self.forwardMessageIds = nil self.editMessage = nil self.selectionState = nil + self.messageActionsState = ChatInterfaceMessageActionsState() } - init(timestamp: Int32, composeInputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?) { + init(timestamp: Int32, composeInputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState) { self.timestamp = timestamp self.composeInputState = composeInputState self.replyMessageId = replyMessageId self.forwardMessageIds = forwardMessageIds self.editMessage = editMessage self.selectionState = selectionState + self.messageActionsState = messageActionsState } init(decoder: Decoder) { @@ -197,6 +267,12 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { self.selectionState = nil } + + if let messageActionsState = decoder.decodeObjectForKey("as", decoder: { ChatInterfaceMessageActionsState(decoder: $0) }) as? ChatInterfaceMessageActionsState { + self.messageActionsState = messageActionsState + } else { + self.messageActionsState = ChatInterfaceMessageActionsState() + } } func encode(_ encoder: Encoder) { @@ -228,6 +304,11 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { encoder.encodeNil(forKey: "ss") } + if self.messageActionsState.isEmpty { + encoder.encodeNil(forKey: "as") + } else { + encoder.encodeObject(self.messageActionsState, forKey: "as") + } } func isEqual(to: PeerChatInterfaceState) -> Bool { @@ -246,13 +327,16 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else if (lhs.forwardMessageIds != nil) != (rhs.forwardMessageIds != nil) { return false } + if lhs.messageActionsState != rhs.messageActionsState { + return false + } return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage } func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { var updatedComposeInputState = inputState - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { @@ -264,15 +348,15 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { updatedComposeInputState = inputState } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -281,7 +365,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -294,18 +378,22 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState) } func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState) } func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) } func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState) + } + + func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState)) } } diff --git a/TelegramUI/ChatListEmptyItem.swift b/TelegramUI/ChatListEmptyItem.swift index 8bf07ceaba..14c7f1f60c 100644 --- a/TelegramUI/ChatListEmptyItem.swift +++ b/TelegramUI/ChatListEmptyItem.swift @@ -11,12 +11,14 @@ class ChatListEmptyItem: ListViewItem { init() { } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListEmptyItemNode() node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) node.updateItemPosition(first: previousItem == nil, last: nextItem == nil) - completion(node, {}) + completion(node, { + return (nil, {}) + }) } } diff --git a/TelegramUI/ChatListHoleItem.swift b/TelegramUI/ChatListHoleItem.swift index 5796a725b2..664e75e77c 100644 --- a/TelegramUI/ChatListHoleItem.swift +++ b/TelegramUI/ChatListHoleItem.swift @@ -13,13 +13,15 @@ class ChatListHoleItem: ListViewItem { init() { } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListHoleItemNode() node.relativePosition = (first: previousItem == nil, last: nextItem == nil) node.insets = ChatListItemNode.insets(first: false, last: false, firstWithHeader: false) node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - completion(node, {}) + completion(node, { + return (nil, {}) + }) } } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 5e1d7edecd..6afe5aaab9 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -28,14 +28,16 @@ class ChatListItem: ListViewItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListItemNode() node.setupItem(account: self.account, message: self.message, combinedReadState: self.combinedReadState, notificationSettings: self.notificationSettings, embeddedState: self.embeddedState) let (first, last, firstWithHeader) = ChatListItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem) node.insets = ChatListItemNode.insets(first: first, last: last, firstWithHeader: firstWithHeader) node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - completion(node, {}) + completion(node, { + return (nil, {}) + }) } } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index b9f761240e..09969abcb5 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -139,16 +139,17 @@ private struct ChatListSearchContainerTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] + let displayingResults: Bool } -private func preparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], account: Account, openPeer: @escaping (Peer) -> Void, openMessage: @escaping (Message) -> Void) -> ChatListSearchContainerTransition { +private func preparedTransition(from fromEntries: [ChatListSearchEntry], to toEntries: [ChatListSearchEntry], displayingResults: Bool, account: Account, openPeer: @escaping (Peer) -> Void, openMessage: @escaping (Message) -> Void) -> ChatListSearchContainerTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, openPeer: openPeer, openMessage: openMessage), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, openPeer: openPeer, openMessage: openMessage), directionHint: nil) } - return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates) + return ChatListSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults) } final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { @@ -229,7 +230,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let previousEntries = previousSearchItems.swap(entries) let firstTime = previousEntries == nil - let transition = preparedTransition(from: previousEntries ?? [], to: entries ?? [], account: account, openPeer: { peer in + let transition = preparedTransition(from: previousEntries ?? [], to: entries ?? [], displayingResults: entries != nil, account: account, openPeer: { peer in openPeer(peer) self?.listNode.clearHighlightAnimated(true) }, openMessage: { message in @@ -239,13 +240,6 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self?.listNode.clearHighlightAnimated(true) }) strongSelf.enqueueTransition(transition, firstTime: firstTime) - if let _ = entries { - strongSelf.listNode.isHidden = false - strongSelf.recentPeersNode.isHidden = true - } else { - strongSelf.listNode.isHidden = true - strongSelf.recentPeersNode.isHidden = false - } } })) } @@ -257,12 +251,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { override func searchTextUpdated(text: String) { if text.isEmpty { self.searchQuery.set(.single(nil)) - self.recentPeersNode.isHidden = false - self.listNode.isHidden = true } else { self.searchQuery.set(.single(text)) - self.recentPeersNode.isHidden = true - self.listNode.isHidden = false } } @@ -281,13 +271,19 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousResourceLoading) if firstTime { } else { //options.insert(.AnimateAlpha) } + let displayingResults = transition.displayingResults self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in - if let strongSelf = self, firstTime { + if let strongSelf = self { + if displayingResults != !strongSelf.listNode.isHidden { + strongSelf.listNode.isHidden = !displayingResults + strongSelf.recentPeersNode.isHidden = displayingResults + } } }) } diff --git a/TelegramUI/ChatListSearchItem.swift b/TelegramUI/ChatListSearchItem.swift index 8b1387662c..e326e9250d 100644 --- a/TelegramUI/ChatListSearchItem.swift +++ b/TelegramUI/ChatListSearchItem.swift @@ -18,7 +18,7 @@ class ChatListSearchItem: ListViewItem { self.activate = activate } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatListSearchItemNode() node.placeholder = self.placeholder @@ -31,7 +31,7 @@ class ChatListSearchItem: ListViewItem { node.activate = self.activate completion(node, { - apply() + return (nil, apply) }) } } diff --git a/TelegramUI/ChatMediaInputStickerPackItem.swift b/TelegramUI/ChatMediaInputStickerPackItem.swift index bea91d9da6..1db0278e1a 100644 --- a/TelegramUI/ChatMediaInputStickerPackItem.swift +++ b/TelegramUI/ChatMediaInputStickerPackItem.swift @@ -26,7 +26,7 @@ final class ChatMediaInputStickerPackItem: ListViewItem { self.index = index } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatMediaInputStickerPackItemNode() node.contentSize = CGSize(width: 41.0, height: 41.0) @@ -34,11 +34,12 @@ final class ChatMediaInputStickerPackItem: ListViewItem { node.inputNodeInteraction = self.inputNodeInteraction node.updateStickerPackItem(account: self.account, item: self.stickerPackItem, collectionId: self.collectionId) completion(node, { + return (nil, {}) }) } } - func updateNode(async: (() -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: (ListViewItemNodeLayout, () -> Void) -> Void) { + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { }) } diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift index 27c5837338..6bc0b352e6 100644 --- a/TelegramUI/ChatMessageActionButtonsNode.swift +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -87,7 +87,7 @@ private final class ChatMessageActionButtonNode: ASDisplayNode { node.button = button node.backgroundNode.image = backgroundImage - node.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) + node.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: max(0.0, width), height: 42.0)) let titleNode = titleApply() if node.titleNode !== titleNode { @@ -137,7 +137,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { var buttonIndex = 0 for row in replyMarkup.rows { var minimumRowWidth: CGFloat = 0.0 - let maximumButtonWidth: CGFloat = floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)) + let maximumButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count))) var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))] = [] var rowButtonIndex = 0 for button in row.buttons { @@ -170,7 +170,7 @@ final class ChatMessageActionButtonsNode: ASDisplayNode { rowButtonIndex += 1 } - let actualButtonWidth: CGFloat = floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)) + let actualButtonWidth: CGFloat = max(1.0, floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count))) var horizontalButtonOffset: CGFloat = 0.0 for finalizeButtonLayout in finalizeRowButtonLayouts { let (buttonSize, buttonApply) = finalizeButtonLayout(actualButtonWidth) diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index c7304e4a67..c8be53f941 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -262,7 +262,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { let b = log(maxVoiceWidth / minVoiceWidth) / (maxVoiceLength - 0.0) let a = minVoiceWidth / exp(CGFloat(0.0)) - let y = a * exp(b * CGFloat(audioDuration)) + let y = a * exp(b * min(maxVoiceLength, CGFloat(audioDuration))) minLayoutWidth = floor(y) } else { diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 255208b849..c31640c652 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -116,7 +116,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { self.accessoryItem = accessoryItem } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { var viewClassName: AnyClass = ChatMessageBubbleItemNode.self for media in message.media { @@ -127,7 +127,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - let configure = { () -> Void in + let configure = { let node = (viewClassName as! ChatMessageItemView.Type).init() node.controllerInteraction = self.controllerInteraction node.setupItem(self) @@ -142,7 +142,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 13dafd412a..560681e4f7 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -18,7 +18,7 @@ final class ChatPanelInterfaceInteraction { let deleteSelectedMessages: () -> Void let forwardSelectedMessages: () -> Void let updateTextInputState: (@escaping (ChatTextInputState) -> ChatTextInputState) -> Void - let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void + let updateInputModeAndDismissedButtonKeyboardMessageId: ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void let editMessage: (MessageId, String) -> Void let beginMessageSearch: () -> Void let openPeerInfo: () -> Void @@ -29,14 +29,14 @@ final class ChatPanelInterfaceInteraction { let finishAudioRecording: (Bool) -> Void let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputModeAndDismissedButtonKeyboardMessageId: @escaping ((ChatPresentationInterfaceState) -> (ChatInputMode, MessageId?)) -> Void, editMessage: @escaping (MessageId, String) -> Void, beginMessageSearch: @escaping () -> Void, openPeerInfo: @escaping () -> Void, togglePeerNotifications: @escaping () -> Void, sendContextResult: @escaping (ChatContextResultCollection, ChatContextResult) -> Void, sendBotCommand: @escaping (Peer, String) -> Void, beginAudioRecording: @escaping () -> Void, finishAudioRecording: @escaping (Bool) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.forwardSelectedMessages = forwardSelectedMessages self.updateTextInputState = updateTextInputState - self.updateInputMode = updateInputMode + self.updateInputModeAndDismissedButtonKeyboardMessageId = updateInputModeAndDismissedButtonKeyboardMessageId self.editMessage = editMessage self.beginMessageSearch = beginMessageSearch self.openPeerInfo = openPeerInfo diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index d16788e9ac..42dae44221 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -96,6 +96,7 @@ enum ChatInputMode { case none case text case media + case inputButtons } enum ChatTitlePanelContext: Comparable { @@ -149,6 +150,7 @@ struct ChatPresentationInterfaceState: Equatable { let inputQueryResult: ChatPresentationInputQueryResult? let inputMode: ChatInputMode let titlePanelContexts: [ChatTitlePanelContext] + let keyboardButtonsMessage: Message? init() { self.interfaceState = ChatInterfaceState() @@ -157,15 +159,17 @@ struct ChatPresentationInterfaceState: Equatable { self.inputQueryResult = nil self.inputMode = .none self.titlePanelContexts = [] + self.keyboardButtonsMessage = nil } - init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext]) { + init(interfaceState: ChatInterfaceState, peer: Peer?, inputTextPanelState: ChatTextInputPanelState, inputQueryResult: ChatPresentationInputQueryResult?, inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?) { self.interfaceState = interfaceState self.peer = peer self.inputTextPanelState = inputTextPanelState self.inputQueryResult = inputQueryResult self.inputMode = inputMode self.titlePanelContexts = titlePanelContexts + self.keyboardButtonsMessage = keyboardButtonsMessage } static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { @@ -196,30 +200,45 @@ struct ChatPresentationInterfaceState: Equatable { return false } + if let lhsMessage = lhs.keyboardButtonsMessage, let rhsMessage = rhs.keyboardButtonsMessage { + if lhsMessage.id != rhsMessage.id { + return false + } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } + } else if (lhs.keyboardButtonsMessage != nil) != (rhs.keyboardButtonsMessage != nil) { + return false + } + return true } func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts) + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage) } func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage) } func updatedInputQueryResult(_ f: (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: f(self.inputQueryResult), inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage) } func updatedInputTextPanelState(_ f: (ChatTextInputPanelState) -> ChatTextInputPanelState) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: f(self.inputTextPanelState), inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage) } func updatedInputMode(_ f: (ChatInputMode) -> ChatInputMode) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: f(self.inputMode), titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage) } func updatedTitlePanelContext(_ f: ([ChatTitlePanelContext]) -> [ChatTitlePanelContext]) -> ChatPresentationInterfaceState { - return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts)) + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: f(self.titlePanelContexts), keyboardButtonsMessage: self.keyboardButtonsMessage) + } + + func updatedKeyboardButtonsMessage(_ message: Message?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputTextPanelState: self.inputTextPanelState, inputQueryResult: self.inputQueryResult, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: message) } } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 8ce65d0d23..83d52184dd 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -358,7 +358,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { return textFieldHeight + self.textFieldInsets.top + self.textFieldInsets.bottom + self.textInputViewInternalInsets.top + self.textInputViewInternalInsets.bottom } - override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { + override func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState) -> CGFloat { if self.presentationInterfaceState != interfaceState { let previousState = self.presentationInterfaceState self.presentationInterfaceState = interfaceState @@ -749,17 +749,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } @objc func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) { - self.interfaceInteraction?.updateInputMode({ _ in .text }) - } - - @objc func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) { - /*self.interfaceInteraction?.updateInputMode({ mode in - if case .text = mode { - return .none - } else { - return mode - } - })*/ + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.text, state.keyboardButtonsMessage?.id) + }) } @objc func sendButtonPressed() { @@ -828,12 +820,18 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { for (item, currentButton) in self.accessoryItemButtons { if currentButton === button { switch item { - case .inputButtons: - break case .stickers: - self.interfaceInteraction?.updateInputMode({ _ in .media }) + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.media, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) + }) case .keyboard: - self.interfaceInteraction?.updateInputMode({ _ in .text }) + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.text, state.keyboardButtonsMessage?.id) + }) + case .inputButtons: + self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in + return (.inputButtons, nil) + }) } break } diff --git a/TelegramUI/ChatUnreadItem.swift b/TelegramUI/ChatUnreadItem.swift index 6809f8ff12..5bfae0c07b 100644 --- a/TelegramUI/ChatUnreadItem.swift +++ b/TelegramUI/ChatUnreadItem.swift @@ -3,6 +3,7 @@ import UIKit import Postbox import AsyncDisplayKit import Display +import SwiftSignalKit private func backgroundImage() -> UIImage? { return generateImage(CGSize(width: 1.0, height: 25.0), contextGenerator: { size, context -> Void in @@ -26,14 +27,20 @@ class ChatUnreadItem: ListViewItem { self.header = ChatMessageDateHeader(timestamp: index.timestamp) } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { - + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ChatUnreadItemNode() node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - completion(node, {}) + completion(node, { + return (nil, {}) + }) } } + + func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + }) + } } class ChatUnreadItemNode: ListViewItemNode { diff --git a/TelegramUI/CommandChatInputPanelItem.swift b/TelegramUI/CommandChatInputPanelItem.swift index 35c9fde29c..0555c3c2ba 100644 --- a/TelegramUI/CommandChatInputPanelItem.swift +++ b/TelegramUI/CommandChatInputPanelItem.swift @@ -18,7 +18,7 @@ final class CommandChatInputPanelItem: ListViewItem { self.commandSelected = commandSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = CommandChatInputPanelItemNode() @@ -30,7 +30,7 @@ final class CommandChatInputPanelItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 6615411c5f..a99c34ea33 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -66,7 +66,7 @@ class ContactsPeerItem: ListViewItem { } } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ContactsPeerItemNode() let makeLayout = node.asyncLayout() @@ -75,9 +75,7 @@ class ContactsPeerItem: ListViewItem { node.contentSize = nodeLayout.contentSize node.insets = nodeLayout.insets - completion(node, { - nodeApply() - }) + completion(node, nodeApply) } } @@ -90,7 +88,7 @@ class ContactsPeerItem: ListViewItem { let (nodeLayout, apply) = layout(self, width, first, last, firstWithHeader) Queue.mainQueue().async { completion(nodeLayout, { - apply() + apply().1() }) } } @@ -224,7 +222,7 @@ class ContactsPeerItemNode: ListViewItemNode { } } - func asyncLayout() -> (_ item: ContactsPeerItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ContactsPeerItem, _ width: CGFloat, _ first: Bool, _ last: Bool, _ firstWithHeader: Bool) -> (ListViewItemNodeLayout, () -> (Signal?, () -> Void)) { let makeTitleLayout = TextNode.asyncLayout(self.titleNode) let makeStatusLayout = TextNode.asyncLayout(self.statusNode) @@ -289,28 +287,35 @@ class ContactsPeerItemNode: ListViewItemNode { return (nodeLayout, { [weak self] in if let strongSelf = self { - strongSelf.layoutParams = (item, width, first, last, firstWithHeader) if let peer = item.peer { strongSelf.avatarNode.setPeer(account: item.account, peer: peer) } - strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) - - let _ = titleApply() - strongSelf.titleNode.frame = titleFrame - - let _ = statusApply() - strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size) - - let topHighlightInset: CGFloat = first ? 0.0 : separatorHeight - strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) - strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) - strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 65.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight)) - strongSelf.separatorNode.isHidden = last - - if let userPresence = userPresence { - strongSelf.peerPresenceManager?.reset(presence: userPresence) - } + return (strongSelf.avatarNode.ready, { [weak strongSelf] in + if let strongSelf = strongSelf { + strongSelf.layoutParams = (item, width, first, last, firstWithHeader) + + strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: 14.0, y: 4.0), size: CGSize(width: 40.0, height: 40.0)) + + let _ = titleApply() + strongSelf.titleNode.frame = titleFrame + + let _ = statusApply() + strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size) + + let topHighlightInset: CGFloat = first ? 0.0 : separatorHeight + strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: nodeLayout.contentSize.width, height: nodeLayout.contentSize.height)) + strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: nodeLayout.size.width, height: nodeLayout.size.height + topHighlightInset)) + strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: 65.0, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: max(0.0, nodeLayout.size.width - 65.0), height: separatorHeight)) + strongSelf.separatorNode.isHidden = last + + if let userPresence = userPresence { + strongSelf.peerPresenceManager?.reset(presence: userPresence) + } + } + }) + } else { + return (nil, {}) } }) } diff --git a/TelegramUI/ContactsVCardItem.swift b/TelegramUI/ContactsVCardItem.swift index 704ebfdbdd..91639cc739 100644 --- a/TelegramUI/ContactsVCardItem.swift +++ b/TelegramUI/ContactsVCardItem.swift @@ -21,7 +21,7 @@ class ContactsVCardItem: ListViewItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ContactsVCardItemNode() let makeLayout = node.asyncLayout() @@ -30,7 +30,7 @@ class ContactsVCardItem: ListViewItem { node.insets = nodeLayout.insets completion(node, { - nodeApply() + return (nil, { nodeApply() }) }) } } diff --git a/TelegramUI/DataAndStorageSettingsController.swift b/TelegramUI/DataAndStorageSettingsController.swift new file mode 100644 index 0000000000..6424a25a3e --- /dev/null +++ b/TelegramUI/DataAndStorageSettingsController.swift @@ -0,0 +1,45 @@ +import Foundation +import Display +import Postbox +import SwiftSignalKit +import TelegramCore +import MtProtoKitDynamic + +public class DataAndStorageSettingsController: ListController { + private let account: Account + + private var currentStatsDisposable: Disposable? + + public init(account: Account) { + self.account = account + + super.init() + + self.title = "Data and Storage" + + let deselectAction = { [weak self] () -> Void in + self?.listDisplayNode.listView.clearHighlightAnimated(true) + } + + self.items = [ + ListControllerDisclosureActionItem(title: "Bytes Sent", action: deselectAction), + ListControllerDisclosureActionItem(title: "Bytes Received", action: deselectAction), + ] + + self.currentStatsDisposable = (((account.currentNetworkStats() |> then(Signal.complete() |> delay(1.0, queue: Queue.concurrentDefaultQueue()))) |> restart) |> deliverOnMainQueue).start(next: { [weak self] stats in + if let strongSelf = self { + let incoming = stats.wwan.incomingBytes + stats.other.incomingBytes + let outgoing = stats.wwan.outgoingBytes + stats.other.outgoingBytes + strongSelf.listDisplayNode.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [ListViewUpdateItem(index: 0, previousIndex: 0, item: ListControllerDisclosureActionItem(title: "Bytes Sent: \(outgoing / 1024) KB", action: deselectAction), directionHint: nil), ListViewUpdateItem(index: 1, previousIndex: 1, item: ListControllerDisclosureActionItem(title: "Bytes Received: \(incoming / 1024) KB", action: deselectAction), directionHint: nil)], options: [.AnimateInsertion], updateOpaqueState: nil) + } + }) + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + currentStatsDisposable?.dispose() + } +} diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index ddc664b36c..d6651e88e9 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -153,7 +153,7 @@ class GalleryController: ViewController { |> filter({ $0 != nil }) |> mapToSignal { message -> Signal in if let tags = tagsForMessage(message!) { - let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, tagMask: tags) + let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: tags) return view |> mapToSignal { (view, _, _) -> Signal in diff --git a/TelegramUI/HashtagChatInputPanelItem.swift b/TelegramUI/HashtagChatInputPanelItem.swift index beaa430d7c..5a7cd99d02 100644 --- a/TelegramUI/HashtagChatInputPanelItem.swift +++ b/TelegramUI/HashtagChatInputPanelItem.swift @@ -16,7 +16,7 @@ final class HashtagChatInputPanelItem: ListViewItem { self.hashtagSelected = hashtagSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = HashtagChatInputPanelItemNode() @@ -28,7 +28,7 @@ final class HashtagChatInputPanelItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift index 1251cdd21b..75c56f1feb 100644 --- a/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/HorizontalListContextResultsChatInputPanelItem.swift @@ -18,7 +18,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { self.resultSelected = resultSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = HorizontalListContextResultsChatInputPanelItemNode() @@ -30,7 +30,7 @@ final class HorizontalListContextResultsChatInputPanelItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index daa61e5d96..994a2954fa 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -3,6 +3,7 @@ import Display import Postbox import AsyncDisplayKit import TelegramCore +import SwiftSignalKit final class HorizontalPeerItem: ListViewItem { let account: Account @@ -15,7 +16,7 @@ final class HorizontalPeerItem: ListViewItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = HorizontalPeerItemNode() node.contentSize = CGSize(width: 92.0, height: 80.0) @@ -23,6 +24,7 @@ final class HorizontalPeerItem: ListViewItem { node.update(account: self.account, peer: self.peer) node.action = self.action completion(node, { + return (nil, {}) }) } } diff --git a/TelegramUI/ImageNode.swift b/TelegramUI/ImageNode.swift index ea99d01507..fc29468776 100644 --- a/TelegramUI/ImageNode.swift +++ b/TelegramUI/ImageNode.swift @@ -77,23 +77,43 @@ public func ==(lhs: ImageCorners, rhs: ImageCorners) -> Bool { public class ImageNode: ASDisplayNode { private var disposable = MetaDisposable() + private let hasImage: ValuePromise? - override init() { + var ready: Signal { + if let hasImage = self.hasImage { + return hasImage.get() + } else { + return .single(true) + } + } + + init(enableHasImage: Bool = false) { + if enableHasImage { + self.hasImage = ValuePromise(false, ignoreRepeated: true) + } else { + self.hasImage = nil + } super.init() } - public func setSignal(_ signal: Signal) { + public func setSignal(_ signal: Signal) { var first = true + var reportedHasImage = false self.disposable.set((signal |> deliverOnMainQueue).start(next: {[weak self] next in dispatcher.dispatch { if let strongSelf = self { - strongSelf.contents = next.cgImage - if first { + strongSelf.contents = next?.cgImage + if first && next != nil { first = false if strongSelf.isNodeLoaded { strongSelf.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) } } + if !reportedHasImage { + if let hasImage = strongSelf.hasImage { + hasImage.set(true) + } + } } } })) diff --git a/TelegramUI/ListControllerGroupableItem.swift b/TelegramUI/ListControllerGroupableItem.swift index 6763daf7a0..bd43496e8d 100644 --- a/TelegramUI/ListControllerGroupableItem.swift +++ b/TelegramUI/ListControllerGroupableItem.swift @@ -11,13 +11,15 @@ protocol ListControllerGroupableItem: ListControllerItem { } extension ListControllerGroupableItem { - func nodeConfiguredForWidth(async: @escaping (@escaping() -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping(ListViewItemNode, @escaping() -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping() -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping(ListViewItemNode, @escaping() -> (Signal?, () -> Void)) -> Void) { self.setupNode(async: async, completion: { node in let asyncLayout = node.asyncLayout() let (layout, apply) = asyncLayout(self, width, previousItem is ListControllerGroupableItem, nextItem is ListControllerGroupableItem) node.contentSize = layout.contentSize node.insets = layout.insets - completion(node, apply) + completion(node, { + return (nil, apply) + }) }) } diff --git a/TelegramUI/ListControllerSpacerItem.swift b/TelegramUI/ListControllerSpacerItem.swift index e1f9795307..1afe59db68 100644 --- a/TelegramUI/ListControllerSpacerItem.swift +++ b/TelegramUI/ListControllerSpacerItem.swift @@ -1,5 +1,6 @@ import Foundation import Display +import SwiftSignalKit class ListControllerSpacerItem: ListControllerItem { private let height: CGFloat @@ -12,14 +13,21 @@ class ListControllerSpacerItem: ListControllerItem { return false } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = ListControllerSpacerItemNode() node.height = self.height node.layoutForWidth(width, item: self, previousItem: previousItem, nextItem: nextItem) - completion(node, {}) + completion(node, { + return (nil, {}) + }) } } + + public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { + completion(ListViewItemNodeLayout(contentSize: node.contentSize, insets: node.insets), { + }) + } } class ListControllerSpacerItemNode: ListViewItemNode { diff --git a/TelegramUI/ListMessageHoleItem.swift b/TelegramUI/ListMessageHoleItem.swift index cb5a85c3cc..9787e19d2d 100644 --- a/TelegramUI/ListMessageHoleItem.swift +++ b/TelegramUI/ListMessageHoleItem.swift @@ -9,7 +9,7 @@ final class ListMessageHoleItem: ListViewItem { public init() { } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = ListMessageHoleItemNode() @@ -23,7 +23,7 @@ final class ListMessageHoleItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/ListMessageItem.swift b/TelegramUI/ListMessageItem.swift index 925b2179f5..6e90344035 100644 --- a/TelegramUI/ListMessageItem.swift +++ b/TelegramUI/ListMessageItem.swift @@ -20,7 +20,7 @@ final class ListMessageItem: ListViewItem { self.message = message } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { var viewClassName: AnyClass = ListMessageFileItemNode.self for media in message.media { @@ -45,7 +45,7 @@ final class ListMessageItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/MediaPlayerScrubbingNode.swift b/TelegramUI/MediaPlayerScrubbingNode.swift index 0f53b8475d..b5e73b96dd 100644 --- a/TelegramUI/MediaPlayerScrubbingNode.swift +++ b/TelegramUI/MediaPlayerScrubbingNode.swift @@ -237,7 +237,7 @@ final class MediaPlayerScrubbingNode: ASDisplayNode { animation.toValue = to animation.duration = duration animation.isRemovedOnCompletion = true - animation.fillMode = kCAFillModeBoth + animation.fillMode = kCAFillModeBackwards animation.speed = speed animation.timeOffset = offset animation.isAdditive = false diff --git a/TelegramUI/MentionChatInputPanelItem.swift b/TelegramUI/MentionChatInputPanelItem.swift index 3259d016d1..16217d32ad 100644 --- a/TelegramUI/MentionChatInputPanelItem.swift +++ b/TelegramUI/MentionChatInputPanelItem.swift @@ -18,7 +18,7 @@ final class MentionChatInputPanelItem: ListViewItem { self.peerSelected = peerSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = MentionChatInputPanelItemNode() @@ -30,7 +30,7 @@ final class MentionChatInputPanelItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index 68a169430c..76a9d2b932 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -19,7 +19,7 @@ private let roundCorners = { () -> UIImage in return image }() -func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { +func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { if let smallProfileImage = peer.smallProfileImage { let resourceData = account.postbox.mediaBox.resourceData(smallProfileImage.resource) let imageData = resourceData @@ -33,6 +33,8 @@ func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = C if data.complete { subscriber.putNext(try? Data(contentsOf: URL(fileURLWithPath: maybeData.path))) subscriber.putCompletion() + } else { + subscriber.putNext(nil) } }, error: { error in subscriber.putError(error) @@ -49,7 +51,7 @@ func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = C } return imageData |> deliverOn(account.graphicsThreadPool) - |> map { data -> UIImage in + |> map { data -> UIImage? in if let data = data, let image = generateImage(displayDimensions, contextGenerator: { size, context -> Void in if let imageSource = CGImageSourceCreateWithData(data as CFData, nil), let dataImage = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { context.setBlendMode(.copy) @@ -60,10 +62,7 @@ func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = C }) { return image } else { - UIGraphicsBeginImageContextWithOptions(displayDimensions, false, 0.0) - let image = UIGraphicsGetImageFromCurrentImageContext()! - UIGraphicsEndImageContext() - return image + return nil } } } else { diff --git a/TelegramUI/PeerInfoActionItem.swift b/TelegramUI/PeerInfoActionItem.swift index de9614f1db..f3ed8f896a 100644 --- a/TelegramUI/PeerInfoActionItem.swift +++ b/TelegramUI/PeerInfoActionItem.swift @@ -30,7 +30,7 @@ class PeerInfoActionItem: ListViewItem, PeerInfoItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = PeerInfoActionItemNode() let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) @@ -39,7 +39,7 @@ class PeerInfoActionItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply() + return (nil, { apply() }) }) } } diff --git a/TelegramUI/PeerInfoAvatarAndNameItem.swift b/TelegramUI/PeerInfoAvatarAndNameItem.swift index 91e8116890..568091c288 100644 --- a/TelegramUI/PeerInfoAvatarAndNameItem.swift +++ b/TelegramUI/PeerInfoAvatarAndNameItem.swift @@ -28,7 +28,7 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { self.style = style } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = PeerInfoAvatarAndNameItemNode() let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) @@ -37,7 +37,7 @@ class PeerInfoAvatarAndNameItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply(false) + return (nil, { apply(false) }) }) } } diff --git a/TelegramUI/PeerInfoDisclosureItem.swift b/TelegramUI/PeerInfoDisclosureItem.swift index 99079e14b2..e7ee0d5633 100644 --- a/TelegramUI/PeerInfoDisclosureItem.swift +++ b/TelegramUI/PeerInfoDisclosureItem.swift @@ -18,7 +18,7 @@ class PeerInfoDisclosureItem: ListViewItem, PeerInfoItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = PeerInfoDisclosureItemNode() let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) @@ -27,7 +27,7 @@ class PeerInfoDisclosureItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply() + return (nil, { apply() }) }) } } diff --git a/TelegramUI/PeerInfoPeerActionItem.swift b/TelegramUI/PeerInfoPeerActionItem.swift index fb4b2c13d4..48af660289 100644 --- a/TelegramUI/PeerInfoPeerActionItem.swift +++ b/TelegramUI/PeerInfoPeerActionItem.swift @@ -16,7 +16,7 @@ class PeerInfoPeerActionItem: ListViewItem, PeerInfoItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = PeerInfoPeerActionItemNode() let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) @@ -25,7 +25,7 @@ class PeerInfoPeerActionItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply() + return (nil, { apply() }) }) } } diff --git a/TelegramUI/PeerInfoPeerItem.swift b/TelegramUI/PeerInfoPeerItem.swift index fe12e2eff7..fb2ff25cb7 100644 --- a/TelegramUI/PeerInfoPeerItem.swift +++ b/TelegramUI/PeerInfoPeerItem.swift @@ -22,7 +22,7 @@ final class PeerInfoPeerItem: ListViewItem, PeerInfoItem { self.action = action } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = PeerInfoPeerItemNode() let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) @@ -31,7 +31,7 @@ final class PeerInfoPeerItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply() + return (nil, { apply() }) }) } } diff --git a/TelegramUI/PeerInfoTextWithLabelItem.swift b/TelegramUI/PeerInfoTextWithLabelItem.swift index 13385f40f8..af8d5029e5 100644 --- a/TelegramUI/PeerInfoTextWithLabelItem.swift +++ b/TelegramUI/PeerInfoTextWithLabelItem.swift @@ -16,7 +16,7 @@ final class PeerInfoTextWithLabelItem: ListViewItem, PeerInfoItem { self.sectionId = sectionId } - func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { async { let node = PeerInfoTextWithLabelItemNode() let (layout, apply) = node.asyncLayout()(self, width, peerInfoItemNeighbors(item: self, topItem: previousItem as? PeerInfoItem, bottomItem: nextItem as? PeerInfoItem)) @@ -25,7 +25,7 @@ final class PeerInfoTextWithLabelItem: ListViewItem, PeerInfoItem { node.insets = layout.insets completion(node, { - apply() + return (nil, { apply() }) }) } } diff --git a/TelegramUI/PeerMediaAudioPlaylist.swift b/TelegramUI/PeerMediaAudioPlaylist.swift index 824586c54d..e12af4a947 100644 --- a/TelegramUI/PeerMediaAudioPlaylist.swift +++ b/TelegramUI/PeerMediaAudioPlaylist.swift @@ -99,7 +99,7 @@ func peerMessageAudioPlaylistAndItemIds(_ message: Message) -> (AudioPlaylistId, func peerMessageHistoryAudioPlaylist(account: Account, messageId: MessageId) -> AudioPlaylist { return AudioPlaylist(id: PeerMessageHistoryAudioPlaylistId(peerId: messageId.peerId), navigate: { item, navigation in if let item = item as? PeerMessageHistoryAudioPlaylistItem { - return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, anchorIndex: item.entry.index, fixedCombinedReadState: nil, tagMask: .Music) + return account.postbox.aroundMessageHistoryViewForPeerId(item.entry.index.id.peerId, index: item.entry.index, count: 10, anchorIndex: item.entry.index, fixedCombinedReadState: nil, topTaggedMessageIdNamespaces: [], tagMask: .Music) |> take(1) |> map { (view, _, _) -> AudioPlaylistItem? in var index = 0 diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index ebf054213a..b54abc57b2 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -198,7 +198,7 @@ public class PeerMediaCollectionController: ViewController { }, deleteSelectedMessages: { }, forwardSelectedMessages: { }, updateTextInputState: { _ in - }, updateInputMode: { _ in + }, updateInputModeAndDismissedButtonKeyboardMessageId: { _ in }, editMessage: { _, _ in }, beginMessageSearch: { }, openPeerInfo: { diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index c14a2d494a..c33fdb4bd6 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,11 +4,9 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?, keyboardButtonsMessage: Message?) -> Signal { return Signal { subscriber in - //let updateIndices: [(Int, ChatHistoryEntry, Int)] = [] let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) - //let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) var adjustedDeleteIndices: [ListViewDeleteItem] = [] let previousCount: Int @@ -160,7 +158,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } } - subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData)) + subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData, keyboardButtonsMessage: keyboardButtonsMessage)) subscriber.putCompletion() return EmptyDisposable diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 67625535d8..df7df4f8b7 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -31,7 +31,13 @@ public class SettingsController: ListController { ListControllerSpacerItem(height: 35.0), ListControllerDisclosureActionItem(title: "Notifications and Sounds", action: deselectAction), ListControllerDisclosureActionItem(title: "Privacy and Security", action: deselectAction), - ListControllerDisclosureActionItem(title: "Chat Settings", action: deselectAction), + ListControllerDisclosureActionItem(title: "Data and Storage", action: { [weak self] in + deselectAction() + + if let strongSelf = self { + strongSelf.navigationController?.pushViewController(DataAndStorageSettingsController(account: strongSelf.account), animated: true) + } + }), //SettingsWallpaperListItem(), ListControllerSpacerItem(height: 35.0), ListControllerDisclosureActionItem(title: "Phone Number", action: deselectAction), diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift index 25bf75b0ac..e2bd789149 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelButtonItem.swift @@ -14,7 +14,7 @@ final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { self.pressed = pressed } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = VerticalListContextResultsChatInputPanelButtonItemNode() @@ -26,7 +26,7 @@ final class VerticalListContextResultsChatInputPanelButtonItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread { diff --git a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift index ae1c3ae122..8d95b0480b 100644 --- a/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift +++ b/TelegramUI/VerticalListContextResultsChatInputPanelItem.swift @@ -18,7 +18,7 @@ final class VerticalListContextResultsChatInputPanelItem: ListViewItem { self.resultSelected = resultSelected } - public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { + public func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { let configure = { () -> Void in let node = VerticalListContextResultsChatInputPanelItemNode() @@ -30,7 +30,7 @@ final class VerticalListContextResultsChatInputPanelItem: ListViewItem { node.insets = layout.insets completion(node, { - apply(.None) + return (nil, { apply(.None) }) }) } if Thread.isMainThread {