diff --git a/Images.xcassets/Chat/Input/Acessory Panels/CloseButton.imageset/Contents.json b/Images.xcassets/Chat/Input/Acessory Panels/CloseButton.imageset/Contents.json new file mode 100644 index 0000000000..0b160dd6d4 --- /dev/null +++ b/Images.xcassets/Chat/Input/Acessory Panels/CloseButton.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ReplyPanelClose@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Acessory Panels/CloseButton.imageset/ReplyPanelClose@2x.png b/Images.xcassets/Chat/Input/Acessory Panels/CloseButton.imageset/ReplyPanelClose@2x.png new file mode 100644 index 0000000000..8cc43a48a7 Binary files /dev/null and b/Images.xcassets/Chat/Input/Acessory Panels/CloseButton.imageset/ReplyPanelClose@2x.png differ diff --git a/Images.xcassets/Chat/Input/Acessory Panels/Contents.json b/Images.xcassets/Chat/Input/Acessory Panels/Contents.json new file mode 100644 index 0000000000..38f0c81fc2 --- /dev/null +++ b/Images.xcassets/Chat/Input/Acessory Panels/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "provides-namespace" : true + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionForward.imageset/Contents.json b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionForward.imageset/Contents.json new file mode 100644 index 0000000000..33cb99e8a2 --- /dev/null +++ b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionForward.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationActionForward@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionForward.imageset/ModernConversationActionForward@2x.png b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionForward.imageset/ModernConversationActionForward@2x.png new file mode 100644 index 0000000000..803598651b Binary files /dev/null and b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionForward.imageset/ModernConversationActionForward@2x.png differ diff --git a/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionThrash.imageset/Contents.json b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionThrash.imageset/Contents.json new file mode 100644 index 0000000000..022e47b53a --- /dev/null +++ b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionThrash.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernConversationActionDelete@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionThrash.imageset/ModernConversationActionDelete@2x.png b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionThrash.imageset/ModernConversationActionDelete@2x.png new file mode 100644 index 0000000000..66fb28fbb9 Binary files /dev/null and b/Images.xcassets/Chat/Input/Acessory Panels/MessageSelectionThrash.imageset/ModernConversationActionDelete@2x.png differ diff --git a/Images.xcassets/Chat/Message/SelectionChecked.imageset/Contents.json b/Images.xcassets/Chat/Message/SelectionChecked.imageset/Contents.json new file mode 100644 index 0000000000..250b52b202 --- /dev/null +++ b/Images.xcassets/Chat/Message/SelectionChecked.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernMessageSelectionChecked@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/SelectionChecked.imageset/ModernMessageSelectionChecked@2x.png b/Images.xcassets/Chat/Message/SelectionChecked.imageset/ModernMessageSelectionChecked@2x.png new file mode 100644 index 0000000000..fcae0992b0 Binary files /dev/null and b/Images.xcassets/Chat/Message/SelectionChecked.imageset/ModernMessageSelectionChecked@2x.png differ diff --git a/Images.xcassets/Chat/Message/SelectionUnchecked.imageset/Contents.json b/Images.xcassets/Chat/Message/SelectionUnchecked.imageset/Contents.json new file mode 100644 index 0000000000..e52ff4e95f --- /dev/null +++ b/Images.xcassets/Chat/Message/SelectionUnchecked.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ModernMessageSelectionUnchecked@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Images.xcassets/Chat/Message/SelectionUnchecked.imageset/ModernMessageSelectionUnchecked@2x.png b/Images.xcassets/Chat/Message/SelectionUnchecked.imageset/ModernMessageSelectionUnchecked@2x.png new file mode 100644 index 0000000000..01436bd883 Binary files /dev/null and b/Images.xcassets/Chat/Message/SelectionUnchecked.imageset/ModernMessageSelectionUnchecked@2x.png differ diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 9c0cfc695f..4831fd84c9 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 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 */; }; + D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */; }; + D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */; }; D08D452E1D5E340300A7428A /* AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D45291D5E340300A7428A /* AsyncDisplayKit.framework */; }; D08D452F1D5E340300A7428A /* Display.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452A1D5E340300A7428A /* Display.framework */; }; D08D45301D5E340300A7428A /* Postbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D08D452B1D5E340300A7428A /* Postbox.framework */; }; @@ -15,6 +20,16 @@ D0AB0BB31D6718EB002C78E7 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB21D6718EB002C78E7 /* libz.tbd */; }; D0AB0BB51D6718F1002C78E7 /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB41D6718F1002C78E7 /* CoreMedia.framework */; }; D0AB0BBB1D6719B5002C78E7 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */; }; + D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */; }; + D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */; }; + D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */; }; + D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */; }; + D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */; }; + D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */; }; + D0D2686C1D788F8200C422DA /* NavigationAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2686B1D788F8200C422DA /* NavigationAccessoryPanelNode.swift */; }; + 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 */; }; 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 */; }; @@ -94,7 +109,7 @@ D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2A1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift */; }; D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */; }; D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */; }; - D0F69E421D6B8B7E0046BCD6 /* ChatInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E401D6B8B7E0046BCD6 /* ChatInputView.swift */; }; + D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */; }; D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */; }; D0F69E461D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */; }; D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */; }; @@ -160,6 +175,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 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 = ""; }; + D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = ""; }; + D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = ""; }; D08D45291D5E340300A7428A /* AsyncDisplayKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AsyncDisplayKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/AsyncDisplayKit.framework"; sourceTree = ""; }; D08D452A1D5E340300A7428A /* Display.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Display.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Display.framework"; sourceTree = ""; }; D08D452B1D5E340300A7428A /* Postbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Postbox.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/Postbox.framework"; sourceTree = ""; }; @@ -171,6 +191,16 @@ D0AB0BB61D67191C002C78E7 /* MtProtoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MtProtoKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/MtProtoKit.framework"; sourceTree = ""; }; D0AB0BB71D67191C002C78E7 /* SSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphoneos/SSignalKit.framework"; sourceTree = ""; }; D0AB0BBA1D6719B5002C78E7 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; + D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPresentationInterfaceState.swift; sourceTree = ""; }; + D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputPanelNode.swift; sourceTree = ""; }; + D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateInputPanels.swift; sourceTree = ""; }; + D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionInputPanelNode.swift; sourceTree = ""; }; + D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateNavigationButtons.swift; sourceTree = ""; }; + D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatAvatarNavigationNode.swift; sourceTree = ""; }; + D0D2686B1D788F8200C422DA /* NavigationAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationAccessoryPanelNode.swift; sourceTree = ""; }; + 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 = ""; }; 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 = ""; }; @@ -250,7 +280,7 @@ D0F69E2A1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageTextBubbleContentNode.swift; sourceTree = ""; }; D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageWebpageBubbleContentNode.swift; sourceTree = ""; }; D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatUnreadItem.swift; sourceTree = ""; }; - D0F69E401D6B8B7E0046BCD6 /* ChatInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputView.swift; sourceTree = ""; }; + D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatTextInputPanelNode.swift; sourceTree = ""; }; D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResizeableTextInputView.swift; sourceTree = ""; }; D0F69E451D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryNavigationButtonNode.swift; sourceTree = ""; }; D0F69E481D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionSheetRollImageItem.swift; sourceTree = ""; }; @@ -340,6 +370,28 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D03ADB461D703250005A521C /* Interface State */ = { + isa = PBXGroup; + children = ( + D03ADB471D703268005A521C /* ChatInterfaceState.swift */, + D0B417C21D7DE54E004562A4 /* ChatPresentationInterfaceState.swift */, + D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */, + D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */, + D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */, + D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */, + ); + name = "Interface State"; + sourceTree = ""; + }; + D03ADB491D704427005A521C /* Accessory Panels */ = { + isa = PBXGroup; + children = ( + D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */, + D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */, + ); + name = "Accessory Panels"; + sourceTree = ""; + }; D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( @@ -362,6 +414,40 @@ name = Frameworks; sourceTree = ""; }; + D0BA6F811D784C3A0034826E /* Input Panels */ = { + isa = PBXGroup; + children = ( + D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */, + D0F69E3F1D6B8B6B0046BCD6 /* Text Input */, + D0BA6F861D784F700034826E /* Message Selection */, + ); + name = "Input Panels"; + sourceTree = ""; + }; + D0BA6F861D784F700034826E /* Message Selection */ = { + isa = PBXGroup; + children = ( + D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */, + ); + name = "Message Selection"; + sourceTree = ""; + }; + D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */ = { + isa = PBXGroup; + children = ( + D0D2686B1D788F8200C422DA /* NavigationAccessoryPanelNode.swift */, + ); + name = "Navigation Accessory Panels"; + sourceTree = ""; + }; + D0D2689B1D79D31500C422DA /* Share Recipients */ = { + isa = PBXGroup; + children = ( + D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */, + ); + name = "Share Recipients"; + sourceTree = ""; + }; D0F69CCE1D6B87950046BCD6 /* Files */ = { isa = PBXGroup; children = ( @@ -418,6 +504,7 @@ isa = PBXGroup; children = ( D0F69CFB1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift */, + D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */, ); name = Gestures; sourceTree = ""; @@ -485,6 +572,7 @@ D0F69E0D1D6B8AB90046BCD6 /* Chat */, D0F69E4E1D6B8BB90046BCD6 /* Media */, D0F69E6C1D6B8C220046BCD6 /* Contacts */, + D0D2689B1D79D31500C422DA /* Share Recipients */, D0F69E791D6B8C3B0046BCD6 /* Settings */, ); name = Controllers; @@ -545,8 +633,12 @@ D0F69E101D6B8ACF0046BCD6 /* ChatControllerNode.swift */, D0F69E111D6B8ACF0046BCD6 /* ChatHistoryEntry.swift */, D0F69E121D6B8ACF0046BCD6 /* ChatHistoryLocation.swift */, + D0D268681D78865300C422DA /* ChatAvatarNavigationNode.swift */, D0F69E181D6B8AD10046BCD6 /* Items */, - D0F69E3F1D6B8B6B0046BCD6 /* Input Panel */, + D03ADB461D703250005A521C /* Interface State */, + D03ADB491D704427005A521C /* Accessory Panels */, + D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */, + D0BA6F811D784C3A0034826E /* Input Panels */, D0F69E441D6B8B850046BCD6 /* History Navigation */, D0F69E471D6B8B9A0046BCD6 /* Input Media Action Sheet */, ); @@ -575,17 +667,18 @@ D0F69E2B1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift */, D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */, D0F69E191D6B8AE60046BCD6 /* ChatHoleItem.swift */, + D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */, ); name = Items; sourceTree = ""; }; - D0F69E3F1D6B8B6B0046BCD6 /* Input Panel */ = { + D0F69E3F1D6B8B6B0046BCD6 /* Text Input */ = { isa = PBXGroup; children = ( - D0F69E401D6B8B7E0046BCD6 /* ChatInputView.swift */, + D0F69E401D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift */, D0F69E411D6B8B7E0046BCD6 /* ResizeableTextInputView.swift */, ); - name = "Input Panel"; + name = "Text Input"; sourceTree = ""; }; D0F69E441D6B8B850046BCD6 /* History Navigation */ = { @@ -875,11 +968,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */, D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */, + D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */, + D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */, D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */, D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */, D0F69EA21D6B8E380046BCD6 /* PhotoResources.swift in Sources */, + D0BA6F831D784C520034826E /* ChatInputPanelNode.swift in Sources */, D0F69DC71D6B89E70046BCD6 /* TransformImageNode.swift in Sources */, + D0D2686C1D788F8200C422DA /* NavigationAccessoryPanelNode.swift in Sources */, D0F69E341D6B8B030046BCD6 /* ChatMessageForwardInfoNode.swift in Sources */, D0F69E561D6B8BDA0046BCD6 /* GalleryControllerNode.swift in Sources */, D0F69E4D1D6B8BB20046BCD6 /* ChatMediaActionSheetRollItem.swift in Sources */, @@ -901,13 +999,16 @@ D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */, D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */, D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */, + D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */, D0F69E651D6B8BF90046BCD6 /* ChatVideoGalleryItemScrubberView.swift in Sources */, - D0F69E421D6B8B7E0046BCD6 /* ChatInputView.swift in Sources */, + D03ADB4F1D70546B005A521C /* AccessoryPanelNode.swift in Sources */, + D0F69E421D6B8B7E0046BCD6 /* ChatTextInputPanelNode.swift in Sources */, D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */, D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */, D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */, D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */, D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, + D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */, @@ -927,6 +1028,7 @@ D0F69E621D6B8BF90046BCD6 /* ChatHoleGalleryItem.swift in Sources */, D0F69E331D6B8B030046BCD6 /* ChatMessageFileBubbleContentNode.swift in Sources */, D0F69E461D6B8B950046BCD6 /* ChatHistoryNavigationButtonNode.swift in Sources */, + D03ADB481D703268005A521C /* ChatInterfaceState.swift in Sources */, D0F69D671D6B87D30046BCD6 /* FFMpegPacket.swift in Sources */, D0F69E321D6B8B030046BCD6 /* ChatMessageDateAndStatusNode.swift in Sources */, D0F69E041D6B8A880046BCD6 /* ChatListSearchItem.swift in Sources */, @@ -942,6 +1044,7 @@ D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, + D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */, D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */, D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */, @@ -965,23 +1068,28 @@ D0F69E7C1D6B8C470046BCD6 /* SettingsAccountInfoItem.swift in Sources */, D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, + D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */, D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */, D0F69DC31D6B89DA0046BCD6 /* TextNode.swift in Sources */, D0F69DC11D6B89D30046BCD6 /* ListSectionHeaderNode.swift in Sources */, + D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */, D0F69E771D6B8C340046BCD6 /* ContactsSectionHeaderAccessoryItem.swift in Sources */, D0F69E631D6B8BF90046BCD6 /* ChatImageGalleryItem.swift in Sources */, + D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */, D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */, D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */, D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */, D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */, D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */, D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */, + D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */, D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */, D0F69E2D1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift in Sources */, D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */, D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */, D0F69DC91D6B89EB0046BCD6 /* ImageNode.swift in Sources */, + D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */, D0F69D311D6B87D30046BCD6 /* FFMpegMediaFrameSource.swift in Sources */, D0F69DDF1D6B8A420046BCD6 /* ListController.swift in Sources */, D0F69E3A1D6B8B030046BCD6 /* ChatMessageReplyInfoNode.swift in Sources */, @@ -1068,6 +1176,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CLANG_MODULES_AUTOLINK = NO; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; DEFINES_MODULE = YES; @@ -1213,6 +1322,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CLANG_MODULES_AUTOLINK = NO; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; DEFINES_MODULE = YES; @@ -1244,6 +1354,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; + CLANG_MODULES_AUTOLINK = NO; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; DEFINES_MODULE = YES; diff --git a/TelegramUI/AccessoryPanelNode.swift b/TelegramUI/AccessoryPanelNode.swift new file mode 100644 index 0000000000..4c177c0260 --- /dev/null +++ b/TelegramUI/AccessoryPanelNode.swift @@ -0,0 +1,13 @@ +import Foundation +import AsyncDisplayKit + +class AccessoryPanelNode: ASDisplayNode { + var dismiss: (() -> Void)? + var interfaceInteraction: ChatPanelInterfaceInteraction? + + var insets = UIEdgeInsets() { + didSet { + self.setNeedsLayout() + } + } +} diff --git a/TelegramUI/AuthorizationCodeController.swift b/TelegramUI/AuthorizationCodeController.swift index 23f996ef68..b929dcb37a 100644 --- a/TelegramUI/AuthorizationCodeController.swift +++ b/TelegramUI/AuthorizationCodeController.swift @@ -1,7 +1,7 @@ import Foundation import Display import SwiftSignalKit -import MtProtoKit +import MtProtoKitDynamic import TelegramCore enum AuthorizationCodeResult { diff --git a/TelegramUI/AuthorizationPasswordController.swift b/TelegramUI/AuthorizationPasswordController.swift index 39971e0e0a..6b4dc29fc7 100644 --- a/TelegramUI/AuthorizationPasswordController.swift +++ b/TelegramUI/AuthorizationPasswordController.swift @@ -1,7 +1,7 @@ import Foundation import Display import SwiftSignalKit -import MtProtoKit +import MtProtoKitDynamic import TelegramCore class AuthorizationPasswordController: ViewController { diff --git a/TelegramUI/AuthorizationPhoneController.swift b/TelegramUI/AuthorizationPhoneController.swift index 9f6ed69ab8..65bbd43b24 100644 --- a/TelegramUI/AuthorizationPhoneController.swift +++ b/TelegramUI/AuthorizationPhoneController.swift @@ -1,7 +1,7 @@ import Foundation import Display import SwiftSignalKit -import MtProtoKit +import MtProtoKitDynamic import TelegramCore class AuthorizationPhoneController: ViewController { @@ -59,7 +59,13 @@ class AuthorizationPhoneController: ViewController { let range = error.errorDescription.range(of: "MIGRATE_")! let updatedMasterDatacenterId = Int32(error.errorDescription.substring(from: range.upperBound))! let updatedAccount = account.changedMasterDatacenterId(updatedMasterDatacenterId) - return updatedAccount.network.request(sendCode) |> map { sentCode in return (sentCode, updatedAccount) } + return updatedAccount + |> mapToSignalPromotingError { updatedAccount -> Signal<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in + return updatedAccount.network.request(sendCode) + |> map { sentCode in + return (sentCode, updatedAccount) + } + } case _: return .fail(error) } diff --git a/TelegramUI/ChatAvatarNavigationNode.swift b/TelegramUI/ChatAvatarNavigationNode.swift new file mode 100644 index 0000000000..5a5aa0583f --- /dev/null +++ b/TelegramUI/ChatAvatarNavigationNode.swift @@ -0,0 +1,43 @@ +import Foundation +import AsyncDisplayKit +import Display + +private let normalFont = Font.medium(16.0) +private let smallFont = Font.medium(12.0) + +final class ChatAvatarNavigationNode: ASDisplayNode { + let avatarNode: ChatListAvatarNode + + override init() { + self.avatarNode = ChatListAvatarNode(font: normalFont) + + super.init() + + self.addSubnode(self.avatarNode) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + if constrainedSize.height.isLessThanOrEqualTo(32.0) { + return CGSize(width: 26.0, height: 26.0) + } else { + return CGSize(width: 37.0, height: 37.0) + } + } + + override func layout() { + super.layout() + + let bounds = self.bounds + if self.bounds.size.height.isLessThanOrEqualTo(26.0) { + if !self.avatarNode.bounds.size.equalTo(bounds.size) { + self.avatarNode.font = smallFont + } + self.avatarNode.frame = bounds + } else { + if !self.avatarNode.bounds.size.equalTo(bounds.size) { + self.avatarNode.font = normalFont + } + self.avatarNode.frame = bounds.offsetBy(dx: 2.0, dy: 1.0) + } + } +} diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 2ec122fa98..36fed61f35 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -397,6 +397,13 @@ public class ChatController: ViewController { return self._chatHistoryLocation.get() } + private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil) + private let chatInterfaceStatePromise = Promise() + + private var leftNavigationButton: ChatNavigationButton? + private var rightNavigationButton: ChatNavigationButton? + private var chatInfoNavigationButton: ChatNavigationButton? + private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private var controllerInteraction: ChatControllerInteraction? @@ -438,7 +445,7 @@ public class ChatController: ViewController { if let galleryMedia = galleryMedia { if let file = galleryMedia as? TelegramMediaFile, file.mimeType == "audio/mpeg" { - debugPlayMedia(account: strongSelf.account, file: file) + //debugPlayMedia(account: strongSelf.account, file: file) } else { let gallery = GalleryController(account: strongSelf.account, messageId: id) @@ -474,30 +481,92 @@ public class ChatController: ViewController { } } } - }, testNavigateToMessage: { [weak self] fromId, id in + }, openPeer: { [weak self] id, navigation in + if let strongSelf = self { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id, messageId: nil)) + } + }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, let historyView = strongSelf.historyView { - var fromIndex: MessageIndex? - - for case let .MessageEntry(message) in historyView.filteredEntries where message.id == fromId { - fromIndex = MessageIndex(message) - break - } - - if let fromIndex = fromIndex { - var found = false - for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { - found = true - - strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex(message), anchorIndex: MessageIndex(message), sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))) + 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(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(message.id), animated: true) + 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.chatInterfaceState.selectionState != nil { + strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withoutSelectionState(), animated: true) + } else { + strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedSelectedMessage(message.id), animated: true) + } + + break + } + } + }) + ]) + strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in + if let node = node { + return (node, frame) + } else { + return nil + } + })) + } + }, navigateToMessage: { [weak self] fromId, id in + if let strongSelf = self, let historyView = strongSelf.historyView { + if id.peerId == strongSelf.peerId { + var fromIndex: MessageIndex? + + for case let .MessageEntry(message) in historyView.filteredEntries where message.id == fromId { + fromIndex = MessageIndex(message) + break } - if !found { - strongSelf.messageIndexDisposable.set((strongSelf.account.postbox.messageIndexAtId(id) |> deliverOnMainQueue).start(next: { [weak strongSelf] index in - if let strongSelf = strongSelf, let index = index { - strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index:index, anchorIndex: index, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))) - } - })) + if let fromIndex = fromIndex { + var found = false + for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { + found = true + + strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index: MessageIndex(message), anchorIndex: MessageIndex(message), sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))) + } + + if !found { + strongSelf.messageIndexDisposable.set((strongSelf.account.postbox.messageIndexAtId(id) |> deliverOnMainQueue).start(next: { [weak strongSelf] index in + if let strongSelf = strongSelf, let index = index { + strongSelf._chatHistoryLocation.set(.single(ChatHistoryLocation.Scroll(index:index, anchorIndex: index, sourceIndex: fromIndex, scrollPosition: .Center(.Bottom), animated: true))) + } + })) + } } + } else { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id.peerId, messageId: id)) + } + } + }, clickThroughMessage: { [weak self] in + self?.view.endEditing(true) + }, 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(strongSelf.chatInterfaceState.withToggledSelectedMessage(messageId), animated: false) + break } } }) @@ -506,10 +575,14 @@ public class ChatController: ViewController { let messageViewQueue = self.messageViewQueue + self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode())) + self.updateChatInterfaceState(self.chatInterfaceState, animated: false) + 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) } })) @@ -672,30 +745,40 @@ public class ChatController: ViewController { self.chatDisplayNode.listView.visibleContentOffsetChanged = { [weak self] offset in if let strongSelf = self { - if let offset = offset, offset < 40.0 { - if strongSelf.chatDisplayNode.navigateToLatestButton.alpha == 1.0 { - UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: { - strongSelf.chatDisplayNode.navigateToLatestButton.alpha = 0.0 - }, completion: nil) - } - } else { - if strongSelf.chatDisplayNode.navigateToLatestButton.alpha == 0.0 { - UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: { - strongSelf.chatDisplayNode.navigateToLatestButton.alpha = 1.0 - }, completion: nil) - } + let offsetAlpha: CGFloat + switch offset { + case let .known(offset): + if offset < 40.0 { + offsetAlpha = 0.0 + } else { + offsetAlpha = 1.0 + } + case .unknown: + offsetAlpha = 1.0 + case .none: + offsetAlpha = 0.0 + } + + if !strongSelf.chatDisplayNode.navigateToLatestButton.alpha.isEqual(to: offsetAlpha) { + UIView.animate(withDuration: 0.2, delay: 0.0, options: [.beginFromCurrentState], animations: { + strongSelf.chatDisplayNode.navigateToLatestButton.alpha = offsetAlpha + }, completion: nil) } } } - self.chatDisplayNode.requestLayout = { [weak self] animated in - self?.requestLayout(transition: animated ? .animated(duration: 0.1, curve: .easeInOut) : .immediate) + self.chatDisplayNode.requestLayout = { [weak self] transition in + self?.requestLayout(transition: transition) } self.chatDisplayNode.setupSendActionOnViewUpdate = { [weak self] f in self?.layoutActionOnViewTransition = f } + self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] state, animated in + self?.updateChatInterfaceState(state, animated: animated) + } + self.chatDisplayNode.displayAttachmentMenu = { [weak self] in if let strongSelf = self { let controller = ChatMediaActionSheetController() @@ -721,6 +804,22 @@ public class ChatController: ViewController { } } + self.chatDisplayNode.interfaceInteraction = ChatPanelInterfaceInteraction(deleteSelectedMessages: { [weak self] in + if let strongSelf = self { + if let messageIds = strongSelf.chatInterfaceState.selectionState?.selectedIds, !messageIds.isEmpty { + strongSelf.account.postbox.modify({ modifier in + modifier.deleteMessages(Array(messageIds)) + }).start() + } + strongSelf.updateChatInterfaceState(strongSelf.chatInterfaceState.withoutSelectionState(), animated: true) + } + }, forwardSelectedMessages: { [weak self] in + if let strongSelf = self { + let controller = ShareRecipientsActionSheetController() + strongSelf.present(controller, in: .window) + } + }) + self.displayNodeDidLoad() self.dequeueHistoryViewTransition() @@ -799,7 +898,7 @@ public class ChatController: ViewController { self.layoutActionOnViewTransition = nil layoutActionOnViewTransition() - self.chatDisplayNode.containerLayoutUpdated(self.containerLayout, navigationBarHeight: self.navigationBar.frame.maxY, transition: .animated(duration: 0.5 * 1.3, curve: .spring), listViewTransaction: { updateSizeAndInsets in + self.chatDisplayNode.containerLayoutUpdated(self.containerLayout, navigationBarHeight: self.navigationBar.frame.maxY, transition: .animated(duration: 0.4, curve: .spring), listViewTransaction: { updateSizeAndInsets in var options = transition.options let _ = options.insert(.Synchronous) let _ = options.insert(.LowLatency) @@ -819,7 +918,7 @@ public class ChatController: ViewController { insertItems.append(ListViewInsertItem(index: item.index, previousIndex: item.previousIndex, item: item.item, directionHint: item.directionHint == .Down ? .Up : nil)) } - let scrollToItem = ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Spring(speed: 1.3), directionHint: .Up) + let scrollToItem = ListViewScrollToItem(index: 0, position: .Top, animated: true, curve: .Spring(duration: 0.4), directionHint: .Up) var stationaryItemRange: (Int, Int)? if let maxInsertedItem = maxInsertedItem { @@ -843,4 +942,75 @@ public class ChatController: ViewController { self.chatDisplayNode.listView.deleteAndInsertItems(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, completion: { _ in }) }) } + + func updateChatInterfaceState(animated: Bool = true, _ f: (ChatInterfaceState) -> Void) { + + + if self.isNodeLoaded { + self.chatDisplayNode.updateChatInterfaceState(chatInterfaceState, animated: animated) + } + self.chatInterfaceState = chatInterfaceState + self.chatInterfaceStatePromise.set(.single(chatInterfaceState)) + + if let button = leftNavigationButtonForChatInterfaceState(chatInterfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { + self.navigationItem.setLeftBarButton(button.buttonItem, animated: true) + self.leftNavigationButton = button + } else if let _ = self.leftNavigationButton { + self.navigationItem.setLeftBarButton(nil, animated: true) + self.leftNavigationButton = nil + } + + if let button = rightNavigationButtonForChatInterfaceState(chatInterfaceState, 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 { + self.navigationItem.setRightBarButton(nil, animated: true) + self.rightNavigationButton = nil + } + + if let controllerInteraction = self.controllerInteraction { + if chatInterfaceState.selectionState != controllerInteraction.selectionState { + let animated = controllerInteraction.selectionState == nil || chatInterfaceState.selectionState == nil + controllerInteraction.selectionState = chatInterfaceState.selectionState + self.chatDisplayNode.listView.forEachItemNode { itemNode in + if let itemNode = itemNode as? ChatMessageItemView { + itemNode.updateSelectionState(animated: animated) + } + } + } + } + } + + @objc func leftNavigationButtonAction() { + if let button = self.leftNavigationButton { + self.navigationButtonAction(button.action) + } + } + + @objc func rightNavigationButtonAction() { + if let button = self.rightNavigationButton { + self.navigationButtonAction(button.action) + } + } + + private func navigationButtonAction(_ action: ChatNavigationButtonAction) { + switch action { + case .cancelMessageSelection: + self.updateChatInterfaceState(self.chatInterfaceState.withoutSelectionState(), animated: true) + case .clearHistory: + let actionSheet = ActionSheetController() + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Delete All Messages", color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + self.present(actionSheet, in: .window) + case .openChatInfo: + break + } + } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index f9598eb19f..8f7a741073 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -2,13 +2,27 @@ import Foundation import Postbox import AsyncDisplayKit +public enum ChatControllerInteractionNavigateToPeer { + case chat + case info +} + public final class ChatControllerInteraction { let openMessage: (MessageId) -> Void - let testNavigateToMessage: (MessageId, MessageId) -> Void + let openPeer: (PeerId, ChatControllerInteractionNavigateToPeer) -> Void + let openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void + let navigateToMessage: (MessageId, MessageId) -> Void + let clickThroughMessage: () -> Void var hiddenMedia: [MessageId: [Media]] = [:] + var selectionState: ChatInterfaceSelectionState? + let toggleMessageSelection: (MessageId) -> Void - public init(openMessage: @escaping (MessageId) -> Void, testNavigateToMessage: @escaping (MessageId, MessageId) -> Void) { + public init(openMessage: @escaping (MessageId) -> Void, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void) { self.openMessage = openMessage - self.testNavigateToMessage = testNavigateToMessage + self.openPeer = openPeer + self.openMessageContextMenu = openMessageContextMenu + self.navigateToMessage = navigateToMessage + self.clickThroughMessage = clickThroughMessage + self.toggleMessageSelection = toggleMessageSelection } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 2900ee5364..2518a54154 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -45,14 +45,26 @@ class ChatControllerNode: ASDisplayNode { let backgroundNode: ASDisplayNode let listView: ListView - let inputNode: ChatInputNode + + let inputPanelBackgroundNode: ASDisplayNode + + var inputPanelNode: ChatInputPanelNode? + var accessoryPanelNode: AccessoryPanelNode? + + var textInputPanelNode: ChatTextInputPanelNode? + let navigateToLatestButton: ChatHistoryNavigationButtonNode private var ignoreUpdateHeight = false + var chatInterfaceState = ChatInterfaceState() + + var requestUpdateChatInterfaceState: (ChatInterfaceState, Bool) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } - var requestLayout: (Bool) -> Void = { _ in } + var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } + + var interfaceInteraction: ChatPanelInterfaceInteraction? init(account: Account, peerId: PeerId) { self.account = account @@ -66,8 +78,9 @@ class ChatControllerNode: ASDisplayNode { self.listView = ListView() self.listView.preloadPages = false - //self.listView.debugInfo = true - self.inputNode = ChatInputNode() + + self.inputPanelBackgroundNode = ASDisplayNode() + self.inputPanelBackgroundNode.backgroundColor = UIColor(0xfafafa) self.navigateToLatestButton = ChatHistoryNavigationButtonNode() self.navigateToLatestButton.alpha = 0.0 @@ -83,38 +96,39 @@ class ChatControllerNode: ASDisplayNode { self.listView.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0) self.addSubnode(self.listView) - self.addSubnode(self.inputNode) + self.addSubnode(self.inputPanelBackgroundNode) self.addSubnode(self.navigateToLatestButton) self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) - self.inputNode.updateHeight = { [weak self] in - if let strongSelf = self, !strongSelf.ignoreUpdateHeight { - strongSelf.requestLayout(true) + self.textInputPanelNode = ChatTextInputPanelNode() + self.textInputPanelNode?.updateHeight = { [weak self] in + if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { + strongSelf.requestLayout(.animated(duration: 0.1, curve: .easeInOut)) } } - - self.inputNode.sendMessage = { [weak self] in - if let strongSelf = self { - if strongSelf.inputNode.textInputNode?.isFirstResponder() ?? false { + self.textInputPanelNode?.sendMessage = { [weak self] in + if let strongSelf = self, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { + if textInputPanelNode.textInputNode?.isFirstResponder() ?? false { applyKeyboardAutocorrection() } - let text = strongSelf.inputNode.text + let text = textInputPanelNode.text strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in - if let strongSelf = strongSelf { + if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { strongSelf.ignoreUpdateHeight = true - strongSelf.inputNode.text = "" + textInputPanelNode.text = "" + strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), false) strongSelf.ignoreUpdateHeight = false } }) - let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text).start() + let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatInterfaceState.replyMessageId).start() } } - self.inputNode.displayAttachmentMenu = { [weak self] in + self.textInputPanelNode?.displayAttachmentMenu = { [weak self] in self?.displayAttachmentMenu() } } @@ -141,37 +155,178 @@ class ChatControllerNode: ASDisplayNode { } } - let messageTextInputSize = self.inputNode.calculateSizeThatFits(CGSize(width: layout.size.width, height: min(layout.size.height / 2.0, 240.0))) - self.backgroundNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.listView.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) let listViewCurve: ListViewAnimationCurve - var speedFactor: CGFloat = 1.0 if curve == 7 { - speedFactor = CGFloat(duration) / 0.5 - listViewCurve = .Spring(speed: CGFloat(speedFactor)) + listViewCurve = .Spring(duration: duration) } else { listViewCurve = .Default } - let inputViewFrame = CGRect(x: 0.0, y: layout.size.height - messageTextInputSize.height - insets.bottom, width: layout.size.width, height: messageTextInputSize.height) + //let inputViewFrame = CGRect(x: 0.0, y: layout.size.height - messageTextInputSize.height - insets.bottom, width: layout.size.width, height: messageTextInputSize.height) - listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.bottom + inputViewFrame.size.height + 4.0, left: insets.right, bottom: insets.top, right: insets.left), duration: duration, curve: listViewCurve)) + var dismissedInputPanelNode: ASDisplayNode? + var dismissedAccessoryPanelNode: ASDisplayNode? + + var inputPanelSize: CGSize? + var immediatelyLayoutInputPanelAndAnimateAppearance = false + if let inputPanelNode = inputPanelForChatIntefaceState(self.chatInterfaceState, 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 { + if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + inputTextPanelNode.ensureUnfocused() + } + dismissedInputPanelNode = self.inputPanelNode + immediatelyLayoutInputPanelAndAnimateAppearance = true + self.inputPanelNode = inputPanelNode + self.insertSubnode(inputPanelNode, belowSubnode: self.navigateToLatestButton) + } + } else { + dismissedInputPanelNode = self.inputPanelNode + self.inputPanelNode = nil + } + + var accessoryPanelSize: CGSize? + var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false + if let accessoryPanelNode = accessoryPanelForChatIntefaceState(self.chatInterfaceState, 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 { + dismissedAccessoryPanelNode = self.accessoryPanelNode + self.accessoryPanelNode = accessoryPanelNode + + if let inputPanelNode = self.inputPanelNode { + self.insertSubnode(accessoryPanelNode, belowSubnode: inputPanelNode) + } else { + self.insertSubnode(accessoryPanelNode, belowSubnode: self.navigateToLatestButton) + } + + accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in + if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { + strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), true) + } + } + + immediatelyLayoutAccessoryPanelAndAnimateAppearance = true + accessoryPanelNode.insets = UIEdgeInsets(top: 0.0, left: 45.0, bottom: 0.0, right: 54.0) + } + } else if let accessoryPanelNode = self.accessoryPanelNode { + dismissedAccessoryPanelNode = self.accessoryPanelNode + self.accessoryPanelNode = nil + } + + var inputPanelsHeight: CGFloat = 0.0 + + var inputPanelFrame: CGRect? + if let inputPanelNode = self.inputPanelNode { + assert(inputPanelSize != nil) + inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) + inputPanelsHeight += inputPanelSize!.height + } + + var accessoryPanelFrame: CGRect? + if let accessoryPanelNode = self.accessoryPanelNode { + assert(accessoryPanelSize != nil) + accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) + inputPanelsHeight += accessoryPanelSize!.height + } + + let inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight)) + + listViewTransaction(ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: insets.bottom + inputPanelsHeight + 4.0, left: insets.right, bottom: insets.top, right: insets.left), duration: duration, curve: listViewCurve)) let navigateToLatestButtonSize = self.navigateToLatestButton.bounds.size - let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: inputViewFrame.minY - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize) + 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) - if duration > DBL_EPSILON { - UIView.animate(withDuration: duration / Double(speedFactor), delay: 0.0, options: UIViewAnimationOptions(rawValue: curve << 16), animations: { - self.inputNode.frame = inputViewFrame - self.navigateToLatestButton.frame = navigateToLatestButtonFrame - }, completion: nil) - } else { - self.inputNode.frame = inputViewFrame - self.navigateToLatestButton.frame = navigateToLatestButtonFrame + transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) + transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame) + + if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) { + if immediatelyLayoutInputPanelAndAnimateAppearance { + inputPanelNode.frame = inputPanelFrame.offsetBy(dx: 0.0, dy: inputPanelFrame.size.height) + inputPanelNode.alpha = 0.0 + } + + transition.updateFrame(node: inputPanelNode, frame: inputPanelFrame) + transition.updateAlpha(node: inputPanelNode, alpha: 1.0) + inputPanelNode.updateFrames(transition: transition) + } + + if let accessoryPanelNode = self.accessoryPanelNode, let accessoryPanelFrame = accessoryPanelFrame, !accessoryPanelNode.frame.equalTo(accessoryPanelFrame) { + if immediatelyLayoutAccessoryPanelAndAnimateAppearance { + accessoryPanelNode.frame = accessoryPanelFrame.offsetBy(dx: 0.0, dy: accessoryPanelFrame.size.height) + accessoryPanelNode.alpha = 0.0 + } + + transition.updateFrame(node: accessoryPanelNode, frame: accessoryPanelFrame) + transition.updateAlpha(node: accessoryPanelNode, alpha: 1.0) + } + + if let dismissedInputPanelNode = dismissedInputPanelNode { + var frameCompleted = false + var alphaCompleted = false + var completed = { [weak self, weak dismissedInputPanelNode] in + if let strongSelf = self, let dismissedInputPanelNode = dismissedInputPanelNode, strongSelf.inputPanelNode === dismissedInputPanelNode { + return + } + if frameCompleted && alphaCompleted { + dismissedInputPanelNode?.removeFromSupernode() + } + } + let transitionTargetY = layout.size.height - insets.bottom + transition.updateFrame(node: dismissedInputPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedInputPanelNode.frame.size), completion: { _ in + frameCompleted = true + completed() + }) + + transition.updateAlpha(node: dismissedInputPanelNode, alpha: 0.0, completion: { _ in + alphaCompleted = true + completed() + }) + } + + if let dismissedAccessoryPanelNode = dismissedAccessoryPanelNode { + var frameCompleted = false + var alphaCompleted = false + var completed = { [weak dismissedAccessoryPanelNode] in + if frameCompleted && alphaCompleted { + dismissedAccessoryPanelNode?.removeFromSupernode() + } + } + var transitionTargetY = layout.size.height - insets.bottom + if let inputPanelFrame = inputPanelFrame { + transitionTargetY = inputPanelFrame.minY + } + transition.updateFrame(node: dismissedAccessoryPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedAccessoryPanelNode.frame.size), completion: { _ in + frameCompleted = true + completed() + }) + + transition.updateAlpha(node: dismissedAccessoryPanelNode, alpha: 0.0, completion: { _ in + alphaCompleted = true + completed() + }) + } + } + + func updateChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, animated: Bool) { + if self.chatInterfaceState != chatInterfaceState { + self.chatInterfaceState = chatInterfaceState + + if !self.ignoreUpdateHeight { + self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) + } + } + } + + func ensureInputViewFocused() { + if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { + inputPanelNode.ensureFocused() } } diff --git a/TelegramUI/ChatInputPanelNode.swift b/TelegramUI/ChatInputPanelNode.swift new file mode 100644 index 0000000000..c245917890 --- /dev/null +++ b/TelegramUI/ChatInputPanelNode.swift @@ -0,0 +1,10 @@ +import Foundation +import AsyncDisplayKit +import Display + +class ChatInputPanelNode: ASDisplayNode { + var interfaceInteraction: ChatPanelInterfaceInteraction? + + func updateFrames(transition: ContainedViewLayoutTransition) { + } +} diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift new file mode 100644 index 0000000000..e0489cd3e5 --- /dev/null +++ b/TelegramUI/ChatInterfaceState.swift @@ -0,0 +1,62 @@ +import Foundation +import Postbox + +struct ChatInterfaceSelectionState: Equatable { + let selectedIds: Set + + static func ==(lhs: ChatInterfaceSelectionState, rhs: ChatInterfaceSelectionState) -> Bool { + return lhs.selectedIds == rhs.selectedIds + } +} + +final class ChatInterfaceState: Equatable { + let inputText: String? + let replyMessageId: MessageId? + let selectionState: ChatInterfaceSelectionState? + + init() { + self.inputText = nil + self.replyMessageId = nil + self.selectionState = nil + } + + init(inputText: String?, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) { + self.inputText = inputText + 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 + } + + func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { + return ChatInterfaceState(inputText: self.inputText, replyMessageId: replyMessageId, selectionState: self.selectionState) + } + + func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { + var selectedIds = Set() + if let selectionState = self.selectionState { + selectedIds.formUnion(selectionState.selectedIds) + } + selectedIds.insert(messageId) + return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + } + + func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { + var selectedIds = Set() + if let selectionState = self.selectionState { + selectedIds.formUnion(selectionState.selectedIds) + } + if selectedIds.contains(messageId) { + let _ = selectedIds.remove(messageId) + } else { + selectedIds.insert(messageId) + } + return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + } + + func withoutSelectionState() -> ChatInterfaceState { + return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: nil) + } +} diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift new file mode 100644 index 0000000000..6c0fc6a390 --- /dev/null +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -0,0 +1,22 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore + +func accessoryPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { + if let _ = chatInterfaceState.selectionState { + return nil + } + + if let replyMessageId = chatInterfaceState.replyMessageId { + if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { + replyPanelNode.interfaceInteraction = interfaceInteraction + return replyPanelNode + } else { + let panelNode = ReplyAccessoryPanelNode(account: account, messageId: replyMessageId) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } + } else { + return nil + } +} diff --git a/TelegramUI/ChatInterfaceStateInputPanels.swift b/TelegramUI/ChatInterfaceStateInputPanels.swift new file mode 100644 index 0000000000..fdf665035d --- /dev/null +++ b/TelegramUI/ChatInterfaceStateInputPanels.swift @@ -0,0 +1,32 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore + +func inputPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { + if let selectionState = chatInterfaceState.selectionState { + if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { + currentPanel.selectedMessageCount = selectionState.selectedIds.count + currentPanel.interfaceInteraction = interfaceInteraction + return currentPanel + } else { + let panel = ChatMessageSelectionInputPanelNode() + 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 + } + } + } +} diff --git a/TelegramUI/ChatInterfaceStateNavigationButtons.swift b/TelegramUI/ChatInterfaceStateNavigationButtons.swift new file mode 100644 index 0000000000..a21bae5627 --- /dev/null +++ b/TelegramUI/ChatInterfaceStateNavigationButtons.swift @@ -0,0 +1,39 @@ +import Foundation + +enum ChatNavigationButtonAction { + case openChatInfo + case clearHistory + case cancelMessageSelection +} + +struct ChatNavigationButton: Equatable { + let action: ChatNavigationButtonAction + let buttonItem: UIBarButtonItem + + static func ==(lhs: ChatNavigationButton, rhs: ChatNavigationButton) -> Bool { + return lhs.action == rhs.action + } +} + +func leftNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?) -> ChatNavigationButton? { + if let _ = chatInterfaceState.selectionState { + if let currentButton = currentButton, currentButton.action == .clearHistory { + return currentButton + } else { + return ChatNavigationButton(action: .clearHistory, buttonItem: UIBarButtonItem(title: "Delete All", style: .plain, target: target, action: selector)) + } + } + return nil +} + +func rightNavigationButtonForChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, currentButton: ChatNavigationButton?, target: Any?, selector: Selector?, chatInfoNavigationButton: ChatNavigationButton?) -> ChatNavigationButton? { + if let _ = chatInterfaceState.selectionState { + if let currentButton = currentButton, currentButton.action == .cancelMessageSelection { + return currentButton + } else { + return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: "Cancel", style: .plain, target: target, action: selector)) + } + } + + return chatInfoNavigationButton +} diff --git a/TelegramUI/ChatListAvatarNode.swift b/TelegramUI/ChatListAvatarNode.swift index 7cbdd2a09a..fd5c8178ab 100644 --- a/TelegramUI/ChatListAvatarNode.swift +++ b/TelegramUI/ChatListAvatarNode.swift @@ -47,7 +47,19 @@ private func ==(lhs: ChatListAvatarNodeState, rhs: ChatListAvatarNodeState) -> B } public final class ChatListAvatarNode: ASDisplayNode { - let font: UIFont + 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) + } + + if !self.displaySuspended { + self.setNeedsDisplay() + } + } + } + } private var parameters: ChatListAvatarNodeParameters? let imageNode: ImageNode @@ -70,8 +82,12 @@ public final class ChatListAvatarNode: ASDisplayNode { get { return super.frame } set(value) { + let updateImage = !value.size.equalTo(super.frame.size) super.frame = value self.imageNode.frame = CGRect(origin: CGPoint(), size: value.size) + if updateImage && !self.displaySuspended { + self.setNeedsDisplay() + } } } diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index 99537fdfa3..6a77aebc89 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -56,8 +56,7 @@ class ChatListControllerNode: ASDisplayNode { let listViewCurve: ListViewAnimationCurve var speedFactor: CGFloat = 1.0 if curve == 7 { - speedFactor = CGFloat(duration) / 0.5 - listViewCurve = .Spring(speed: CGFloat(speedFactor)) + listViewCurve = .Spring(duration: duration) } else { listViewCurve = .Default } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 86a15ab596..0ddd5db712 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -29,6 +29,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { super.init() self.backgroundColor = UIColor.white + self.addSubnode(self.recentPeersNode) self.addSubnode(self.listNode) @@ -112,10 +113,8 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let listViewCurve: ListViewAnimationCurve - var speedFactor: CGFloat = 1.0 if curve == 7 { - speedFactor = CGFloat(duration) / 0.5 - listViewCurve = .Spring(speed: CGFloat(speedFactor)) + listViewCurve = .Spring(duration: duration) } else { listViewCurve = .Default } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 4085b31932..8f66db7768 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -153,6 +153,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private let backgroundNode: ChatMessageBackground private var transitionClippingNode: ASDisplayNode? + private var selectionNode: ChatMessageSelectionNode? + private var nameNode: TextNode? private var forwardInfoNode: ChatMessageForwardInfoNode? private var replyInfoNode: ChatMessageReplyInfoNode? @@ -202,7 +204,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { override func didLoad() { super.didLoad() - self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:))) + recognizer.doNotWaitForDoubleTapAtPoint = { [weak self] point in + if let strongSelf = self { + if let replyInfoNode = strongSelf.replyInfoNode, replyInfoNode.frame.contains(point) { + return true + } + if let forwardInfoNode = strongSelf.forwardInfoNode, forwardInfoNode.frame.contains(point) { + return true + } + } + return false + } + self.view.addGestureRecognizer(recognizer) } override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { @@ -221,9 +235,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let message = item.message let incoming = item.account.peerId != message.author?.id - let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroup && item.message.author != nil + let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil - let avatarInset: CGFloat = (item.peerId.isGroup && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 + let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 let tmpWidth = width * layoutConstants.bubble.maximumWidthFillFactor let maximumContentWidth = floor(tmpWidth - layoutConstants.bubble.edgeInset - layoutConstants.bubble.edgeInset - layoutConstants.bubble.contentInsets.left - layoutConstants.bubble.contentInsets.right - avatarInset) @@ -559,6 +573,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.backgroundNode.frame = backgroundFrame strongSelf.disableTransitionClippingNode() } + let offset: CGFloat = incoming ? 42.0 : 0.0 + strongSelf.selectionNode?.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: width, height: layout.size.height)) } }) } @@ -619,26 +635,60 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + @objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) { switch recognizer.state { case .ended: - let location = recognizer.location(in: self.view) - if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { - if let item = self.item { - for attribute in item.message.attributes { - if let attribute = attribute as? ReplyMessageAttribute { - self.controllerInteraction?.testNavigateToMessage(item.message.id, attribute.messageId) - break + if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation { + switch gesture { + case .tap: + if let replyInfoNode = self.replyInfoNode, replyInfoNode.frame.contains(location) { + if let item = self.item { + for attribute in item.message.attributes { + if let attribute = attribute as? ReplyMessageAttribute { + self.controllerInteraction?.navigateToMessage(item.message.id, attribute.messageId) + return + } + } + } + } + if let forwardInfoNode = self.forwardInfoNode, forwardInfoNode.frame.contains(location) { + if let item = self.item, let forwardInfo = item.message.forwardInfo { + if let sourceMessageId = forwardInfo.sourceMessageId { + self.controllerInteraction?.navigateToMessage(item.message.id, sourceMessageId) + } else { + self.controllerInteraction?.openPeer(forwardInfo.source?.id ?? forwardInfo.author.id, .chat) + } + return + } + } + self.controllerInteraction?.clickThroughMessage() + case .longTap, .doubleTap: + if let item = self.item { + self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.backgroundNode.frame) } - } } - //self.controllerInteraction?.testNavigateToMessage(messageId) } default: break } } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if let selectionNode = self.selectionNode { + if selectionNode.frame.offsetBy(dx: 42.0, dy: 0.0).contains(point) { + return selectionNode.view + } else { + return nil + } + } + + if !self.backgroundNode.frame.contains(point) { + return nil + } + + return super.hitTest(point, with: event) + } + override func transitionNode(id: MessageId, media: Media) -> ASDisplayNode? { if let item = self.item, item.message.id == id { for contentNode in self.contentNodes { @@ -657,4 +707,66 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } } + + override func updateSelectionState(animated: Bool) { + guard let controllerInteraction = self.controllerInteraction else { + return + } + + if let selectionState = controllerInteraction.selectionState { + var selected = false + var incoming = true + if let item = self.item { + selected = selectionState.selectedIds.contains(item.message.id) + incoming = item.message.flags.contains(.Incoming) + } + let offset: CGFloat = incoming ? 42.0 : 0.0 + + if let selectionNode = self.selectionNode { + selectionNode.updateSelected(selected, animated: false) + selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height)) + self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); + } else { + let selectionNode = ChatMessageSelectionNode(toggle: { [weak self] in + if let strongSelf = self, let item = strongSelf.item { + strongSelf.controllerInteraction?.toggleMessageSelection(item.message.id) + } + }) + + selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.bounds.size.width, height: self.bounds.size.height)) + self.addSubnode(selectionNode) + self.selectionNode = selectionNode + selectionNode.updateSelected(selected, animated: false) + let previousSubnodeTransform = self.subnodeTransform + self.subnodeTransform = CATransform3DMakeTranslation(offset, 0.0, 0.0); + if animated { + selectionNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4) + + if !incoming { + let position = selectionNode.layer.position + selectionNode.layer.animatePosition(from: CGPoint(x: position.x - 42.0, y: position.y), to: position, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring) + } + } + } + } else { + if let selectionNode = self.selectionNode { + self.selectionNode = nil + let previousSubnodeTransform = self.subnodeTransform + self.subnodeTransform = CATransform3DIdentity + if animated { + self.layer.animate(from: NSValue(caTransform3D: previousSubnodeTransform), to: NSValue(caTransform3D: self.subnodeTransform), keyPath: "sublayerTransform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, completion: { [weak selectionNode]_ in + selectionNode?.removeFromSupernode() + }) + selectionNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + if CGFloat(0.0).isLessThanOrEqualTo(selectionNode.frame.origin.x) { + let position = selectionNode.layer.position + selectionNode.layer.animatePosition(from: position, to: CGPoint(x: position.x - 42.0, y: position.y), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } + } else { + selectionNode.removeFromSupernode() + } + } + } + } } diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index fa233b45e1..ebd76a5026 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -85,8 +85,7 @@ final class ChatMessageInteractiveFileNode: ASTransformNode { if let fetchStatus = self.fetchStatus, case .Local = fetchStatus { self.activateLocalContent() } else { - self.activateLocalContent() - //self.progressPressed() + self.progressPressed() } } } diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 7ed5442a9d..4142fa5679 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -52,7 +52,7 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { var accessoryItem: ListViewAccessoryItem? let incoming = account.peerId != message.author?.id - let displayAuthorInfo = incoming && message.author != nil && peerId.isGroup + let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel if displayAuthorInfo { var hasActionMedia = false @@ -91,6 +91,8 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { let (top, bottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) let (layout, apply) = nodeLayout(self, width, top, bottom) + node.updateSelectionState(animated: false) + node.contentSize = layout.contentSize node.insets = layout.insets @@ -125,6 +127,8 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { Queue.mainQueue().async { node.setupItem(self) + node.updateSelectionState(animated: false) + let nodeLayout = node.asyncLayout() async { diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 51f0dc2486..04a5caa738 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -117,4 +117,7 @@ public class ChatMessageItemView: ListViewItemNode { func updateHiddenMedia() { } + + func updateSelectionState(animated: Bool) { + } } diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift new file mode 100644 index 0000000000..5b9981807e --- /dev/null +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -0,0 +1,57 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { + private let deleteButton: UIButton + private let forwardButton: UIButton + + var selectedMessageCount: Int = 0 { + didSet { + self.deleteButton.isEnabled = self.selectedMessageCount != 0 + self.forwardButton.isEnabled = self.selectedMessageCount != 0 + } + } + + override init() { + self.deleteButton = UIButton() + self.forwardButton = UIButton() + + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0x1195f2)), for: [.normal]) + self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionThrash"), color: UIColor(0xdededf)), for: [.disabled]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0x1195f2)), for: [.normal]) + self.forwardButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Acessory Panels/MessageSelectionForward"), color: UIColor(0xdededf)), for: [.disabled]) + + super.init() + + self.view.addSubview(self.deleteButton) + self.view.addSubview(self.forwardButton) + + self.forwardButton.isEnabled = false + self.deleteButton.isEnabled = false + + self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) + self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside]) + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 45.0) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + self.deleteButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: 53.0, height: 45.0)) + self.forwardButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - 57.0, y: 0.0), size: CGSize(width: 57.0, height: 45.0)) + } + + @objc func deleteButtonPressed() { + self.interfaceInteraction?.deleteSelectedMessages() + } + + @objc func forwardButtonPressed() { + self.interfaceInteraction?.forwardSelectedMessages() + } +} diff --git a/TelegramUI/ChatMessageSelectionNode.swift b/TelegramUI/ChatMessageSelectionNode.swift new file mode 100644 index 0000000000..c8cdbc04c6 --- /dev/null +++ b/TelegramUI/ChatMessageSelectionNode.swift @@ -0,0 +1,51 @@ +import Foundation +import AsyncDisplayKit + +private let checkedImage = UIImage(bundleImageName: "Chat/Message/SelectionChecked")?.precomposed() +private let uncheckedImage = UIImage(bundleImageName: "Chat/Message/SelectionUnchecked")?.precomposed() + +final class ChatMessageSelectionNode: ASDisplayNode { + private let toggle: () -> Void + + private var selected = false + private let checkNode: ASImageNode + + init(toggle: @escaping () -> Void) { + self.toggle = toggle + self.checkNode = ASImageNode() + self.checkNode.displaysAsynchronously = false + self.checkNode.displayWithoutProcessing = true + self.checkNode.isLayerBacked = true + + super.init() + + self.checkNode.image = uncheckedImage + self.addSubnode(self.checkNode) + } + + override func didLoad() { + super.didLoad() + + self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) + } + + func updateSelected(_ selected: Bool, animated: Bool) { + if self.selected != selected { + self.selected = selected + self.checkNode.image = selected ? checkedImage : uncheckedImage + } + } + + @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.toggle() + } + } + + override func layout() { + super.layout() + + let checkSize = self.checkNode.measure(CGSize(width: 200.0, height: 200.0)) + self.checkNode.frame = CGRect(origin: CGPoint(x: 4.0, y: floor((self.bounds.size.height - checkSize.height) / 2.0)), size: checkSize) + } +} diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index 00d8455d40..b7f49fb5be 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -64,7 +64,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - let avatarInset: CGFloat = (item.peerId.isGroup && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 + let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 let layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift new file mode 100644 index 0000000000..668e922a21 --- /dev/null +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -0,0 +1,11 @@ +import Foundation + +final class ChatPanelInterfaceInteraction { + let deleteSelectedMessages: () -> Void + let forwardSelectedMessages: () -> Void + + init(deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void) { + self.deleteSelectedMessages = deleteSelectedMessages + self.forwardSelectedMessages = forwardSelectedMessages + } +} diff --git a/TelegramUI/ChatPresentationInterfaceState.swift b/TelegramUI/ChatPresentationInterfaceState.swift new file mode 100644 index 0000000000..009197499e --- /dev/null +++ b/TelegramUI/ChatPresentationInterfaceState.swift @@ -0,0 +1,7 @@ +import Foundation +import Postbox + +struct ChatPresentationInterfaceState { + let interfaceState: ChatInterfaceState + let peer: Peer? +} diff --git a/TelegramUI/ChatInputView.swift b/TelegramUI/ChatTextInputPanelNode.swift similarity index 78% rename from TelegramUI/ChatInputView.swift rename to TelegramUI/ChatTextInputPanelNode.swift index df3dbc2eeb..9b3164c304 100644 --- a/TelegramUI/ChatInputView.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -24,7 +24,7 @@ private let textInputViewBackground: UIImage = { private let attachmentIcon = UIImage(bundleImageName: "Chat/Input/Text/IconAttachment")?.precomposed() -class ChatInputNode: ASDisplayNode, ASEditableTextNodeDelegate { +class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { var textPlaceholderNode: TextNode var textInputNode: ASEditableTextNode? @@ -58,15 +58,13 @@ class ChatInputNode: ASDisplayNode, ASEditableTextNodeDelegate { super.init() - self.backgroundColor = UIColor(0xfafafa) - self.attachmentButton.setImage(attachmentIcon, for: []) self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) self.view.addSubview(self.attachmentButton) self.sendButton.titleLabel?.font = Font.medium(17.0) self.sendButton.contentEdgeInsets = UIEdgeInsets(top: 8.0, left: 6.0, bottom: 8.0, right: 6.0) - self.sendButton.setTitleColor(UIColor.blue, for: []) + self.sendButton.setTitleColor(UIColor(0x1195f2), for: []) self.sendButton.setTitleColor(UIColor.gray, for: [.highlighted]) self.sendButton.setTitle("Send", for: []) self.sendButton.sizeToFit() @@ -124,21 +122,27 @@ class ChatInputNode: ASDisplayNode, ASEditableTextNodeDelegate { return super.frame } set(value) { super.frame = value - - let sendButtonSize = self.sendButton.bounds.size - let minimalHeight: CGFloat = 45.0 - self.sendButton.frame = CGRect(x: value.size.width - sendButtonSize.width, y: value.height - minimalHeight + floor((minimalHeight - sendButtonSize.height) / 2.0), width: sendButtonSize.width, height: sendButtonSize.height) - - self.attachmentButton.frame = CGRect(origin: CGPoint(x: 0.0, y: value.height - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight)) - - self.textInputNode?.frame = CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: value.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: value.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom) - - self.textPlaceholderNode.frame = CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5), size: self.textPlaceholderNode.frame.size) - - self.textInputBackgroundView.frame = CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top, width: value.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width, height: value.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom) } } + override func updateFrames(transition: ContainedViewLayoutTransition) { + let bounds = self.bounds + + let sendButtonSize = self.sendButton.bounds.size + let minimalHeight: CGFloat = 45.0 + transition.updateFrame(layer: self.sendButton.layer, frame: CGRect(x: bounds.size.width - sendButtonSize.width, y: bounds.height - minimalHeight + floor((minimalHeight - sendButtonSize.height) / 2.0), width: sendButtonSize.width, height: sendButtonSize.height)) + + transition.updateFrame(layer: self.attachmentButton.layer, frame: CGRect(origin: CGPoint(x: 0.0, y: bounds.height - minimalHeight), size: CGSize(width: 40.0, height: minimalHeight))) + + if let textInputNode = self.textInputNode { + transition.updateFrame(node: textInputNode, frame: CGRect(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top, width: bounds.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width - self.textInputViewInternalInsets.left - self.textInputViewInternalInsets.right, height: bounds.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom - self.textInputViewInternalInsets.top - self.textInputViewInternalInsets.bottom)) + } + + transition.updateFrame(node: self.textPlaceholderNode, frame: CGRect(origin: CGPoint(x: self.textFieldInsets.left + self.textInputViewInternalInsets.left, y: self.textFieldInsets.top + self.textInputViewInternalInsets.top + 0.5), size: self.textPlaceholderNode.frame.size)) + + transition.updateFrame(layer: self.textInputBackgroundView.layer, frame: CGRect(x: self.textFieldInsets.left, y: self.textFieldInsets.top, width: bounds.size.width - self.textFieldInsets.left - self.textFieldInsets.right - sendButtonSize.width, height: bounds.size.height - self.textFieldInsets.top - self.textFieldInsets.bottom)) + } + @objc func editableTextNodeDidUpdateText(_ editableTextNode: ASEditableTextNode) { if let textInputNode = self.textInputNode { self.textPlaceholderNode.isHidden = editableTextNode.attributedText?.length ?? 0 != 0 @@ -167,14 +171,22 @@ class ChatInputNode: ASDisplayNode, ASEditableTextNodeDelegate { @objc func textInputBackgroundViewTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { - if self.textInputNode == nil { - self.loadTextInputNode() - } - - self.textInputNode?.becomeFirstResponder() + self.ensureFocused() } } + func ensureUnfocused() { + self.textInputNode?.resignFirstResponder() + } + + func ensureFocused() { + if self.textInputNode == nil { + self.loadTextInputNode() + } + + self.textInputNode?.becomeFirstResponder() + } + func animateTextSend() { /*if let textInputNode = self.textInputNode { let snapshot = textInputNode.view.snapshotViewAfterScreenUpdates(false) diff --git a/TelegramUI/ContactsControllerNode.swift b/TelegramUI/ContactsControllerNode.swift index a376bf7f83..40ab354738 100644 --- a/TelegramUI/ContactsControllerNode.swift +++ b/TelegramUI/ContactsControllerNode.swift @@ -53,10 +53,8 @@ final class ContactsControllerNode: ASDisplayNode { } let listViewCurve: ListViewAnimationCurve - var speedFactor: CGFloat = 1.0 if curve == 7 { - speedFactor = CGFloat(duration) / 0.5 - listViewCurve = .Spring(speed: CGFloat(speedFactor)) + listViewCurve = .Spring(duration: duration) } else { listViewCurve = .Default } diff --git a/TelegramUI/ContactsPeerItem.swift b/TelegramUI/ContactsPeerItem.swift index 4112b8daf1..0d3f694d42 100644 --- a/TelegramUI/ContactsPeerItem.swift +++ b/TelegramUI/ContactsPeerItem.swift @@ -44,6 +44,10 @@ class ContactsPeerItem: ListViewItem { if !group.title.isEmpty { letter = group.title.substring(to: group.title.index(after: group.title.startIndex)).uppercased() } + } else if let channel = peer as? TelegramChannel { + if !channel.title.isEmpty { + letter = channel.title.substring(to: channel.title.index(after: channel.title.startIndex)).uppercased() + } } self.headerAccessoryItem = ContactsSectionHeaderAccessoryItem(sectionHeader: .letter(letter)) } else { @@ -224,6 +228,8 @@ class ContactsPeerItemNode: ListViewItemNode { statusAttributedString = NSAttributedString(string: "last seen recently", font: statusFont, textColor: UIColor(0xa6a6a6)) } else if let group = peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: titleBoldFont, textColor: UIColor.black) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleBoldFont, textColor: UIColor.black) } } diff --git a/TelegramUI/ContactsVCardItem.swift b/TelegramUI/ContactsVCardItem.swift index 6810c46120..c1a7aa2673 100644 --- a/TelegramUI/ContactsVCardItem.swift +++ b/TelegramUI/ContactsVCardItem.swift @@ -166,6 +166,9 @@ class ContactsVCardItemNode: ListViewItemNode { } else if let group = peer as? TelegramGroup { titleAttributedString = NSAttributedString(string: group.title, font: titleFont, textColor: UIColor.black) statusAttributedString = NSAttributedString(string: "group", font: statusFont, textColor: UIColor(0xa6a6a6)) + } else if let channel = peer as? TelegramChannel { + titleAttributedString = NSAttributedString(string: channel.title, font: titleFont, textColor: UIColor.black) + statusAttributedString = NSAttributedString(string: "channel", font: statusFont, textColor: UIColor(0xa6a6a6)) } } diff --git a/TelegramUI/FastBlur.h b/TelegramUI/FastBlur.h index 09e9270644..4f01258ee1 100644 --- a/TelegramUI/FastBlur.h +++ b/TelegramUI/FastBlur.h @@ -4,6 +4,5 @@ #import void telegramFastBlur(int imageWidth, int imageHeight, int imageStride, void *pixels); -void telegramDspBlur(int imageWidth, int imageHeight, int imageStride, void *pixels); #endif diff --git a/TelegramUI/FastBlur.m b/TelegramUI/FastBlur.m index a79943b134..3cad442542 100644 --- a/TelegramUI/FastBlur.m +++ b/TelegramUI/FastBlur.m @@ -1,7 +1,5 @@ #import "FastBlur.h" -#import - static inline uint64_t get_colors (const uint8_t *p) { return p[0] + (p[1] << 16) + ((uint64_t)p[2] << 32); } @@ -102,63 +100,3 @@ yi += stride; free(rgb); } - -void telegramDspBlur(int imageWidth, int imageHeight, int imageStride, void *pixels) { - uint8_t *srcData = pixels; - int bytesPerRow = imageStride; - int width = imageWidth; - int height = imageHeight; - bool shouldClip = false; - static const float matrix[] = { 1/9.0f, 1/9.0f, 1/9.0f, 1/9.0f, 1/9.0f, 1/9.0f, 1/9.0f, 1/9.0f, 1/9.0f }; - -//void telegramDspBlur(uint8_t *srcData, int bytesPerRow, int width, int height, float *matrix, int matrixRows, int matrixCols, bool shouldClip) { - unsigned char *finalData = malloc(bytesPerRow * height * sizeof(unsigned char)); - if (srcData != NULL && finalData != NULL) - { - size_t dataSize = bytesPerRow * height; - // copy src to destination: technically this is a bit wasteful as we'll overwrite - // all but the "alpha" portion of finalData during processing but I'm unaware of - // a memcpy with stride function - memcpy(finalData, srcData, dataSize); - // alloc space for our dsp arrays - float *srcAsFloat = malloc(width*height*sizeof(float)); - float *resultAsFloat = malloc(width*height*sizeof(float)); - // loop through each colour (color) chanel (skip the first chanel, it's alpha and is left alone) - for (int i=1; i<4; i++) { - // convert src pixels into float data type - vDSP_vfltu8(srcData+i,4,srcAsFloat,1,width * height); - // apply matrix using dsp - /*switch (matrixSize) { - case DSPMatrixSize3x3:*/ - vDSP_f3x3(srcAsFloat, height, width, matrix, resultAsFloat); - /*break; - case DSPMatrixSize5x5: - vDSP_f5x5(srcAsFloat, height, width, matrix, resultAsFloat); - break; - case DSPMatrixSizeCustom: - NSAssert(matrixCols > 0 && matrixRows > 0, - @"invalid usage: please use full method definition and pass rows/cols for matrix"); - vDSP_imgfir(srcAsFloat, height, width, matrix, resultAsFloat, matrixRows, matrixCols); - break; - default: - break; - }*/ - // certain operations may result in values to large or too small in our output float array - // so if necessary we clip the results here. This param is optional so that we don't need to take - // the speed hit on blur operations or others which can't result in invalid float values. - if (shouldClip) { - float min = 0; - float max = 255; - vDSP_vclip(resultAsFloat, 1, &min, &max, resultAsFloat, 1, width * height); - } - // convert back into bytes and copy into finalData - vDSP_vfixu8(resultAsFloat, 1, finalData+i, 4, width * height); - } - // clean up dsp space - free(srcAsFloat); - free(resultAsFloat); - memcpy(srcData, finalData, bytesPerRow * height * sizeof(unsigned char)); - free(finalData); - } -} - diff --git a/TelegramUI/NavigationAccessoryPanelNode.swift b/TelegramUI/NavigationAccessoryPanelNode.swift new file mode 100644 index 0000000000..cad6b9f354 --- /dev/null +++ b/TelegramUI/NavigationAccessoryPanelNode.swift @@ -0,0 +1,5 @@ +import Foundation +import AsyncDisplayKit + +class NavigationAccessoryPanelNode: ASDisplayNode { +} diff --git a/TelegramUI/PeerAvatar.swift b/TelegramUI/PeerAvatar.swift index 1b6017dfc0..0a3c8952ee 100644 --- a/TelegramUI/PeerAvatar.swift +++ b/TelegramUI/PeerAvatar.swift @@ -30,6 +30,10 @@ func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = C 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 { diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift new file mode 100644 index 0000000000..f2b1ddce1f --- /dev/null +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -0,0 +1,97 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +final class ReplyAccessoryPanelNode: AccessoryPanelNode { + private let messageDisposable = MetaDisposable() + let messageId: MessageId + + let closeButton: ASButtonNode + let lineNode: ASDisplayNode + let titleNode: ASTextNode + let textNode: ASTextNode + + init(account: Account, messageId: MessageId) { + self.messageId = messageId + + self.closeButton = ASButtonNode() + self.closeButton.setImage(UIImage(bundleImageName: "Chat/Input/Acessory Panels/CloseButton")?.precomposed(), for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + self.lineNode = ASDisplayNode() + self.lineNode.backgroundColor = UIColor(0x1195f2) + + self.titleNode = ASTextNode() + self.titleNode.truncationMode = .byTruncatingTail + self.titleNode.maximumNumberOfLines = 1 + self.titleNode.displaysAsynchronously = false + + self.textNode = ASTextNode() + self.textNode.truncationMode = .byTruncatingTail + self.textNode.maximumNumberOfLines = 1 + self.textNode.displaysAsynchronously = false + + super.init() + + self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) + self.addSubnode(self.closeButton) + + self.addSubnode(self.lineNode) + self.addSubnode(self.titleNode) + self.addSubnode(self.textNode) + + self.messageDisposable.set((account.postbox.messageAtId(messageId) + |> deliverOnMainQueue).start(next: { [weak self] message in + if let strongSelf = self { + var authorName = "" + var text = "" + if let author = message?.author { + authorName = author.displayTitle + } + if let messageText = message?.text { + text = messageText + } + + strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.regular(14.5), textColor: UIColor(0x1195f2)) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.5), textColor: UIColor.black) + + strongSelf.setNeedsLayout() + } + })) + } + + deinit { + self.messageDisposable.dispose() + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 40.0) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - self.insets.right - closeButtonSize.width, y: 12.0), size: closeButtonSize) + + self.lineNode.frame = CGRect(origin: CGPoint(x: self.insets.left, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 5.0)) + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - 11.0 - insets.left - insets.right - 14.0, height: bounds.size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: self.insets.left + 11.0, y: 7.0), size: titleSize) + + let textSize = self.textNode.measure(CGSize(width: bounds.size.width - 11.0 - insets.left - insets.right - 14.0, height: bounds.size.height)) + self.textNode.frame = CGRect(origin: CGPoint(x: self.insets.left + 11.0, y: 25.0), size: textSize) + } + + @objc func closePressed() { + if let dismiss = self.dismiss { + dismiss() + } + } +} diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index 2046d9d65a..6f84717c10 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -58,13 +58,15 @@ final class SearchDisplayController { searchBar.deactivate() if let placeholder = placeholder { - searchBar.animateOut(to: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { - self.searchBar.removeFromSupernode() + let searchBar = self.searchBar + searchBar.animateOut(to: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak searchBar] in + searchBar?.removeFromSupernode() }) } - self.contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in - self.contentNode.removeFromSupernode() + let contentNode = self.contentNode + contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() }) } } diff --git a/TelegramUI/ShareRecipientsActionSheetController.swift b/TelegramUI/ShareRecipientsActionSheetController.swift new file mode 100644 index 0000000000..71bad690b1 --- /dev/null +++ b/TelegramUI/ShareRecipientsActionSheetController.swift @@ -0,0 +1,34 @@ +import Foundation +import Display +import AsyncDisplayKit +import UIKit +import SwiftSignalKit + +final class ShareRecipientsActionSheetController: ActionSheetController { + private let _ready = Promise() + override var ready: Promise { + return self._ready + } + private var didSetReady = false + + var location: () -> Void = { } + var contacts: () -> Void = { } + + override init() { + super.init() + + self._ready.set(.single(true)) + + self.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: "Cancel", action: { [weak self] in + self?.dismissAnimated() + }), + ]) + ]) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift new file mode 100644 index 0000000000..747c883218 --- /dev/null +++ b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -0,0 +1,131 @@ +import Foundation +import UIKit.UIGestureRecognizerSubclass + +private class TapLongTapOrDoubleTapGestureRecognizerTimerTarget: NSObject { + weak var target: TapLongTapOrDoubleTapGestureRecognizer? + + init(target: TapLongTapOrDoubleTapGestureRecognizer) { + self.target = target + + super.init() + } + + @objc func longTapEvent() { + self.target?.longTapEvent() + } + + @objc func tapEvent() { + self.target?.tapEvent() + } +} + +enum TapLongTapOrDoubleTapGesture { + case tap + case doubleTap + case longTap +} + +final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { + private var touchLocationAndTimestamp: (CGPoint, Double)? + private var tapCount: Int = 0 + + private var timer: Foundation.Timer? + private(set) var lastRecognizedGestureAndLocation: (TapLongTapOrDoubleTapGesture, CGPoint)? + + var doNotWaitForDoubleTapAtPoint: ((CGPoint) -> Bool)? + + override init(target: Any?, action: Selector?) { + super.init(target: target, action: action) + + self.delegate = self + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + if otherGestureRecognizer is UIPanGestureRecognizer { + return false + } + return false + } + + override func reset() { + self.timer?.invalidate() + self.timer = nil + self.touchLocationAndTimestamp = nil + self.tapCount = 0 + + super.reset() + } + + fileprivate func longTapEvent() { + self.timer?.invalidate() + self.timer = nil + if let (location, _) = self.touchLocationAndTimestamp { + self.lastRecognizedGestureAndLocation = (.longTap, location) + } else { + self.lastRecognizedGestureAndLocation = nil + } + self.state = .ended + } + + fileprivate func tapEvent() { + self.timer?.invalidate() + self.timer = nil + if let (location, _) = self.touchLocationAndTimestamp { + self.lastRecognizedGestureAndLocation = (.tap, location) + } else { + self.lastRecognizedGestureAndLocation = nil + } + self.state = .ended + } + + override func touchesBegan(_ touches: Set, with event: UIEvent) { + super.touchesBegan(touches, with: event) + + if let touch = touches.first { + self.tapCount += 1 + if self.tapCount == 2 { + self.timer?.invalidate() + self.timer = nil + self.lastRecognizedGestureAndLocation = (.doubleTap, self.location(in: self.view)) + self.state = .ended + } else { + self.touchLocationAndTimestamp = (touch.location(in: self.view), CACurrentMediaTime()) + + self.timer?.invalidate() + let timer = Timer(timeInterval: 0.3, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.longTapEvent), userInfo: nil, repeats: false) + self.timer = 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 (touchLocation, _) = self.touchLocationAndTimestamp { + let location = touch.location(in: self.view) + let distance = CGPoint(x: location.x - touchLocation.x, y: location.y - 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.timer?.invalidate() + + if self.tapCount == 1 { + if let doNotWaitForDoubleTapAtPoint = self.doNotWaitForDoubleTapAtPoint, let (touchLocation, _) = self.touchLocationAndTimestamp, doNotWaitForDoubleTapAtPoint(touchLocation) { + self.lastRecognizedGestureAndLocation = (.tap, touchLocation) + self.state = .ended + } else { + self.state = .began + let timer = Timer(timeInterval: 0.2, target: TapLongTapOrDoubleTapGestureRecognizerTimerTarget(target: self), selector: #selector(TapLongTapOrDoubleTapGestureRecognizerTimerTarget.tapEvent), userInfo: nil, repeats: false) + self.timer = timer + RunLoop.main.add(timer, forMode: RunLoopMode.commonModes) + } + } + } +}