diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 5c1f0738cf..de37bfbcfb 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -25,8 +25,17 @@ D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; }; D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; }; D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; }; + D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; }; + D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */; }; D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */; }; D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */; }; + D07CFF741DCA207200761F81 /* PeerSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF731DCA207200761F81 /* PeerSelectionController.swift */; }; + D07CFF761DCA224100761F81 /* PeerSelectionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF751DCA224100761F81 /* PeerSelectionControllerNode.swift */; }; + D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF781DCA226F00761F81 /* ChatListNode.swift */; }; + D07CFF7B1DCA24BF00761F81 /* ChatListNodeEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */; }; + D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */; }; + D07CFF7F1DCA308500761F81 /* ChatListNodeLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */; }; + D07CFF871DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */; }; D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */; }; D08C36811DB66AAC0064C744 /* ChatMediaInputGridEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */; }; D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */; }; @@ -245,8 +254,17 @@ D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; + D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = ""; }; + D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = ""; }; D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = ""; }; D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = ""; }; + D07CFF731DCA207200761F81 /* PeerSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionController.swift; sourceTree = ""; }; + D07CFF751DCA224100761F81 /* PeerSelectionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionControllerNode.swift; sourceTree = ""; }; + D07CFF781DCA226F00761F81 /* ChatListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNode.swift; sourceTree = ""; }; + D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeEntries.swift; sourceTree = ""; }; + D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListViewTransition.swift; sourceTree = ""; }; + D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListNodeLocation.swift; sourceTree = ""; }; + D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ForwardAccessoryPanelNode.swift; sourceTree = ""; }; D08C367E1DB66A820064C744 /* ChatMediaInputPanelEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputPanelEntries.swift; sourceTree = ""; }; D08C36801DB66AAC0064C744 /* ChatMediaInputGridEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputGridEntries.swift; sourceTree = ""; }; D08C36821DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerGridItem.swift; sourceTree = ""; }; @@ -541,10 +559,34 @@ children = ( D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */, D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */, + D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */, ); name = "Accessory Panels"; sourceTree = ""; }; + D073CE611DCBBE09007511FD /* Sounds */ = { + isa = PBXGroup; + children = ( + D073CE621DCBBE5D007511FD /* MessageSent.caf */, + ); + name = Sounds; + sourceTree = ""; + }; + D07CFF771DCA226200761F81 /* Chat List Node */ = { + isa = PBXGroup; + children = ( + D07CFF781DCA226F00761F81 /* ChatListNode.swift */, + D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */, + D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */, + D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */, + D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */, + D07CFF7A1DCA24BF00761F81 /* ChatListNodeEntries.swift */, + D07CFF7C1DCA273400761F81 /* ChatListViewTransition.swift */, + D07CFF7E1DCA308500761F81 /* ChatListNodeLocation.swift */, + ); + name = "Chat List Node"; + sourceTree = ""; + }; D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( @@ -615,12 +657,14 @@ name = "Navigation Accessory Panels"; sourceTree = ""; }; - D0D2689B1D79D31500C422DA /* Share Recipients */ = { + D0D2689B1D79D31500C422DA /* Peer Selection */ = { isa = PBXGroup; children = ( + D07CFF731DCA207200761F81 /* PeerSelectionController.swift */, + D07CFF751DCA224100761F81 /* PeerSelectionControllerNode.swift */, D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */, ); - name = "Share Recipients"; + name = "Peer Selection"; sourceTree = ""; }; D0DE772C1D934DCB002B8809 /* List Items */ = { @@ -761,6 +805,7 @@ D0F69DBE1D6B89880046BCD6 /* Gestures */, D0F69DBF1D6B89AE0046BCD6 /* Nodes */, D0F69DD31D6B8A160046BCD6 /* Controllers */, + D07CFF771DCA226200761F81 /* Chat List Node */, D0E7A1BB1D8C17EB00C37A6F /* Chat History Node */, ); name = Components; @@ -840,7 +885,7 @@ D0F69E4E1D6B8BB90046BCD6 /* Media */, D0F69E6C1D6B8C220046BCD6 /* Contacts */, D0EE97131D88BB1A006C18E1 /* Peer Info */, - D0D2689B1D79D31500C422DA /* Share Recipients */, + D0D2689B1D79D31500C422DA /* Peer Selection */, D0F69E791D6B8C3B0046BCD6 /* Settings */, ); name = Controllers; @@ -865,10 +910,6 @@ children = ( D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */, D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */, - D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */, - D0F69DFB1D6B8A880046BCD6 /* ChatListHoleItem.swift */, - D0F69DFC1D6B8A880046BCD6 /* ChatListItem.swift */, - D0F69DFD1D6B8A880046BCD6 /* ChatListSearchItem.swift */, D0F69E051D6B8A8B0046BCD6 /* Search */, ); name = "Chat List"; @@ -1064,6 +1105,7 @@ D0F69E941D6B8C9B0046BCD6 /* WebP.swift */, D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */, D0ED5D4A1DC806D7007CBB15 /* ApplicationSpecificData.swift */, + D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */, ); name = Utils; sourceTree = ""; @@ -1085,6 +1127,7 @@ children = ( D0F69DB91D6B88190046BCD6 /* TelegramUI.xcconfig */, D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */, + D073CE611DCBBE09007511FD /* Sounds */, D0FC40811D5B8E7400261D9D /* TelegramUI */, D0FC408C1D5B8E7500261D9D /* TelegramUITests */, D0FC40801D5B8E7400261D9D /* Products */, @@ -1227,6 +1270,7 @@ files = ( D0AB0BBB1D6719B5002C78E7 /* Images.xcassets in Resources */, D0F69DBA1D6B88190046BCD6 /* TelegramUI.xcconfig in Resources */, + D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1253,6 +1297,7 @@ D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */, D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */, D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */, + D07CFF7F1DCA308500761F81 /* ChatListNodeLocation.swift in Sources */, D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, D08C367F1DB66A820064C744 /* ChatMediaInputPanelEntries.swift in Sources */, D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, @@ -1269,6 +1314,7 @@ D0B843DB1DAAB138005F29E1 /* PeerInfoPeerActionItem.swift in Sources */, D0F69EA11D6B8E380046BCD6 /* FileResources.swift in Sources */, D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */, + D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */, D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */, D0F69E031D6B8A880046BCD6 /* ChatListItem.swift in Sources */, D0F69E081D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift in Sources */, @@ -1292,6 +1338,7 @@ D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */, D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */, D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */, + D07CFF871DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift in Sources */, D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */, D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */, D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */, @@ -1303,6 +1350,7 @@ D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */, D0B843921DA7F13E005F29E1 /* PeerInfoDisclosureItem.swift in Sources */, + D07CFF791DCA226F00761F81 /* ChatListNode.swift in Sources */, D0B7F8E81D8A1F5F0045D939 /* PeerMediaCollectionControllerNode.swift in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, @@ -1323,6 +1371,7 @@ D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */, D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */, D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */, + D07CFF7B1DCA24BF00761F81 /* ChatListNodeEntries.swift in Sources */, D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */, D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */, D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */, @@ -1373,6 +1422,7 @@ D0B843D91DAAAA0C005F29E1 /* PeerInfoPeerItem.swift in Sources */, D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */, D0DE77271D932627002B8809 /* ChatHistoryNode.swift in Sources */, + D07CFF7D1DCA273400761F81 /* ChatListViewTransition.swift in Sources */, D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */, D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */, D0DE772B1D932E16002B8809 /* PeerMediaCollectionModeSelectionNode.swift in Sources */, @@ -1384,6 +1434,7 @@ D0F69DF11D6B8A6C0046BCD6 /* AuthorizationController.swift in Sources */, D0E7A1BF1D8C24B900C37A6F /* ChatHistoryViewForLocation.swift in Sources */, D0F69E891D6B8C850046BCD6 /* FastBlur.m in Sources */, + D07CFF761DCA224100761F81 /* PeerSelectionControllerNode.swift in Sources */, D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */, D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */, D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */, @@ -1418,6 +1469,7 @@ D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */, D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */, D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */, + D07CFF741DCA207200761F81 /* PeerSelectionController.swift in Sources */, D0EE971A1D88BCA0006C18E1 /* ChatInfo.swift in Sources */, D0F69DE31D6B8A420046BCD6 /* ListControllerItem.swift in Sources */, D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */, diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 0d29202b48..f098408c0c 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -35,6 +35,9 @@ public class ChatController: ViewController { private var controllerInteraction: ChatControllerInteraction? private var interfaceInteraction: ChatPanelInterfaceInteraction? + private let controllerNavigationDisposable = MetaDisposable() + private let sentMessageEventsDisposable = MetaDisposable() + public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId @@ -164,7 +167,7 @@ public class ChatController: ViewController { }, sendSticker: { [weak self] file in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) - enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: "", replyMessageId: nil, media: file).start() + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", media: file, replyToMessageId: nil)]).start() } }) @@ -178,7 +181,7 @@ public class ChatController: ViewController { chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) - self.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0 }) + self.updateChatPresentationInterfaceState(animated: false, interactive: false, { return $0 }) self.peerView.set(account.viewTracker.peerView(peerId)) @@ -207,6 +210,8 @@ public class ChatController: ViewController { self.navigationActionDisposable.dispose() self.galleryHiddenMesageAndMediaDisposable.dispose() self.peerDisposable.dispose() + self.controllerNavigationDisposable.dispose() + self.sentMessageEventsDisposable.dispose() } var chatDisplayNode: ChatControllerNode { @@ -218,7 +223,19 @@ public class ChatController: ViewController { override public func loadDisplayNode() { self.displayNode = ChatControllerNode(account: self.account, peerId: self.peerId, messageId: self.messageId, controllerInteraction: self.controllerInteraction!) - self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyReady.get(), self._peerReady.get()) |> map { $0 && $1 }) + 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 }) }) + } + } + } + + self.ready.set(combineLatest(self.chatDisplayNode.historyNode.historyReady.get(), self._peerReady.get(), initialData) |> map { historyReady, peerReady, _ in + return historyReady && peerReady + }) self.chatDisplayNode.historyNode.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { @@ -282,7 +299,7 @@ public class ChatController: ViewController { stationaryItemRange = (maxInsertedItem + 1, Int.max) } - mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange), updateSizeAndInsets) + mappedTransition = (ChatHistoryListViewTransition(historyView: transition.historyView, deleteItems: deleteItems, insertItems: insertItems, updateItems: transition.updateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: transition.initialData), updateSizeAndInsets) }) if let mappedTransition = mappedTransition { @@ -309,7 +326,7 @@ public class ChatController: ViewController { let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) - enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: "", replyMessageId: nil, media: media).start() + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", media: media, replyToMessageId: nil)]).start() } } controller.location = { [weak strongSelf] in @@ -356,8 +373,48 @@ public class ChatController: ViewController { } }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { - let controller = ShareRecipientsActionSheetController() - strongSelf.present(controller, in: .window) + //let controller = ShareRecipientsActionSheetController() + //strongSelf.present(controller, in: .window) + + if let forwardMessageIdsSet = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds { + let forwardMessageIds = Array(forwardMessageIdsSet).sorted() + + let controller = PeerSelectionController(account: strongSelf.account) + controller.peerSelected = { [weak controller] peerId in + if let strongSelf = self, let strongController = controller { + if peerId == strongSelf.peerId { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedForwardMessageIds(forwardMessageIds).withoutSelectionState() }) }) + strongController.dismiss() + } else { + (strongSelf.account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { currentState in + if let currentState = currentState as? ChatInterfaceState { + return currentState.withUpdatedForwardMessageIds(forwardMessageIds) + } else { + return ChatInterfaceState().withUpdatedForwardMessageIds(forwardMessageIds) + } + return currentState + }) + }) |> deliverOnMainQueue).start(completed: { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) }) + + let ready = ValuePromise() + + strongSelf.controllerNavigationDisposable.set((ready.get() |> take(1) |> deliverOnMainQueue).start(next: { _ in + if let strongController = controller { + strongController.dismiss() + } + })) + + (strongSelf.navigationController as? NavigationController)?.replaceTopController(ChatController(account: strongSelf.account, peerId: peerId), animated: false, ready: ready) + } + }) + } + } + } + strongSelf.present(controller, in: .window) + } } }, updateTextInputState: { [weak self] textInputState in if let strongSelf = self { @@ -373,6 +430,10 @@ public class ChatController: ViewController { self.chatDisplayNode.interfaceInteraction = interfaceInteraction self.displayNodeDidLoad() + + self.sentMessageEventsDisposable.set(self.account.pendingMessageManager.deliveredMessageEvents(peerId: self.peerId).start(next: { _ in + serviceSoundManager.playMessageDeliveredSound() + })) } override public func viewWillAppear(_ animated: Bool) { @@ -388,6 +449,18 @@ public class ChatController: ViewController { self.chatDisplayNode.loadInputPanels() } + override public func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + let peerId = self.peerId + let interfaceState = self.presentationInterfaceState.interfaceState + self.account.postbox.modify({ modifier -> Void in + modifier.updatePeerChatInterfaceState(peerId, update: { _ in + return interfaceState + }) + }).start() + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index d8358114b5..438d5e6616 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -104,16 +104,28 @@ class ChatControllerNode: ASDisplayNode { } let text = textInputPanelNode.text - strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in - if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { - strongSelf.ignoreUpdateHeight = true - textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil) }) - strongSelf.ignoreUpdateHeight = false + if !text.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { + strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in + if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { + strongSelf.ignoreUpdateHeight = true + textInputPanelNode.text = "" + strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil) }) + strongSelf.ignoreUpdateHeight = false + } + }) + + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, media: nil, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId)) } - }) - - let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId).start() + if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { + for id in forwardMessageIds { + messages.append(.forward(source: id)) + } + } + + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start() + } } } @@ -240,7 +252,11 @@ class ChatControllerNode: ASDisplayNode { accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) }) + if let _ = accessoryPanelNode as? ReplyAccessoryPanelNode { + strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) }) + } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { + strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedForwardMessageIds(nil) }) + } } } @@ -439,8 +455,11 @@ class ChatControllerNode: ASDisplayNode { var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState self.chatPresentationInterfaceState = chatPresentationInterfaceState + let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { - textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState + textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.inputState, keepSendButtonEnabled: keepSendButtonEnabled, animated: animated) + } else { + textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, animated: animated) } let layoutTransition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.4, curve: .spring) : .immediate diff --git a/TelegramUI/ChatHistoryGridNode.swift b/TelegramUI/ChatHistoryGridNode.swift index f12b08791a..97295460c1 100644 --- a/TelegramUI/ChatHistoryGridNode.swift +++ b/TelegramUI/ChatHistoryGridNode.swift @@ -149,7 +149,7 @@ public final class ChatHistoryGridNode: GridNode, ChatHistoryNode { } } return .complete() - case let .HistoryView(view, type, scrollPosition): + case let .HistoryView(view, type, scrollPosition, _): let reason: ChatHistoryViewTransitionReason var prepareOnMainQueue = false switch type { @@ -172,7 +172,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) |> 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) |> 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 88bea0c2fb..574dc17c58 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -21,8 +21,8 @@ enum ChatHistoryViewUpdateType { } enum ChatHistoryViewUpdate { - case Loading - case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?) + case Loading(initialData: InitialMessageHistoryData?) + case HistoryView(view: MessageHistoryView, type: ChatHistoryViewUpdateType, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?) } struct ChatHistoryView { @@ -59,6 +59,7 @@ struct ChatHistoryViewTransition { let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? + let initialData: InitialMessageHistoryData? } struct ChatHistoryListViewTransition { @@ -69,6 +70,7 @@ struct ChatHistoryListViewTransition { let options: ListViewDeleteAndInsertOptions let scrollToItem: ListViewScrollToItem? let stationaryItemRange: (Int, Int)? + let initialData: InitialMessageHistoryData? } private func maxIncomingMessageIdForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageId? { @@ -121,7 +123,7 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } 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) + 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) } private final class ChatHistoryTransactionOpaqueState { @@ -153,6 +155,12 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { public let historyReady = Promise() private var didSetHistoryReady = false + private let _initialData = Promise() + private var didSetInitialData = false + public var initialData: Signal { + return self._initialData.get() + } + private let maxVisibleIncomingMessageId = ValuePromise() let canReadHistory = ValuePromise() @@ -191,7 +199,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { |> mapToSignal { location in return chatHistoryViewForLocation(location, account: account, peerId: peerId, fixedCombinedReadState: fixedCombinedReadState.with { $0 }, tagMask: tagMask) |> beforeNext { viewUpdate in switch viewUpdate { - case let .HistoryView(view, _, _): + case let .HistoryView(view, _, _, _): let _ = fixedCombinedReadState.swap(view.combinedReadState) default: break @@ -202,10 +210,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { let previousView = Atomic(value: nil) let historyViewTransition = historyViewUpdate |> mapToQueue { [weak self] update -> Signal in + let initialData: InitialMessageHistoryData? switch update { - case .Loading: + case let .Loading(data): + initialData = data Queue.mainQueue().async { [weak self] in if let strongSelf = self { + if !strongSelf.didSetInitialData { + strongSelf.didSetInitialData = true + strongSelf._initialData.set(.single(data)) + } if !strongSelf.didSetHistoryReady { strongSelf.didSetHistoryReady = true strongSelf.historyReady.set(.single(true)) @@ -213,7 +227,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } return .complete() - case let .HistoryView(view, type, scrollPosition): + case let .HistoryView(view, type, scrollPosition, data): + initialData = data let reason: ChatHistoryViewTransitionReason var prepareOnMainQueue = false switch type { @@ -236,7 +251,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) |> 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) |> map({ mappedChatHistoryViewListTransition(account: account, peerId: peerId, controllerInteraction: controllerInteraction, mode: mode, transition: $0) }) |> runOn(prepareOnMainQueue ? Queue.mainQueue() : messageViewQueue) } } @@ -344,6 +359,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { + if !strongSelf.didSetInitialData { + strongSelf.didSetInitialData = true + strongSelf._initialData.set(.single(transition.initialData)) + } if !strongSelf.didSetHistoryReady { strongSelf.didSetHistoryReady = true strongSelf.historyReady.set(.single(true)) @@ -374,7 +393,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { } } } - + if !strongSelf.didSetInitialData { + strongSelf.didSetInitialData = true + strongSelf._initialData.set(.single(transition.initialData)) + } if !strongSelf.didSetHistoryReady { strongSelf.didSetHistoryReady = true strongSelf.historyReady.set(.single(true)) diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 3148578f6d..33d43ccc7f 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -9,15 +9,15 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun case let .Initial(count): var preloaded = false var fadeIn = false - let signal: Signal<(MessageHistoryView, ViewUpdateType), NoError> + let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError> if let tagMask = tagMask { signal = account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: MessageIndex.upperBound(peerId: peerId), count: count, anchorIndex: MessageIndex.upperBound(peerId: peerId), fixedCombinedReadState: nil, tagMask: tagMask) } else { signal = account.viewTracker.aroundUnreadMessageHistoryViewForPeerId(peerId, count: count, tagMask: tagMask) } - return signal |> map { view, updateType -> ChatHistoryViewUpdate in + return signal |> map { view, updateType, initialData -> ChatHistoryViewUpdate in if preloaded { - return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData) } else { if let maxReadIndex = view.maxReadIndex { var targetIndex = 0 @@ -33,25 +33,25 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun for i in targetIndex ..< maxIndex { if case .HoleEntry = view.entries[i] { fadeIn = true - return .Loading + return .Loading(initialData: initialData) } } } preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex), initialData: initialData) } else { preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil, initialData: initialData) } } } case let .InitialSearch(messageId, count): var preloaded = false var fadeIn = false - return account.viewTracker.aroundIdMessageHistoryViewForPeerId(peerId, count: count, messageId: messageId, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in + 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) + return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData) } else { let anchorIndex = view.anchorIndex @@ -68,19 +68,19 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun for i in targetIndex ..< maxIndex { if case .HoleEntry = view.entries[i] { fadeIn = true - return .Loading + return .Loading(initialData: initialData) } } } 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)) + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Index(index: anchorIndex, position: .Center(.Bottom), directionHint: .Down, animated: false), initialData: initialData) } } case let .Navigation(index, anchorIndex): var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in + return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let genericType: ViewUpdateType if first { first = false @@ -88,13 +88,13 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, initialData: initialData) } case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated): let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up let chatScrollPosition = ChatHistoryViewScrollPosition.Index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) var first = true - return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType -> ChatHistoryViewUpdate in + return account.viewTracker.aroundMessageHistoryViewForPeerId(peerId, index: index, count: 140, anchorIndex: anchorIndex, fixedCombinedReadState: fixedCombinedReadState, tagMask: tagMask) |> map { view, updateType, initialData -> ChatHistoryViewUpdate in let genericType: ViewUpdateType let scrollPosition: ChatHistoryViewScrollPosition? = first ? chatScrollPosition : nil if first { @@ -103,7 +103,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } else { genericType = updateType } - return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition) + return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: scrollPosition, initialData: initialData) } } } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index 6897deedc4..a5b4386eb3 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -1,15 +1,33 @@ import Foundation import Postbox -struct ChatInterfaceSelectionState: Equatable { +struct ChatInterfaceSelectionState: Coding, Equatable { let selectedIds: Set static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool { return lhs.selectedIds == rhs.selectedIds } + + init(selectedIds: Set) { + self.selectedIds = selectedIds + } + + init(decoder: Decoder) { + if let data = decoder.decodeBytesForKeyNoCopy("i") { + self.selectedIds = Set(MessageId.decodeArrayFromBuffer(data)) + } else { + self.selectedIds = Set() + } + } + + func encode(_ encoder: Encoder) { + let buffer = WriteBuffer() + MessageId.encodeArrayToBuffer(Array(selectedIds), buffer: buffer) + encoder.encodeBytes(buffer, forKey: "i") + } } -struct ChatTextInputState: Equatable { +struct ChatTextInputState: Coding, Equatable { let inputText: String let selectionRange: Range @@ -26,35 +44,123 @@ struct ChatTextInputState: Equatable { self.inputText = inputText self.selectionRange = selectionRange } + + init(decoder: Decoder) { + self.inputText = decoder.decodeStringForKey("t") + self.selectionRange = Int(decoder.decodeInt32ForKey("s0")) ..< Int(decoder.decodeInt32ForKey("s1")) + } + + func encode(_ encoder: Encoder) { + encoder.encodeString(self.inputText, forKey: "t") + encoder.encodeInt32(Int32(self.selectionRange.lowerBound), forKey: "s0") + encoder.encodeInt32(Int32(self.selectionRange.upperBound), forKey: "s1") + } } -final class ChatInterfaceState: Equatable { +final class ChatInterfaceState: PeerChatInterfaceState, Equatable { let inputState: ChatTextInputState let replyMessageId: MessageId? + let forwardMessageIds: [MessageId]? let selectionState: ChatInterfaceSelectionState? + var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { + return nil + } + init() { self.inputState = ChatTextInputState() self.replyMessageId = nil + self.forwardMessageIds = nil self.selectionState = nil } - init(inputState: ChatTextInputState, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) { + init(inputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, selectionState: ChatInterfaceSelectionState?) { self.inputState = inputState self.replyMessageId = replyMessageId + self.forwardMessageIds = forwardMessageIds self.selectionState = selectionState } + init(decoder: Decoder) { + if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { + self.inputState = inputState + } else { + self.inputState = ChatTextInputState() + } + let replyMessageIdPeerId: Int64? = decoder.decodeInt64ForKey("r.p") + let replyMessageIdNamespace: Int32? = decoder.decodeInt32ForKey("r.n") + let replyMessageIdId: Int32? = decoder.decodeInt32ForKey("r.i") + if let replyMessageIdPeerId = replyMessageIdPeerId, let replyMessageIdNamespace = replyMessageIdNamespace, let replyMessageIdId = replyMessageIdId { + self.replyMessageId = MessageId(peerId: PeerId(replyMessageIdPeerId), namespace: replyMessageIdNamespace, id: replyMessageIdId) + } else { + self.replyMessageId = nil + } + if let forwardMessageIdsData = decoder.decodeBytesForKeyNoCopy("fm") { + self.forwardMessageIds = MessageId.decodeArrayFromBuffer(forwardMessageIdsData) + } else { + self.forwardMessageIds = nil + } + if let selectionState = decoder.decodeObjectForKey("ss", decoder: { return ChatInterfaceSelectionState(decoder: $0) }) as? ChatInterfaceSelectionState { + self.selectionState = selectionState + } else { + self.selectionState = nil + } + } + + func encode(_ encoder: Encoder) { + encoder.encodeObject(self.inputState, forKey: "is") + if let replyMessageId = self.replyMessageId { + encoder.encodeInt64(replyMessageId.peerId.toInt64(), forKey: "r.p") + encoder.encodeInt32(replyMessageId.namespace, forKey: "r.n") + encoder.encodeInt32(replyMessageId.id, forKey: "r.i") + } else { + encoder.encodeNil(forKey: "r.p") + encoder.encodeNil(forKey: "r.n") + encoder.encodeNil(forKey: "r.i") + } + if let forwardMessageIds = self.forwardMessageIds { + let buffer = WriteBuffer() + MessageId.encodeArrayToBuffer(forwardMessageIds, buffer: buffer) + encoder.encodeBytes(buffer, forKey: "fm") + } else { + encoder.encodeNil(forKey: "fm") + } + if let selectionState = self.selectionState { + encoder.encodeObject(selectionState, forKey: "ss") + } else { + encoder.encodeNil(forKey: "ss") + } + } + + func isEqual(to: PeerChatInterfaceState) -> Bool { + if let to = to as? ChatInterfaceState, self == to { + return true + } else { + return false + } + } + static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { + if let lhsForwardMessageIds = lhs.forwardMessageIds, let rhsForwardMessageIds = rhs.forwardMessageIds { + if lhsForwardMessageIds != rhsForwardMessageIds { + return false + } + } else if (lhs.forwardMessageIds != nil) != (rhs.forwardMessageIds != nil) { + return false + } return lhs.inputState == rhs.inputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState } func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { - return ChatInterfaceState(inputState: inputState, replyMessageId: self.replyMessageId, selectionState: self.selectionState) + return ChatInterfaceState(inputState: inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState) } func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(inputState: self.inputState, replyMessageId: replyMessageId, selectionState: self.selectionState) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState) + } + + func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState { + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, selectionState: self.selectionState) } func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -63,7 +169,7 @@ final class ChatInterfaceState: Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -76,10 +182,10 @@ final class ChatInterfaceState: Equatable { } else { selectedIds.insert(messageId) } - return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) } func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: nil) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: nil) } } diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index 65ae7174a1..70714f03f3 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -7,7 +7,16 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS return nil } - if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId { + if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds { + if let forwardPanelNode = currentPanel as? ForwardAccessoryPanelNode, forwardPanelNode.messageIds == forwardMessageIds { + forwardPanelNode.interfaceInteraction = interfaceInteraction + return forwardPanelNode + } else { + let panelNode = ForwardAccessoryPanelNode(account: account, messageIds: forwardMessageIds) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } + } else if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId { if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { replyPanelNode.interfaceInteraction = interfaceInteraction return replyPanelNode diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index 13d4972712..25a6a093f2 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -4,201 +4,13 @@ import SwiftSignalKit import Display import TelegramCore -enum ChatListMessageViewPosition: Equatable { - case Tail(count: Int) - case Around(index: MessageIndex, anchorIndex: MessageIndex, scrollPosition: ListViewScrollPosition?) -} - -func ==(lhs: ChatListMessageViewPosition, rhs: ChatListMessageViewPosition) -> Bool { - switch lhs { - case let .Tail(lhsCount): - switch rhs { - case let .Tail(rhsCount) where lhsCount == rhsCount: - return true - default: - return false - } - case let .Around(lhsId, lhsAnchorIndex, lhsScrollPosition): - switch rhs { - case let .Around(rhsId, rhsAnchorIndex, rhsScrollPosition) where lhsId == rhsId && lhsAnchorIndex == rhsAnchorIndex && lhsScrollPosition == rhsScrollPosition: - return true - default: - return false - } - } -} - -private enum ChatListControllerEntryId: Hashable, CustomStringConvertible { - case Search - case Hole(Int64) - case PeerId(Int64) - - var hashValue: Int { - switch self { - case .Search: - return 0 - case let .Hole(peerId): - return peerId.hashValue - case let .PeerId(peerId): - return peerId.hashValue - } - } - - var description: String { - switch self { - case .Search: - return "search" - case let .Hole(value): - return "hole(\(value))" - case let .PeerId(value): - return "peerId(\(value))" - } - } -} - -private func <(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) -> Bool { - return lhs.hashValue < rhs.hashValue -} - -private func ==(lhs: ChatListControllerEntryId, rhs: ChatListControllerEntryId) -> Bool { - switch lhs { - case .Search: - switch rhs { - case .Search: - return true - default: - return false - } - case let .Hole(lhsId): - switch rhs { - case .Hole(lhsId): - return true - default: - return false - } - case let .PeerId(lhsId): - switch rhs { - case let .PeerId(rhsId): - return lhsId == rhsId - default: - return false - } - } -} - -private enum ChatListControllerEntry: Comparable, Identifiable { - case SearchEntry - case MessageEntry(Message, CombinedPeerReadState?, PeerNotificationSettings?) - case HoleEntry(ChatListHole) - case Nothing(MessageIndex) - - var index: MessageIndex { - switch self { - case .SearchEntry: - return MessageIndex.absoluteUpperBound() - case let .MessageEntry(message, _, _): - return MessageIndex(message) - case let .HoleEntry(hole): - return hole.index - case let .Nothing(index): - return index - } - } - - var stableId: ChatListControllerEntryId { - switch self { - case .SearchEntry: - return .Search - case let .MessageEntry(message, _, _): - return .PeerId(message.id.peerId.toInt64()) - case let .HoleEntry(hole): - return .Hole(Int64(hole.index.id.id)) - case let .Nothing(index): - return .PeerId(index.id.peerId.toInt64()) - } - } -} - -private func <(lhs: ChatListControllerEntry, rhs: ChatListControllerEntry) -> Bool { - return lhs.index < rhs.index -} - -private func ==(lhs: ChatListControllerEntry, rhs: ChatListControllerEntry) -> Bool { - switch lhs { - case .SearchEntry: - switch rhs { - case .SearchEntry: - return true - default: - return false - } - case let .MessageEntry(lhsMessage, lhsUnreadCount, lhsNotificationSettings): - switch rhs { - case let .MessageEntry(rhsMessage, rhsUnreadCount, rhsNotificationSettings): - if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags || lhsUnreadCount != rhsUnreadCount { - return false - } - if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { - if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { - return false - } - } else if (lhsNotificationSettings != nil) != (rhsNotificationSettings != nil) { - return false - } - return true - default: - break - } - case let .HoleEntry(lhsHole): - switch rhs { - case let .HoleEntry(rhsHole): - return lhsHole == rhsHole - default: - return false - } - case let .Nothing(lhsIndex): - switch rhs { - case let .Nothing(rhsIndex): - return lhsIndex == rhsIndex - default: - return false - } - } - return false -} - -extension ChatListEntry: Identifiable { - public var stableId: Int64 { - return self.index.id.peerId.toInt64() - } -} - -private final class ChatListOpaqueTransactionState { - let chatListViewAndEntries: (ChatListView, [ChatListControllerEntry]) - - init(chatListViewAndEntries: (ChatListView, [ChatListControllerEntry])) { - self.chatListViewAndEntries = chatListViewAndEntries - } -} - public class ChatListController: ViewController { - let account: Account - - private var chatListViewAndEntries: (ChatListView, [ChatListControllerEntry])? - - var chatListPosition: ChatListMessageViewPosition? - let chatListDisposable: MetaDisposable = MetaDisposable() - - let messageViewQueue = Queue() - let messageViewTransactionQueue = ListViewTransactionQueue() - var settingView = false + private let account: Account let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable() - var chatListDisplayNode: ChatListControllerNode { - get { - return super.displayNode as! ChatListControllerNode - } + private var chatListDisplayNode: ChatListControllerNode { + return super.displayNode as! ChatListControllerNode } public init(account: Account) { @@ -216,15 +28,9 @@ public class ChatListController: ViewController { self.scrollToTop = { [weak self] in if let strongSelf = self { - if let (view, _) = strongSelf.chatListViewAndEntries, view.laterIndex == nil { - strongSelf.chatListDisplayNode.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } else { - strongSelf.setMessageViewPosition(.Around(index: MessageIndex.absoluteUpperBound(), anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: .Top), hint: "later", force: true) - } + strongSelf.chatListDisplayNode.chatListNode.scrollToLatest() } } - - self.setMessageViewPosition(.Tail(count: 50), hint: "initial", force: false) } required public init(coder aDecoder: NSCoder) { @@ -232,31 +38,29 @@ public class ChatListController: ViewController { } deinit { - self.chatListDisposable.dispose() self.openMessageFromSearchDisposable.dispose() } override public func loadDisplayNode() { self.displayNode = ChatListControllerNode(account: self.account) - self.chatListDisplayNode.listView.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in - if let strongSelf = self, !strongSelf.settingView { - if let range = range.loadedRange, let (view, _) = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListViewAndEntries { - if range.firstIndex < 5 && view.laterIndex != nil { - strongSelf.setMessageViewPosition(.Around(index: view.entries[view.entries.count - 1].index, anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: nil), hint: "later", force: false) - } else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil { - strongSelf.setMessageViewPosition(.Around(index: view.entries[0].index, anchorIndex: MessageIndex.absoluteUpperBound(), scrollPosition: nil), hint: "earlier", force: false) - } - } - } - } - self.chatListDisplayNode.navigationBar = self.navigationBar self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in self?.deactivateSearch() } + self.chatListDisplayNode.chatListNode.activateSearch = { [weak self] in + self?.activateSearch() + } + + self.chatListDisplayNode.chatListNode.peerSelected = { [weak self] peerId in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + strongSelf.chatListDisplayNode.chatListNode.clearHighlightAnimated(true) + } + } + self.chatListDisplayNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in if let strongSelf = self { let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in @@ -283,56 +87,6 @@ public class ChatListController: ViewController { self.displayNodeDidLoad() } - private func setMessageViewPosition(_ position: ChatListMessageViewPosition, hint: String, force: Bool) { - if self.chatListPosition == nil || self.chatListPosition! != position || force { - let signal: Signal<(ChatListView, ViewUpdateType), NoError> - self.chatListPosition = position - var scrollPosition: (MessageIndex, ListViewScrollPosition, ListViewScrollToItemDirectionHint)? - switch position { - case let .Tail(count): - signal = self.account.postbox.tailChatListView(count) - case let .Around(index, _, position): - trace("request around \(index.id.id) \(hint)") - signal = self.account.postbox.aroundChatListView(index, count: 80) - if let position = position { - var directionHint: ListViewScrollToItemDirectionHint = .Up - if let visibleItemRange = self.chatListDisplayNode.listView.displayedItemRange.loadedRange, let (_, entries) = self.chatListViewAndEntries { - if visibleItemRange.firstIndex >= 0 && visibleItemRange.firstIndex < entries.count { - if entries[visibleItemRange.firstIndex].index < index { - directionHint = .Up - } else { - directionHint = .Down - } - } - } - scrollPosition = (index, position, directionHint) - } - } - - var firstTime = true - chatListDisposable.set(( - signal |> deliverOnMainQueue - ).start(next: {[weak self] (view, updateType) in - if let strongSelf = self { - let animated: Bool - switch updateType { - case .Generic: - animated = !firstTime - case .FillHole: - animated = false - case .InitialUnread: - animated = false - case .UpdateVisible: - animated = false - } - - strongSelf.setPeerView(view, firstTime: strongSelf.chatListViewAndEntries == nil, scrollPosition: firstTime ? scrollPosition : nil, animated: animated) - firstTime = false - } - })) - } - } - override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) } @@ -341,179 +95,6 @@ public class ChatListController: ViewController { super.viewDidDisappear(animated) } - private func chatListControllerEntries(_ view: ChatListView) -> [ChatListControllerEntry] { - var result: [ChatListControllerEntry] = [] - for entry in view.entries { - switch entry { - case let .MessageEntry(message, combinedReadState, notificationSettings): - result.append(.MessageEntry(message, combinedReadState, notificationSettings)) - case let .HoleEntry(hole): - result.append(.HoleEntry(hole)) - case let .Nothing(index): - result.append(.Nothing(index)) - } - } - if view.laterIndex == nil { - result.append(.SearchEntry) - } - return result - } - - private func setPeerView(_ view: ChatListView, firstTime: Bool, scrollPosition: (MessageIndex, ListViewScrollPosition, ListViewScrollToItemDirectionHint)?, animated: Bool) { - self.messageViewTransactionQueue.addTransaction { [weak self] completed in - if let strongSelf = self { - strongSelf.settingView = true - let currentEntries = strongSelf.chatListViewAndEntries?.1 ?? [] - let viewEntries = strongSelf.chatListControllerEntries(view) - - strongSelf.messageViewQueue.async { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: currentEntries, rightList: viewEntries) - //let (deleteIndices, indicesAndItems) = mergeListsStable(leftList: currentEntries, rightList: viewEntries) - //let updateIndices: [(Int, ChatListControllerEntry)] = [] - - Queue.mainQueue().async { - var adjustedDeleteIndices: [ListViewDeleteItem] = [] - let previousCount = currentEntries.count - if deleteIndices.count != 0 { - for index in deleteIndices { - adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) - } - } - - let updatedCount = viewEntries.count - - var maxAnimatedInsertionIndex = -1 - if animated { - for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { - let adjustedIndex = updatedCount - 1 - index - if adjustedIndex == maxAnimatedInsertionIndex + 1 { - maxAnimatedInsertionIndex += 1 - } - } - } - - var adjustedIndicesAndItems: [ListViewInsertItem] = [] - for (index, entry, previousIndex) in indicesAndItems { - let adjustedIndex = updatedCount - 1 - index - - var adjustedPreviousIndex: Int? - if let previousIndex = previousIndex { - adjustedPreviousIndex = previousCount - 1 - previousIndex - } - - var directionHint: ListViewItemOperationDirectionHint? - if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex { - directionHint = .Down - } - - switch entry { - case .SearchEntry: - adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { [weak self] in - self?.activateSearch() - }), directionHint: directionHint)) - case let .MessageEntry(message, combinedReadState, notificationSettings): - adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListItem(account: strongSelf.account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { [weak self] message in - if let strongSelf = self { - strongSelf.entrySelected(entry) - strongSelf.chatListDisplayNode.listView.clearHighlightAnimated(true) - } - }), directionHint: directionHint)) - case .HoleEntry: - adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListHoleItem(), directionHint: directionHint)) - case .Nothing: - adjustedIndicesAndItems.append(ListViewInsertItem(index: updatedCount - 1 - index, previousIndex: adjustedPreviousIndex, item: ChatListEmptyItem(), directionHint: directionHint)) - } - } - - var adjustedUpdateItems: [ListViewUpdateItem] = [] - for (index, entry, previousIndex) in updateIndices { - let adjustedIndex = updatedCount - 1 - index - let adjustedPreviousIndex = previousCount - 1 - previousIndex - - let directionHint: ListViewItemOperationDirectionHint? = nil - - switch entry { - case .SearchEntry: - adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { [weak self] in - self?.activateSearch() - }), directionHint: directionHint)) - case let .MessageEntry(message, combinedReadState, notificationSettings): - adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListItem(account: strongSelf.account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { [weak self] message in - if let strongSelf = self { - strongSelf.entrySelected(entry) - strongSelf.chatListDisplayNode.listView.clearHighlightAnimated(true) - } - }), directionHint: directionHint)) - case .HoleEntry: - adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListHoleItem(), directionHint: directionHint)) - case .Nothing: - adjustedUpdateItems.append(ListViewUpdateItem(index: adjustedIndex, previousIndex: adjustedPreviousIndex, item: ChatListEmptyItem(), directionHint: directionHint)) - } - } - - if !adjustedDeleteIndices.isEmpty || !adjustedIndicesAndItems.isEmpty || !adjustedUpdateItems.isEmpty || scrollPosition != nil { - var options: ListViewDeleteAndInsertOptions = [] - if firstTime { - } else { - let _ = options.insert(.AnimateAlpha) - - if animated { - let _ = options.insert(.AnimateInsertion) - } - } - - var scrollToItem: ListViewScrollToItem? - if let (itemIndex, itemPosition, directionHint) = scrollPosition { - var index = viewEntries.count - 1 - for entry in viewEntries { - if entry.index >= itemIndex { - scrollToItem = ListViewScrollToItem(index: index, position: itemPosition, animated: true, curve: .Default, directionHint: directionHint) - break - } - index -= 1 - } - - if scrollToItem == nil { - var index = 0 - for entry in viewEntries.reversed() { - if entry.index < itemIndex { - scrollToItem = ListViewScrollToItem(index: index, position: itemPosition, animated: true, curve: .Default, directionHint: directionHint) - break - } - index += 1 - } - } - } - - strongSelf.chatListDisplayNode.listView.transaction(deleteIndices: adjustedDeleteIndices, insertIndicesAndItems: adjustedIndicesAndItems, updateIndicesAndItems: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, updateOpaqueState: ChatListOpaqueTransactionState(chatListViewAndEntries: (view, viewEntries)), completion: { [weak self] _ in - if let strongSelf = self { - strongSelf.ready.set(single(true, NoError.self)) - strongSelf.settingView = false - completed() - } - }) - } else { - strongSelf.ready.set(single(true, NoError.self)) - strongSelf.settingView = false - completed() - } - - strongSelf.chatListViewAndEntries = (view, viewEntries) - } - } - } else { - completed() - } - } - } - - private func entrySelected(_ entry: ChatListControllerEntry) { - if case let .MessageEntry(message, _, _) = entry { - //(self.navigationController as? NavigationController)?.pushViewController(PeerMediaCollectionController(account: self.account, peerId: message.id.peerId)) - (self.navigationController as? NavigationController)?.pushViewController(ChatController(account: self.account, peerId: message.id.peerId)) - } - } - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index 3d4767baa8..c563865b6d 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -7,7 +7,7 @@ import TelegramCore class ChatListControllerNode: ASDisplayNode { private let account: Account - let listView: ListView + let chatListNode: ChatListNode var navigationBar: NavigationBar? private var searchDisplayController: SearchDisplayController? @@ -20,13 +20,13 @@ class ChatListControllerNode: ASDisplayNode { init(account: Account) { self.account = account - self.listView = ListView() + self.chatListNode = ChatListNode(account: account, mode: .chatList) super.init(viewBlock: { return UITracingLayerView() }, didLoad: nil) - self.addSubnode(self.listView) + self.addSubnode(self.chatListNode) } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { @@ -35,8 +35,8 @@ class ChatListControllerNode: ASDisplayNode { var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) - self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) var duration: Double = 0.0 var curve: UInt = 0 @@ -63,7 +63,7 @@ class ChatListControllerNode: ASDisplayNode { let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) - self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) if let searchDisplayController = self.searchDisplayController { searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) @@ -76,7 +76,7 @@ class ChatListControllerNode: ASDisplayNode { } var maybePlaceholderNode: SearchBarPlaceholderNode? - self.listView.forEachItemNode { node in + self.chatListNode.forEachItemNode { node in if let node = node as? ChatListSearchItemNode { maybePlaceholderNode = node.searchBarNode } @@ -111,7 +111,7 @@ class ChatListControllerNode: ASDisplayNode { func deactivateSearch() { if let searchDisplayController = self.searchDisplayController { var maybePlaceholderNode: SearchBarPlaceholderNode? - self.listView.forEachItemNode { node in + self.chatListNode.forEachItemNode { node in if let node = node as? ChatListSearchItemNode { maybePlaceholderNode = node.searchBarNode } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 5c665c2da0..a777491cea 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -476,7 +476,7 @@ class ChatListItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift new file mode 100644 index 0000000000..b1160dc267 --- /dev/null +++ b/TelegramUI/ChatListNode.swift @@ -0,0 +1,292 @@ +import Foundation +import Display +import AsyncDisplayKit +import SwiftSignalKit +import TelegramCore +import Postbox + +enum ChatListNodeMode { + case chatList + case peers +} + +struct ChatListNodeListViewTransition { + let chatListView: ChatListNodeView + let deleteItems: [ListViewDeleteItem] + let insertItems: [ListViewInsertItem] + let updateItems: [ListViewUpdateItem] + let options: ListViewDeleteAndInsertOptions + let scrollToItem: ListViewScrollToItem? + let stationaryItemRange: (Int, Int)? +} + +final class ChatListNodeInteraction { + let activateSearch: () -> Void + let peerSelected: (PeerId) -> Void + + init(activateSearch: @escaping () -> Void, peerSelected: @escaping (PeerId) -> Void) { + self.activateSearch = activateSearch + self.peerSelected = peerSelected + } +} + +private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionInsertEntry]) -> [ListViewInsertItem] { + return entries.map { entry -> ListViewInsertItem in + switch entry.entry { + case .SearchEntry: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { + nodeInteraction.activateSearch() + }), directionHint: entry.directionHint) + case let .MessageEntry(message, combinedReadState, notificationSettings): + switch mode { + case .chatList: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(account: account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { _ in + nodeInteraction.peerSelected(message.id.peerId) + }), directionHint: entry.directionHint) + case .peers: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(account: account, peer: message.peers[message.id.peerId], presence: nil, index: nil, action: { _ in + nodeInteraction.peerSelected(message.id.peerId) + }), directionHint: entry.directionHint) + } + case .HoleEntry: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + case .Nothing: + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyItem(), directionHint: entry.directionHint) + } + } +} + +private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, entries: [ChatListNodeViewTransitionUpdateEntry]) -> [ListViewUpdateItem] { + return entries.map { entry -> ListViewUpdateItem in + switch entry.entry { + case .SearchEntry: + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListSearchItem(placeholder: "Search for messages or users", activate: { + nodeInteraction.activateSearch() + }), directionHint: entry.directionHint) + case let .MessageEntry(message, combinedReadState, notificationSettings): + switch mode { + case .chatList: + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListItem(account: account, message: message, combinedReadState: combinedReadState, notificationSettings: notificationSettings, action: { _ in + nodeInteraction.peerSelected(message.id.peerId) + }), directionHint: entry.directionHint) + case .peers: + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(account: account, peer: message.peers[message.id.peerId], presence: nil, index: nil, action: { _ in + nodeInteraction.peerSelected(message.id.peerId) + }), directionHint: entry.directionHint) + } + case .HoleEntry: + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListHoleItem(), directionHint: entry.directionHint) + case .Nothing: + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatListEmptyItem(), directionHint: entry.directionHint) + } + } +} + +private func mappedChatListNodeViewListTransition(account: Account, nodeInteraction: ChatListNodeInteraction, mode: ChatListNodeMode, transition: ChatListNodeViewTransition) -> ChatListNodeListViewTransition { + return ChatListNodeListViewTransition(chatListView: transition.chatListView, deleteItems: transition.deleteItems, insertItems: mappedInsertEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.insertEntries), updateItems: mappedUpdateEntries(account: account, nodeInteraction: nodeInteraction, mode: mode, entries: transition.updateEntries), options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange) +} + +private final class ChatListOpaqueTransactionState { + let chatListView: ChatListNodeView + + init(chatListView: ChatListNodeView) { + self.chatListView = chatListView + } +} + +final class ChatListNode: ListView { + private let _ready = ValuePromise() + private var didSetReady = false + var ready: Signal { + return _ready.get() + } + + var peerSelected: ((PeerId) -> Void)? + var activateSearch: (() -> Void)? + + private let viewProcessingQueue = Queue() + private var chatListView: ChatListNodeView? + + private var dequeuedInitialTransitionOnLayout = false + private var enqueuedTransition: (ChatListNodeListViewTransition, () -> Void)? + + private var currentLocation: ChatListNodeLocation? + private let chatListLocation = ValuePromise() + private let chatListDisposable = MetaDisposable() + + init(account: Account, mode: ChatListNodeMode) { + super.init() + + let nodeInteraction = ChatListNodeInteraction(activateSearch: { [weak self] in + if let strongSelf = self, let activateSearch = strongSelf.activateSearch { + activateSearch() + } + }, peerSelected: { [weak self] peerId in + if let strongSelf = self, let peerSelected = strongSelf.peerSelected { + peerSelected(peerId) + } + }) + + let viewProcessingQueue = self.viewProcessingQueue + + let chastListViewUpdate = self.chatListLocation.get() + |> distinctUntilChanged + |> mapToSignal { location in + return chatListViewForLocation(location, account: account) + } + + let previousView = Atomic(value: nil) + + let chatListNodeViewTransition = chastListViewUpdate |> mapToQueue { [weak self] update -> Signal in + let processedView = ChatListNodeView(originalView: update.view, filteredEntries: chatListNodeEntriesForView(update.view)) + let previous = previousView.swap(processedView) + + let reason: ChatListNodeViewTransitionReason + var prepareOnMainQueue = false + + var previousWasEmptyOrSingleHole = false + if let previous = previous { + if previous.filteredEntries.count == 1 { + if case .HoleEntry = previous.filteredEntries[0] { + previousWasEmptyOrSingleHole = true + } + } + } else { + previousWasEmptyOrSingleHole = true + } + + if previousWasEmptyOrSingleHole { + reason = .initial + if previous == nil { + prepareOnMainQueue = true + } + } else { + switch update.type { + case .InitialUnread: + reason = .initial + prepareOnMainQueue = true + case .Generic: + reason = .interactiveChanges + case .UpdateVisible: + reason = .reload + case .FillHole: + reason = .reload + } + } + + return preparedChatListNodeViewTransition(from: previous, to: processedView, reason: reason, account: account, scrollPosition: update.scrollPosition) + |> map({ mappedChatListNodeViewListTransition(account: account, nodeInteraction: nodeInteraction, mode: mode, transition: $0) }) + |> runOn(prepareOnMainQueue ? Queue.mainQueue() : viewProcessingQueue) + } + + let appliedTransition = chatListNodeViewTransition |> deliverOnMainQueue |> mapToQueue { [weak self] transition -> Signal in + if let strongSelf = self { + return strongSelf.enqueueTransition(transition) + } + return .complete() + } + + self.displayedItemRangeChanged = { [weak self] range, transactionOpaqueState in + if let strongSelf = self, let range = range.loadedRange, let view = (transactionOpaqueState as? ChatListOpaqueTransactionState)?.chatListView.originalView { + var location: ChatListNodeLocation? + if range.firstIndex < 5 && view.laterIndex != nil { + location = .navigation(index: view.entries[view.entries.count - 1].index) + } else if range.firstIndex >= 5 && range.lastIndex >= view.entries.count - 5 && view.earlierIndex != nil { + location = .navigation(index: view.entries[0].index) + } + + if let location = location, location != strongSelf.currentLocation { + strongSelf.currentLocation = location + strongSelf.chatListLocation.set(location) + } + } + } + + self.chatListDisposable.set(appliedTransition.start()) + + let initialLocation: ChatListNodeLocation = .initial(count: 50) + self.currentLocation = initialLocation + self.chatListLocation.set(initialLocation) + } + + deinit { + self.chatListDisposable.dispose() + } + + private func enqueueTransition(_ transition: ChatListNodeListViewTransition) -> Signal { + return Signal { [weak self] subscriber in + if let strongSelf = self { + if let _ = strongSelf.enqueuedTransition { + preconditionFailure() + } + + strongSelf.enqueuedTransition = (transition, { + subscriber.putCompletion() + }) + + if strongSelf.isNodeLoaded { + strongSelf.dequeueTransition() + } else { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + } + } else { + subscriber.putCompletion() + } + + return EmptyDisposable + } |> runOn(Queue.mainQueue()) + } + + private func dequeueTransition() { + if let (transition, completion) = self.enqueuedTransition { + self.enqueuedTransition = nil + + let completion: (ListViewDisplayedItemRange) -> Void = { [weak self] visibleRange in + if let strongSelf = self { + strongSelf.chatListView = transition.chatListView + + /*if let range = visibleRange.loadedRange { + strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index) + + if let visible = visibleRange.visibleRange { + if let messageId = maxIncomingMessageIdForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) { + strongSelf.updateMaxVisibleReadIncomingMessageId(messageId) + } + } + }*/ + + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + + completion() + } + } + + self.transaction(deleteIndices: transition.deleteItems, insertIndicesAndItems: transition.insertItems, updateIndicesAndItems: transition.updateItems, options: transition.options, scrollToItem: transition.scrollToItem, stationaryItemRange: transition.stationaryItemRange, updateOpaqueState: ChatListOpaqueTransactionState(chatListView: transition.chatListView), completion: completion) + } + } + + func updateLayout(transition: ContainedViewLayoutTransition, updateSizeAndInsets: ListViewUpdateSizeAndInsets) { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !self.dequeuedInitialTransitionOnLayout { + self.dequeuedInitialTransitionOnLayout = true + self.dequeueTransition() + } + } + + func scrollToLatest() { + if let view = self.chatListView?.originalView, view.laterIndex == nil { + self.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Default, directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } else { + let location: ChatListNodeLocation = .scroll(index: MessageIndex.absoluteUpperBound(), sourceIndex: MessageIndex.absoluteLowerBound(), scrollPosition: .Top, animated: true) + self.currentLocation = location + self.chatListLocation.set(location) + } + } +} diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift new file mode 100644 index 0000000000..a77b84de42 --- /dev/null +++ b/TelegramUI/ChatListNodeEntries.swift @@ -0,0 +1,160 @@ +import Foundation +import Postbox +import TelegramCore + +enum ChatListNodeEntryId: Hashable, CustomStringConvertible { + case Search + case Hole(Int64) + case PeerId(Int64) + + var hashValue: Int { + switch self { + case .Search: + return 0 + case let .Hole(peerId): + return peerId.hashValue + case let .PeerId(peerId): + return peerId.hashValue + } + } + + var description: String { + switch self { + case .Search: + return "search" + case let .Hole(value): + return "hole(\(value))" + case let .PeerId(value): + return "peerId(\(value))" + } + } + + static func <(lhs: ChatListNodeEntryId, rhs: ChatListNodeEntryId) -> Bool { + return lhs.hashValue < rhs.hashValue + } + + static func ==(lhs: ChatListNodeEntryId, rhs: ChatListNodeEntryId) -> Bool { + switch lhs { + case .Search: + switch rhs { + case .Search: + return true + default: + return false + } + case let .Hole(lhsId): + switch rhs { + case .Hole(lhsId): + return true + default: + return false + } + case let .PeerId(lhsId): + switch rhs { + case let .PeerId(rhsId): + return lhsId == rhsId + default: + return false + } + } + } +} + +enum ChatListNodeEntry: Comparable, Identifiable { + case SearchEntry + case MessageEntry(Message, CombinedPeerReadState?, PeerNotificationSettings?) + case HoleEntry(ChatListHole) + case Nothing(MessageIndex) + + var index: MessageIndex { + switch self { + case .SearchEntry: + return MessageIndex.absoluteUpperBound() + case let .MessageEntry(message, _, _): + return MessageIndex(message) + case let .HoleEntry(hole): + return hole.index + case let .Nothing(index): + return index + } + } + + var stableId: ChatListNodeEntryId { + switch self { + case .SearchEntry: + return .Search + case let .MessageEntry(message, _, _): + return .PeerId(message.id.peerId.toInt64()) + case let .HoleEntry(hole): + return .Hole(Int64(hole.index.id.id)) + case let .Nothing(index): + return .PeerId(index.id.peerId.toInt64()) + } + } + + static func <(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool { + return lhs.index < rhs.index + } + + static func ==(lhs: ChatListNodeEntry, rhs: ChatListNodeEntry) -> Bool { + switch lhs { + case .SearchEntry: + switch rhs { + case .SearchEntry: + return true + default: + return false + } + case let .MessageEntry(lhsMessage, lhsUnreadCount, lhsNotificationSettings): + switch rhs { + case let .MessageEntry(rhsMessage, rhsUnreadCount, rhsNotificationSettings): + if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags || lhsUnreadCount != rhsUnreadCount { + return false + } + if let lhsNotificationSettings = lhsNotificationSettings, let rhsNotificationSettings = rhsNotificationSettings { + if !lhsNotificationSettings.isEqual(to: rhsNotificationSettings) { + return false + } + } else if (lhsNotificationSettings != nil) != (rhsNotificationSettings != nil) { + return false + } + return true + default: + break + } + case let .HoleEntry(lhsHole): + switch rhs { + case let .HoleEntry(rhsHole): + return lhsHole == rhsHole + default: + return false + } + case let .Nothing(lhsIndex): + switch rhs { + case let .Nothing(rhsIndex): + return lhsIndex == rhsIndex + default: + return false + } + } + return false + } +} + +func chatListNodeEntriesForView(_ view: ChatListView) -> [ChatListNodeEntry] { + var result: [ChatListNodeEntry] = [] + for entry in view.entries { + switch entry { + case let .MessageEntry(message, combinedReadState, notificationSettings): + result.append(.MessageEntry(message, combinedReadState, notificationSettings)) + case let .HoleEntry(hole): + result.append(.HoleEntry(hole)) + case let .Nothing(index): + result.append(.Nothing(index)) + } + } + if view.laterIndex == nil { + result.append(.SearchEntry) + } + return result +} diff --git a/TelegramUI/ChatListNodeLocation.swift b/TelegramUI/ChatListNodeLocation.swift new file mode 100644 index 0000000000..06e99a89d0 --- /dev/null +++ b/TelegramUI/ChatListNodeLocation.swift @@ -0,0 +1,69 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +enum ChatListNodeLocation: Equatable { + case initial(count: Int) + case navigation(index: MessageIndex) + case scroll(index: MessageIndex, sourceIndex: MessageIndex, scrollPosition: ListViewScrollPosition, animated: Bool) + + static func ==(lhs: ChatListNodeLocation, rhs: ChatListNodeLocation) -> Bool { + switch lhs { + case let .navigation(index): + switch rhs { + case .navigation(index): + return true + default: + return false + } + default: + return false + } + } +} + +struct ChatListNodeViewUpdate { + let view: ChatListView + let type: ViewUpdateType + let scrollPosition: ChatListNodeViewScrollPosition? +} + +func chatListViewForLocation(_ location: ChatListNodeLocation, account: Account) -> Signal { + switch location { + case let .initial(count): + let signal: Signal<(ChatListView, ViewUpdateType), NoError> + signal = account.postbox.tailChatListView(count) + return signal |> map { view, updateType -> ChatListNodeViewUpdate in + return ChatListNodeViewUpdate(view: view, type: updateType, scrollPosition: nil) + } + case let .navigation(index): + var first = true + return account.postbox.aroundChatListView(index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in + let genericType: ViewUpdateType + if first { + first = false + genericType = ViewUpdateType.UpdateVisible + } else { + genericType = updateType + } + return ChatListNodeViewUpdate(view: view, type: genericType, scrollPosition: nil) + } + case let .scroll(index, sourceIndex, scrollPosition, animated): + let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up + let chatScrollPosition: ChatListNodeViewScrollPosition = .index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated) + var first = true + return account.postbox.aroundChatListView(index, count: 80) |> map { view, updateType -> ChatListNodeViewUpdate in + let genericType: ViewUpdateType + let scrollPosition: ChatListNodeViewScrollPosition? = first ? chatScrollPosition : nil + if first { + first = false + genericType = ViewUpdateType.UpdateVisible + } else { + genericType = updateType + } + return ChatListNodeViewUpdate(view: view, type: genericType, scrollPosition: scrollPosition) + } + } +} diff --git a/TelegramUI/ChatListViewTransition.swift b/TelegramUI/ChatListViewTransition.swift new file mode 100644 index 0000000000..8ec669aae4 --- /dev/null +++ b/TelegramUI/ChatListViewTransition.swift @@ -0,0 +1,170 @@ +import Foundation +import Postbox +import TelegramCore +import SwiftSignalKit +import Display + +struct ChatListNodeView { + let originalView: ChatListView + let filteredEntries: [ChatListNodeEntry] +} + +enum ChatListNodeViewTransitionReason { + case initial + case interactiveChanges + case holeChanges(filledHoleDirections: [MessageIndex: HoleFillDirection], removeHoleDirections: [MessageIndex: HoleFillDirection]) + case reload +} + +struct ChatListNodeViewTransitionInsertEntry { + let index: Int + let previousIndex: Int? + let entry: ChatListNodeEntry + let directionHint: ListViewItemOperationDirectionHint? +} + +struct ChatListNodeViewTransitionUpdateEntry { + let index: Int + let previousIndex: Int + let entry: ChatListNodeEntry + let directionHint: ListViewItemOperationDirectionHint? +} + +struct ChatListNodeViewTransition { + let chatListView: ChatListNodeView + let deleteItems: [ListViewDeleteItem] + let insertEntries: [ChatListNodeViewTransitionInsertEntry] + let updateEntries: [ChatListNodeViewTransitionUpdateEntry] + let options: ListViewDeleteAndInsertOptions + let scrollToItem: ListViewScrollToItem? + let stationaryItemRange: (Int, Int)? +} + +enum ChatListNodeViewScrollPosition { + case index(index: MessageIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool) +} + +func preparedChatListNodeViewTransition(from fromView: ChatListNodeView?, to toView: ChatListNodeView, reason: ChatListNodeViewTransitionReason, account: Account, scrollPosition: ChatListNodeViewScrollPosition?) -> Signal { + return Signal { subscriber in + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) + + var adjustedDeleteIndices: [ListViewDeleteItem] = [] + let previousCount: Int + if let fromView = fromView { + previousCount = fromView.filteredEntries.count + } else { + previousCount = 0; + } + for index in deleteIndices { + adjustedDeleteIndices.append(ListViewDeleteItem(index: previousCount - 1 - index, directionHint: nil)) + } + var adjustedIndicesAndItems: [ChatListNodeViewTransitionInsertEntry] = [] + var adjustedUpdateItems: [ChatListNodeViewTransitionUpdateEntry] = [] + let updatedCount = toView.filteredEntries.count + + var options: ListViewDeleteAndInsertOptions = [] + var maxAnimatedInsertionIndex = -1 + var stationaryItemRange: (Int, Int)? + var scrollToItem: ListViewScrollToItem? + + switch reason { + case .initial: + let _ = options.insert(.LowLatency) + let _ = options.insert(.Synchronous) + case .interactiveChanges: + let _ = options.insert(.AnimateAlpha) + let _ = options.insert(.AnimateInsertion) + + for (index, _, _) in indicesAndItems.sorted(by: { $0.0 > $1.0 }) { + let adjustedIndex = updatedCount - 1 - index + if adjustedIndex == maxAnimatedInsertionIndex + 1 { + maxAnimatedInsertionIndex += 1 + } + } + case .reload: + break + case let .holeChanges(filledHoleDirections, removeHoleDirections): + if let (_, removeDirection) = removeHoleDirections.first { + switch removeDirection { + case .LowerToUpper: + var holeIndex: MessageIndex? + for (index, _) in filledHoleDirections { + if holeIndex == nil || index < holeIndex! { + holeIndex = index + } + } + + if let holeIndex = holeIndex { + for i in 0 ..< toView.filteredEntries.count { + if toView.filteredEntries[i].index >= holeIndex { + let index = toView.filteredEntries.count - 1 - (i - 1) + stationaryItemRange = (index, Int.max) + break + } + } + } + case .UpperToLower: + break + case .AroundIndex: + break + } + } + } + + for (index, entry, previousIndex) in indicesAndItems { + let adjustedIndex = updatedCount - 1 - index + + let adjustedPrevousIndex: Int? + if let previousIndex = previousIndex { + adjustedPrevousIndex = previousCount - 1 - previousIndex + } else { + adjustedPrevousIndex = nil + } + + var directionHint: ListViewItemOperationDirectionHint? + if maxAnimatedInsertionIndex >= 0 && adjustedIndex <= maxAnimatedInsertionIndex { + directionHint = .Down + } + + adjustedIndicesAndItems.append(ChatListNodeViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint)) + } + + for (index, entry, previousIndex) in updateIndices { + let adjustedIndex = updatedCount - 1 - index + let adjustedPreviousIndex = previousCount - 1 - previousIndex + + let directionHint: ListViewItemOperationDirectionHint? = nil + adjustedUpdateItems.append(ChatListNodeViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint)) + } + + if let scrollPosition = scrollPosition { + switch scrollPosition { + case let .index(scrollIndex, position, directionHint, animated): + var index = toView.filteredEntries.count - 1 + for entry in toView.filteredEntries { + if entry.index >= scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + break + } + index -= 1 + } + + if scrollToItem == nil { + var index = 0 + for entry in toView.filteredEntries.reversed() { + if entry.index < scrollIndex { + scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: .Default, directionHint: directionHint) + break + } + index += 1 + } + } + } + } + + subscriber.putNext(ChatListNodeViewTransition(chatListView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange)) + subscriber.putCompletion() + + return EmptyDisposable + } +} diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index 0144c454f1..9695919856 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -135,12 +135,13 @@ final class ChatMediaInputStickerGridItemNode: GridItemNode { override func layout() { super.layout() - let boundingSize = self.bounds.insetBy(dx: 6.0, dy: 6.0).size + let bounds = self.bounds + let boundingSize = bounds.insetBy(dx: 6.0, dy: 6.0).size if let (_, _, mediaDimensions) = self.currentState { let imageSize = mediaDimensions.aspectFitted(boundingSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: imageSize) + self.imageNode.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - imageSize.width) / 2.0), y: (bounds.size.height - imageSize.height) / 2.0), size: imageSize) } } diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index ac756b2b6d..2873ce763b 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -117,8 +117,8 @@ class ChatMessageActionItemNode: ChatMessageItemView { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index ac215057b8..680727eb99 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -177,8 +177,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { fatalError("init(coder:) has not been implemented") } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index f2b3831569..1245007d52 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -95,11 +95,13 @@ public class ChatMessageItemView: ListViewItemNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) - - self.transitionOffset = -self.bounds.size.height * 1.6 - self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + if short { + self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } else { + self.transitionOffset = -self.bounds.size.height * 1.6 + self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) + } //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index b56945f0be..decdce574d 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -84,8 +84,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) self.imageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 26ef251f85..6c68e6919e 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -102,22 +102,37 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { private var presentationInterfaceState = ChatPresentationInterfaceState() + private var keepSendButtonEnabled = false + var inputTextState: ChatTextInputState { - get { - if let textInputNode = self.textInputNode { - let text = textInputNode.attributedText?.string ?? "" - let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) - return ChatTextInputState(inputText: text, selectionRange: selectionRange) - } else { - return ChatTextInputState() - } - } set(value) { - if let textInputNode = self.textInputNode { - self.updatingInputState = true - textInputNode.attributedText = NSAttributedString(string: value.inputText, font: Font.regular(17.0), textColor: UIColor.black) - textInputNode.selectedRange = NSMakeRange(value.selectionRange.lowerBound, value.selectionRange.count) - self.updatingInputState = false - } + if let textInputNode = self.textInputNode { + let text = textInputNode.attributedText?.string ?? "" + let selectionRange: Range = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length) + return ChatTextInputState(inputText: text, selectionRange: selectionRange) + } else { + return ChatTextInputState() + } + } + + func updateInputTextState(_ state: ChatTextInputState, keepSendButtonEnabled: Bool, animated: Bool) { + if !state.inputText.isEmpty && self.textInputNode == nil { + self.loadTextInputNode() + } + + if let textInputNode = self.textInputNode { + self.updatingInputState = true + textInputNode.attributedText = NSAttributedString(string: state.inputText, font: Font.regular(17.0), textColor: UIColor.black) + textInputNode.selectedRange = NSMakeRange(state.selectionRange.lowerBound, state.selectionRange.count) + self.updatingInputState = false + self.keepSendButtonEnabled = keepSendButtonEnabled + self.updateTextNodeText(animated: animated) + } + } + + func updateKeepSendButtonEnabled(keepSendButtonEnabled: Bool, animated: Bool) { + if keepSendButtonEnabled != self.keepSendButtonEnabled { + self.keepSendButtonEnabled = keepSendButtonEnabled + self.updateTextNodeText(animated: animated) } } @@ -360,33 +375,40 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode { - self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0 - - if let text = self.textInputNode?.attributedText, text.length != 0 { - if self.sendButton.alpha.isZero { - self.sendButton.alpha = 1.0 - self.micButton.alpha = 0.0 - self.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) - self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - } - } else { - if self.micButton.alpha.isZero { - self.micButton.alpha = 1.0 - self.sendButton.alpha = 0.0 - self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) - self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) - } - } - self.interfaceInteraction?.updateTextInputState(self.inputTextState) - - let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width) - let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) - if !self.bounds.size.height.isEqual(to: panelHeight) { - self.updateHeight() + self.updateTextNodeText(animated: true) + } + } + + private func updateTextNodeText(animated: Bool) { + var hasText = false + if let textInputNode = self.textInputNode, let attributedText = textInputNode.attributedText, attributedText.length != 0 { + hasText = true + } + self.textPlaceholderNode.isHidden = hasText + + if hasText || self.keepSendButtonEnabled { + if self.sendButton.alpha.isZero { + self.sendButton.alpha = 1.0 + self.micButton.alpha = 0.0 + self.sendButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.sendButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.micButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } + } else { + if self.micButton.alpha.isZero { + self.micButton.alpha = 1.0 + self.sendButton.alpha = 0.0 + self.micButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) + self.micButton.layer.animateSpring(from: NSNumber(value: Float(0.1)), to: NSNumber(value: Float(1.0)), keyPath: "transform.scale", duration: 0.6) + self.sendButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) + } + } + + let (accessoryButtonsWidth, textFieldHeight) = self.calculateTextFieldMetrics(width: self.bounds.size.width) + let panelHeight = self.panelHeight(textFieldHeight: textFieldHeight) + if !self.bounds.size.height.isEqual(to: panelHeight) { + self.updateHeight() } } @@ -412,9 +434,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { @objc func sendButtonPressed() { let text = self.textInputNode?.attributedText?.string ?? "" - if !text.isEmpty { - self.sendMessage() - } + self.sendMessage() } @objc func attachmentButtonPressed() { diff --git a/TelegramUI/ChatUnreadItem.swift b/TelegramUI/ChatUnreadItem.swift index 3cad436c33..244cd9dea1 100644 --- a/TelegramUI/ChatUnreadItem.swift +++ b/TelegramUI/ChatUnreadItem.swift @@ -52,8 +52,8 @@ class ChatUnreadItemNode: ListViewItemNode { self.scrollPositioningInsets = UIEdgeInsets(top: 5.0, left: 0.0, bottom: 5.0, right: 0.0) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) self.labelNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 3535978ee2..2c3d5c06db 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -12,14 +12,14 @@ private let statusFont = Font.regular(13.0) class ContactsPeerItem: ListViewItem { let account: Account - let peer: Peer + let peer: Peer? let presence: PeerPresence? let action: (Peer) -> Void let selectable: Bool = true let headerAccessoryItem: ListViewAccessoryItem? - init(account: Account, peer: Peer, presence: PeerPresence?, index: PeerNameIndex?, action: @escaping (Peer) -> Void) { + init(account: Account, peer: Peer?, presence: PeerPresence?, index: PeerNameIndex?, action: @escaping (Peer) -> Void) { self.account = account self.peer = peer self.presence = presence @@ -115,7 +115,9 @@ class ContactsPeerItem: ListViewItem { } func selected(listView: ListView) { - self.action(self.peer) + if let peer = self.peer { + self.action(peer) + } } } @@ -130,7 +132,7 @@ class ContactsPeerItemNode: ListViewItemNode { private let titleNode: TextNode private let statusNode: TextNode - private var avatarState: (Account, Peer)? + private var avatarState: (Account, Peer?)? private var peerPresenceManager: PeerPresenceStatusManager? private var layoutParams: (ContactsPeerItem, CGFloat, Bool, Bool)? @@ -228,34 +230,32 @@ class ContactsPeerItemNode: ListViewItemNode { var titleAttributedString: NSAttributedString? var statusAttributedString: NSAttributedString? - let peer = item.peer - - if let user = peer as? TelegramUser { - if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { - let string = NSMutableAttributedString() - string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) - string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) - titleAttributedString = string - } else if let firstName = user.firstName, !firstName.isEmpty { - titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) - } else if let lastName = user.lastName, !lastName.isEmpty { - titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) - } else { - titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) + if let peer = item.peer { + if let user = peer as? TelegramUser { + if let firstName = user.firstName, let lastName = user.lastName, !firstName.isEmpty, !lastName.isEmpty { + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: firstName, font: titleFont, textColor: .black)) + string.append(NSAttributedString(string: " ", font: titleFont, textColor: .black)) + string.append(NSAttributedString(string: lastName, font: titleBoldFont, textColor: .black)) + titleAttributedString = string + } else if let firstName = user.firstName, !firstName.isEmpty { + titleAttributedString = NSAttributedString(string: firstName, font: titleBoldFont, textColor: UIColor.black) + } else if let lastName = user.lastName, !lastName.isEmpty { + titleAttributedString = NSAttributedString(string: lastName, font: titleBoldFont, textColor: UIColor.black) + } else { + titleAttributedString = NSAttributedString(string: "Deleted User", font: titleBoldFont, textColor: UIColor(0xa6a6a6)) + } + + if let presence = item.presence as? TelegramUserPresence { + let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 + let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) + statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6)) + } + } else if let group = peer as? TelegramGroup { + titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) } - - if let presence = item.presence as? TelegramUserPresence { - let timestamp = CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970 - let (string, activity) = stringAndActivityForUserPresence(presence, relativeTo: Int32(timestamp)) - statusAttributedString = NSAttributedString(string: string, font: statusFont, textColor: activity ? UIColor(0x007ee5) : UIColor(0xa6a6a6)) - } else { - statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) - } - } else if let group = peer as? TelegramGroup { - titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) - } else if let channel = peer as? TelegramChannel { - titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) } let (titleLayout, titleApply) = makeTitleLayout(titleAttributedString, nil, 1, .end, CGSize(width: max(0.0, width - leftInset - rightInset), height: CGFloat.infinity), nil) @@ -264,18 +264,24 @@ class ContactsPeerItemNode: ListViewItemNode { let nodeLayout = ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 48.0), insets: UIEdgeInsets(top: first ? 29.0 : 0.0, left: 0.0, bottom: 0.0, right: 0.0)) + let titleFrame: CGRect + if statusAttributedString != nil { + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 4.0), size: titleLayout.size) + } else { + titleFrame = CGRect(origin: CGPoint(x: leftInset, y: 13.0), size: titleLayout.size) + } + return (nodeLayout, { [weak self] in if let strongSelf = self { strongSelf.layoutParams = (item, width, first, last) - - if strongSelf.avatarState == nil || strongSelf.avatarState!.0 !== item.account || !strongSelf.avatarState!.1.isEqual(peer) { - strongSelf.avatarNode.setPeer(account: item.account, peer: item.peer) + 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 = CGRect(origin: CGPoint(x: leftInset, y: 4.0), size: titleLayout.size) + strongSelf.titleNode.frame = titleFrame let _ = statusApply() strongSelf.statusNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 25.0), size: statusLayout.size) @@ -299,7 +305,7 @@ class ContactsPeerItemNode: ListViewItemNode { accessoryItemNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -29.0), size: CGSize(width: bounds.size.width, height: 29.0)) } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5) } diff --git a/TelegramUI/ForwardAccessoryPanelNode.swift b/TelegramUI/ForwardAccessoryPanelNode.swift new file mode 100644 index 0000000000..2e7828ef61 --- /dev/null +++ b/TelegramUI/ForwardAccessoryPanelNode.swift @@ -0,0 +1,106 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +final class ForwardAccessoryPanelNode: AccessoryPanelNode { + private let messageDisposable = MetaDisposable() + let messageIds: [MessageId] + + let closeButton: ASButtonNode + let lineNode: ASDisplayNode + let titleNode: ASTextNode + let textNode: ASTextNode + + init(account: Account, messageIds: [MessageId]) { + self.messageIds = messageIds + + self.closeButton = ASButtonNode() + self.closeButton.setImage(UIImage(bundleImageName: "Chat/Input/Acessory Panels/CloseButton")?.precomposed(), for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + self.lineNode = ASDisplayNode() + self.lineNode.backgroundColor = UIColor(0x007ee5) + + self.titleNode = ASTextNode() + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.displaysAsynchronously = false + + self.textNode = ASTextNode() + self.textNode.truncationMode = .byTruncatingTail + self.textNode.maximumNumberOfLines = 1 + self.textNode.displaysAsynchronously = false + + super.init() + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.lineNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.messageDisposable.set((account.postbox.messagesAtIds(messageIds) + |> deliverOnMainQueue).start(next: { [weak self] messages in + if let strongSelf = self { + var authors = "" + var uniquePeerIds = Set() + var text = "" + for message in messages { + if let author = message.author, !uniquePeerIds.contains(author.id) { + uniquePeerIds.insert(author.id) + if !authors.isEmpty { + authors.append(", ") + } + authors.append(author.compactDisplayTitle) + } + } + if messages.count == 1 { + text = messages[0].text + } else { + text = "\(messages.count) messages" + } + + strongSelf.titleNode.attributedText = NSAttributedString(string: authors, font: Font.regular(14.5), textColor: UIColor(0x007ee5)) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.5), textColor: UIColor.black) + + strongSelf.setNeedsLayout() + } + })) + } + + deinit { + self.messageDisposable.dispose() + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 40.0) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - self.insets.right - closeButtonSize.width, y: 12.0), size: closeButtonSize) + + self.lineNode.frame = CGRect(origin: CGPoint(x: self.insets.left, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 5.0)) + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 11.0 - insets.left - insets.right - 14.0, height: bounds.size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: self.insets.left + 11.0, y: 7.0), size: titleSize) + + let textSize = self.textNode.measure(CGSize(width: bounds.size.width - 11.0 - insets.left - insets.right - 14.0, height: bounds.size.height)) + self.textNode.frame = CGRect(origin: CGPoint(x: self.insets.left + 11.0, y: 25.0), size: textSize) + } + + @objc func closePressed() { + if let dismiss = self.dismiss { + dismiss() + } + } +} diff --git a/TelegramUI/FrameworkBundle.swift b/TelegramUI/FrameworkBundle.swift index b863239d79..adc1f674ee 100644 --- a/TelegramUI/FrameworkBundle.swift +++ b/TelegramUI/FrameworkBundle.swift @@ -3,7 +3,7 @@ import Foundation private class FrameworkBundleClass: NSObject { } -private let frameworkBundle: Bundle = Bundle(for: FrameworkBundleClass.self) +let frameworkBundle: Bundle = Bundle(for: FrameworkBundleClass.self) private let screenScaleFactor = Int(UIScreen.main.scale) extension UIImage { diff --git a/TelegramUI/GalleryController.swift b/TelegramUI/GalleryController.swift index 03321bd051..bcdf4fede7 100644 --- a/TelegramUI/GalleryController.swift +++ b/TelegramUI/GalleryController.swift @@ -156,7 +156,7 @@ class GalleryController: ViewController { let view = account.postbox.aroundMessageHistoryViewForPeerId(messageId.peerId, index: MessageIndex(message!), count: 50, anchorIndex: MessageIndex(message!), fixedCombinedReadState: nil, tagMask: tags) return view - |> mapToSignal { (view, _) -> Signal in + |> mapToSignal { (view, _, _) -> Signal in let mapped = GalleryMessageHistoryView.view(view) return .single(mapped) } diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index a064d5360f..645938cb8b 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -213,8 +213,8 @@ final class ListMessageFileItemNode: ListMessageNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) self.transitionOffset = self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index 12050501c5..de18155d8d 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -79,8 +79,8 @@ final class ListMessageSnippetItemNode: ListMessageNode { } } - override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { - super.animateInsertion(currentTimestamp, duration: duration) + override public func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { + super.animateInsertion(currentTimestamp, duration: duration, short: short) self.transitionOffset = self.bounds.size.height * 1.6 self.addTransitionOffsetAnimation(0.0, duration: duration, beginAt: currentTimestamp) diff --git a/TelegramUI/PeerInfoActionItem.swift b/TelegramUI/PeerInfoActionItem.swift index f7d7f0a068..de9614f1db 100644 --- a/TelegramUI/PeerInfoActionItem.swift +++ b/TelegramUI/PeerInfoActionItem.swift @@ -246,7 +246,7 @@ class PeerInfoActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/TelegramUI/PeerInfoDisclosureItem.swift b/TelegramUI/PeerInfoDisclosureItem.swift index 653e00cec6..99079e14b2 100644 --- a/TelegramUI/PeerInfoDisclosureItem.swift +++ b/TelegramUI/PeerInfoDisclosureItem.swift @@ -243,7 +243,7 @@ class PeerInfoDisclosureItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/TelegramUI/PeerInfoPeerActionItem.swift b/TelegramUI/PeerInfoPeerActionItem.swift index 3065539bc1..fb4b2c13d4 100644 --- a/TelegramUI/PeerInfoPeerActionItem.swift +++ b/TelegramUI/PeerInfoPeerActionItem.swift @@ -205,7 +205,7 @@ class PeerInfoPeerActionItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/TelegramUI/PeerInfoPeerItem.swift b/TelegramUI/PeerInfoPeerItem.swift index a78b837845..fe12e2eff7 100644 --- a/TelegramUI/PeerInfoPeerItem.swift +++ b/TelegramUI/PeerInfoPeerItem.swift @@ -299,7 +299,7 @@ class PeerInfoPeerItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/TelegramUI/PeerInfoTextWithLabelItem.swift b/TelegramUI/PeerInfoTextWithLabelItem.swift index 2565b2fb2f..13385f40f8 100644 --- a/TelegramUI/PeerInfoTextWithLabelItem.swift +++ b/TelegramUI/PeerInfoTextWithLabelItem.swift @@ -110,7 +110,7 @@ class PeerInfoTextWithLabelItemNode: ListViewItemNode { } } - override func animateInsertion(_ currentTimestamp: Double, duration: Double) { + override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift new file mode 100644 index 0000000000..9efec538d3 --- /dev/null +++ b/TelegramUI/PeerSelectionController.swift @@ -0,0 +1,134 @@ +import Foundation +import SwiftSignalKit +import Display +import TelegramCore +import Postbox + +public final class PeerSelectionController: ViewController { + private let account: Account + + var peerSelected: ((PeerId) -> Void)? + + private var peerSelectionNode: PeerSelectionControllerNode { + return super.displayNode as! PeerSelectionControllerNode + } + + let openMessageFromSearchDisposable: MetaDisposable = MetaDisposable() + + public init(account: Account) { + self.account = account + + super.init() + + self.title = "Forward" + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(self.cancelPressed)) + + self.scrollToTop = { [weak self] in + if let strongSelf = self { + strongSelf.peerSelectionNode.chatListNode.scrollToLatest() + } + } + } + + required public init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.openMessageFromSearchDisposable.dispose() + } + + override public func loadDisplayNode() { + self.displayNode = PeerSelectionControllerNode(account: self.account, dismiss: { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + }) + self.displayNode.backgroundColor = .white + + self.peerSelectionNode.navigationBar = self.navigationBar + + self.peerSelectionNode.requestDeactivateSearch = { [weak self] in + self?.deactivateSearch() + } + + self.peerSelectionNode.chatListNode.activateSearch = { [weak self] in + self?.activateSearch() + } + + self.peerSelectionNode.chatListNode.peerSelected = { [weak self] peerId in + if let strongSelf = self, let peerSelected = strongSelf.peerSelected { + peerSelected(peerId) + } + } + + self.peerSelectionNode.requestOpenMessageFromSearch = { [weak self] peer, messageId in + if let strongSelf = self { + let storedPeer = strongSelf.account.postbox.modify { modifier -> Void in + if modifier.getPeer(peer.id) == nil { + modifier.updatePeers([peer], update: { previousPeer, updatedPeer in + return updatedPeer + }) + } + } + strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in + if let strongSelf = strongSelf { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: messageId.peerId, messageId: messageId)) + } + })) + } + } + + self.peerSelectionNode.requestOpenPeerFromSearch = { [weak self] peerId in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId)) + } + } + + self.displayNodeDidLoad() + } + + override public func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + self.peerSelectionNode.animateIn() + } + + override public func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.peerSelectionNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + } + + @objc func cancelPressed() { + self.dismiss() + } + + private func activateSearch() { + if self.displayNavigationBar { + if let scrollToTop = self.scrollToTop { + scrollToTop() + } + self.peerSelectionNode.activateSearch() + self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + private func deactivateSearch() { + if !self.displayNavigationBar { + self.peerSelectionNode.deactivateSearch() + self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + } + } + + public func dismiss() { + self.peerSelectionNode.animateOut() + } +} diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift new file mode 100644 index 0000000000..953a2771e9 --- /dev/null +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -0,0 +1,139 @@ +import Foundation +import AsyncDisplayKit +import Display +import Postbox +import TelegramCore + +final class PeerSelectionControllerNode: ASDisplayNode { + private let account: Account + private let dismiss: () -> Void + + let chatListNode: ChatListNode + var navigationBar: NavigationBar? + + private var searchDisplayController: SearchDisplayController? + + private var containerLayout: (ContainerViewLayout, CGFloat)? + + var requestDeactivateSearch: (() -> Void)? + var requestOpenPeerFromSearch: ((PeerId) -> Void)? + var requestOpenMessageFromSearch: ((Peer, MessageId) -> Void)? + + init(account: Account, dismiss: @escaping () -> Void) { + self.account = account + self.dismiss = dismiss + self.chatListNode = ChatListNode(account: account, mode: .peers) + + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + + self.addSubnode(self.chatListNode) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = (layout, navigationBarHeight) + + var insets = layout.insets(options: [.input]) + insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) + + self.chatListNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.chatListNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + var speedFactor: CGFloat = 1.0 + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve) + + self.chatListNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets) + + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + } + + func activateSearch() { + guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else { + return + } + + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.chatListNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(contentNode: ChatListSearchContainerNode(account: self.account, openPeer: { [weak self] peerId in + if let requestOpenPeerFromSearch = self?.requestOpenPeerFromSearch { + requestOpenPeerFromSearch(peerId) + } + }, openMessage: { [weak self] peer, messageId in + if let requestOpenMessageFromSearch = self?.requestOpenMessageFromSearch { + requestOpenMessageFromSearch(peer, messageId) + } + }), cancel: { [weak self] in + if let requestDeactivateSearch = self?.requestDeactivateSearch { + requestDeactivateSearch() + } + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: navigationBar) + }, placeholder: placeholderNode) + } + } + + func deactivateSearch() { + if let searchDisplayController = self.searchDisplayController { + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.chatListNode.forEachItemNode { node in + if let node = node as? ChatListSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + self.searchDisplayController = nil + } + } + + func animateIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak self] _ in + }) + } + + func animateOut() { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { [weak self] _ in + if let strongSelf = self { + strongSelf.dismiss() + } + }) + } +} diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 21ab0d2dc7..8f642c1c7b 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -4,7 +4,7 @@ import Postbox import TelegramCore import Display -func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?) -> Signal { +func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toView: ChatHistoryView, reason: ChatHistoryViewTransitionReason, account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, scrollPosition: ChatHistoryViewScrollPosition?, initialData: InitialMessageHistoryData?) -> Signal { return Signal { subscriber in //let updateIndices: [(Int, ChatHistoryEntry, Int)] = [] let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromView?.filteredEntries ?? [], rightList: toView.filteredEntries) @@ -169,7 +169,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)) + subscriber.putNext(ChatHistoryViewTransition(historyView: toView, deleteItems: adjustedDeleteIndices, insertEntries: adjustedIndicesAndItems, updateEntries: adjustedUpdateItems, options: options, scrollToItem: scrollToItem, stationaryItemRange: stationaryItemRange, initialData: initialData)) subscriber.putCompletion() return EmptyDisposable diff --git a/TelegramUI/ServiceSoundManager.swift b/TelegramUI/ServiceSoundManager.swift new file mode 100644 index 0000000000..7faa9b4ba0 --- /dev/null +++ b/TelegramUI/ServiceSoundManager.swift @@ -0,0 +1,34 @@ +import Foundation +import SwiftSignalKit +import AudioToolbox + +private func loadSystemSoundFromBundle(name: String) -> SystemSoundID? { + let path = "\(frameworkBundle.resourcePath!)/\(name)" + let url = URL(fileURLWithPath: path) + var sound: SystemSoundID = 0 + if AudioServicesCreateSystemSoundID(url as CFURL, &sound) == noErr { + return sound + } + return nil +} + +final class ServiceSoundManager { + private let queue = Queue() + private var messageDeliverySound: SystemSoundID? + + init() { + self.queue.async { + self.messageDeliverySound = loadSystemSoundFromBundle(name: "MessageSent.caf") + } + } + + func playMessageDeliveredSound() { + self.queue.async { + if let messageDeliverySound = self.messageDeliverySound { + AudioServicesPlaySystemSound(messageDeliverySound) + } + } + } +} + +let serviceSoundManager = ServiceSoundManager() diff --git a/TelegramUI/Sounds/MessageSent.caf b/TelegramUI/Sounds/MessageSent.caf new file mode 100644 index 0000000000..68164c0ea9 Binary files /dev/null and b/TelegramUI/Sounds/MessageSent.caf differ