diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 4831fd84c9..5f15d1b028 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */; }; D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */; }; D03ADB481D703268005A521C /* ChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB471D703268005A521C /* ChatInterfaceState.swift */; }; D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; }; @@ -30,6 +31,14 @@ D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */; }; D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */; }; D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */; }; + D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */; }; + D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */; }; + D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */; }; + D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */; }; + D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */; }; + D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA01D821B28008AEB01 /* HashtagsTableCell.swift */; }; + D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */; }; + D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA51D82BCE0008AEB01 /* MentionsTableCell.swift */; }; D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */; }; D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */; }; D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */; }; @@ -75,7 +84,7 @@ D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */; }; D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */; }; D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */; }; - D0F69DFE1D6B8A880046BCD6 /* ChatListAvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */; }; + D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */; }; D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */; }; D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */; }; D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */; }; @@ -145,7 +154,6 @@ D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E851D6B8C850046BCD6 /* RingBuffer.h */; }; D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E861D6B8C850046BCD6 /* RingBuffer.m */; }; D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */; }; - D0F69E951D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */; }; D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */; }; D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E941D6B8C9B0046BCD6 /* WebP.swift */; }; D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */; }; @@ -175,6 +183,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; D03ADB471D703268005A521C /* ChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceState.swift; sourceTree = ""; }; D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = ""; }; @@ -201,6 +210,14 @@ D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionNode.swift; sourceTree = ""; }; D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanelInterfaceInteraction.swift; sourceTree = ""; }; D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareRecipientsActionSheetController.swift; sourceTree = ""; }; + D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateContextMenus.swift; sourceTree = ""; }; + D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputContextPanelNode.swift; sourceTree = ""; }; + D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputContextPanelNode.swift; sourceTree = ""; }; + D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContextPanels.swift; sourceTree = ""; }; + D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = ""; }; + D0DF0CA01D821B28008AEB01 /* HashtagsTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagsTableCell.swift; sourceTree = ""; }; + D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = ""; }; + D0DF0CA51D82BCE0008AEB01 /* MentionsTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionsTableCell.swift; sourceTree = ""; }; D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = ""; }; D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = ""; }; D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = ""; }; @@ -246,7 +263,7 @@ D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordControllerNode.swift; sourceTree = ""; }; D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneController.swift; sourceTree = ""; }; D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneControllerNode.swift; sourceTree = ""; }; - D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListAvatarNode.swift; sourceTree = ""; }; + D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarNode.swift; sourceTree = ""; }; D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = ""; }; D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = ""; }; D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListEmptyItem.swift; sourceTree = ""; }; @@ -316,7 +333,6 @@ D0F69E851D6B8C850046BCD6 /* RingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RingBuffer.h; sourceTree = ""; }; D0F69E861D6B8C850046BCD6 /* RingBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RingBuffer.m; sourceTree = ""; }; D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingByteBuffer.swift; sourceTree = ""; }; - D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRepresentationsUtils.swift; sourceTree = ""; }; D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressiveImage.swift; sourceTree = ""; }; D0F69E941D6B8C9B0046BCD6 /* WebP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebP.swift; sourceTree = ""; }; D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+WebP.h"; sourceTree = ""; }; @@ -379,6 +395,9 @@ D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */, D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */, D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */, + D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */, + D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */, + D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */, ); name = "Interface State"; sourceTree = ""; @@ -419,19 +438,12 @@ children = ( D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */, D0F69E3F1D6B8B6B0046BCD6 /* Text Input */, - D0BA6F861D784F700034826E /* Message Selection */, + D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */, + D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */, ); name = "Input Panels"; sourceTree = ""; }; - D0BA6F861D784F700034826E /* Message Selection */ = { - isa = PBXGroup; - children = ( - D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */, - ); - name = "Message Selection"; - sourceTree = ""; - }; D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */ = { isa = PBXGroup; children = ( @@ -448,6 +460,34 @@ name = "Share Recipients"; sourceTree = ""; }; + D0DF0C961D81FD87008AEB01 /* Input Context Panels */ = { + isa = PBXGroup; + children = ( + D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */, + D0DF0C9F1D8219C7008AEB01 /* Hashtags */, + D0DF0CA21D82BCBC008AEB01 /* Mentions */, + ); + name = "Input Context Panels"; + sourceTree = ""; + }; + D0DF0C9F1D8219C7008AEB01 /* Hashtags */ = { + isa = PBXGroup; + children = ( + D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */, + D0DF0CA01D821B28008AEB01 /* HashtagsTableCell.swift */, + ); + name = Hashtags; + sourceTree = ""; + }; + D0DF0CA21D82BCBC008AEB01 /* Mentions */ = { + isa = PBXGroup; + children = ( + D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */, + D0DF0CA51D82BCE0008AEB01 /* MentionsTableCell.swift */, + ); + name = Mentions; + sourceTree = ""; + }; D0F69CCE1D6B87950046BCD6 /* Files */ = { isa = PBXGroup; children = ( @@ -517,6 +557,7 @@ D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */, D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */, D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */, + D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */, D0F69DCA1D6B89F20046BCD6 /* Search */, ); name = Nodes; @@ -595,7 +636,6 @@ D0F69DF61D6B8A720046BCD6 /* Chat List */ = { isa = PBXGroup; children = ( - D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */, D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */, D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */, D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */, @@ -637,6 +677,7 @@ D0F69E181D6B8AD10046BCD6 /* Items */, D03ADB461D703250005A521C /* Interface State */, D03ADB491D704427005A521C /* Accessory Panels */, + D0DF0C961D81FD87008AEB01 /* Input Context Panels */, D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */, D0BA6F811D784C3A0034826E /* Input Panels */, D0F69E441D6B8B850046BCD6 /* History Navigation */, @@ -787,7 +828,6 @@ D0F69E911D6B8C8E0046BCD6 /* Utils */ = { isa = PBXGroup; children = ( - D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, D0F69E941D6B8C9B0046BCD6 /* WebP.swift */, ); @@ -971,6 +1011,7 @@ D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */, D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */, D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */, + D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */, D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */, D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */, D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */, @@ -995,7 +1036,6 @@ D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */, D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */, D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */, - D0F69E951D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift in Sources */, D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */, D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */, D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */, @@ -1006,20 +1046,26 @@ D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */, D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */, D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */, + D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */, D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */, D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, + D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */, D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, + D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */, D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */, D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */, D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */, + D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */, D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */, + D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */, D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */, + D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */, D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */, D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */, D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */, @@ -1036,7 +1082,7 @@ D0F69E0A1D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift in Sources */, D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */, D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, - D0F69DFE1D6B8A880046BCD6 /* ChatListAvatarNode.swift in Sources */, + D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */, D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */, D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */, D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */, @@ -1046,6 +1092,7 @@ D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */, + D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */, D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */, D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */, D0F69D2C1D6B87D30046BCD6 /* MediaPlayerNode.swift in Sources */, @@ -1058,6 +1105,7 @@ D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */, + D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */, D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */, D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */, D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */, diff --git a/TelegramUI/AuthorizationController.swift b/TelegramUI/AuthorizationController.swift index d324c10f59..13d9be7172 100644 --- a/TelegramUI/AuthorizationController.swift +++ b/TelegramUI/AuthorizationController.swift @@ -19,50 +19,50 @@ public class AuthorizationController: NavigationController { self.pushViewController(phoneController, animated: false) - let authorizationSequence = phoneController.result |> mapToSignal { (account, sentCode, phone) -> Signal in + let authorizationSequence = phoneController.result |> mapToSignal { (account, sentCode, phone) -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in return deferred { [weak self] in if let strongSelf = self { strongSelf.account = account let codeController = AuthorizationCodeController(account: account, phone: phone, sentCode: sentCode) strongSelf.pushViewController(codeController, animated: true) - return codeController.result |> mapToSignal { result -> Signal in + return codeController.result |> mapToSignal { result -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in switch result { case let .Authorization(authorization): - return single(authorization, NoError.self) + return single((authorization, account), NoError.self) case .Password: - return deferred { [weak self] () -> Signal in + return deferred { [weak self] () -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in if let strongSelf = self { let passwordController = AuthorizationPasswordController(account: account) strongSelf.pushViewController(passwordController, animated: true) - return passwordController.result + return passwordController.result |> map { ($0, account) } } else { - return complete(Api.auth.Authorization.self, NoError.self) + return .complete() } } |> runOn(Queue.mainQueue()) } } } else { - return complete(Api.auth.Authorization.self, NoError.self) + return .complete() } } |> runOn(Queue.mainQueue()) } - let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization -> Signal in + let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization, account -> Signal in if let strongSelf = self { switch authorization { - case let .authorization(user): - let user = TelegramUser(user: user) - - return account.postbox.modify { modifier -> AccountState in - let state = AuthorizedAccountState(masterDatacenterId: strongSelf.account.masterDatacenterId, peerId: user.id, state: nil) - modifier.setState(state) - return state - } |> map { state -> Account in - return Account(id: account.id, postbox: account.postbox, network: account.network, peerId: user.id) + case let .authorization(user): + let user = TelegramUser(user: user) + + return account.postbox.modify { modifier -> AccountState in + let state = AuthorizedAccountState(masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) + modifier.setState(state) + return state + } |> map { state -> Account in + return Account(id: account.id, postbox: account.postbox, network: account.network, peerId: user.id) + } } - } } else { return .complete() } diff --git a/TelegramUI/ChatListAvatarNode.swift b/TelegramUI/AvatarNode.swift similarity index 69% rename from TelegramUI/ChatListAvatarNode.swift rename to TelegramUI/AvatarNode.swift index fd5c8178ab..7fc6e7f893 100644 --- a/TelegramUI/ChatListAvatarNode.swift +++ b/TelegramUI/AvatarNode.swift @@ -5,7 +5,7 @@ import UIKit import Display import TelegramCore -private class ChatListAvatarNodeParameters: NSObject { +private class AvatarNodeParameters: NSObject { let account: Account let peerId: PeerId let letters: [String] @@ -30,28 +30,28 @@ let gradientColors: [NSArray] = [ [UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor] ] -private enum ChatListAvatarNodeState: Equatable { +private enum AvatarNodeState: Equatable { case Empty - case PeerAvatar(Peer) + case PeerAvatar(PeerId, [String], TelegramMediaImageRepresentation?) } -private func ==(lhs: ChatListAvatarNodeState, rhs: ChatListAvatarNodeState) -> Bool { +private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool { switch (lhs, rhs) { case (.Empty, .Empty): return true - case let (.PeerAvatar(lhsPeer), .PeerAvatar(rhsPeer)) where lhsPeer.isEqual(rhsPeer): - return true + case let (.PeerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .PeerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)): + return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations default: return false } } -public final class ChatListAvatarNode: ASDisplayNode { +public final class AvatarNode: ASDisplayNode { var font: UIFont { didSet { if oldValue !== font { if let parameters = self.parameters { - self.parameters = ChatListAvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font) + self.parameters = AvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font) } if !self.displaySuspended { @@ -60,10 +60,10 @@ public final class ChatListAvatarNode: ASDisplayNode { } } } - private var parameters: ChatListAvatarNodeParameters? + private var parameters: AvatarNodeParameters? let imageNode: ImageNode - private var state: ChatListAvatarNodeState = .Empty + private var state: AvatarNodeState = .Empty public init(font: UIFont) { self.font = font @@ -92,11 +92,11 @@ public final class ChatListAvatarNode: ASDisplayNode { } public func setPeer(account: Account, peer: Peer) { - let updatedState = ChatListAvatarNodeState.PeerAvatar(peer) + let updatedState = AvatarNodeState.PeerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage) if updatedState != self.state { self.state = updatedState - let parameters = ChatListAvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font) + let parameters = AvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font) self.displaySuspended = true self.contents = nil @@ -134,7 +134,7 @@ public final class ChatListAvatarNode: ASDisplayNode { context.clip() let colorIndex: Int - if let parameters = parameters as? ChatListAvatarNodeParameters { + if let parameters = parameters as? AvatarNodeParameters { colorIndex = Int(parameters.account.peerId.id + parameters.peerId.id) } else { colorIndex = 0 @@ -149,11 +149,9 @@ public final class ChatListAvatarNode: ASDisplayNode { context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions()) - //CGContextDrawRadialGradient(context, gradient, CGPoint(x: bounds.size.width * 0.5, y: -bounds.size.width * 0.2), 0.0, CGPoint(x: bounds.midX, y: bounds.midY), bounds.width, CGGradientDrawingOptions()) - context.setBlendMode(.normal) - if let parameters = parameters as? ChatListAvatarNodeParameters { + if let parameters = parameters as? AvatarNodeParameters { let letters = parameters.letters let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) let attributedString = NSAttributedString(string: string, attributes: [NSFontAttributeName: parameters.font, NSForegroundColorAttributeName: UIColor.white]) @@ -161,19 +159,9 @@ public final class ChatListAvatarNode: ASDisplayNode { let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) - /*var ascent: CGFloat = 0.0 - var descent: CGFloat = 0.0 - var leading: CGFloat = 0.0 - let lineWidth = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading)) - let opticalBounds = CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: ascent + descent + leading))*/ - - //let opticalBounds = CTLineGetImageBounds(line, context) - let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0)) - //let lineOrigin = CGPoint(x: floorToScreenPixels(-opticalBounds.origin.x + (bounds.size.width - opticalBounds.size.width) / 2.0), y: floorToScreenPixels(-opticalBounds.origin.y + (bounds.size.height - opticalBounds.size.height) / 2.0)) - context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.scaleBy(x: 1.0, y: -1.0) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) @@ -181,13 +169,6 @@ public final class ChatListAvatarNode: ASDisplayNode { context.translateBy(x: lineOrigin.x, y: lineOrigin.y) CTLineDraw(line, context) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) - - /*var attributes: [String : AnyObject] = [:] - attributes[NSFontAttributeName] = parameters.font - attributes[NSForegroundColorAttributeName] = UIColor.whiteColor() - let lettersSize = string.sizeWithAttributes(attributes) - - string.drawAtPoint(CGPoint(x: floor((bounds.size.width - lettersSize.width) / 2.0), y: floor((bounds.size.height - lettersSize.height) / 2.0)), withAttributes: attributes)*/ } } } diff --git a/TelegramUI/ChatAvatarNavigationNode.swift b/TelegramUI/ChatAvatarNavigationNode.swift index 5a5aa0583f..1cb6e8f686 100644 --- a/TelegramUI/ChatAvatarNavigationNode.swift +++ b/TelegramUI/ChatAvatarNavigationNode.swift @@ -6,10 +6,10 @@ private let normalFont = Font.medium(16.0) private let smallFont = Font.medium(12.0) final class ChatAvatarNavigationNode: ASDisplayNode { - let avatarNode: ChatListAvatarNode + let avatarNode: AvatarNode override init() { - self.avatarNode = ChatListAvatarNode(font: normalFont) + self.avatarNode = AvatarNode(font: normalFont) super.init() diff --git a/TelegramUI/ChatChannelSubscriberInputPanelNode.swift b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift new file mode 100644 index 0000000000..e9cad531fb --- /dev/null +++ b/TelegramUI/ChatChannelSubscriberInputPanelNode.swift @@ -0,0 +1,138 @@ +import Foundation +import AsyncDisplayKit +import Display +import TelegramCore +import Postbox +import SwiftSignalKit + +private enum SubscriberAction { + case join + case kicked + case muteNotifications + case unmuteNotifications +} + +private func titleAndColorForAction(_ action: SubscriberAction) -> (String, UIColor) { + switch action { + case .join: + return ("Join", UIColor(0x1195f2)) + case .kicked: + return ("Join", UIColor.gray) + case .muteNotifications: + return ("Mute", UIColor(0x1195f2)) + case .unmuteNotifications: + return ("Unmute", UIColor(0x1195f2)) + } +} + +private func actionForPeer(_ peer: Peer) -> SubscriberAction? { + if let channel = peer as? TelegramChannel { + switch channel.participationStatus { + case .kicked: + return .kicked + case .left: + return .join + case .member: + return .muteNotifications + } + return .join + } else { + return nil + } +} + +final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode { + private let button: UIButton + private let activityIndicator: UIActivityIndicatorView + + private var action: SubscriberAction? + + private let actionDisposable = MetaDisposable() + + override var peer: Peer? { + didSet { + if let peer = self.peer, oldValue == nil || !peer.isEqual(oldValue!) { + if let action = actionForPeer(peer) { + self.action = action + let (title, color) = titleAndColorForAction(action) + self.button.setTitle(title, for: []) + self.button.setTitleColor(color, for: [.normal]) + self.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted]) + self.button.sizeToFit() + self.setNeedsLayout() + } else { + self.action = nil + } + } + } + } + + override init() { + self.button = UIButton() + self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + self.activityIndicator.isHidden = true + + super.init() + + self.view.addSubview(self.button) + self.view.addSubview(self.activityIndicator) + + button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) + } + + deinit { + self.actionDisposable.dispose() + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 45.0) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let buttonSize = self.button.bounds.size + self.button.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - buttonSize.width) / 2.0), y: floor((bounds.size.height - buttonSize.height) / 2.0)), size: buttonSize) + + //_activityIndicator.frame = CGRectMake(self.frame.size.width - _activityIndicator.frame.size.width - 12.0f, CGFloor((self.frame.size.height - _activityIndicator.frame.size.height) / 2.0f), _activityIndicator.frame.size.width, _activityIndicator.frame.size.height); + let indicatorSize = self.activityIndicator.bounds.size + self.activityIndicator.frame = CGRect(origin: CGPoint(x: bounds.size.width - indicatorSize.width - 12.0, y: floor((bounds.size.height - indicatorSize.height) / 2.0)), size: indicatorSize) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.bounds.contains(point) { + return self.button + } else { + return nil + } + } + + @objc func buttonPressed() { + guard let account = self.account, let action = self.action, let peer = self.peer else { + return + } + + switch action { + case .join: + self.activityIndicator.isHidden = false + self.activityIndicator.startAnimating() + self.actionDisposable.set((joinChannel(account: account, peerId: peer.id) + |> afterDisposed { [weak self] in + Queue.mainQueue().async { + if let strongSelf = self { + strongSelf.activityIndicator.isHidden = true + strongSelf.activityIndicator.stopAnimating() + } + } + }).start()) + case .kicked: + break + case .muteNotifications: + break + case .unmuteNotifications: + break + } + } +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 15ce383d5d..0d51649eb4 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -383,11 +383,10 @@ public class ChatController: ViewController { private var enqueuedHistoryViewTransition: (ChatHistoryViewTransition, () -> Void)? private var layoutActionOnViewTransition: (@escaping () -> Void)? - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - private var didSetReady = false + private let _historyReady = Promise() + private var didSetHistoryReady = false + private let _peerReady = Promise() + private var didSetPeerReady = false private let maxVisibleIncomingMessageId = Promise() private let canReadHistory = Promise() @@ -397,7 +396,7 @@ public class ChatController: ViewController { return self._chatHistoryLocation.get() } - private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil) + private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil, inputContext: nil) private let chatInterfaceStatePromise = Promise() private var leftNavigationButton: ChatNavigationButton? @@ -407,6 +406,7 @@ public class ChatController: ViewController { private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private var controllerInteraction: ChatControllerInteraction? + private var interfaceInteraction: ChatPanelInterfaceInteraction? public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account @@ -415,6 +415,8 @@ public class ChatController: ViewController { super.init() + self.ready.set(combineLatest(self._historyReady.get(), self._peerReady.get()) |> map { $0 && $1 }) + self.setupThemeWithDarkMode(useDarkMode) self.scrollToTop = { [weak self] in @@ -487,47 +489,21 @@ public class ChatController: ViewController { } }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, let historyView = strongSelf.historyView { - let contextMenuController = ContextMenuController(actions: [ - ContextMenuAction(content: .text("Reply"), action: { [weak strongSelf] in - if let strongSelf = strongSelf, let historyView = strongSelf.historyView { - for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { - strongSelf.updateChatInterfaceState(animated: true, { $0.withUpdatedReplyMessageId(message.id) }) - strongSelf.chatDisplayNode.ensureInputViewFocused() - break - } - } - }), - ContextMenuAction(content: .text("Copy"), action: { [weak strongSelf] in - if let strongSelf = strongSelf, let historyView = strongSelf.historyView { - for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { - if !message.text.isEmpty { - UIPasteboard.general.string = message.text - } - break - } - } - }), - ContextMenuAction(content: .text("More..."), action: { [weak strongSelf] in - if let strongSelf = strongSelf, let historyView = strongSelf.historyView { - for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { - if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil { - strongSelf.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() }) + if let strongSelf = self, let historyView = strongSelf.historyView { + for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { + if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) { + strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in + if let node = node { + return (node, frame) } else { - strongSelf.updateChatInterfaceState(animated: true, { $0.withUpdatedSelectedMessage(message.id) }) + return nil } - - break - } + })) } - }) - ]) - strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in - if let node = node { - return (node, frame) - } else { - return nil + + break } - })) + } } }, navigateToMessage: { [weak self] fromId, id in if let strongSelf = self, let historyView = strongSelf.historyView { @@ -564,7 +540,7 @@ public class ChatController: ViewController { }, toggleMessageSelection: { [weak self] messageId in if let strongSelf = self, let historyView = strongSelf.historyView { for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId { - strongSelf.updateChatInterfaceState(animated: false, { $0.withToggledSelectedMessage(messageId) }) + strongSelf.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(messageId) } }) break } } @@ -576,13 +552,20 @@ public class ChatController: ViewController { self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode())) - self.updateChatInterfaceState(animated: false, { return $0 }) + self.updateChatPresentationInterfaceState(animated: false, { return $0 }) peerDisposable.set((account.postbox.peerWithId(peerId) |> deliverOnMainQueue).start(next: { [weak self] peer in if let strongSelf = self { - strongSelf.title = peer.displayTitle - (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) + if let peer = peer { + strongSelf.title = peer.displayTitle + (strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) + } + strongSelf.updateChatPresentationInterfaceState(animated: false, { return $0.updatedPeer { _ in return peer } }) + if !strongSelf.didSetPeerReady { + strongSelf.didSetPeerReady = true + strongSelf._peerReady.set(.single(true)) + } } })) @@ -608,9 +591,9 @@ public class ChatController: ViewController { case .Loading: Queue.mainQueue().async { [weak self] in if let strongSelf = self { - if !strongSelf.didSetReady { - strongSelf.didSetReady = true - strongSelf._ready.set(.single(true)) + if !strongSelf.didSetHistoryReady { + strongSelf.didSetHistoryReady = true + strongSelf._historyReady.set(.single(true)) } } } @@ -775,8 +758,8 @@ public class ChatController: ViewController { self?.layoutActionOnViewTransition = f } - self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] state, animated in - self?.updateChatInterfaceState(animated: animated, { _ in return state }) + self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in + self?.updateChatPresentationInterfaceState(animated: animated, { $0.updatedInterfaceState(f) }) } self.chatDisplayNode.displayAttachmentMenu = { [weak self] in @@ -804,7 +787,22 @@ public class ChatController: ViewController { } } - self.chatDisplayNode.interfaceInteraction = ChatPanelInterfaceInteraction(deleteSelectedMessages: { [weak self] in + let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in + if let strongSelf = self, let historyView = strongSelf.historyView { + for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId { + strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(message.id) } }) + strongSelf.chatDisplayNode.ensureInputViewFocused() + break + } + } + }, beginMessageSelection: { [weak self] messageId in + if let strongSelf = self, let historyView = strongSelf.historyView { + for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId { + strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withUpdatedSelectedMessage(message.id) } }) + break + } + } + }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { @@ -812,15 +810,22 @@ public class ChatController: ViewController { modifier.deleteMessages(Array(messageIds)) }).start() } - strongSelf.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() }) + strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) } }, forwardSelectedMessages: { [weak self] in if let strongSelf = self { let controller = ShareRecipientsActionSheetController() strongSelf.present(controller, in: .window) } + }, updateTextInputState: { [weak self] textInputState in + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState { $0.updatedInterfaceState { $0.withUpdatedInputState(textInputState) } } + } }) + self.interfaceInteraction = interfaceInteraction + self.chatDisplayNode.interfaceInteraction = interfaceInteraction + self.displayNodeDidLoad() self.dequeueHistoryViewTransition() @@ -851,9 +856,9 @@ public class ChatController: ViewController { if strongSelf.isNodeLoaded { strongSelf.dequeueHistoryViewTransition() } else { - if !strongSelf.didSetReady { - strongSelf.didSetReady = true - strongSelf._ready.set(.single(true)) + if !strongSelf.didSetHistoryReady { + strongSelf.didSetHistoryReady = true + strongSelf._historyReady.set(.single(true)) } } } else { @@ -886,9 +891,9 @@ public class ChatController: ViewController { } } - if !strongSelf.didSetReady { - strongSelf.didSetReady = true - strongSelf._ready.set(.single(true)) + if !strongSelf.didSetHistoryReady { + strongSelf.didSetHistoryReady = true + strongSelf._historyReady.set(.single(true)) } completion() @@ -944,16 +949,18 @@ public class ChatController: ViewController { }) } - func updateChatInterfaceState(animated: Bool = true, _ f: (ChatInterfaceState) -> ChatInterfaceState) { - let updatedChatInterfaceState = f(self.presentationInterfaceState.interfaceState) + func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) { + let temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState) + let inputContext = inputContextForChatPresentationIntefaceState(temporaryChatPresentationInterfaceState, account: self.account) + let updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputContext { _ in return inputContext } if self.isNodeLoaded { - self.chatDisplayNode.updateChatInterfaceState(updatedChatInterfaceState, animated: animated) + self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated) } - self.presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: updatedChatInterfaceState, peer: nil) - self.chatInterfaceStatePromise.set(.single(updatedChatInterfaceState)) + self.presentationInterfaceState = updatedChatPresentationInterfaceState + self.chatInterfaceStatePromise.set(.single(updatedChatPresentationInterfaceState.interfaceState)) - if let button = leftNavigationButtonForChatInterfaceState(updatedChatInterfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { + if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { self.navigationItem.setLeftBarButton(button.buttonItem, animated: true) self.leftNavigationButton = button } else if let _ = self.leftNavigationButton { @@ -961,7 +968,7 @@ public class ChatController: ViewController { self.leftNavigationButton = nil } - if let button = rightNavigationButtonForChatInterfaceState(updatedChatInterfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { + if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { self.navigationItem.setRightBarButton(button.buttonItem, animated: true) self.rightNavigationButton = button } else if let _ = self.rightNavigationButton { @@ -970,9 +977,9 @@ public class ChatController: ViewController { } if let controllerInteraction = self.controllerInteraction { - if updatedChatInterfaceState.selectionState != controllerInteraction.selectionState { - let animated = controllerInteraction.selectionState == nil || updatedChatInterfaceState.selectionState == nil - controllerInteraction.selectionState = updatedChatInterfaceState.selectionState + if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState { + let animated = controllerInteraction.selectionState == nil || updatedChatPresentationInterfaceState.interfaceState.selectionState == nil + controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState self.chatDisplayNode.listView.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateSelectionState(animated: animated) @@ -997,7 +1004,7 @@ public class ChatController: ViewController { private func navigationButtonAction(_ action: ChatNavigationButtonAction) { switch action { case .cancelMessageSelection: - self.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() }) + self.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } }) case .clearHistory: let actionSheet = ActionSheetController() actionSheet.setItemGroups([ActionSheetItemGroup(items: [ diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 2518a54154..2e44168fb7 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -7,6 +7,11 @@ import TelegramCore private let backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0") +private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: ChatPresentationInterfaceState, _ rhs: ChatPresentationInterfaceState) -> Bool { + + return false +} + enum ChatMessageViewPosition: Equatable { case AroundUnread(count: Int) case Around(index: MessageIndex, anchorIndex: MessageIndex) @@ -46,20 +51,22 @@ class ChatControllerNode: ASDisplayNode { let backgroundNode: ASDisplayNode let listView: ListView - let inputPanelBackgroundNode: ASDisplayNode + private let inputPanelBackgroundNode: ASDisplayNode + private let inputPanelBackgroundSeparatorNode: ASDisplayNode - var inputPanelNode: ChatInputPanelNode? - var accessoryPanelNode: AccessoryPanelNode? + private var inputPanelNode: ChatInputPanelNode? + private var accessoryPanelNode: AccessoryPanelNode? + private var inputContextPanelNode: ChatInputContextPanelNode? - var textInputPanelNode: ChatTextInputPanelNode? + private var textInputPanelNode: ChatTextInputPanelNode? let navigateToLatestButton: ChatHistoryNavigationButtonNode private var ignoreUpdateHeight = false - var chatInterfaceState = ChatInterfaceState() + var chatPresentationInterfaceState = ChatPresentationInterfaceState() - var requestUpdateChatInterfaceState: (ChatInterfaceState, Bool) -> Void = { _ in } + var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } @@ -81,6 +88,11 @@ class ChatControllerNode: ASDisplayNode { self.inputPanelBackgroundNode = ASDisplayNode() self.inputPanelBackgroundNode.backgroundColor = UIColor(0xfafafa) + self.inputPanelBackgroundNode.isLayerBacked = true + + self.inputPanelBackgroundSeparatorNode = ASDisplayNode() + self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xcdccd3) + self.inputPanelBackgroundSeparatorNode.isLayerBacked = true self.navigateToLatestButton = ChatHistoryNavigationButtonNode() self.navigateToLatestButton.alpha = 0.0 @@ -97,10 +109,11 @@ class ChatControllerNode: ASDisplayNode { self.addSubnode(self.listView) self.addSubnode(self.inputPanelBackgroundNode) + self.addSubnode(self.inputPanelBackgroundSeparatorNode) self.addSubnode(self.navigateToLatestButton) - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + self.listView.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.textInputPanelNode = ChatTextInputPanelNode() self.textInputPanelNode?.updateHeight = { [weak self] in @@ -119,12 +132,12 @@ class ChatControllerNode: ASDisplayNode { if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), false) + strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil) }) strongSelf.ignoreUpdateHeight = false } }) - let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatInterfaceState.replyMessageId).start() + let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId).start() } } @@ -171,10 +184,11 @@ class ChatControllerNode: ASDisplayNode { var dismissedInputPanelNode: ASDisplayNode? var dismissedAccessoryPanelNode: ASDisplayNode? + var dismissedInputContextPanelNode: ChatInputContextPanelNode? var inputPanelSize: CGSize? var immediatelyLayoutInputPanelAndAnimateAppearance = false - if let inputPanelNode = inputPanelForChatIntefaceState(self.chatInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) { + if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) { inputPanelSize = inputPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) if inputPanelNode !== self.inputPanelNode { @@ -193,7 +207,7 @@ class ChatControllerNode: ASDisplayNode { var accessoryPanelSize: CGSize? var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false - if let accessoryPanelNode = accessoryPanelForChatIntefaceState(self.chatInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { + if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) if accessoryPanelNode !== self.accessoryPanelNode { @@ -208,7 +222,7 @@ class ChatControllerNode: ASDisplayNode { accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { - strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), true) + strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) }) } } @@ -220,6 +234,21 @@ class ChatControllerNode: ASDisplayNode { self.accessoryPanelNode = nil } + var immediatelyLayoutInputContextPanelAndAnimateAppearance = false + if let inputContextPanelNode = inputContextPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputContextPanelNode, interfaceInteraction: self.interfaceInteraction) { + if inputContextPanelNode !== self.inputContextPanelNode { + dismissedInputContextPanelNode = self.inputContextPanelNode + self.inputContextPanelNode = inputContextPanelNode + + self.insertSubnode(inputContextPanelNode, aboveSubnode: self.navigateToLatestButton) + immediatelyLayoutInputContextPanelAndAnimateAppearance = true + + } + } else if let inputContextPanelNode = self.inputContextPanelNode { + dismissedInputContextPanelNode = inputContextPanelNode + self.inputContextPanelNode = nil + } + var inputPanelsHeight: CGFloat = 0.0 var inputPanelFrame: CGRect? @@ -244,6 +273,7 @@ class ChatControllerNode: ASDisplayNode { let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) + transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: inputBackgroundFrame.origin, size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame) if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) { @@ -267,6 +297,19 @@ class ChatControllerNode: ASDisplayNode { transition.updateAlpha(node: accessoryPanelNode, alpha: 1.0) } + let inputContextPanelsFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - insets.bottom - inputPanelsHeight - insets.top))) + + if let inputContextPanelNode = self.inputContextPanelNode { + if immediatelyLayoutInputContextPanelAndAnimateAppearance { + inputContextPanelNode.frame = inputContextPanelsFrame + inputContextPanelNode.updateFrames(transition: .immediate) + inputContextPanelNode.animateIn() + } else if !inputContextPanelNode.frame.equalTo(inputContextPanelsFrame) { + transition.updateFrame(node: inputContextPanelNode, frame: inputContextPanelsFrame) + inputContextPanelNode.updateFrames(transition: transition) + } + } + if let dismissedInputPanelNode = dismissedInputPanelNode { var frameCompleted = false var alphaCompleted = false @@ -312,11 +355,43 @@ class ChatControllerNode: ASDisplayNode { completed() }) } + + if let dismissedInputContextPanelNode = dismissedInputContextPanelNode { + var frameCompleted = false + var animationCompleted = false + var completed = { [weak dismissedInputContextPanelNode] in + if let dismissedInputContextPanelNode = dismissedInputContextPanelNode, frameCompleted, animationCompleted { + dismissedInputContextPanelNode.removeFromSupernode() + } + } + if !dismissedInputContextPanelNode.frame.equalTo(inputContextPanelsFrame) { + transition.updateFrame(node: dismissedInputContextPanelNode, frame: inputContextPanelsFrame, completion: { _ in + frameCompleted = true + completed() + }) + } else { + frameCompleted = true + } + + dismissedInputContextPanelNode.animateOut(completion: { + animationCompleted = true + completed() + }) + } } - func updateChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, animated: Bool) { - if self.chatInterfaceState != chatInterfaceState { - self.chatInterfaceState = chatInterfaceState + func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool) { + if let textInputPanelNode = self.textInputPanelNode { + self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedInputState(textInputPanelNode.inputTextState) } + } + + if self.chatPresentationInterfaceState != chatPresentationInterfaceState { + var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState + self.chatPresentationInterfaceState = chatPresentationInterfaceState + + if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { + textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState + } if !self.ignoreUpdateHeight { self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) diff --git a/TelegramUI/ChatInputContextPanelNode.swift b/TelegramUI/ChatInputContextPanelNode.swift new file mode 100644 index 0000000000..9e42ca349f --- /dev/null +++ b/TelegramUI/ChatInputContextPanelNode.swift @@ -0,0 +1,18 @@ +import Foundation +import AsyncDisplayKit +import Display + +class ChatInputContextPanelNode: ASDisplayNode { + var interfaceInteraction: ChatPanelInterfaceInteraction? + + func updateFrames(transition: ContainedViewLayoutTransition) { + } + + func animateIn() { + + } + + func animateOut(completion: @escaping () -> Void) { + completion() + } +} diff --git a/TelegramUI/ChatInputPanelNode.swift b/TelegramUI/ChatInputPanelNode.swift index c245917890..2934a34f9f 100644 --- a/TelegramUI/ChatInputPanelNode.swift +++ b/TelegramUI/ChatInputPanelNode.swift @@ -1,9 +1,13 @@ import Foundation import AsyncDisplayKit import Display +import Postbox +import TelegramCore class ChatInputPanelNode: ASDisplayNode { + var account: Account? var interfaceInteraction: ChatPanelInterfaceInteraction? + var peer: Peer? func updateFrames(transition: ContainedViewLayoutTransition) { } diff --git a/TelegramUI/ChatInterfaceInputContextPanels.swift b/TelegramUI/ChatInterfaceInputContextPanels.swift new file mode 100644 index 0000000000..0707a836e7 --- /dev/null +++ b/TelegramUI/ChatInterfaceInputContextPanels.swift @@ -0,0 +1,30 @@ +import Foundation +import TelegramCore + +func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? { + guard let inputContext = chatPresentationInterfaceState.inputContext, let peer = chatPresentationInterfaceState.peer else { + return nil + } + + switch inputContext { + case .hashtag: + if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode { + return currentPanel + } else { + let panel = HashtagChatInputContextPanelNode() + panel.interfaceInteraction = interfaceInteraction + return panel + } + case .mention: + if let currentPanel = currentPanel as? MentionChatInputContextPanelNode { + return currentPanel + } else { + let panel = MentionChatInputContextPanelNode() + panel.interfaceInteraction = interfaceInteraction + panel.setup(account: account, peerId: peer.id, query: "") + return panel + } + } + + return nil +} diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift new file mode 100644 index 0000000000..994d84e489 --- /dev/null +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -0,0 +1,12 @@ +import Foundation +import TelegramCore +import Postbox + +func inputContextForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatPresentationInputContext? { + if chatPresentationInterfaceState.interfaceState.inputState.inputText == "#" { + return .hashtag + } else if chatPresentationInterfaceState.interfaceState.inputState.inputText == "@" { + return .mention + } + return nil +} diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index e0489cd3e5..6897deedc4 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -9,29 +9,52 @@ struct ChatInterfaceSelectionState: Equatable { } } +struct ChatTextInputState: Equatable { + let inputText: String + let selectionRange: Range + + static func ==(lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool { + return lhs.inputText == rhs.inputText && lhs.selectionRange == rhs.selectionRange + } + + init() { + self.inputText = "" + self.selectionRange = 0 ..< 0 + } + + init(inputText: String, selectionRange: Range) { + self.inputText = inputText + self.selectionRange = selectionRange + } +} + final class ChatInterfaceState: Equatable { - let inputText: String? + let inputState: ChatTextInputState let replyMessageId: MessageId? let selectionState: ChatInterfaceSelectionState? init() { - self.inputText = nil + self.inputState = ChatTextInputState() self.replyMessageId = nil self.selectionState = nil } - init(inputText: String?, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) { - self.inputText = inputText + init(inputState: ChatTextInputState, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) { + self.inputState = inputState self.replyMessageId = replyMessageId self.selectionState = selectionState } static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { - return lhs.inputText == rhs.inputText && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState + 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) } func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(inputText: self.inputText, replyMessageId: replyMessageId, selectionState: self.selectionState) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: replyMessageId, selectionState: self.selectionState) } func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -40,7 +63,7 @@ final class ChatInterfaceState: Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -53,10 +76,10 @@ final class ChatInterfaceState: Equatable { } else { selectedIds.insert(messageId) } - return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) } func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: nil) + return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: nil) } } diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index 6c0fc6a390..65ae7174a1 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -2,12 +2,12 @@ import Foundation import AsyncDisplayKit import TelegramCore -func accessoryPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { - if let _ = chatInterfaceState.selectionState { +func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { + if let _ = chatPresentationInterfaceState.interfaceState.selectionState { return nil } - if let replyMessageId = chatInterfaceState.replyMessageId { + if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId { if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { replyPanelNode.interfaceInteraction = interfaceInteraction return replyPanelNode diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift new file mode 100644 index 0000000000..8a7d887f39 --- /dev/null +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -0,0 +1,48 @@ +import Foundation +import Postbox +import TelegramCore +import Display +import UIKit + +func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, message: Message, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ContextMenuController? { + guard let peer = chatPresentationInterfaceState.peer, let interfaceInteraction = interfaceInteraction else { + return nil + } + + var actions: [ContextMenuAction] = [] + + var canReply = false + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + switch channel.role { + case .creator, .editor, .moderator: + canReply = true + case .member: + canReply = false + } + } else { + canReply = true + } + if canReply { + actions.append(ContextMenuAction(content: .text("Reply"), action: { + interfaceInteraction.setupReplyMessage(message.id) + })) + } + + actions.append(ContextMenuAction(content: .text("Copy"), action: { + if !message.text.isEmpty { + UIPasteboard.general.string = message.text + } + })) + + actions.append(ContextMenuAction(content: .text("More..."), action: { + interfaceInteraction.beginMessageSelection(message.id) + })) + + + if !actions.isEmpty { + let contextMenuController = ContextMenuController(actions: actions) + return contextMenuController + } else { + return nil + } +} diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift index fdf665035d..7268e45e93 100644 --- a/TelegramUI/ChatInterfaceStateInputPanels.swift +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -2,31 +2,78 @@ import Foundation import AsyncDisplayKit import TelegramCore -func inputPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { - if let selectionState = chatInterfaceState.selectionState { +func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { + if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState { if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { currentPanel.selectedMessageCount = selectionState.selectedIds.count currentPanel.interfaceInteraction = interfaceInteraction + currentPanel.peer = chatPresentationInterfaceState.peer return currentPanel } else { let panel = ChatMessageSelectionInputPanelNode() + panel.account = account + panel.peer = chatPresentationInterfaceState.peer panel.selectedMessageCount = selectionState.selectedIds.count panel.interfaceInteraction = interfaceInteraction return panel } } else { - if let currentPanel = currentPanel as? ChatTextInputPanelNode { - currentPanel.interfaceInteraction = interfaceInteraction - return currentPanel - } else { - if let textInputPanelNode = textInputPanelNode { - textInputPanelNode.interfaceInteraction = interfaceInteraction - return textInputPanelNode - } else { - let panel = ChatTextInputPanelNode() - panel.interfaceInteraction = interfaceInteraction - return panel + if let peer = chatPresentationInterfaceState.peer { + if let channel = peer as? TelegramChannel { + switch channel.info { + case .broadcast: + switch channel.role { + case .creator, .editor, .moderator: + break + case .member: + if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { + currentPanel.peer = peer + return currentPanel + } else { + let panel = ChatChannelSubscriberInputPanelNode() + panel.account = account + panel.peer = peer + return panel + } + } + case .group: + switch channel.participationStatus { + case .kicked, .left: + if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode { + currentPanel.peer = peer + return currentPanel + } else { + let panel = ChatChannelSubscriberInputPanelNode() + panel.account = account + panel.peer = peer + return panel + } + case .member: + break + } + } } + + if let currentPanel = currentPanel as? ChatTextInputPanelNode { + currentPanel.interfaceInteraction = interfaceInteraction + currentPanel.peer = peer + return currentPanel + } else { + if let textInputPanelNode = textInputPanelNode { + textInputPanelNode.interfaceInteraction = interfaceInteraction + textInputPanelNode.account = account + textInputPanelNode.peer = peer + return textInputPanelNode + } else { + let panel = ChatTextInputPanelNode() + panel.interfaceInteraction = interfaceInteraction + panel.account = account + panel.peer = peer + return panel + } + } + } else { + return nil } } } diff --git a/TelegramUI/ChatListItem.swift b/TelegramUI/ChatListItem.swift index 69f4fc46a5..91b408417f 100644 --- a/TelegramUI/ChatListItem.swift +++ b/TelegramUI/ChatListItem.swift @@ -108,7 +108,7 @@ class ChatListItemNode: ListViewItemNode { private let highlightedBackgroundNode: ASDisplayNode - let avatarNode: ChatListAvatarNode + let avatarNode: AvatarNode let contentNode: ASDisplayNode let titleNode: TextNode let textNode: TextNode @@ -121,7 +121,7 @@ class ChatListItemNode: ListViewItemNode { var relativePosition: (first: Bool, last: Bool) = (false, false) required init() { - self.avatarNode = ChatListAvatarNode(font: Font.regular(24.0)) + self.avatarNode = AvatarNode(font: Font.regular(24.0)) self.avatarNode.isLayerBacked = true self.highlightedBackgroundNode = ASDisplayNode() diff --git a/TelegramUI/ChatMessageAvatarAccessoryItem.swift b/TelegramUI/ChatMessageAvatarAccessoryItem.swift index 3ae4cdd0e4..ba1bcf5e86 100644 --- a/TelegramUI/ChatMessageAvatarAccessoryItem.swift +++ b/TelegramUI/ChatMessageAvatarAccessoryItem.swift @@ -35,10 +35,10 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem { } final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode { - let avatarNode: ChatListAvatarNode + let avatarNode: AvatarNode override init() { - self.avatarNode = ChatListAvatarNode(font: Font.regular(14.0)) + self.avatarNode = AvatarNode(font: Font.regular(14.0)) self.avatarNode.isLayerBacked = true self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 8f66db7768..4873a08ff0 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -234,7 +234,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return { item, width, mergedTop, mergedBottom in let message = item.message - let incoming = item.account.peerId != message.author?.id + let incoming = item.message.effectivelyIncoming + let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 @@ -718,7 +719,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var incoming = true if let item = self.item { selected = selectionState.selectedIds.contains(item.message.id) - incoming = item.message.flags.contains(.Incoming) + incoming = item.message.effectivelyIncoming } let offset: CGFloat = incoming ? 42.0 : 0.0 diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 4142fa5679..cbc7302cb3 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -51,7 +51,7 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { self.message = message var accessoryItem: ListViewAccessoryItem? - let incoming = account.peerId != message.author?.id + let incoming = message.effectivelyIncoming let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel if displayAuthorInfo { diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 04a5caa738..f2b3831569 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -40,7 +40,7 @@ struct ChatMessageItemLayoutConstants { self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.5, mergedSpacing: 0.0, maximumWidthFillFactor: 0.9, minimumSize: CGSize(width: 40.0, height: 33.0), contentInsets: UIEdgeInsets(top: 1.0, left: 6.0, bottom: 1.0, right: 1.0)) self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 5.0, left: 9.0, bottom: 4.0, right: 9.0)) - self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0), defaultCornerRadius: 15.0, mergedCornerRadius: 4.0, contentMergedCornerRadius: 2.0) + self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0), defaultCornerRadius: 15.0, mergedCornerRadius: 4.0, contentMergedCornerRadius: 5.0) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) } } diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 5b9981807e..158573a072 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -1,11 +1,40 @@ import Foundation import AsyncDisplayKit import Display +import Postbox +import TelegramCore final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { private let deleteButton: UIButton private let forwardButton: UIButton + override var peer: Peer? { + didSet { + var canDelete = false + if let channel = self.peer as? TelegramChannel { + switch channel.info { + case .broadcast: + switch channel.role { + case .creator, .editor, .moderator: + canDelete = true + case .member: + canDelete = false + } + case .group: + switch channel.role { + case .creator, .editor, .moderator: + canDelete = true + case .member: + canDelete = false + } + } + } else { + canDelete = true + } + self.deleteButton.isHidden = !canDelete + } + } + var selectedMessageCount: Int = 0 { didSet { self.deleteButton.isEnabled = self.selectedMessageCount != 0 diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index b7f49fb5be..ecadc2f20c 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -56,7 +56,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { let imageLayout = self.imageNode.asyncLayout() return { item, width, mergedTop, mergedBottom in - let incoming = item.account.peerId != item.message.author?.id + let incoming = item.message.effectivelyIncoming var imageSize: CGSize = CGSize(width: 100.0, height: 100.0) if let telegramFile = telegramFile { if let thumbnailSize = telegramFile.previewRepresentations.first?.dimensions { diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 64ac5f6439..7db880bd3c 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -35,7 +35,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return (CGFloat.greatestFiniteMagnitude, { constrainedSize in let message = item.message - let incoming = item.account.peerId != message.author?.id + let incoming = item.message.effectivelyIncoming let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index 668e922a21..07e00d6164 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -1,11 +1,18 @@ import Foundation +import Postbox final class ChatPanelInterfaceInteraction { + let setupReplyMessage: (MessageId) -> Void + let beginMessageSelection: (MessageId) -> Void let deleteSelectedMessages: () -> Void let forwardSelectedMessages: () -> Void + let updateTextInputState: (ChatTextInputState) -> Void - init(deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void) { + init(setupReplyMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping (ChatTextInputState) -> Void) { + self.setupReplyMessage = setupReplyMessage + self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.forwardSelectedMessages = forwardSelectedMessages + self.updateTextInputState = updateTextInputState } } diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift index 009197499e..025c1e42e3 100644 --- a/TelegramUI/ChatPresentationInterfaceState.swift +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -1,7 +1,56 @@ import Foundation import Postbox -struct ChatPresentationInterfaceState { +enum ChatPresentationInputContext { + case hashtag + case mention +} + +struct ChatPresentationInterfaceState: Equatable { let interfaceState: ChatInterfaceState let peer: Peer? + let inputContext: ChatPresentationInputContext? + + init() { + self.interfaceState = ChatInterfaceState() + self.peer = nil + self.inputContext = nil + } + + init(interfaceState: ChatInterfaceState, peer: Peer?, inputContext: ChatPresentationInputContext?) { + self.interfaceState = interfaceState + self.peer = peer + self.inputContext = inputContext + } + + static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool { + if lhs.interfaceState != rhs.interfaceState { + return false + } + if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer { + if !lhsPeer.isEqual(rhsPeer) { + return false + } + } else if (lhs.peer == nil) != (rhs.peer == nil) { + return false + } + + if lhs.inputContext != rhs.inputContext { + return false + } + + return true + } + + func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputContext: self.inputContext) + } + + func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputContext: self.inputContext) + } + + func updatedInputContext(_ f: (ChatPresentationInputContext?) -> ChatPresentationInputContext?) -> ChatPresentationInterfaceState { + return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputContext: f(self.inputContext)) + } } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 9b3164c304..901bea25f1 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -2,7 +2,8 @@ import Foundation import UIKit import Display import AsyncDisplayKit -import WebKit +import Postbox +import TelegramCore private let textInputViewBackground: UIImage = { let diameter: CGFloat = 10.0 @@ -36,6 +37,48 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var sendMessage: () -> Void = { } var updateHeight: () -> Void = { } + private var updatingInputState = false + + private var currentPlaceholder: String? + override var peer: Peer? { + didSet { + if let peer = self.peer, oldValue == nil || !peer.isEqual(oldValue!) { + let placeholder: String + if let channel = peer as? TelegramChannel, case .broadcast = channel.info { + placeholder = "Broadcast" + } else { + placeholder = "Message" + } + if self.currentPlaceholder != placeholder { + self.currentPlaceholder = placeholder + let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) + let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil) + self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) + let _ = placeholderApply() + } + } + } + } + + 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(16.0), textColor: UIColor.black) + textInputNode.selectedRange = NSMakeRange(value.selectionRange.lowerBound, value.selectionRange.count) + self.updatingInputState = false + } + } + } + var text: String { get { return self.textInputNode?.attributedText?.string ?? "" @@ -81,7 +124,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.view.addSubview(self.sendButton) self.textInputBackgroundView.clipsToBounds = true - self.textInputBackgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))) + let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) + recognizer.touchDown = { [weak self] in + if let strongSelf = self { + strongSelf.ensureFocused() + } + } + self.textInputBackgroundView.addGestureRecognizer(recognizer) self.textInputBackgroundView.isUserInteractionEnabled = true } @@ -94,6 +143,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)] textInputNode.clipsToBounds = true textInputNode.delegate = self + textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0) self.addSubnode(textInputNode) self.textInputNode = textInputNode @@ -103,6 +153,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundView.isUserInteractionEnabled = false self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0]) + + let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:))) + recognizer.touchDown = { [weak self] in + if let strongSelf = self { + strongSelf.ensureFocused() + } + } + textInputNode.view.addGestureRecognizer(recognizer) } override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { @@ -155,6 +213,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.invalidateCalculatedLayout() self.updateHeight() } + + self.interfaceInteraction?.updateTextInputState(self.inputTextState) + } + } + + @objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) { + if !dueToEditing && !updatingInputState { + self.interfaceInteraction?.updateTextInputState(self.inputTextState) } } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 0d3f694d42..3bb7775ab3 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -123,7 +123,7 @@ class ContactsPeerItemNode: ListViewItemNode { private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode - private let avatarNode: ChatListAvatarNode + private let avatarNode: AvatarNode private let titleNode: TextNode private let statusNode: TextNode @@ -140,7 +140,7 @@ class ContactsPeerItemNode: ListViewItemNode { self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - self.avatarNode = ChatListAvatarNode(font: Font.regular(15.0)) + self.avatarNode = AvatarNode(font: Font.regular(15.0)) self.avatarNode.isLayerBacked = true self.titleNode = TextNode() diff --git a/TelegramUI/ContactsVCardItem.swift b/TelegramUI/ContactsVCardItem.swift index c1a7aa2673..d514409f11 100644 --- a/TelegramUI/ContactsVCardItem.swift +++ b/TelegramUI/ContactsVCardItem.swift @@ -65,7 +65,7 @@ class ContactsVCardItemNode: ListViewItemNode { private let separatorNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode - private let avatarNode: ChatListAvatarNode + private let avatarNode: AvatarNode private let titleNode: TextNode private let statusNode: TextNode @@ -82,7 +82,7 @@ class ContactsVCardItemNode: ListViewItemNode { self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.isLayerBacked = true - self.avatarNode = ChatListAvatarNode(font: Font.regular(15.0)) + self.avatarNode = AvatarNode(font: Font.regular(15.0)) self.avatarNode.isLayerBacked = true self.titleNode = TextNode() diff --git a/TelegramUI/HashtagChatInputContextPanelNode.swift b/TelegramUI/HashtagChatInputContextPanelNode.swift new file mode 100644 index 0000000000..d641ad0658 --- /dev/null +++ b/TelegramUI/HashtagChatInputContextPanelNode.swift @@ -0,0 +1,76 @@ +import Foundation +import AsyncDisplayKit +import Postbox +import TelegramCore +import Display + +final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode, UITableViewDelegate, UITableViewDataSource { + private let tableView: UITableView + private let tableBackgroundView: UIView + + private var results: [String] = [] + + override init() { + self.tableView = UITableView(frame: CGRect(), style: .plain) + self.tableBackgroundView = UIView() + self.tableBackgroundView.backgroundColor = UIColor.white + + super.init() + + self.clipsToBounds = true + + self.tableView.dataSource = self + self.tableView.delegate = self + self.tableView.rowHeight = 42.0 + self.tableView.showsVerticalScrollIndicator = false + self.tableView.backgroundColor = nil + self.tableView.isOpaque = false + + self.view.addSubview(self.tableBackgroundView) + self.view.addSubview(self.tableView) + + self.results = (0 ..< 50).map { "#tag \($0)" } + } + + func setup(account: Account, peerId: PeerId, query: String) { + } + + private func updateTable() { + let itemsHeight = CGFloat(self.results.count) * self.tableView.rowHeight + let minimalDisplayedItemsHeight = floor(self.tableView.rowHeight * 3.5) + let topInset = max(0.0, self.bounds.size.height - min(itemsHeight, minimalDisplayedItemsHeight)) + + self.tableView.contentInset = UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0) + self.tableView.contentOffset = CGPoint(x: 0.0, y: -topInset) + self.tableView.setNeedsLayout() + } + + override func updateFrames(transition: ContainedViewLayoutTransition) { + self.tableView.frame = self.bounds + self.updateTable() + } + + override func animateIn() { + self.layer.animateBounds(from: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), to: self.layer.bounds, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring) + } + + override func animateOut(completion: @escaping () -> Void) { + self.layer.animateBounds(from: self.layer.bounds, to: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.results.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + var cell = (tableView.dequeueReusableCell(withIdentifier: "C") as? HashtagsTableCell) ?? HashtagsTableCell() + cell.textLabel?.text = self.results[indexPath.row] + return cell + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.tableBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -self.tableView.contentOffset.y)), size: self.bounds.size) + } +} diff --git a/TelegramUI/HashtagsTableCell.swift b/TelegramUI/HashtagsTableCell.swift new file mode 100644 index 0000000000..3d61b42434 --- /dev/null +++ b/TelegramUI/HashtagsTableCell.swift @@ -0,0 +1,12 @@ +import Foundation +import UIKit + +final class HashtagsTableCell: UITableViewCell { + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/TelegramUI/HorizontalPeerItem.swift b/TelegramUI/HorizontalPeerItem.swift index 3e1ccd5300..a841daef8e 100644 --- a/TelegramUI/HorizontalPeerItem.swift +++ b/TelegramUI/HorizontalPeerItem.swift @@ -34,13 +34,13 @@ final class HorizontalPeerItem: ListViewItem { } private final class HorizontalPeerItemNode: ListViewItemNode { - private let avatarNode: ChatListAvatarNode + private let avatarNode: AvatarNode private let titleNode: ASTextNode private var peer: Peer? fileprivate var action: ((PeerId) -> Void)? init() { - self.avatarNode = ChatListAvatarNode(font: Font.regular(14.0)) + self.avatarNode = AvatarNode(font: Font.regular(14.0)) //self.avatarNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((92.0 - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) diff --git a/TelegramUI/ImageRepresentationsUtils.swift b/TelegramUI/ImageRepresentationsUtils.swift deleted file mode 100644 index 784919d36e..0000000000 --- a/TelegramUI/ImageRepresentationsUtils.swift +++ /dev/null @@ -1,39 +0,0 @@ -import TelegramCore - -func smallestImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? { - if representations.count == 0 { - return nil - } else { - var dimensions = representations[0].dimensions - var index = 0 - - for i in 1 ..< representations.count { - let representationDimensions = representations[i].dimensions - if representationDimensions.width < dimensions.width && representationDimensions.height < dimensions.height { - dimensions = representationDimensions - index = i - } - } - - return representations[index] - } -} - -func largestImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? { - if representations.count == 0 { - return nil - } else { - var dimensions = representations[0].dimensions - var index = 0 - - for i in 1 ..< representations.count { - let representationDimensions = representations[i].dimensions - if representationDimensions.width > dimensions.width && representationDimensions.height > dimensions.height { - dimensions = representationDimensions - index = i - } - } - - return representations[index] - } -} diff --git a/TelegramUI/MentionChatInputContextPanelNode.swift b/TelegramUI/MentionChatInputContextPanelNode.swift new file mode 100644 index 0000000000..43f0663b92 --- /dev/null +++ b/TelegramUI/MentionChatInputContextPanelNode.swift @@ -0,0 +1,107 @@ +import Foundation +import AsyncDisplayKit +import Postbox +import TelegramCore +import Display +import SwiftSignalKit + +final class MentionChatInputContextPanelNode: ChatInputContextPanelNode, UITableViewDelegate, UITableViewDataSource { + private let tableView: UITableView + private let tableBackgroundView: UIView + + private var account: Account? + private var results: [Peer] = [] + + private let disposable = MetaDisposable() + + override init() { + self.tableView = UITableView(frame: CGRect(), style: .plain) + self.tableBackgroundView = UIView() + self.tableBackgroundView.backgroundColor = UIColor.white + + super.init() + + self.clipsToBounds = true + + self.tableView.dataSource = self + self.tableView.delegate = self + self.tableView.rowHeight = 42.0 + self.tableView.showsVerticalScrollIndicator = false + self.tableView.backgroundColor = nil + self.tableView.isOpaque = false + self.tableView.separatorInset = UIEdgeInsets(top: 0.0, left: 61.0, bottom: 0.0, right: 0.0) + + self.view.addSubview(self.tableBackgroundView) + self.view.addSubview(self.tableView) + } + + deinit { + self.disposable.dispose() + } + + func setup(account: Account, peerId: PeerId, query: String) { + self.account = account + let signal = peerParticipants(account: account, id: peerId) + |> deliverOnMainQueue + + self.disposable.set(signal.start(next: { [weak self] peers in + if let strongSelf = self { + strongSelf.results = peers + strongSelf.tableView.reloadData() + strongSelf.updateTable(animated: true) + } + })) + } + + private func updateTable(animated: Bool = false) { + let itemsHeight = CGFloat(self.results.count) * self.tableView.rowHeight + let minimalDisplayedItemsHeight = floor(self.tableView.rowHeight * 3.5) + let topInset = max(0.0, self.bounds.size.height - min(itemsHeight, minimalDisplayedItemsHeight)) + + if animated { + self.layer.animateBounds(from: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), to: self.layer.bounds, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring) + } + + self.tableView.contentInset = UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0) + self.tableView.contentOffset = CGPoint(x: 0.0, y: -topInset) + self.tableView.setNeedsLayout() + } + + override func updateFrames(transition: ContainedViewLayoutTransition) { + self.tableView.frame = self.bounds + self.updateTable() + } + + override func animateIn() { + self.layer.animateBounds(from: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), to: self.layer.bounds, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring) + } + + override func animateOut(completion: @escaping () -> Void) { + self.layer.animateBounds(from: self.layer.bounds, to: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { _ in + completion() + }) + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.results.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + var cell = (tableView.dequeueReusableCell(withIdentifier: "C") as? MentionsTableCell) ?? MentionsTableCell() + if let account = self.account { + cell.setupPeer(account: account, peer: self.results[indexPath.row]) + } + return cell + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + self.tableBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -self.tableView.contentOffset.y)), size: self.bounds.size) + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let addressName = self.results[indexPath.row].addressName { + let string = "@" + addressName + " " + self.interfaceInteraction?.updateTextInputState(ChatTextInputState(inputText: string, selectionRange: string.characters.count ..< string.characters.count)) + } + } +} diff --git a/TelegramUI/MentionsTableCell.swift b/TelegramUI/MentionsTableCell.swift new file mode 100644 index 0000000000..e3b35541c0 --- /dev/null +++ b/TelegramUI/MentionsTableCell.swift @@ -0,0 +1,45 @@ +import Foundation +import UIKit +import Display +import Postbox +import TelegramCore + +final class MentionsTableCell: UITableViewCell { + private let avatarNode = AvatarNode(font: Font.regular(16.0)) + private let labelNode = TextNode() + private var peer: Peer? + + override init(style: UITableViewCellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.contentView.addSubnode(self.avatarNode) + self.contentView.addSubnode(self.labelNode) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setupPeer(account: Account, peer: Peer) { + self.peer = peer + self.avatarNode.setPeer(account: account, peer: peer) + self.setNeedsLayout() + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.avatarNode.frame = CGRect(origin: CGPoint(x: 16.0, y: floor((self.bounds.size.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0)) + if let peer = self.peer { + let makeLayout = TextNode.asyncLayout(self.labelNode) + let string = NSMutableAttributedString() + string.append(NSAttributedString(string: peer.displayTitle, font: Font.medium(15.0), textColor: UIColor.black)) + if let addressName = peer.addressName { + string.append(NSAttributedString(string: " @" + addressName, font: Font.regular(15.0), textColor: UIColor(0x9099a2))) + } + let (layout, apply) = makeLayout(string, nil, 1, .end, CGSize(width: self.bounds.size.width - 61.0 - 10.0, height: self.bounds.size.height), nil) + self.labelNode.frame = CGRect(origin: CGPoint(x: 61.0, y: floor((self.bounds.size.height - layout.size.height) / 2.0) + 2.0), size: layout.size) + let _ = apply() + } + } +} diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index 0a3c8952ee..d609da203f 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -20,23 +20,7 @@ private let roundCorners = { () -> UIImage in }() func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal? { - var location: TelegramCloudMediaLocation? - - if let user = peer as? TelegramUser { - if let photo = user.photo.first { - location = photo.location.cloudLocation - } - } else if let group = peer as? TelegramGroup { - if let photo = group.photo.first { - location = photo.location.cloudLocation - } - } else if let channel = peer as? TelegramChannel { - if let photo = channel.photo.first { - location = photo.location.cloudLocation - } - } - - if let location = location { + if let location = peer.smallProfileImage?.location.cloudLocation { return deferred { () -> Signal in return cachedCloudFileLocation(location) |> `catch` { _ in diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index d62ddb74ce..02874ca9dd 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -412,10 +412,10 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { c.interpolationQuality = .low c.draw(cgImage, in: fittedRect) + c.setBlendMode(.normal) } if let fullSizeImage = fullSizeImage { - c.setBlendMode(.normal) c.interpolationQuality = .medium c.draw(fullSizeImage, in: fittedRect) } diff --git a/TelegramUI/SettingsAccountInfoItem.swift b/TelegramUI/SettingsAccountInfoItem.swift index 014a0f3ca0..662f46ea32 100644 --- a/TelegramUI/SettingsAccountInfoItem.swift +++ b/TelegramUI/SettingsAccountInfoItem.swift @@ -28,13 +28,13 @@ private let nameFont = Font.medium(19.0) private let statusFont = Font.regular(15.0) class SettingsAccountInfoItemNode: ListControllerGroupableItemNode { - let avatarNode: ChatListAvatarNode + let avatarNode: AvatarNode let nameNode: TextNode let statusNode: TextNode override init() { - self.avatarNode = ChatListAvatarNode(font: Font.regular(20.0)) + self.avatarNode = AvatarNode(font: Font.regular(20.0)) self.nameNode = TextNode() self.nameNode.isLayerBacked = true diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index aa5733e01a..c5896d03cc 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -55,7 +55,7 @@ public class SettingsController: ListController { } peerAndConnectionStatusDisposable.set(peerAndConnectionStatus.start()) - peer.set(account.postbox.peerWithId(account.peerId)) + peer.set(account.postbox.loadedPeerWithId(account.peerId)) connectionStatus.set(account.network.connectionStatus) } diff --git a/TelegramUI/TouchDownGestureRecognizer.swift b/TelegramUI/TouchDownGestureRecognizer.swift index aa425c6aef..2ddb130ba3 100644 --- a/TelegramUI/TouchDownGestureRecognizer.swift +++ b/TelegramUI/TouchDownGestureRecognizer.swift @@ -1,23 +1,8 @@ import Foundation import UIKit.UIGestureRecognizerSubclass -private class TouchDownGestureRecognizerTimerTarget: NSObject { - weak var target: TouchDownGestureRecognizer? - - init(target: TouchDownGestureRecognizer) { - self.target = target - - super.init() - } - - @objc func event() { - self.target?.timerEvent() - } -} - class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { - private var touchLocation = CGPoint() - private var timer: Foundation.Timer? + var touchDown: (() -> Void)? override init(target: Any?, action: Selector?) { super.init(target: target, action: action) @@ -26,53 +11,14 @@ class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelega } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - if otherGestureRecognizer is UIPanGestureRecognizer { - return true - } - return false - } - - override func reset() { - self.timer?.invalidate() - self.timer = nil - - super.reset() - } - - func timerEvent() { - self.state = .began + return true } override func touchesBegan(_ touches: Set, with event: UIEvent) { super.touchesBegan(touches, with: event) - if let touch = touches.first { - self.touchLocation = touch.location(in: self.view) + if let touchDown = self.touchDown { + touchDown() } - - self.timer?.invalidate() - self.timer = Timer(timeInterval: 0.08, target: TouchDownGestureRecognizerTimerTarget(target: self), selector: #selector(TouchDownGestureRecognizerTimerTarget.event), userInfo: nil, repeats: false) - - if let timer = self.timer { - RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) - } - } - - override func touchesMoved(_ touches: Set, with event: UIEvent) { - super.touchesMoved(touches, with: event) - - if let touch = touches.first { - let location = touch.location(in: self.view) - let distance = CGPoint(x: location.x - self.touchLocation.x, y: location.y - self.touchLocation.y) - if distance.x * distance.x + distance.y * distance.y > 4.0 { - self.state = .cancelled - } - } - } - - override func touchesEnded(_ touches: Set, with event: UIEvent) { - super.touchesEnded(touches, with: event) - - self.state = .ended } }