diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 46a8349703..831028d2d0 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -7,14 +7,23 @@ objects = { /* Begin PBXBuildFile section */ + D00219041DDCC86400BE708A /* PerformanceSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00219031DDCC86400BE708A /* PerformanceSpinner.swift */; }; + D00219061DDD1C9E00BE708A /* ImageContainingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */; }; D003702E1DA43052004308D3 /* PeerInfoAvatarAndNameItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D003702D1DA43052004308D3 /* PeerInfoAvatarAndNameItem.swift */; }; D00370301DA43077004308D3 /* PeerInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D003702F1DA43077004308D3 /* PeerInfoItem.swift */; }; D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */; }; + D00E15261DDBD4E700ACF65C /* LegacyCamera.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */; }; D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */; }; + D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */; }; + D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */; }; D021E0CE1DB4135500C6B04F /* ChatMediaInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */; }; D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */; }; D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */; }; D021E0E51DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */; }; + D023EBB21DDA800700BD496D /* LegacyMediaPickers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023EBB11DDA800700BD496D /* LegacyMediaPickers.swift */; }; + D023ED2E1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023ED2D1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift */; }; + D023ED301DDB605D00BD496D /* LegacyEmptyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */; }; + D023ED321DDB60CF00BD496D /* LegacyNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */; }; D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */; }; D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */; }; D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02BE0761D9190EF000889C2 /* GridMessageItem.swift */; }; @@ -23,11 +32,19 @@ 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 */; }; + D04B66B81DD672D00049C3D2 /* GeoLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04B66B71DD672D00049C3D2 /* GeoLocation.swift */; }; + D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */; }; D06879551DB8F1FC00424BBD /* CachedResourceRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */; }; D06879571DB8F22200424BBD /* FetchCachedRepresentations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */; }; D073CE631DCBBE5D007511FD /* MessageSent.caf in Resources */ = {isa = PBXBuildFile; fileRef = D073CE621DCBBE5D007511FD /* MessageSent.caf */; }; D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */; }; D073CE711DCBF23F007511FD /* DeclareEncodables.swift in Sources */ = {isa = PBXBuildFile; fileRef = D073CE701DCBF23F007511FD /* DeclareEncodables.swift */; }; + D07551881DDA4BB50073E051 /* TelegramLegacyComponents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D07551871DDA4BB50073E051 /* TelegramLegacyComponents.framework */; }; + D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D075518A1DDA4D7D0073E051 /* LegacyController.swift */; }; + D075518D1DDA4E0B0073E051 /* LegacyControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */; }; + D075518F1DDA4F9E0073E051 /* SSignalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D075518E1DDA4F9E0073E051 /* SSignalKit.framework */; }; + D07551911DDA4FC70073E051 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D07551901DDA4FC70073E051 /* libc++.tbd */; }; + D07551931DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07551921DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift */; }; D07A7DA31D957671005BCD27 /* ListMessageSnippetItemNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */; }; D07A7DA51D95783C005BCD27 /* ListMessageNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */; }; D07CFF741DCA207200761F81 /* PeerSelectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07CFF731DCA207200761F81 /* PeerSelectionController.swift */; }; @@ -221,6 +238,8 @@ D0F69EAD1D6B9BCB0046BCD6 /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */; }; D0F69EAE1D6B9BCB0046BCD6 /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */; }; D0F69EAF1D6B9BCB0046BCD6 /* libswresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */; }; + D0F7AB351DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */; }; + D0F7AB391DCFF87B009AD9A1 /* ChatMessageDateHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */; }; D0FC40891D5B8E7500261D9D /* TelegramUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0FC407F1D5B8E7400261D9D /* TelegramUI.framework */; }; D0FC408E1D5B8E7500261D9D /* TelegramUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0FC408D1D5B8E7500261D9D /* TelegramUITests.swift */; }; D0FC40901D5B8E7500261D9D /* TelegramUI.h in Headers */ = {isa = PBXBuildFile; fileRef = D0FC40821D5B8E7400261D9D /* TelegramUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -237,14 +256,23 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + D00219031DDCC86400BE708A /* PerformanceSpinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceSpinner.swift; sourceTree = ""; }; + D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = ""; }; D003702D1DA43052004308D3 /* PeerInfoAvatarAndNameItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoAvatarAndNameItem.swift; sourceTree = ""; }; D003702F1DA43077004308D3 /* PeerInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoItem.swift; sourceTree = ""; }; D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoTextWithLabelItem.swift; sourceTree = ""; }; + D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCamera.swift; sourceTree = ""; }; D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = ""; }; + D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageActionButtonsNode.swift; sourceTree = ""; }; + D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditAccessoryPanelNode.swift; sourceTree = ""; }; D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputNode.swift; sourceTree = ""; }; D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputNode.swift; sourceTree = ""; }; D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputNodes.swift; sourceTree = ""; }; D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerPackItem.swift; sourceTree = ""; }; + D023EBB11DDA800700BD496D /* LegacyMediaPickers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyMediaPickers.swift; sourceTree = ""; }; + D023ED2D1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyAttachmentMenu.swift; sourceTree = ""; }; + D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyEmptyController.swift; sourceTree = ""; }; + D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyNavigationController.swift; sourceTree = ""; }; D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = ""; }; D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = ""; }; D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = ""; }; @@ -253,11 +281,19 @@ 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 = ""; }; + D04B66B71DD672D00049C3D2 /* GeoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoLocation.swift; sourceTree = ""; }; + D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = ""; }; D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = ""; }; D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = ""; }; D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = ""; }; D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = ""; }; D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = ""; }; + D07551871DDA4BB50073E051 /* TelegramLegacyComponents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramLegacyComponents.framework; path = "../TelegramLegacyComponents/build/Debug-iphoneos/TelegramLegacyComponents.framework"; sourceTree = ""; }; + D075518A1DDA4D7D0073E051 /* LegacyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyController.swift; sourceTree = ""; }; + D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyControllerNode.swift; sourceTree = ""; }; + D075518E1DDA4F9E0073E051 /* SSignalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SSignalKit.framework; path = "../../../../Library/Developer/Xcode/DerivedData/Telegram-iOS-diblohvjozhgaifjcniwdlixlilx/Build/Products/Debug-iphonesimulator/SSignalKit.framework"; sourceTree = ""; }; + D07551901DDA4FC70073E051 /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; + D07551921DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramInitializeLegacyComponents.swift; sourceTree = ""; }; D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = ""; }; D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = ""; }; D07CFF731DCA207200761F81 /* PeerSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionController.swift; sourceTree = ""; }; @@ -454,6 +490,8 @@ D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = "third-party/FFmpeg-iOS/lib/libavformat.a"; sourceTree = ""; }; D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = "third-party/FFmpeg-iOS/lib/libavutil.a"; sourceTree = ""; }; D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "third-party/FFmpeg-iOS/lib/libswresample.a"; sourceTree = ""; }; + D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleImages.swift; sourceTree = ""; }; + D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageDateHeader.swift; sourceTree = ""; }; D0FC407F1D5B8E7400261D9D /* TelegramUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TelegramUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D0FC40821D5B8E7400261D9D /* TelegramUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TelegramUI.h; sourceTree = ""; }; D0FC40831D5B8E7400261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -467,6 +505,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D07551911DDA4FC70073E051 /* libc++.tbd in Frameworks */, + D075518F1DDA4F9E0073E051 /* SSignalKit.framework in Frameworks */, + D07551881DDA4BB50073E051 /* TelegramLegacyComponents.framework in Frameworks */, D0F69EAC1D6B9BCB0046BCD6 /* libavcodec.a in Frameworks */, D0F69EAD1D6B9BCB0046BCD6 /* libavformat.a in Frameworks */, D0F69EAE1D6B9BCB0046BCD6 /* libavutil.a in Frameworks */, @@ -562,6 +603,7 @@ D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */, D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */, D07CFF861DCAAE5E00761F81 /* ForwardAccessoryPanelNode.swift */, + D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */, ); name = "Accessory Panels"; sourceTree = ""; @@ -574,6 +616,21 @@ name = Sounds; sourceTree = ""; }; + D07551891DDA4C7C0073E051 /* Legacy Components */ = { + isa = PBXGroup; + children = ( + D075518A1DDA4D7D0073E051 /* LegacyController.swift */, + D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */, + D07551921DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift */, + D023ED2D1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift */, + D023EBB11DDA800700BD496D /* LegacyMediaPickers.swift */, + D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */, + D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */, + D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */, + ); + name = "Legacy Components"; + sourceTree = ""; + }; D07CFF771DCA226200761F81 /* Chat List Node */ = { isa = PBXGroup; children = ( @@ -592,6 +649,9 @@ D08D45281D5E340200A7428A /* Frameworks */ = { isa = PBXGroup; children = ( + D07551901DDA4FC70073E051 /* libc++.tbd */, + D075518E1DDA4F9E0073E051 /* SSignalKit.framework */, + D07551871DDA4BB50073E051 /* TelegramLegacyComponents.framework */, D0F69EA81D6B9BCB0046BCD6 /* libavcodec.a */, D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */, D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */, @@ -632,14 +692,6 @@ name = "Peer Media Collection"; sourceTree = ""; }; - D0B844541DAC3ADF005F29E1 /* Strings */ = { - isa = PBXGroup; - children = ( - D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, - ); - name = Strings; - sourceTree = ""; - }; D0BA6F811D784C3A0034826E /* Input Panels */ = { isa = PBXGroup; children = ( @@ -832,6 +884,7 @@ D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */, D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */, D0F69DCA1D6B89F20046BCD6 /* Search */, + D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */, ); name = Nodes; sourceTree = ""; @@ -982,6 +1035,9 @@ D0F69E2C1D6B8B030046BCD6 /* ChatUnreadItem.swift */, D0F69E191D6B8AE60046BCD6 /* ChatHoleItem.swift */, D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */, + D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */, + D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */, + D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */, ); name = Items; sourceTree = ""; @@ -1102,13 +1158,16 @@ D0F69E911D6B8C8E0046BCD6 /* Utils */ = { isa = PBXGroup; children = ( - D0B844541DAC3ADF005F29E1 /* Strings */, + D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, D0F69E941D6B8C9B0046BCD6 /* WebP.swift */, D0B844571DAC44E8005F29E1 /* PeerPresenceStatusManager.swift */, D0ED5D4A1DC806D7007CBB15 /* ApplicationSpecificData.swift */, D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */, D073CE701DCBF23F007511FD /* DeclareEncodables.swift */, + D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */, + D04B66B71DD672D00049C3D2 /* GeoLocation.swift */, + D00219031DDCC86400BE708A /* PerformanceSpinner.swift */, ); name = Utils; sourceTree = ""; @@ -1150,6 +1209,7 @@ D0FC40811D5B8E7400261D9D /* TelegramUI */ = { isa = PBXGroup; children = ( + D07551891DDA4C7C0073E051 /* Legacy Components */, D0F69E911D6B8C8E0046BCD6 /* Utils */, D0F69DBB1D6B88330046BCD6 /* Media */, D0F69DBD1D6B897A0046BCD6 /* Components */, @@ -1294,6 +1354,7 @@ D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */, D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */, D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */, + D0F7AB391DCFF87B009AD9A1 /* ChatMessageDateHeader.swift in Sources */, D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */, D021E0D21DB4147500C6B04F /* ChatInterfaceInputNodes.swift in Sources */, D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */, @@ -1319,8 +1380,10 @@ D0F69D271D6B87D30046BCD6 /* FFMpegAudioFrameDecoder.swift in Sources */, D073CE651DCBC26B007511FD /* ServiceSoundManager.swift in Sources */, D0F69D521D6B87D30046BCD6 /* MediaPlayer.swift in Sources */, + D0F7AB351DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift in Sources */, D0F69E031D6B8A880046BCD6 /* ChatListItem.swift in Sources */, D0F69E081D6B8A9C0046BCD6 /* ChatListSearchContainerNode.swift in Sources */, + D04B66B81DD672D00049C3D2 /* GeoLocation.swift in Sources */, D0DE77291D932923002B8809 /* GridMessageSelectionNode.swift in Sources */, D0F69E4C1D6B8BB20046BCD6 /* ChatMediaActionSheetController.swift in Sources */, D02BE0771D9190EF000889C2 /* GridMessageItem.swift in Sources */, @@ -1360,20 +1423,24 @@ D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */, D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */, D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */, + D023ED301DDB605D00BD496D /* LegacyEmptyController.swift in Sources */, D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */, D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */, D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */, + D07551931DDA540F0073E051 /* TelegramInitializeLegacyComponents.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */, D0E7A1BD1D8C246D00C37A6F /* ChatHistoryListNode.swift in Sources */, D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */, + D023ED2E1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift in Sources */, D00370321DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift in Sources */, D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */, D0DE77301D934DEF002B8809 /* ListMessageItem.swift in Sources */, D08C36831DB66AD40064C744 /* ChatMediaInputStickerGridItem.swift in Sources */, D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */, + D075518D1DDA4E0B0073E051 /* LegacyControllerNode.swift in Sources */, D07CFF7B1DCA24BF00761F81 /* ChatListNodeEntries.swift in Sources */, D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */, D021E0D01DB413BC00C6B04F /* ChatInputNode.swift in Sources */, @@ -1399,10 +1466,12 @@ D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */, D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */, + D05811941DD5F9380057C769 /* TelegramApplicationContext.swift in Sources */, D0ED5D4B1DC806D7007CBB15 /* ApplicationSpecificData.swift in Sources */, D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */, D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */, D0F69E2F1D6B8B030046BCD6 /* ChatMessageBubbleContentCalclulateImageCorners.swift in Sources */, + D075518B1DDA4D7D0073E051 /* LegacyController.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, @@ -1443,6 +1512,7 @@ D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */, D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */, D0F69E6A1D6B8C160046BCD6 /* MapInputController.swift in Sources */, + D00219041DDCC86400BE708A /* PerformanceSpinner.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */, D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, @@ -1456,14 +1526,18 @@ D0BA6F881D784F880034826E /* ChatMessageSelectionInputPanelNode.swift in Sources */, D0F69E3B1D6B8B030046BCD6 /* ChatMessageStickerItemNode.swift in Sources */, D0F69DEF1D6B8A6C0046BCD6 /* AuthorizationCodeController.swift in Sources */, + D01AC9181DD5033100E8160F /* ChatMessageActionButtonsNode.swift in Sources */, D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */, D0DE77231D932043002B8809 /* PeerMediaCollectionInterfaceState.swift in Sources */, D0F69D781D6B87DF0046BCD6 /* MediaTrackFrameBuffer.swift in Sources */, + D01AC91F1DD5E09000E8160F /* EditAccessoryPanelNode.swift in Sources */, + D023EBB21DDA800700BD496D /* LegacyMediaPickers.swift in Sources */, D0F69DD01D6B8A0D0046BCD6 /* SearchBarPlaceholderNode.swift in Sources */, D03120F61DA534C1006A2A60 /* PeerInfoActionItem.swift in Sources */, D0F69E781D6B8C340046BCD6 /* ContactsVCardItem.swift in Sources */, D0D268691D78865300C422DA /* ChatAvatarNavigationNode.swift in Sources */, D0F69DA51D6B87EC0046BCD6 /* MediaTrackFrameDecoder.swift in Sources */, + D023ED321DDB60CF00BD496D /* LegacyNavigationController.swift in Sources */, D0F69E2D1D6B8B030046BCD6 /* ChatMessageActionItemNode.swift in Sources */, D0F69DE21D6B8A420046BCD6 /* ListControllerGroupableItem.swift in Sources */, D0F69D791D6B87DF0046BCD6 /* MediaTrackFrame.swift in Sources */, @@ -1481,6 +1555,8 @@ D02BE0711D91814C000889C2 /* ChatHistoryGridNode.swift in Sources */, D0F69DCF1D6B8A0D0046BCD6 /* SearchBarNode.swift in Sources */, D0F69DE41D6B8A420046BCD6 /* ListControllerNode.swift in Sources */, + D00E15261DDBD4E700ACF65C /* LegacyCamera.swift in Sources */, + D00219061DDD1C9E00BE708A /* ImageContainingNode.swift in Sources */, D0F69E2E1D6B8B030046BCD6 /* ChatMessageAvatarAccessoryItem.swift in Sources */, D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */, D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */, @@ -1581,7 +1657,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 3.0.1; }; name = Hockeyapp; }; @@ -1729,7 +1805,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 3.0.1; }; name = Debug; }; @@ -1760,7 +1836,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.telegram.TelegramUI; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 3.0.1; }; name = Release; }; diff --git a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/TelegramUI.xcscheme b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/TelegramUI.xcscheme index d3e17a9527..7dae29418e 100644 --- a/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/TelegramUI.xcscheme +++ b/TelegramUI.xcodeproj/xcuserdata/peter.xcuserdatad/xcschemes/TelegramUI.xcscheme @@ -1,6 +1,6 @@ mapToSignal { [weak self] authorization, account -> Signal in if let strongSelf = self { switch authorization { - case let .authorization(user): + case let .authorization(_, _, user): let user = TelegramUser(user: user) return account.postbox.modify { modifier -> AccountState in diff --git a/TelegramUI/ChannelInfoEntries.swift b/TelegramUI/ChannelInfoEntries.swift index d547a886c1..a5c6832824 100644 --- a/TelegramUI/ChannelInfoEntries.swift +++ b/TelegramUI/ChannelInfoEntries.swift @@ -67,7 +67,7 @@ enum ChannelInfoEntry: PeerInfoEntry { if !lhsCachedData.isEqual(to: rhsCachedData) { return false } - } else if (rhsCachedData == nil) != (rhsCachedData != nil) { + } else if (lhsCachedData != nil) != (rhsCachedData != nil) { return false } return true diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index a127d341ed..83f33ef649 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -37,12 +37,20 @@ public class ChatController: ViewController { private let controllerNavigationDisposable = MetaDisposable() private let sentMessageEventsDisposable = MetaDisposable() + private let messageActionCallbackDisposable = MetaDisposable() + private let editMessageDisposable = MetaDisposable() + private let enqueueMediaMessageDisposable = MetaDisposable() + private var resolvePeerByNameDisposable: MetaDisposable? + + private let editingMessage = ValuePromise(false, ignoreRepeated: true) public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { self.account = account self.peerId = peerId self.messageId = messageId + performanceSpinnerAcquire() + super.init() self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil) @@ -118,6 +126,23 @@ public class ChatController: ViewController { if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id, messageId: nil)) } + }, openPeerMention: { [weak self] name in + if let strongSelf = self { + let disposable: MetaDisposable + if let resolvePeerByNameDisposable = strongSelf.resolvePeerByNameDisposable { + disposable = resolvePeerByNameDisposable + } else { + disposable = MetaDisposable() + strongSelf.resolvePeerByNameDisposable = disposable + } + disposable.set((resolvePeerByName(account: strongSelf.account, name: name, ageLimit: 10) |> take(1) |> deliverOnMainQueue).start(next: { peerId in + if let strongSelf = self { + if let peerId = peerId { + (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peerId, messageId: nil)) + } + } + })) + } }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(id) { @@ -164,11 +189,42 @@ public class ChatController: ViewController { strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(id) } }) } } + }, sendMessage: { [weak self] text in + if let strongSelf = self { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: text, media: nil, replyToMessageId: nil)]).start() + } }, sendSticker: { [weak self] file in if let strongSelf = self { strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", media: file, replyToMessageId: nil)]).start() } + }, requestMessageActionCallback: { [weak self] messageId, data in + if let strongSelf = self { + strongSelf.messageActionCallbackDisposable.set(requestMessageActionCallback(account: strongSelf.account, messageId: messageId, data: data).start()) + } + }, openUrl: { [weak self] url in + if let strongSelf = self { + if let applicationContext = strongSelf.account.applicationContext as? TelegramApplicationContext { + applicationContext.openUrl(url) + } + } + }, shareCurrentLocation: { [weak self] in + if let strongSelf = self { + + } + }, shareAccountContact: { [weak self] in + if let strongSelf = self { + } + }, sendBotCommand: { [weak self] messageId, command in + if let strongSelf = self { + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) + var postAsReply = false + if strongSelf.peerId.namespace == Namespaces.Peer.CloudChannel || strongSelf.peerId.namespace == Namespaces.Peer.CloudGroup { + postAsReply = true + } + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: command, media: nil, replyToMessageId: postAsReply ? messageId : nil)]).start() + } }) self.controllerInteraction = controllerInteraction @@ -212,6 +268,10 @@ public class ChatController: ViewController { self.peerDisposable.dispose() self.controllerNavigationDisposable.dispose() self.sentMessageEventsDisposable.dispose() + self.messageActionCallbackDisposable.dispose() + self.editMessageDisposable.dispose() + self.enqueueMediaMessageDisposable.dispose() + self.resolvePeerByNameDisposable?.dispose() } var chatDisplayNode: ChatControllerNode { @@ -316,6 +376,63 @@ public class ChatController: ViewController { self.chatDisplayNode.displayAttachmentMenu = { [weak self] in if let strongSelf = self { + if true { + let emptyController = LegacyEmptyController() + let navigationController = makeLegacyNavigationController(rootController: emptyController) + navigationController.setNavigationBarHidden(true, animated: false) + + let legacyController = LegacyController(legacyController: navigationController, presentation: .custom) + + var presentOverlayController: ((UIViewController) -> (() -> Void))? + let controller = legacyAttachmentMenu(parentController: legacyController, presentOverlayController: { controller in + if let presentOverlayController = presentOverlayController { + return presentOverlayController(controller) + } else { + return { + } + } + }, openGallery: { + self?.presentMediaPicker() + }, openCamera: { cameraView, menuController in + if let strongSelf = self { + presentedLegacyCamera(cameraView: cameraView, menuController: menuController, parentController: strongSelf, sendMessagesWithSignals: { signals in + self?.enqueueMediaMessages(signals: signals) + }) + } + }, sendMessagesWithSignals: { [weak self] signals in + self?.enqueueMediaMessages(signals: signals) + }) + controller.applicationInterface = legacyController.applicationInterface + controller.didDismiss = { [weak legacyController] _ in + legacyController?.dismiss() + } + + strongSelf.present(legacyController, in: .window) + controller.present(in: emptyController, sourceView: nil, animated: true) + + presentOverlayController = { [weak legacyController] controller in + if let strongSelf = self, let legacyController = legacyController { + let childController = LegacyController(legacyController: controller, presentation: .custom) + legacyController.present(childController, in: .window) + return { [weak childController] in + childController?.dismiss() + } + } else { + return { + } + } + } + + //controller presentInViewController:self sourceView:_inputTextPanel.attachButton animated:true]; + + return + } + + if true { + strongSelf.presentMediaPicker() + return + } + let controller = ChatMediaActionSheetController() controller.photo = { [weak strongSelf] asset in if let strongSelf = strongSelf { @@ -325,7 +442,7 @@ public class ChatController: ViewController { let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0)) let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) - if true { + if false { let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({}) enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: [.message(text: "", media: media, replyToMessageId: nil)]).start() @@ -367,6 +484,13 @@ public class ChatController: ViewController { strongSelf.chatDisplayNode.ensureInputViewFocused() } } + }, setupEditMessage: { [weak self] messageId in + if let strongSelf = self, strongSelf.isNodeLoaded { + if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: message.text))) } }) + strongSelf.chatDisplayNode.ensureInputViewFocused() + } + } }, beginMessageSelection: { [weak self] messageId in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) { @@ -429,13 +553,25 @@ public class ChatController: ViewController { } }, updateTextInputState: { [weak self] textInputState in if let strongSelf = self { - strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedInputState(textInputState) } }) + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputState) } }) } }, updateInputMode: { [weak self] f in if let strongSelf = self { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInputMode(f) }) } - }) + }, editMessage: { [weak self] messageId, text in + if let strongSelf = self { + let editingMessage = strongSelf.editingMessage + editingMessage.set(true) + strongSelf.editMessageDisposable.set((requestEditMessage(account: strongSelf.account, messageId: messageId, text: text) |> deliverOnMainQueue |> afterDisposed({ + editingMessage.set(false) + })).start(completed: { + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedEditMessage(nil) }) }) + } + })) + } + }, statuses: ChatPanelInterfaceInteractionStatuses(editingMessage: self.editingMessage.get())) self.interfaceInteraction = interfaceInteraction self.chatDisplayNode.interfaceInteraction = interfaceInteraction @@ -463,6 +599,7 @@ public class ChatController: ViewController { override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + self.chatDisplayNode.historyNode.canReadHistory.set(false) let peerId = self.peerId let timestamp = Int32(Date().timeIntervalSince1970) let interfaceState = self.presentationInterfaceState.interfaceState.withUpdatedTimestamp(timestamp) @@ -564,4 +701,60 @@ public class ChatController: ViewController { break } } + + private func presentMediaPicker() { + legacyAssetPicker().start(next: { [weak self] generator in + if let strongSelf = self { + var presentOverlayController: ((UIViewController) -> (() -> Void))? + let controller = generator({ controller in + return presentOverlayController!(controller) + }) + let legacyController = LegacyController(legacyController: controller, presentation: .modal) + + presentOverlayController = { [weak legacyController] controller in + if let strongSelf = self, let legacyController = legacyController { + let childController = LegacyController(legacyController: controller, presentation: .custom) + legacyController.present(childController, in: .window) + return { [weak childController] in + childController?.dismiss() + } + } else { + return { + } + } + } + + configureLegacyAssetPicker(controller) + controller.descriptionGenerator = legacyAssetPickerItemGenerator() + controller.completionBlock = { [weak self, weak legacyController] signals in + if let strongSelf = self, let legacyController = legacyController { + legacyController.dismiss() + strongSelf.enqueueMediaMessages(signals: signals) + } + } + controller.dismissalBlock = { [weak legacyController] in + if let legacyController = legacyController { + legacyController.dismiss() + } + } + strongSelf.present(legacyController, in: .window) + } + }) + } + + private func enqueueMediaMessages(signals: [Any]?) { + self.enqueueMediaMessageDisposable.set((legacyAssetPickerEnqueueMessages(account: self.account, peerId: self.peerId, signals: signals!) |> deliverOnMainQueue).start(next: { [weak self] messages in + if let strongSelf = self { + let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId + strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({ + if let strongSelf = self { + strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { + $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) } + }) + } + }) + enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages.map { $0.withUpdatedReplyToMessageId(replyMessageId) }).start() + } + })) + } } diff --git a/TelegramUI/ChatControllerInteraction.swift b/TelegramUI/ChatControllerInteraction.swift index 3f82264aaa..6e89c0cdc5 100644 --- a/TelegramUI/ChatControllerInteraction.swift +++ b/TelegramUI/ChatControllerInteraction.swift @@ -11,21 +11,35 @@ public enum ChatControllerInteractionNavigateToPeer { public final class ChatControllerInteraction { let openMessage: (MessageId) -> Void let openPeer: (PeerId, ChatControllerInteractionNavigateToPeer) -> Void + let openPeerMention: (String) -> Void let openMessageContextMenu: (MessageId, ASDisplayNode, CGRect) -> Void let navigateToMessage: (MessageId, MessageId) -> Void let clickThroughMessage: () -> Void var hiddenMedia: [MessageId: [Media]] = [:] var selectionState: ChatInterfaceSelectionState? let toggleMessageSelection: (MessageId) -> Void + let sendMessage: (String) -> Void let sendSticker: (TelegramMediaFile) -> Void + let requestMessageActionCallback: (MessageId, MemoryBuffer?) -> Void + let openUrl: (String) -> Void + let shareCurrentLocation: () -> Void + let shareAccountContact: () -> Void + let sendBotCommand: (MessageId, String) -> 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, sendSticker: @escaping (TelegramMediaFile) -> Void) { + public init(openMessage: @escaping (MessageId) -> Void, openPeer: @escaping (PeerId, ChatControllerInteractionNavigateToPeer) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (MessageId, ASDisplayNode, CGRect) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, clickThroughMessage: @escaping () -> Void, toggleMessageSelection: @escaping (MessageId) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (TelegramMediaFile) -> Void, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?) -> Void, openUrl: @escaping (String) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId, String) -> Void) { self.openMessage = openMessage self.openPeer = openPeer + self.openPeerMention = openPeerMention self.openMessageContextMenu = openMessageContextMenu self.navigateToMessage = navigateToMessage self.clickThroughMessage = clickThroughMessage self.toggleMessageSelection = toggleMessageSelection + self.sendMessage = sendMessage self.sendSticker = sendSticker + self.requestMessageActionCallback = requestMessageActionCallback + self.openUrl = openUrl + self.shareCurrentLocation = shareCurrentLocation + self.shareAccountContact = shareAccountContact + self.sendBotCommand = sendBotCommand } } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 90b66cebbf..6cf1e42a04 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -102,29 +102,43 @@ class ChatControllerNode: ASDisplayNode { if textInputPanelNode.textInputNode?.isFirstResponder() ?? false { applyKeyboardAutocorrection() } - let text = textInputPanelNode.text - if !text.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { - strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in - if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { - strongSelf.ignoreUpdateHeight = true - textInputPanelNode.text = "" - strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil) }) - strongSelf.ignoreUpdateHeight = false - } - }) + var effectivePresentationInterfaceState = strongSelf.chatPresentationInterfaceState + if let textInputPanelNode = strongSelf.textInputPanelNode { + effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } + } + + if let editMessage = effectivePresentationInterfaceState.interfaceState.editMessage { + let text = editMessage.inputState.inputText - var messages: [EnqueueMessage] = [] - if !text.isEmpty { - messages.append(.message(text: text, media: nil, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId)) - } - if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { - for id in forwardMessageIds { - messages.append(.forward(source: id)) - } + if let interfaceInteraction = strongSelf.interfaceInteraction, !text.isEmpty { + interfaceInteraction.editMessage(editMessage.messageId, editMessage.inputState.inputText) } + } else { + let text = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText - let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start() + if !text.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { + strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in + if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { + strongSelf.ignoreUpdateHeight = true + textInputPanelNode.text = "" + strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil) }) + strongSelf.ignoreUpdateHeight = false + } + }) + + var messages: [EnqueueMessage] = [] + if !text.isEmpty { + messages.append(.message(text: text, media: nil, replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId)) + } + if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { + for id in forwardMessageIds { + messages.append(.forward(source: id)) + } + } + + let _ = enqueueMessages(account: strongSelf.account, peerId: strongSelf.peerId, messages: messages).start() + } } } } @@ -260,6 +274,8 @@ class ChatControllerNode: ASDisplayNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) }) } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedForwardMessageIds(nil) }) + } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { + strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedEditMessage(nil) }) } } } @@ -452,17 +468,17 @@ class ChatControllerNode: ASDisplayNode { func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool, interactive: Bool) { if let textInputPanelNode = self.textInputPanelNode { - self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedInputState(textInputPanelNode.inputTextState) } + self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } if self.chatPresentationInterfaceState != chatPresentationInterfaceState { var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) - var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState + var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState - let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil + let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { - textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.inputState, keepSendButtonEnabled: keepSendButtonEnabled, animated: animated) + textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.effectiveInputState, keepSendButtonEnabled: keepSendButtonEnabled, animated: animated) } else { textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, animated: animated) } diff --git a/TelegramUI/ChatHistoryEntry.swift b/TelegramUI/ChatHistoryEntry.swift index a36ef7e352..dd530aba76 100644 --- a/TelegramUI/ChatHistoryEntry.swift +++ b/TelegramUI/ChatHistoryEntry.swift @@ -41,6 +41,9 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { case let .MessageEntry(lhsMessage, lhsRead): switch rhs { case let .MessageEntry(rhsMessage, rhsRead) where MessageIndex(lhsMessage) == MessageIndex(rhsMessage) && lhsMessage.flags == rhsMessage.flags && lhsRead == rhsRead: + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } if lhsMessage.media.count != rhsMessage.media.count { return false } @@ -49,6 +52,18 @@ func ==(lhs: ChatHistoryEntry, rhs: ChatHistoryEntry) -> Bool { return false } } + if lhsMessage.associatedMessages.count != rhsMessage.associatedMessages.count { + return false + } + if !lhsMessage.associatedMessages.isEmpty { + for (id, message) in lhsMessage.associatedMessages { + if let otherMessage = rhsMessage.associatedMessages[id] { + if otherMessage.stableVersion != message.stableVersion { + return false + } + } + } + } return true default: return false diff --git a/TelegramUI/ChatHistoryListNode.swift b/TelegramUI/ChatHistoryListNode.swift index 574dc17c58..0c41bcdd31 100644 --- a/TelegramUI/ChatHistoryListNode.swift +++ b/TelegramUI/ChatHistoryListNode.swift @@ -73,10 +73,10 @@ struct ChatHistoryListViewTransition { let initialData: InitialMessageHistoryData? } -private func maxIncomingMessageIdForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageId? { +private func maxIncomingMessageIndexForEntries(_ entries: [ChatHistoryEntry], indexRange: (Int, Int)) -> MessageIndex? { for i in (indexRange.0 ... indexRange.1).reversed() { if case let .MessageEntry(message, _) = entries[i], message.flags.contains(.Incoming) { - return message.id + return MessageIndex(message) } } return nil @@ -95,9 +95,9 @@ private func mappedInsertEntries(account: Account, peerId: PeerId, controllerInt } return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case .HoleEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatHoleItem(), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatHoleItem(index: entry.entry.index), directionHint: entry.directionHint) case .UnreadEntry: - return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(), directionHint: entry.directionHint) + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index), directionHint: entry.directionHint) } } } @@ -115,9 +115,9 @@ private func mappedUpdateEntries(account: Account, peerId: PeerId, controllerInt } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: item, directionHint: entry.directionHint) case .HoleEntry: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatHoleItem(), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatHoleItem(index: entry.entry.index), directionHint: entry.directionHint) case .UnreadEntry: - return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(), directionHint: entry.directionHint) + return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ChatUnreadItem(index: entry.entry.index), directionHint: entry.directionHint) } } } @@ -161,7 +161,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return self._initialData.get() } - private let maxVisibleIncomingMessageId = ValuePromise() + private let maxVisibleIncomingMessageIndex = ValuePromise(ignoreRepeated: true) let canReadHistory = ValuePromise() private let _chatHistoryLocation = ValuePromise() @@ -264,22 +264,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { self.historyDisposable.set(appliedTransition.start()) - let previousMaxIncomingMessageId = Atomic(value: nil) - let readHistory = combineLatest(self.maxVisibleIncomingMessageId.get(), self.canReadHistory.get()) - |> map { messageId, canRead in + let previousMaxIncomingMessageIdByNamespace = Atomic<[MessageId.Namespace: MessageId]>(value: [:]) + let readHistory = combineLatest(self.maxVisibleIncomingMessageIndex.get(), self.canReadHistory.get()) + |> map { messageIndex, canRead in if canRead { var apply = false - let _ = previousMaxIncomingMessageId.modify { previousId in - if previousId == nil || previousId! < messageId { + let _ = previousMaxIncomingMessageIdByNamespace.modify { dict in + let previousIndex = dict[messageIndex.id.namespace] + if previousIndex == nil || previousIndex!.id < messageIndex.id.id { apply = true - return messageId - } else { - return previousId + var dict = dict + dict[messageIndex.id.namespace] = messageIndex.id + return dict } + return dict } if apply { let _ = account.postbox.modify({ modifier in - modifier.applyInteractiveReadMaxId(messageId) + modifier.applyInteractiveReadMaxId(messageIndex.id) }).start() } } @@ -297,8 +299,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { if let strongSelf = self { if let historyView = (opaqueTransactionState as? ChatHistoryTransactionOpaqueState)?.historyView { if let visible = displayedRange.visibleRange { - if let messageId = maxIncomingMessageIdForEntries(historyView.filteredEntries, indexRange: (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)) { - strongSelf.updateMaxVisibleReadIncomingMessageId(messageId) + if let messageIndex = maxIncomingMessageIndexForEntries(historyView.filteredEntries, indexRange: (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)) { + strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex) } } @@ -341,8 +343,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { return nil } - private func updateMaxVisibleReadIncomingMessageId(_ id: MessageId) { - self.maxVisibleIncomingMessageId.set(id) + private func updateMaxVisibleReadIncomingMessageIndex(_ index: MessageIndex) { + self.maxVisibleIncomingMessageIndex.set(index) } private func enqueueHistoryViewTransition(_ transition: ChatHistoryListViewTransition) -> Signal { @@ -388,8 +390,8 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode { strongSelf.account.postbox.updateMessageHistoryViewVisibleRange(transition.historyView.originalView.id, earliestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.lastIndex].index, latestVisibleIndex: transition.historyView.filteredEntries[transition.historyView.filteredEntries.count - 1 - range.firstIndex].index) if let visible = visibleRange.visibleRange { - if let messageId = maxIncomingMessageIdForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) { - strongSelf.updateMaxVisibleReadIncomingMessageId(messageId) + if let messageIndex = maxIncomingMessageIndexForEntries(transition.historyView.filteredEntries, indexRange: (transition.historyView.filteredEntries.count - 1 - visible.lastIndex, transition.historyView.filteredEntries.count - 1 - visible.firstIndex)) { + strongSelf.updateMaxVisibleReadIncomingMessageIndex(messageIndex) } } } diff --git a/TelegramUI/ChatHistoryViewForLocation.swift b/TelegramUI/ChatHistoryViewForLocation.swift index 33d43ccc7f..cdb87e08fe 100644 --- a/TelegramUI/ChatHistoryViewForLocation.swift +++ b/TelegramUI/ChatHistoryViewForLocation.swift @@ -19,10 +19,15 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun if preloaded { return .HistoryView(view: view, type: .Generic(type: updateType), scrollPosition: nil, initialData: initialData) } else { + var scrollPosition: ChatHistoryViewScrollPosition? + if let maxReadIndex = view.maxReadIndex { + let aroundIndex = maxReadIndex + scrollPosition = .Unread(index: maxReadIndex) + var targetIndex = 0 for i in 0 ..< view.entries.count { - if view.entries[i].index >= maxReadIndex { + if view.entries[i].index >= aroundIndex { targetIndex = i break } @@ -37,13 +42,23 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocation, account: Accoun } } } - - preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: .Unread(index: maxReadIndex), initialData: initialData) } else { - preloaded = true - return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil, initialData: initialData) + var messageCount = 0 + for entry in view.entries.reversed() { + if case .HoleEntry = entry { + fadeIn = true + return .Loading(initialData: initialData) + } else { + messageCount += 1 + } + if messageCount >= 1 { + break + } + } } + + preloaded = true + return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: initialData) } } case let .InitialSearch(messageId, count): diff --git a/TelegramUI/ChatHoleItem.swift b/TelegramUI/ChatHoleItem.swift index 5583f9044a..5b1669dd18 100644 --- a/TelegramUI/ChatHoleItem.swift +++ b/TelegramUI/ChatHoleItem.swift @@ -15,6 +15,14 @@ private func backgroundImage(color: UIColor) -> UIImage? { private let titleFont = UIFont.systemFont(ofSize: 13.0) class ChatHoleItem: ListViewItem { + let index: MessageIndex + let header: ChatMessageDateHeader + + init(index: MessageIndex) { + self.index = index + self.header = ChatMessageDateHeader(timestamp: index.timestamp) + } + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { async { @@ -26,9 +34,12 @@ class ChatHoleItem: ListViewItem { } class ChatHoleItemNode: ListViewItemNode { + var item: ChatHoleItem? let backgroundNode: ASImageNode let labelNode: TextNode + private let layoutConstants = ChatMessageItemLayoutConstants() + init() { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -49,21 +60,27 @@ class ChatHoleItemNode: ListViewItemNode { } override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - let (layout, apply) = self.asyncLayout()(width) - apply() - self.contentSize = layout.contentSize - self.insets = layout.insets + if let item = item as? ChatHoleItem { + let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) + let (layout, apply) = self.asyncLayout()(item, width, dateAtBottom) + apply() + self.contentSize = layout.contentSize + self.insets = layout.insets + } } - func asyncLayout() -> (_ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ChatHoleItem, _ width: CGFloat, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) - return { width in + let layoutConstants = self.layoutConstants + return { item, width, dateAtBottom in let (size, apply) = labelLayout(NSAttributedString(string: "Loading", font: titleFont, textColor: UIColor.white), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] in + return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: UIEdgeInsets(top: 4.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] in if let strongSelf = self { + strongSelf.item = item + let _ = apply() strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((width - backgroundSize.width) / 2.0), y: 0.0), size: backgroundSize) @@ -72,4 +89,12 @@ class ChatHoleItemNode: ListViewItemNode { }) } } + + override public func header() -> ListViewItemHeader? { + if let item = self.item { + return item.header + } else { + return nil + } + } } diff --git a/TelegramUI/ChatInterfaceInputContexts.swift b/TelegramUI/ChatInterfaceInputContexts.swift index 659b05c5f1..dcfca65d80 100644 --- a/TelegramUI/ChatInterfaceInputContexts.swift +++ b/TelegramUI/ChatInterfaceInputContexts.swift @@ -3,12 +3,16 @@ import TelegramCore import Postbox func inputContextForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatPresentationInputContext? { - if chatPresentationInterfaceState.interfaceState.inputState.inputText == "#" { - return .hashtag - } else if chatPresentationInterfaceState.interfaceState.inputState.inputText == "@" { - return .mention + if let _ = chatPresentationInterfaceState.interfaceState.editMessage { + return nil + } else { + if chatPresentationInterfaceState.interfaceState.composeInputState.inputText == "#" { + return .hashtag + } else if chatPresentationInterfaceState.interfaceState.composeInputState.inputText == "@" { + return .mention + } + return nil } - return nil } func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatTextInputPanelState { @@ -16,10 +20,14 @@ func inputTextPanelStateForChatPresentationInterfaceState(_ chatPresentationInte case .media: return ChatTextInputPanelState(accessoryItems: [.keyboard]) case .none, .text: - if chatPresentationInterfaceState.interfaceState.inputState.inputText.isEmpty { - return ChatTextInputPanelState(accessoryItems: [.stickers]) - } else { + if let _ = chatPresentationInterfaceState.interfaceState.editMessage { return ChatTextInputPanelState(accessoryItems: []) + } else { + if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty { + return ChatTextInputPanelState(accessoryItems: [.stickers]) + } else { + return ChatTextInputPanelState(accessoryItems: []) + } } } } diff --git a/TelegramUI/ChatInterfaceState.swift b/TelegramUI/ChatInterfaceState.swift index 4c5f7bd107..f6f64b07a5 100644 --- a/TelegramUI/ChatInterfaceState.swift +++ b/TelegramUI/ChatInterfaceState.swift @@ -45,6 +45,12 @@ struct ChatTextInputState: Coding, Equatable { self.selectionRange = selectionRange } + init(inputText: String) { + self.inputText = inputText + let length = (inputText as NSString).length + self.selectionRange = length ..< length + } + init(decoder: Decoder) { self.inputText = decoder.decodeStringForKey("t") self.selectionRange = Int(decoder.decodeInt32ForKey("s0")) ..< Int(decoder.decodeInt32ForKey("s1")) @@ -57,6 +63,40 @@ struct ChatTextInputState: Coding, Equatable { } } +struct ChatEditMessageState: Coding, Equatable { + let messageId: MessageId + let inputState: ChatTextInputState + + init(messageId: MessageId, inputState: ChatTextInputState) { + self.messageId = messageId + self.inputState = inputState + } + + init(decoder: Decoder) { + self.messageId = MessageId(peerId: PeerId(decoder.decodeInt64ForKey("mp")), namespace: decoder.decodeInt32ForKey("mn"), id: decoder.decodeInt32ForKey("mi")) + if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { + self.inputState = inputState + } else { + self.inputState = ChatTextInputState() + } + } + + func encode(_ encoder: Encoder) { + encoder.encodeInt64(self.messageId.peerId.toInt64(), forKey: "mp") + encoder.encodeInt32(self.messageId.namespace, forKey: "mn") + encoder.encodeInt32(self.messageId.id, forKey: "mi") + encoder.encodeObject(self.inputState, forKey: "is") + } + + static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool { + return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState + } + + func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState { + return ChatEditMessageState(messageId: self.messageId, inputState: inputState) + } +} + final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { let timestamp: Int32 let text: String @@ -87,41 +127,52 @@ final class ChatEmbeddedInterfaceState: PeerChatListEmbeddedInterfaceState { final class ChatInterfaceState: PeerChatInterfaceState, Equatable { let timestamp: Int32 - let inputState: ChatTextInputState + let composeInputState: ChatTextInputState let replyMessageId: MessageId? let forwardMessageIds: [MessageId]? + let editMessage: ChatEditMessageState? let selectionState: ChatInterfaceSelectionState? var chatListEmbeddedState: PeerChatListEmbeddedInterfaceState? { - if !self.inputState.inputText.isEmpty && self.timestamp != 0 { - return ChatEmbeddedInterfaceState(timestamp: self.timestamp, text: self.inputState.inputText) + if !self.composeInputState.inputText.isEmpty && self.timestamp != 0 { + return ChatEmbeddedInterfaceState(timestamp: self.timestamp, text: self.composeInputState.inputText) } else { return nil } } + var effectiveInputState: ChatTextInputState { + if let editMessage = self.editMessage { + return editMessage.inputState + } else { + return self.composeInputState + } + } + init() { self.timestamp = 0 - self.inputState = ChatTextInputState() + self.composeInputState = ChatTextInputState() self.replyMessageId = nil self.forwardMessageIds = nil + self.editMessage = nil self.selectionState = nil } - init(timestamp: Int32, inputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, selectionState: ChatInterfaceSelectionState?) { + init(timestamp: Int32, composeInputState: ChatTextInputState, replyMessageId: MessageId?, forwardMessageIds: [MessageId]?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?) { self.timestamp = timestamp - self.inputState = inputState + self.composeInputState = composeInputState self.replyMessageId = replyMessageId self.forwardMessageIds = forwardMessageIds + self.editMessage = editMessage self.selectionState = selectionState } init(decoder: Decoder) { self.timestamp = decoder.decodeInt32ForKey("ts") if let inputState = decoder.decodeObjectForKey("is", decoder: { return ChatTextInputState(decoder: $0) }) as? ChatTextInputState { - self.inputState = inputState + self.composeInputState = inputState } else { - self.inputState = ChatTextInputState() + self.composeInputState = ChatTextInputState() } let replyMessageIdPeerId: Int64? = decoder.decodeInt64ForKey("r.p") let replyMessageIdNamespace: Int32? = decoder.decodeInt32ForKey("r.n") @@ -136,6 +187,11 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { self.forwardMessageIds = nil } + if let editMessage = decoder.decodeObjectForKey("em", decoder: { ChatEditMessageState(decoder: $0) }) as? ChatEditMessageState { + self.editMessage = editMessage + } else { + self.editMessage = nil + } if let selectionState = decoder.decodeObjectForKey("ss", decoder: { return ChatInterfaceSelectionState(decoder: $0) }) as? ChatInterfaceSelectionState { self.selectionState = selectionState } else { @@ -145,7 +201,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { func encode(_ encoder: Encoder) { encoder.encodeInt32(self.timestamp, forKey: "ts") - encoder.encodeObject(self.inputState, forKey: "is") + encoder.encodeObject(self.composeInputState, forKey: "is") if let replyMessageId = self.replyMessageId { encoder.encodeInt64(replyMessageId.peerId.toInt64(), forKey: "r.p") encoder.encodeInt32(replyMessageId.namespace, forKey: "r.n") @@ -162,6 +218,11 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { encoder.encodeNil(forKey: "fm") } + if let editMessage = self.editMessage { + encoder.encodeObject(editMessage, forKey: "em") + } else { + encoder.encodeNil(forKey: "em") + } if let selectionState = self.selectionState { encoder.encodeObject(selectionState, forKey: "ss") } else { @@ -185,19 +246,27 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else if (lhs.forwardMessageIds != nil) != (rhs.forwardMessageIds != nil) { return false } - return lhs.inputState == rhs.inputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState + return lhs.composeInputState == rhs.composeInputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState && lhs.editMessage == rhs.editMessage } - func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState) + func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState { + var updatedEditMessage = self.editMessage + var updatedComposeInputState = self.composeInputState + if let editMessage = self.editMessage { + updatedEditMessage = editMessage.withUpdatedInputState(inputState) + } else { + updatedComposeInputState = inputState + } + + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: updatedEditMessage, selectionState: self.selectionState) } func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) } func withUpdatedForwardMessageIds(_ forwardMessageIds: [MessageId]?) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) } func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -206,7 +275,7 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { selectedIds.formUnion(selectionState.selectedIds) } selectedIds.insert(messageId) - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) } func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { @@ -219,14 +288,18 @@ final class ChatInterfaceState: PeerChatInterfaceState, Equatable { } else { selectedIds.insert(messageId) } - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) } func withoutSelectionState() -> ChatInterfaceState { - return ChatInterfaceState(timestamp: self.timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: nil) + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: nil) } func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState { - return ChatInterfaceState(timestamp: timestamp, inputState: self.inputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, selectionState: self.selectionState) + return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: self.editMessage, selectionState: self.selectionState) + } + + func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState { + return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, replyMessageId: self.replyMessageId, forwardMessageIds: self.forwardMessageIds, editMessage: editMessage, selectionState: self.selectionState) } } diff --git a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift index 70714f03f3..ee535cfb39 100644 --- a/TelegramUI/ChatInterfaceStateAccessoryPanels.swift +++ b/TelegramUI/ChatInterfaceStateAccessoryPanels.swift @@ -7,7 +7,16 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS return nil } - if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds { + if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage { + if let editPanelNode = currentPanel as? EditAccessoryPanelNode, editPanelNode.messageId == editMessage.messageId { + editPanelNode.interfaceInteraction = interfaceInteraction + return editPanelNode + } else { + let panelNode = EditAccessoryPanelNode(account: account, messageId: editMessage.messageId) + panelNode.interfaceInteraction = interfaceInteraction + return panelNode + } + } else if let forwardMessageIds = chatPresentationInterfaceState.interfaceState.forwardMessageIds { if let forwardPanelNode = currentPanel as? ForwardAccessoryPanelNode, forwardPanelNode.messageIds == forwardMessageIds { forwardPanelNode.interfaceInteraction = interfaceInteraction return forwardPanelNode diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 8a7d887f39..eac30c1e04 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -22,12 +22,37 @@ func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceStat } else { canReply = true } + + var canEdit = false + if let author = message.author, author.id == account.peerId { + var hasUneditableAttributes = false + for attribute in message.attributes { + if let _ = attribute as? InlineBotMessageAttribute { + hasUneditableAttributes = true + break + } + } + + if !hasUneditableAttributes { + let timestamp = Int32(CFAbsoluteTimeGetCurrent()) + if message.timestamp >= timestamp - 60 * 60 * 24 * 2 { + canEdit = true + } + } + } + if canReply { actions.append(ContextMenuAction(content: .text("Reply"), action: { interfaceInteraction.setupReplyMessage(message.id) })) } + if canEdit { + actions.append(ContextMenuAction(content: .text("Edit"), action: { + interfaceInteraction.setupEditMessage(message.id) + })) + } + actions.append(ContextMenuAction(content: .text("Copy"), action: { if !message.text.isEmpty { UIPasteboard.general.string = message.text diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index 86e7735b6c..cb7cb7641a 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -111,6 +111,9 @@ enum ChatListNodeEntry: Comparable, Identifiable { if lhsIndex != rhsIndex { return false } + if lhsMessage.stableVersion != rhsMessage.stableVersion { + return false + } if lhsMessage.id != rhsMessage.id || lhsMessage.flags != rhsMessage.flags || lhsUnreadCount != rhsUnreadCount { return false } diff --git a/TelegramUI/ChatMessageActionButtonsNode.swift b/TelegramUI/ChatMessageActionButtonsNode.swift new file mode 100644 index 0000000000..27c5837338 --- /dev/null +++ b/TelegramUI/ChatMessageActionButtonsNode.swift @@ -0,0 +1,240 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore +import Display + +private let titleFont = Font.medium(16.0) +private let middleImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .middle) +private let bottomLeftImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .bottomLeft) +private let bottomRightImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .bottomRight) +private let bottomSingleImage = messageBubbleActionButtonImage(color: UIColor(0x596E89), position: .bottomSingle) + +private final class ChatMessageActionButtonNode: ASDisplayNode { + private let backgroundNode: ASImageNode + private var titleNode: TextNode? + private var buttonView: HighlightTrackingButton? + + private var button: ReplyMarkupButton? + var pressed: ((ReplyMarkupButton) -> Void)? + + override init() { + self.backgroundNode = ASImageNode() + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + self.backgroundNode.isLayerBacked = true + self.backgroundNode.alpha = 0.35 + self.backgroundNode.isUserInteractionEnabled = false + + super.init() + + self.addSubnode(self.backgroundNode) + } + + override func didLoad() { + super.didLoad() + + let buttonView = HighlightTrackingButton(frame: self.bounds) + buttonView.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside]) + self.buttonView = buttonView + self.view.addSubview(buttonView) + buttonView.highligthedChanged = { [weak self] highlighted in + if let strongSelf = self { + if highlighted { + strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") + strongSelf.backgroundNode.alpha = 0.55 + } else { + strongSelf.backgroundNode.alpha = 0.35 + strongSelf.backgroundNode.layer.animateAlpha(from: 0.55, to: 0.35, duration: 0.2) + } + } + } + } + + @objc func buttonPressed() { + if let button = self.button, let pressed = self.pressed { + pressed(button) + } + } + + class func asyncLayout(_ maybeNode: ChatMessageActionButtonNode?) -> (_ button: ReplyMarkupButton, _ constrainedWidth: CGFloat, _ position: MessageBubbleActionButtonPosition) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) { + let titleLayout = TextNode.asyncLayout(maybeNode?.titleNode) + + return { button, constrainedWidth, position in + let sideInset: CGFloat = 5.0 + let (titleSize, titleApply) = titleLayout(NSAttributedString(string: button.title, font: titleFont, textColor: .white), nil, 1, .end, CGSize(width: max(1.0, constrainedWidth - sideInset - sideInset), height: CGFloat.greatestFiniteMagnitude), nil) + + let backgroundImage: UIImage + switch position { + case .middle: + backgroundImage = middleImage + case .bottomLeft: + backgroundImage = bottomLeftImage + case .bottomRight: + backgroundImage = bottomRightImage + case .bottomSingle: + backgroundImage = bottomSingleImage + } + + return (titleSize.size.width + sideInset + sideInset, { width in + return (CGSize(width: width, height: 42.0), { + let node: ChatMessageActionButtonNode + if let maybeNode = maybeNode { + node = maybeNode + } else { + node = ChatMessageActionButtonNode() + } + + node.button = button + + node.backgroundNode.image = backgroundImage + node.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) + + let titleNode = titleApply() + if node.titleNode !== titleNode { + node.titleNode = titleNode + node.addSubnode(titleNode) + titleNode.isUserInteractionEnabled = false + } + titleNode.frame = CGRect(origin: CGPoint(x: floor((width - titleSize.size.width) / 2.0), y: floor((42.0 - titleSize.size.height) / 2.0) + 1.0), size: titleSize.size) + + node.buttonView?.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: 42.0)) + + return node + }) + }) + } + } +} + +final class ChatMessageActionButtonsNode: ASDisplayNode { + private var buttonNodes: [ChatMessageActionButtonNode] = [] + + private var buttonPressedWrapper: ((ReplyMarkupButton) -> Void)? + var buttonPressed: ((ReplyMarkupButton) -> Void)? + + override init() { + super.init() + + self.buttonPressedWrapper = { [weak self] button in + if let buttonPressed = self?.buttonPressed { + buttonPressed(button) + } + } + } + + class func asyncLayout(_ maybeNode: ChatMessageActionButtonsNode?) -> (_ replyMarkup: ReplyMarkupMessageAttribute, _ constrainedWidth: CGFloat) -> (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode) { + let currentButtonLayouts = maybeNode?.buttonNodes.map { ChatMessageActionButtonNode.asyncLayout($0) } ?? [] + + return { replyMarkup, constrainedWidth in + var buttonFramesAndApply: [(CGRect, () -> ChatMessageActionButtonNode)] = [] + var verticalRowOffset: CGFloat = 0.0 + let buttonHeight: CGFloat = 42.0 + let buttonSpacing: CGFloat = 4.0 + + verticalRowOffset += buttonSpacing + + var rowIndex = 0 + var buttonIndex = 0 + for row in replyMarkup.rows { + var minimumRowWidth: CGFloat = 0.0 + let maximumButtonWidth: CGFloat = floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)) + var finalizeRowButtonLayouts: [((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))] = [] + var rowButtonIndex = 0 + for button in row.buttons { + let buttonPosition: MessageBubbleActionButtonPosition + if rowIndex == replyMarkup.rows.count - 1 { + if row.buttons.count == 1 { + buttonPosition = .bottomSingle + } else if rowButtonIndex == 0 { + buttonPosition = .bottomLeft + } else if rowButtonIndex == row.buttons.count - 1 { + buttonPosition = .bottomRight + } else { + buttonPosition = .middle + } + } else { + buttonPosition = .middle + } + + let prepareButtonLayout: (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, () -> ChatMessageActionButtonNode))) + if buttonIndex < currentButtonLayouts.count { + prepareButtonLayout = currentButtonLayouts[buttonIndex](button, maximumButtonWidth, buttonPosition) + } else { + prepareButtonLayout = ChatMessageActionButtonNode.asyncLayout(nil)(button, maximumButtonWidth, buttonPosition) + } + + minimumRowWidth += prepareButtonLayout.minimumWidth + finalizeRowButtonLayouts.append(prepareButtonLayout.layout) + + buttonIndex += 1 + rowButtonIndex += 1 + } + + let actualButtonWidth: CGFloat = floor((constrainedWidth - CGFloat(max(0, row.buttons.count - 1)) * buttonSpacing) / CGFloat(row.buttons.count)) + var horizontalButtonOffset: CGFloat = 0.0 + for finalizeButtonLayout in finalizeRowButtonLayouts { + let (buttonSize, buttonApply) = finalizeButtonLayout(actualButtonWidth) + let buttonFrame = CGRect(origin: CGPoint(x: horizontalButtonOffset, y: verticalRowOffset), size: buttonSize) + buttonFramesAndApply.append((buttonFrame, buttonApply)) + horizontalButtonOffset += buttonSize.width + buttonSpacing + } + + verticalRowOffset += buttonHeight + buttonSpacing + rowIndex += 1 + } + if verticalRowOffset > 0.0 { + verticalRowOffset = max(0.0, verticalRowOffset - buttonSpacing) + } + + return (CGSize(width: constrainedWidth, height: verticalRowOffset), { animated in + let node: ChatMessageActionButtonsNode + if let maybeNode = maybeNode { + node = maybeNode + } else { + node = ChatMessageActionButtonsNode() + } + + var updatedButtons: [ChatMessageActionButtonNode] = [] + var index = 0 + for (buttonFrame, buttonApply) in buttonFramesAndApply { + let buttonNode = buttonApply() + buttonNode.frame = buttonFrame + updatedButtons.append(buttonNode) + if buttonNode.supernode == nil { + node.addSubnode(buttonNode) + buttonNode.pressed = node.buttonPressedWrapper + } + index += 1 + } + + var buttonsUpdated = false + if node.buttonNodes.count != updatedButtons.count { + buttonsUpdated = true + } else { + for i in 0 ..< updatedButtons.count { + if updatedButtons[i] !== node.buttonNodes[i] { + buttonsUpdated = true + break + } + } + } + if buttonsUpdated { + for currentButton in node.buttonNodes { + if !updatedButtons.contains(currentButton) { + currentButton.removeFromSupernode() + } + } + } + node.buttonNodes = updatedButtons + + if animated { + /*UIView.transition(with: node.view, duration: 0.2, options: [.transitionCrossDissolve], animations: { + + }, completion: nil)*/ + } + + return node + }) + } + } +} diff --git a/TelegramUI/ChatMessageActionItemNode.swift b/TelegramUI/ChatMessageActionItemNode.swift index 2873ce763b..28e4e18442 100644 --- a/TelegramUI/ChatMessageActionItemNode.swift +++ b/TelegramUI/ChatMessageActionItemNode.swift @@ -50,10 +50,11 @@ class ChatMessageActionItemNode: ChatMessageItemView { super.setupItem(item) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) + let layoutConstants = self.layoutConstants - return { item, width, mergedTop, mergedBottom in + return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in var attributedString: NSAttributedString? for media in item.message.media { @@ -105,8 +106,12 @@ class ChatMessageActionItemNode: ChatMessageItemView { let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil) let backgroundSize = CGSize(width: size.size.width + 8.0 + 8.0, height: 20.0) + var layoutInsets = UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0) + if dateHeaderAtBottom { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), { [weak self] animation in + return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 20.0), insets: layoutInsets), { [weak self] animation in if let strongSelf = self { let _ = apply() diff --git a/TelegramUI/ChatMessageBubbleContentNode.swift b/TelegramUI/ChatMessageBubbleContentNode.swift index 951515e808..3d745a43a0 100644 --- a/TelegramUI/ChatMessageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageBubbleContentNode.swift @@ -29,6 +29,14 @@ struct ChatMessageBubbleContentPosition { let bottom: ChatMessageBubbleRelativePosition } +enum ChatMessageBubbleContentTapAction { + case none + case url(String) + case textMention(String) + case peerMention(PeerId) + case botCommand(String) +} + class ChatMessageBubbleContentNode: ASDisplayNode { var properties: ChatMessageBubbleContentProperties { return ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0) @@ -41,7 +49,7 @@ class ChatMessageBubbleContentNode: ASDisplayNode { super.init() } - func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { + func asyncLayoutContent() -> (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (maxWidth: CGFloat, layout: (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))) { preconditionFailure() } @@ -63,4 +71,8 @@ class ChatMessageBubbleContentNode: ASDisplayNode { func updateHiddenMedia(_ media: [Media]?) { } + + func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + return .none + } } diff --git a/TelegramUI/ChatMessageBubbleImages.swift b/TelegramUI/ChatMessageBubbleImages.swift new file mode 100644 index 0000000000..636190d67a --- /dev/null +++ b/TelegramUI/ChatMessageBubbleImages.swift @@ -0,0 +1,117 @@ +import Foundation +import Display + +private let incomingFillColor = UIColor(0xffffff) +private let incomingStrokeColor = UIColor(0x86A9C9, 0.5) + +private let outgoingFillColor = UIColor(0xE1FFC7) +private let outgoingStrokeColor = UIColor(0x86A9C9, 0.5) + +enum MessageBubbleImageNeighbors { + case none + case top + case bottom + case both +} + +func messageBubbleImage(incoming: Bool, neighbors: MessageBubbleImageNeighbors) -> UIImage { + let diameter: CGFloat = 36.0 + let corner: CGFloat = 7.0 + return generateImage(CGSize(width: 42.0, height: diameter), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + + let additionalOffset: CGFloat + switch neighbors { + case .none, .bottom: + additionalOffset = 0.0 + case .both, .top: + additionalOffset = 6.0 + } + + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.scaleBy(x: incoming ? 1.0 : -1.0, y: -1.0) + context.translateBy(x: -size.width / 2.0 + 0.5 + additionalOffset, y: -size.height / 2.0 + 0.5) + + let lineWidth: CGFloat = 1.0 + + context.setFillColor((incoming ? incomingFillColor : outgoingFillColor).cgColor) + context.setLineWidth(lineWidth) + context.setStrokeColor((incoming ? incomingStrokeColor : outgoingStrokeColor).cgColor) + + switch neighbors { + case .none: + let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M6,17.5 C6,7.83289181 13.8350169,0 23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41102995e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.fillPath() + case .top: + let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L17.5,0 C7.83501688,0 0,7.83289181 0,17.5 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.fillPath() + case .bottom: + let _ = try? drawSvgPath(context, path: "M6,17.5 L6,5.99681848 C6,2.6882755 8.68486709,0 11.9968185,0 L23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41103066e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M6,17.5 L6,5.99681848 C6,2.6882755 8.68486709,0 11.9968185,0 L23.5,0 C33.1671082,0 41,7.83501688 41,17.5 C41,27.1671082 33.1649831,35 23.5,35 C19.2941198,35 15.4354328,33.5169337 12.4179496,31.0453367 C9.05531719,34.9894816 -2.41103066e-08,35 0,35 C5.972003,31.5499861 6,26.8616169 6,26.8616169 L6,17.5 L6,17.5 ") + context.fillPath() + case .both: + let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L5.99681848,0 C2.68486709,0 0,2.6882755 0,5.99681848 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.strokePath() + let _ = try? drawSvgPath(context, path: "M35,17.5 C35,7.83501688 27.1671082,0 17.5,0 L5.99681848,0 C2.68486709,0 0,2.6882755 0,5.99681848 L0,29.0031815 C0,32.3151329 2.6882755,35 5.99681848,35 L17.5,35 C27.1649831,35 35,27.1671082 35,17.5 L35,17.5 L35,17.5 ") + context.fillPath() + } + })!.stretchableImage(withLeftCapWidth: incoming ? Int(corner + diameter / 2.0) : Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0)) +} + +enum MessageBubbleActionButtonPosition { + case middle + case bottomLeft + case bottomRight + case bottomSingle +} + +func messageBubbleActionButtonImage(color: UIColor, position: MessageBubbleActionButtonPosition) -> UIImage { + let largeRadius: CGFloat = 17.0 + let smallRadius: CGFloat = 6.0 + let size: CGSize + if case .middle = position { + size = CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius) + } else { + size = CGSize(width: 35.0, height: 35.0) + } + return generateImage(size, contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.translateBy(x: size.width / 2.0, y: size.height / 2.0) + if case .bottomRight = position { + context.scaleBy(x: -1.0, y: -1.0) + } else { + context.scaleBy(x: 1.0, y: -1.0) + } + context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) + context.setFillColor(color.cgColor) + context.setBlendMode(.copy) + switch position { + case .middle: + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + case .bottomLeft, .bottomRight: + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - smallRadius - smallRadius, y: 0.0), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fill(CGRect(origin: CGPoint(x: smallRadius, y: 0.0), size: CGSize(width: size.width - smallRadius - smallRadius, height: smallRadius + smallRadius))) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - smallRadius - smallRadius, y: 0.0), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fill(CGRect(origin: CGPoint(x: smallRadius, y: 0.0), size: CGSize(width: size.width - smallRadius - smallRadius, height: smallRadius + smallRadius))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: smallRadius), size: CGSize(width: size.width, height: size.height - largeRadius - smallRadius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - smallRadius - smallRadius, y: size.height - smallRadius - smallRadius), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fill(CGRect(origin: CGPoint(x: largeRadius, y: size.height - largeRadius - largeRadius), size: CGSize(width: size.width - smallRadius - largeRadius, height: largeRadius + largeRadius))) + context.fill(CGRect(origin: CGPoint(x: size.width - smallRadius, y: size.height - largeRadius), size: CGSize(width: smallRadius, height: largeRadius - smallRadius))) + case .bottomSingle: + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fillEllipse(in: CGRect(origin: CGPoint(x: size.width - smallRadius - smallRadius, y: 0.0), size: CGSize(width: smallRadius + smallRadius, height: smallRadius + smallRadius))) + context.fill(CGRect(origin: CGPoint(x: smallRadius, y: 0.0), size: CGSize(width: size.width - smallRadius - smallRadius, height: smallRadius + smallRadius))) + context.fill(CGRect(origin: CGPoint(x: 0.0, y: smallRadius), size: CGSize(width: size.width, height: size.height - largeRadius - smallRadius))) + } + })!.stretchableImage(withLeftCapWidth: Int(size.width / 2.0), topCapHeight: Int(size.height / 2.0)) +} diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index d952a65a28..39e98d82f4 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -43,15 +43,15 @@ private func ==(lhs: ChatMessageBackgroundType, rhs: ChatMessageBackgroundType) } } -private let chatMessageBackgroundIncomingImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncoming")?.precomposed() -private let chatMessageBackgroundOutgoingImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoing")?.precomposed() -private let chatMessageBackgroundIncomingMergedTopImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedTop")?.precomposed() -private let chatMessageBackgroundIncomingMergedBottomImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedBottom")?.precomposed() -private let chatMessageBackgroundIncomingMergedBothImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleIncomingMergedBoth")?.precomposed() -private let chatMessageBackgroundOutgoingMergedImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed() -private let chatMessageBackgroundOutgoingMergedTopImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed() -private let chatMessageBackgroundOutgoingMergedBottomImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed() -private let chatMessageBackgroundOutgoingMergedBothImage = UIImage(bundleImageName: "Chat/Message/Background/BubbleOutgoingMerged")?.precomposed() +private let chatMessageBackgroundIncomingImage = messageBubbleImage(incoming: true, neighbors: .none) +private let chatMessageBackgroundIncomingMergedTopImage = messageBubbleImage(incoming: true, neighbors: .top) +private let chatMessageBackgroundIncomingMergedBottomImage = messageBubbleImage(incoming: true, neighbors: .bottom) +private let chatMessageBackgroundIncomingMergedBothImage = messageBubbleImage(incoming: true, neighbors: .both) + +private let chatMessageBackgroundOutgoingImage = messageBubbleImage(incoming: false, neighbors: .none) +private let chatMessageBackgroundOutgoingMergedTopImage = messageBubbleImage(incoming: false, neighbors: .top) +private let chatMessageBackgroundOutgoingMergedBottomImage = messageBubbleImage(incoming: false, neighbors: .bottom) +private let chatMessageBackgroundOutgoingMergedBothImage = messageBubbleImage(incoming: false, neighbors: .both) class ChatMessageBackground: ASImageNode { private var type: ChatMessageBackgroundType? @@ -77,9 +77,9 @@ class ChatMessageBackground: ASImageNode { case .None: image = chatMessageBackgroundIncomingImage case .Top: - image = chatMessageBackgroundIncomingMergedBottomImage - case .Bottom: image = chatMessageBackgroundIncomingMergedTopImage + case .Bottom: + image = chatMessageBackgroundIncomingMergedBottomImage case .Both: image = chatMessageBackgroundIncomingMergedBothImage } @@ -160,6 +160,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var replyInfoNode: ChatMessageReplyInfoNode? private var contentNodes: [ChatMessageBubbleContentNode] = [] + private var actionButtonsNode: ChatMessageActionButtonsNode? private var messageId: MessageId? @@ -180,20 +181,24 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) { super.animateInsertion(currentTimestamp, duration: duration, short: short) - self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + for node in self.subnodes { + if node !== self.accessoryItemNode { + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + } + } for contentNode in self.contentNodes { - contentNode.animateInsertion(currentTimestamp, duration: duration) + //contentNode.animateInsertion(currentTimestamp, duration: duration) } } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { super.animateRemoved(currentTimestamp, duration: duration) - self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) for contentNode in self.contentNodes { - contentNode.animateRemoved(currentTimestamp, duration: duration) + //contentNode.animateRemoved(currentTimestamp, duration: duration) } } @@ -223,13 +228,22 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let forwardInfoNode = strongSelf.forwardInfoNode, forwardInfoNode.frame.contains(point) { return true } + for contentNode in strongSelf.contentNodes { + let tapAction = contentNode.tapActionAtPoint(CGPoint(x: point.x - contentNode.frame.minX, y: point.y - contentNode.frame.minY)) + switch tapAction { + case .none: + break + case .url, .peerMention, .textMention, .botCommand: + return true + } + } } return false } self.view.addGestureRecognizer(recognizer) } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { var currentContentClassesPropertiesAndLayouts: [(AnyClass, ChatMessageBubbleContentProperties, (_ item: ChatMessageItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ position: ChatMessageBubbleContentPosition, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))))] = [] for contentNode in self.contentNodes { currentContentClassesPropertiesAndLayouts.append((type(of: contentNode) as AnyClass, contentNode.properties, contentNode.asyncLayoutContent())) @@ -238,10 +252,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let authorNameLayout = TextNode.asyncLayout(self.nameNode) let forwardInfoLayout = ChatMessageForwardInfoNode.asyncLayout(self.forwardInfoNode) let replyInfoLayout = ChatMessageReplyInfoNode.asyncLayout(self.replyInfoNode) + let actionButtonsLayout = ChatMessageActionButtonsNode.asyncLayout(self.actionButtonsNode) let layoutConstants = self.layoutConstants - return { item, width, mergedTop, mergedBottom in + return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in let message = item.message let incoming = item.message.effectivelyIncoming @@ -279,12 +294,15 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { var authorNameString: String? var inlineBotNameString: String? var replyMessage: Message? + var replyMarkup: ReplyMarkupMessageAttribute? for attribute in message.attributes { if let attribute = attribute as? InlineBotMessageAttribute, let bot = message.peers[attribute.peerId] as? TelegramUser { inlineBotNameString = bot.username } else if let attribute = attribute as? ReplyMessageAttribute { replyMessage = message.associatedMessages[attribute.messageId] + } else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty { + replyMarkup = attribute } } @@ -455,14 +473,26 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { index += 1 } + var actionButtonsSizeApply: (CGSize, (_ animated: Bool) -> ChatMessageActionButtonsNode)? + if let replyMarkup = replyMarkup { + actionButtonsSizeApply = actionButtonsLayout(replyMarkup, maxContentWidth) + } + let layoutBubbleSize = CGSize(width: max(contentSize.width, headerSize.width) + layoutConstants.bubble.contentInsets.left + layoutConstants.bubble.contentInsets.right, height: max(layoutConstants.bubble.minimumSize.height, headerSize.height + contentSize.height + layoutConstants.bubble.contentInsets.top + layoutConstants.bubble.contentInsets.bottom)) let backgroundFrame = CGRect(origin: CGPoint(x: incoming ? (layoutConstants.bubble.edgeInset + avatarInset) : (width - layoutBubbleSize.width - layoutConstants.bubble.edgeInset), y: 0.0), size: layoutBubbleSize) let contentOrigin = CGPoint(x: backgroundFrame.origin.x + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.origin.y + layoutConstants.bubble.contentInsets.top + headerSize.height) - let layoutSize = CGSize(width: width, height: layoutBubbleSize.height) - 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) + var layoutSize = CGSize(width: width, height: layoutBubbleSize.height) + if let actionButtonsSizeApply = actionButtonsSizeApply { + layoutSize.height += actionButtonsSizeApply.0.height + } + + var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) + if dateHeaderAtBottom { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } let layout = ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets) @@ -486,10 +516,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let forwardInfoNode = forwardInfoSizeApply.1() { strongSelf.forwardInfoNode = forwardInfoNode + var animateFrame = true if forwardInfoNode.supernode == nil { strongSelf.addSubnode(forwardInfoNode) + animateFrame = false } + let previousForwardInfoNodeFrame = forwardInfoNode.frame forwardInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + forwardInfoOriginY), size: forwardInfoSizeApply.0) + if case let .System(duration) = animation { + if animateFrame { + forwardInfoNode.layer.animateFrame(from: previousForwardInfoNodeFrame, to: forwardInfoNode.frame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } + } } else { strongSelf.forwardInfoNode?.removeFromSupernode() strongSelf.forwardInfoNode = nil @@ -497,10 +535,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let replyInfoNode = replyInfoSizeApply.1() { strongSelf.replyInfoNode = replyInfoNode + var animateFrame = true if replyInfoNode.supernode == nil { strongSelf.addSubnode(replyInfoNode) + animateFrame = false } + let previousReplyInfoNodeFrame = replyInfoNode.frame replyInfoNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + replyInfoOriginY), size: replyInfoSizeApply.0) + if case let .System(duration) = animation { + if animateFrame { + replyInfoNode.layer.animateFrame(from: previousReplyInfoNodeFrame, to: replyInfoNode.frame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } + } } else { strongSelf.replyInfoNode?.removeFromSupernode() strongSelf.replyInfoNode = nil @@ -573,8 +619,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } if case .System = animation { - strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame) - strongSelf.enableTransitionClippingNode() + if !strongSelf.backgroundNode.frame.equalTo(backgroundFrame) { + strongSelf.backgroundFrameTransition = (strongSelf.backgroundNode.frame, backgroundFrame) + strongSelf.enableTransitionClippingNode() + } } else { if let _ = strongSelf.backgroundFrameTransition { strongSelf.animateFrameTransition(1.0) @@ -585,6 +633,35 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } 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)) + + if let actionButtonsSizeApply = actionButtonsSizeApply { + var animated = false + if let _ = strongSelf.actionButtonsNode { + if case .System = animation { + animated = true + } + } + let actionButtonsNode = actionButtonsSizeApply.1(animated) + let previousFrame = actionButtonsNode.frame + let actionButtonsFrame = CGRect(origin: CGPoint(x: backgroundFrame.minX + (incoming ? layoutConstants.bubble.contentInsets.left : layoutConstants.bubble.contentInsets.right), y: backgroundFrame.maxY), size: actionButtonsSizeApply.0) + actionButtonsNode.frame = actionButtonsFrame + if actionButtonsNode !== strongSelf.actionButtonsNode { + strongSelf.actionButtonsNode = actionButtonsNode + actionButtonsNode.buttonPressed = { button in + if let strongSelf = self { + strongSelf.performMessageButtonAction(button: button) + } + } + strongSelf.addSubnode(actionButtonsNode) + } else { + if case let .System(duration) = animation { + actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } + } + } else if let actionButtonsNode = strongSelf.actionButtonsNode { + actionButtonsNode.removeFromSupernode() + strongSelf.actionButtonsNode = nil + } } }) } @@ -606,6 +683,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { backgroundFrame = backgroundFrame.insetBy(dx: 0.0, dy: 1.0) node.frame = backgroundFrame node.bounds = CGRect(origin: CGPoint(x: backgroundFrame.origin.x, y: backgroundFrame.origin.y), size: backgroundFrame.size) + if let forwardInfoNode = self.forwardInfoNode { + node.addSubnode(forwardInfoNode) + } + if let replyInfoNode = self.replyInfoNode { + node.addSubnode(replyInfoNode) + } for contentNode in self.contentNodes { node.addSubnode(contentNode) } @@ -616,6 +699,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private func disableTransitionClippingNode() { if let transitionClippingNode = self.transitionClippingNode { + if let forwardInfoNode = self.forwardInfoNode { + self.addSubnode(forwardInfoNode) + } + if let replyInfoNode = self.replyInfoNode { + self.addSubnode(replyInfoNode) + } for contentNode in self.contentNodes { self.addSubnode(contentNode) } @@ -650,6 +739,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.disableTransitionClippingNode() } } + + if CGFloat(1.0).isLessThanOrEqualTo(progress) { + self.backgroundFrameTransition = nil + } } } @@ -679,9 +772,43 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return } } - self.controllerInteraction?.clickThroughMessage() + var foundTapAction = false + loop: for contentNode in self.contentNodes { + let tapAction = contentNode.tapActionAtPoint(CGPoint(x: location.x - contentNode.frame.minX, y: location.y - contentNode.frame.minY)) + switch tapAction { + case .none: + break + case let .url(url): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openUrl(url) + } + break loop + case let .peerMention(peerId): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openPeer(peerId, .info) + } + break loop + case let .textMention(name): + foundTapAction = true + if let controllerInteraction = self.controllerInteraction { + controllerInteraction.openPeerMention(name) + } + break loop + case let .botCommand(command): + foundTapAction = true + if let item = self.item, let controllerInteraction = self.controllerInteraction { + controllerInteraction.sendBotCommand(item.message.id, command) + } + break loop + } + } + if !foundTapAction { + self.controllerInteraction?.clickThroughMessage() + } case .longTap, .doubleTap: - if let item = self.item { + if let item = self.item, self.backgroundNode.frame.contains(location) { self.controllerInteraction?.openMessageContextMenu(item.message.id, self, self.backgroundNode.frame) } } @@ -701,7 +828,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } if !self.backgroundNode.frame.contains(point) { - return nil + if self.actionButtonsNode == nil || !self.actionButtonsNode!.frame.contains(point) { + return nil + } } return super.hitTest(point, with: event) @@ -787,4 +916,25 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } } + + private func performMessageButtonAction(button: ReplyMarkupButton) { + if let item = self.item, let controllerInteraction = self.controllerInteraction { + switch button.action { + case .text: + controllerInteraction.sendMessage(button.title) + case let .url(url): + controllerInteraction.openUrl(url) + case .requestMap: + controllerInteraction.shareCurrentLocation() + case .requestPhone: + controllerInteraction.shareAccountContact() + case .openWebApp: + controllerInteraction.requestMessageActionCallback(item.message.id, nil) + case let .callback(data): + controllerInteraction.requestMessageActionCallback(item.message.id, data) + case let .switchInline(samePeer, query): + break + } + } + } } diff --git a/TelegramUI/ChatMessageDateHeader.swift b/TelegramUI/ChatMessageDateHeader.swift new file mode 100644 index 0000000000..f19274f616 --- /dev/null +++ b/TelegramUI/ChatMessageDateHeader.swift @@ -0,0 +1,173 @@ +import Foundation +import Display +import AsyncDisplayKit + +private let timezoneOffset: Int = { + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + return Int(timeinfoNow.tm_gmtoff) +}() + +private let granularity: Int32 = 60 * 60 * 24 +//private let granularity: Int32 = 60 * 60 + +final class ChatMessageDateHeader: ListViewItemHeader { + private let timestamp: Int32 + private let roundedTimestamp: Int32 + + let id: Int64 + + init(timestamp: Int32) { + self.timestamp = timestamp + if timestamp == Int32.max { + self.roundedTimestamp = timestamp / (granularity) * (granularity) + } else { + self.roundedTimestamp = ((timestamp + timezoneOffset) / (granularity)) * (granularity) + } + self.id = Int64(self.roundedTimestamp) + } + + let stickDirection: ListViewItemHeaderStickDirection = .bottom + + let height: CGFloat = 34.0 + + func node() -> ListViewItemHeaderNode { + return ChatMessageDateHeaderNode(timestamp: self.roundedTimestamp) + } +} + +private func backgroundImage(color: UIColor) -> UIImage? { + return generateImage(CGSize(width: 26.0, height: 26.0), contextGenerator: { size, context -> Void in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setFillColor(UIColor(0x748391, 0.45).cgColor) + context.fillEllipse(in: CGRect(origin: CGPoint(), size: size)) + })?.stretchableImage(withLeftCapWidth: 13, topCapHeight: 13) +} + +private let titleFont = Font.medium(13.0) + +private let months: [String] = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" +] + +final class ChatMessageDateHeaderNode: ListViewItemHeaderNode { + let labelNode: TextNode + let backgroundNode: ASImageNode + + private let timestamp: Int32 + + private var flashingOnScrolling = false + private var stickDistanceFactor: CGFloat = 0.0 + + init(timestamp: Int32) { + self.timestamp = timestamp + + self.labelNode = TextNode() + self.labelNode.isLayerBacked = true + self.labelNode.displaysAsynchronously = true + + self.backgroundNode = ASImageNode() + self.backgroundNode.isLayerBacked = true + self.backgroundNode.displayWithoutProcessing = true + self.backgroundNode.displaysAsynchronously = false + + super.init(dynamicBounce: true) + + self.isLayerBacked = true + self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0) + + self.backgroundNode.image = backgroundImage(color: UIColor(0x007ee5)) + self.addSubnode(self.backgroundNode) + self.addSubnode(self.labelNode) + + let nowTimestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + + var t: time_t = time_t(timestamp) + var timeinfo: tm = tm() + localtime_r(&t, &timeinfo) + + var now: time_t = time_t(nowTimestamp) + var timeinfoNow: tm = tm() + localtime_r(&now, &timeinfoNow) + + let text: String + if timeinfo.tm_year == timeinfoNow.tm_year && timeinfo.tm_yday == timeinfoNow.tm_yday { + text = "Today" + } else { + text = "\(months[Int(timeinfo.tm_mon)]) \(timeinfo.tm_mday)" + } + + let attributedString = NSAttributedString(string: text, font: titleFont, textColor: UIColor.white) + let labelLayout = TextNode.asyncLayout(self.labelNode) + + let (size, apply) = labelLayout(attributedString, nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil) + apply() + self.labelNode.frame = CGRect(origin: CGPoint(), size: size.size) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + + let labelLayout = TextNode.asyncLayout(self.labelNode) + + let size = self.labelNode.bounds.size + let backgroundSize = CGSize(width: size.width + 8.0 + 8.0, height: 26.0) + + let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((bounds.size.width - backgroundSize.width) / 2.0), y: (34.0 - 26.0) / 2.0), size: backgroundSize) + self.backgroundNode.frame = backgroundFrame + self.labelNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.origin.x + 8.0, y: backgroundFrame.origin.y + floorToScreenPixels((backgroundSize.height - size.height) / 2.0) - 1.0), size: size) + } + + override func updateStickDistanceFactor(_ factor: CGFloat, transition: ContainedViewLayoutTransition) { + if !self.stickDistanceFactor.isEqual(to: factor) { + let wasZero = self.stickDistanceFactor < 0.5 + let isZero = factor < 0.5 + self.stickDistanceFactor = factor + + if wasZero != isZero { + var animated = true + if case .immediate = transition { + animated = false + } + self.updateFlashing(animated: animated) + } + } + } + + override func updateFlashingOnScrolling(_ isFlashingOnScrolling: Bool, animated: Bool) { + self.flashingOnScrolling = isFlashingOnScrolling + self.updateFlashing(animated: animated) + } + + private func updateFlashing(animated: Bool) { + let flashing = self.flashingOnScrolling || self.stickDistanceFactor < 0.5 + + let alpha: CGFloat = flashing ? 1.0 : 0.0 + let previousAlpha = self.backgroundNode.alpha + + if !previousAlpha.isEqual(to: alpha) { + self.backgroundNode.alpha = alpha + self.labelNode.alpha = alpha + if animated { + let duration: Double = flashing ? 0.3 : 0.4 + self.backgroundNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) + self.labelNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: duration) + } + } + } +} diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index f4643a8316..ad61d1844c 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -82,120 +82,105 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - func asyncLayout() -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { + func asyncLayout() -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) { let currentMessageIdAndFlags = self.messageIdAndFlags let currentMedia = self.media let imageLayout = self.imageNode.asyncLayout() - return { account, message, media, corners, automaticDownload, constrainedSize in - var initialBoundingSize: CGSize + return { account, message, media, corners, automaticDownload, constrainedSize, layoutConstants in var nativeSize: CGSize if let image = media as? TelegramMediaImage, let dimensions = largestImageRepresentation(image.representations)?.dimensions { - initialBoundingSize = dimensions.fitted(CGSize(width: min(200.0, constrainedSize.width - 60.0), height: 200.0)) nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) } else if let file = media as? TelegramMediaFile, let dimensions = file.dimensions { - initialBoundingSize = dimensions.fitted(CGSize(width: min(200.0, constrainedSize.width - 60.0), height: 200.0)) nativeSize = CGSize(width: floor(dimensions.width * 0.5), height: floor(dimensions.height * 0.5)).fitted(constrainedSize) } else { - initialBoundingSize = CGSize(width: 32.0, height: 32.0) - nativeSize = initialBoundingSize + nativeSize = CGSize(width: 54.0, height: 54.0) } - initialBoundingSize.width = max(initialBoundingSize.width, 60.0) - initialBoundingSize.height = max(initialBoundingSize.height, 60.0) - nativeSize.width = max(nativeSize.width, 60.0) - nativeSize.height = max(nativeSize.height, 60.0) - - return (nativeSize.width, { constrainedSize in - let boundingSize = initialBoundingSize.fitted(constrainedSize) - - var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext, NoError>? - var updatedStatusSignal: Signal? - var updatedFetchControls: FetchControls? - - var mediaUpdated = false - if let currentMedia = currentMedia { - mediaUpdated = !media.isEqual(currentMedia) - } else { - mediaUpdated = true - } - - var statusUpdated = mediaUpdated - if currentMessageIdAndFlags?.0 != message.id || currentMessageIdAndFlags?.1 != message.flags { - statusUpdated = true - } - - if mediaUpdated { - if let image = media as? TelegramMediaImage { - updateImageSignal = chatMessagePhoto(account: account, photo: image) - - updatedFetchControls = FetchControls(fetch: { [weak self] in - if let strongSelf = self { - strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) - } - }, cancel: { - chatMessagePhotoCancelInteractiveFetch(account: account, photo: image) - }) - } else if let file = media as? TelegramMediaFile { - updateImageSignal = chatMessageVideo(account: account, video: file) - updatedFetchControls = FetchControls(fetch: { [weak self] in - if let strongSelf = self { - strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start()) - } - }, cancel: { - chatMessageFileCancelInteractiveFetch(account: account, file: file) - }) + return (layoutConstants.image.maxDimensions.width, { constrainedSize in + return (min(layoutConstants.image.maxDimensions.width, nativeSize.width), { boundingWidth in + let drawingSize = nativeSize.fittedToWidthOrSmaller(boundingWidth) + let boundingSize = CGSize(width: max(boundingWidth, drawingSize.width), height: drawingSize.height).cropped(layoutConstants.image.maxDimensions) + + var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext, NoError>? + var updatedStatusSignal: Signal? + var updatedFetchControls: FetchControls? + + var mediaUpdated = false + if let currentMedia = currentMedia { + mediaUpdated = !media.isEqual(currentMedia) + } else { + mediaUpdated = true } - } - - if statusUpdated { - if let image = media as? TelegramMediaImage { - if message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { - updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photo: image), account.pendingMessageManager.pendingMessageStatus(message.id)) + + var statusUpdated = mediaUpdated + if currentMessageIdAndFlags?.0 != message.id || currentMessageIdAndFlags?.1 != message.flags { + statusUpdated = true + } + + if mediaUpdated { + if let image = media as? TelegramMediaImage { + updateImageSignal = chatMessagePhoto(account: account, photo: image) + + updatedFetchControls = FetchControls(fetch: { [weak self] in + if let strongSelf = self { + strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(account: account, photo: image).start()) + } + }, cancel: { + chatMessagePhotoCancelInteractiveFetch(account: account, photo: image) + }) + } else if let file = media as? TelegramMediaFile { + updateImageSignal = chatMessageVideo(account: account, video: file) + updatedFetchControls = FetchControls(fetch: { [weak self] in + if let strongSelf = self { + strongSelf.fetchDisposable.set(chatMessageFileInteractiveFetched(account: account, file: file).start()) + } + }, cancel: { + chatMessageFileCancelInteractiveFetch(account: account, file: file) + }) + } + } + + if statusUpdated { + if let image = media as? TelegramMediaImage { + if message.flags.contains(.Unsent) && !message.flags.contains(.Failed) { + updatedStatusSignal = combineLatest(chatMessagePhotoStatus(account: account, photo: image), account.pendingMessageManager.pendingMessageStatus(message.id)) + |> map { resourceStatus, pendingStatus -> MediaResourceStatus in + if let pendingStatus = pendingStatus { + return .Fetching(progress: pendingStatus.progress) + } else { + return resourceStatus + } + } + } else { + updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image) + } + } else if let file = media as? TelegramMediaFile { + updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) |> map { resourceStatus, pendingStatus -> MediaResourceStatus in if let pendingStatus = pendingStatus { return .Fetching(progress: pendingStatus.progress) } else { return resourceStatus } - } - } else { - updatedStatusSignal = chatMessagePhotoStatus(account: account, photo: image) - } - } else if let file = media as? TelegramMediaFile { - updatedStatusSignal = combineLatest(chatMessageFileStatus(account: account, file: file), account.pendingMessageManager.pendingMessageStatus(message.id)) - |> map { resourceStatus, pendingStatus -> MediaResourceStatus in - if let pendingStatus = pendingStatus { - return .Fetching(progress: pendingStatus.progress) - } else { - return resourceStatus - } } + } } - } - - let arguments = TransformImageArguments(corners: corners, imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) - - let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) - - return (boundingSize.width, { boundingWidth in - let adjustedWidth = boundingWidth - let adjustedHeight = boundingSize.aspectFitted(CGSize(width: adjustedWidth, height: CGFloat.greatestFiniteMagnitude)).height - let adjustedImageSize = CGSize(width: adjustedWidth, height: min(adjustedHeight, floorToScreenPixels(boundingSize.height * 1.4))) - let adjustedArguments = TransformImageArguments(corners: corners, imageSize: nativeSize, boundingSize: adjustedImageSize, intrinsicInsets: UIEdgeInsets()) + let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets()) - let adjustedImageFrame = CGRect(origin: imageFrame.origin, size: adjustedArguments.drawingSize) - let imageApply = imageLayout(adjustedArguments) + let imageFrame = CGRect(origin: CGPoint(x: -arguments.insets.left, y: -arguments.insets.top), size: arguments.drawingSize) - return (CGSize(width: adjustedImageSize.width, height: adjustedImageSize.height), { [weak self] in + let imageApply = imageLayout(arguments) + + return (boundingSize, { [weak self] in if let strongSelf = self { strongSelf.account = account strongSelf.messageIdAndFlags = (message.id, message.flags) strongSelf.media = media - strongSelf.imageNode.frame = adjustedImageFrame - strongSelf.progressNode?.position = CGPoint(x: adjustedImageFrame.midX, y: adjustedImageFrame.midY) + strongSelf.imageNode.frame = imageFrame + strongSelf.progressNode?.position = CGPoint(x: imageFrame.midX, y: imageFrame.midY) if let updateImageSignal = updateImageSignal { strongSelf.imageNode.setSignal(account: account, signal: updateImageSignal) @@ -265,12 +250,12 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { } } - static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { + static func asyncLayout(_ node: ChatMessageInteractiveMediaNode?) -> (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageInteractiveMediaNode))) { let currentAsyncLayout = node?.asyncLayout() - return { account, message, media, corners, automaticDownload, constrainedSize in + return { account, message, media, corners, automaticDownload, constrainedSize, layoutConstants in var imageNode: ChatMessageInteractiveMediaNode - var imageLayout: (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) + var imageLayout: (_ account: Account, _ message: Message, _ media: Media, _ corners: ImageCorners, _ automaticDownload: Bool, _ constrainedSize: CGSize, _ layoutConstants: ChatMessageItemLayoutConstants) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, () -> Void))) if let node = node, let currentAsyncLayout = currentAsyncLayout { imageNode = node @@ -280,7 +265,7 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode { imageLayout = imageNode.asyncLayout() } - let (initialWidth, continueLayout) = imageLayout(account, message, media, corners, automaticDownload, constrainedSize) + let (initialWidth, continueLayout) = imageLayout(account, message, media, corners, automaticDownload, constrainedSize, layoutConstants) return (initialWidth, { constrainedSize in let (finalWidth, finalLayout) = continueLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageItem.swift b/TelegramUI/ChatMessageItem.swift index 23296f2844..255208b849 100644 --- a/TelegramUI/ChatMessageItem.swift +++ b/TelegramUI/ChatMessageItem.swift @@ -29,6 +29,14 @@ private func messagesShouldBeMerged(_ lhs: Message, _ rhs: Message) -> Bool { return false } } + for attribute in lhs.attributes { + if let attribute = attribute as? ReplyMarkupMessageAttribute { + if attribute.flags.contains(.inline) && !attribute.rows.isEmpty { + return false + } + break + } + } return true } @@ -36,7 +44,39 @@ private func messagesShouldBeMerged(_ lhs: Message, _ rhs: Message) -> Bool { return false } -public class ChatMessageItem: ListViewItem, CustomStringConvertible { +func chatItemsHaveCommonDateHeader(_ lhs: ListViewItem, _ rhs: ListViewItem?) -> Bool{ + let lhsHeader: ChatMessageDateHeader? + let rhsHeader: ChatMessageDateHeader? + if let lhs = lhs as? ChatMessageItem { + lhsHeader = lhs.header + } else if let lhs = lhs as? ChatHoleItem { + lhsHeader = lhs.header + } else if let lhs = lhs as? ChatUnreadItem { + lhsHeader = lhs.header + } else { + lhsHeader = nil + } + if let rhs = rhs { + if let rhs = rhs as? ChatMessageItem { + rhsHeader = rhs.header + } else if let rhs = rhs as? ChatHoleItem { + rhsHeader = rhs.header + } else if let rhs = rhs as? ChatUnreadItem { + rhsHeader = rhs.header + } else { + rhsHeader = nil + } + } else { + rhsHeader = nil + } + if let lhsHeader = lhsHeader, let rhsHeader = rhsHeader { + return lhsHeader.id == rhsHeader.id + } else { + return false + } +} + +public final class ChatMessageItem: ListViewItem, CustomStringConvertible { let account: Account let peerId: PeerId let controllerInteraction: ChatControllerInteraction @@ -44,6 +84,7 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { let read: Bool public let accessoryItem: ListViewAccessoryItem? + let header: ChatMessageDateHeader public init(account: Account, peerId: PeerId, controllerInteraction: ChatControllerInteraction, message: Message, read: Bool) { self.account = account @@ -56,6 +97,8 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { let incoming = message.effectivelyIncoming let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel + self.header = ChatMessageDateHeader(timestamp: message.timestamp) + if displayAuthorInfo { var hasActionMedia = false for media in message.media { @@ -90,8 +133,8 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { node.setupItem(self) let nodeLayout = node.asyncLayout() - let (top, bottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) + let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) node.updateSelectionState(animated: false) @@ -111,17 +154,37 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { } } - final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: Bool, bottom: Bool) { + final func mergedWithItems(top: ListViewItem?, bottom: ListViewItem?) -> (top: Bool, bottom: Bool, dateAtBottom: Bool) { var mergedTop = false var mergedBottom = false + var dateAtBottom = false if let top = top as? ChatMessageItem { - mergedBottom = messagesShouldBeMerged(message, top.message) + if top.header.id != self.header.id { + mergedBottom = false + } else { + mergedBottom = messagesShouldBeMerged(message, top.message) + } } if let bottom = bottom as? ChatMessageItem { - mergedTop = messagesShouldBeMerged(message, bottom.message) + if bottom.header.id != self.header.id { + mergedTop = false + dateAtBottom = true + } else { + mergedTop = messagesShouldBeMerged(bottom.message, message) + } + } else if let bottom = bottom as? ChatUnreadItem { + if bottom.header.id != self.header.id { + dateAtBottom = true + } + } else if let bottom = bottom as? ChatHoleItem { + if bottom.header.id != self.header.id { + dateAtBottom = true + } + } else { + dateAtBottom = true } - return (mergedTop, mergedBottom) + return (mergedTop, mergedBottom, dateAtBottom) } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: ListViewItemNode, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping () -> Void) -> Void) { @@ -134,9 +197,9 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { let nodeLayout = node.asyncLayout() async { - let (top, bottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) Queue.mainQueue().async { completion(layout, { apply(animation) @@ -150,4 +213,6 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible { public var description: String { return "(ChatMessageItem id: \(self.message.id), text: \"\(self.message.text)\")" } + + } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 1245007d52..6e6d3a1300 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -21,6 +21,7 @@ struct ChatMessageItemImageLayoutConstants { let defaultCornerRadius: CGFloat let mergedCornerRadius: CGFloat let contentMergedCornerRadius: CGFloat + let maxDimensions: CGSize } struct ChatMessageItemFileLayoutConstants { @@ -29,6 +30,7 @@ struct ChatMessageItemFileLayoutConstants { struct ChatMessageItemLayoutConstants { let avatarDiameter: CGFloat + let timestampHeaderHeight: CGFloat let bubble: ChatMessageItemBubbleLayoutConstants let image: ChatMessageItemImageLayoutConstants @@ -37,10 +39,11 @@ struct ChatMessageItemLayoutConstants { init() { self.avatarDiameter = 37.0 + self.timestampHeaderHeight = 34.0 - self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.5, mergedSpacing: 0.0, maximumWidthFillFactor: 0.9, minimumSize: CGSize(width: 40.0, height: 33.0), contentInsets: UIEdgeInsets(top: 1.0, left: 6.0, bottom: 1.0, right: 1.0)) - self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 5.0, left: 9.0, bottom: 4.0, right: 9.0)) - self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0), defaultCornerRadius: 15.0, mergedCornerRadius: 4.0, contentMergedCornerRadius: 5.0) + self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 1.0, maximumWidthFillFactor: 0.85, minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 1.0)) + self.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 12.0, bottom: 6.0 - UIScreenPixel, right: 12.0)) + self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 0.5, left: 0.5, bottom: 0.5, right: 0.5), defaultCornerRadius: 17.0, mergedCornerRadius: 5.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 260.0, height: 260.0)) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) } } @@ -58,7 +61,7 @@ public class ChatMessageItemView: ListViewItemNode { } public init(layerBacked: Bool) { - super.init(layerBacked: layerBacked, dynamicBounce: true) + super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true) self.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0.0, 0.0, 1.0) } @@ -82,7 +85,7 @@ public class ChatMessageItemView: ListViewItemNode { if let item = item as? ChatMessageItem { let doLayout = self.asyncLayout() let merged = item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) @@ -105,8 +108,8 @@ public class ChatMessageItemView: ListViewItemNode { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } - func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { _, _, _, _ in + func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { _, _, _, _, _ in return (ListViewItemNodeLayout(contentSize: CGSize(width: 32.0, height: 32.0), insets: UIEdgeInsets()), { _ in }) @@ -122,4 +125,12 @@ public class ChatMessageItemView: ListViewItemNode { func updateSelectionState(animated: Bool) { } + + override public func header() -> ListViewItemHeader? { + if let item = self.item { + return item.header + } else { + return nil + } + } } diff --git a/TelegramUI/ChatMessageMediaBubbleContentNode.swift b/TelegramUI/ChatMessageMediaBubbleContentNode.swift index 15585167bf..60bd46d7a8 100644 --- a/TelegramUI/ChatMessageMediaBubbleContentNode.swift +++ b/TelegramUI/ChatMessageMediaBubbleContentNode.swift @@ -50,7 +50,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode { let imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: position, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius) - let (initialWidth, refineLayout) = interactiveImageLayout(item.account, item.message, selectedMedia!, imageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhoto, CGSize(width: constrainedSize.width, height: constrainedSize.height)) + let (initialWidth, refineLayout) = interactiveImageLayout(item.account, item.message, selectedMedia!, imageCorners, item.account.settings.automaticDownloadSettingsForPeerId(item.peerId).downloadPhoto, CGSize(width: constrainedSize.width, height: constrainedSize.height), layoutConstants) return (initialWidth + layoutConstants.image.bubbleInsets.left + layoutConstants.image.bubbleInsets.right, { constrainedSize in let (refinedWidth, finishLayout) = refineLayout(constrainedSize) diff --git a/TelegramUI/ChatMessageStickerItemNode.swift b/TelegramUI/ChatMessageStickerItemNode.swift index decdce574d..52aed4fa36 100644 --- a/TelegramUI/ChatMessageStickerItemNode.swift +++ b/TelegramUI/ChatMessageStickerItemNode.swift @@ -49,13 +49,13 @@ class ChatMessageStickerItemNode: ChatMessageItemView { } } - override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ChatMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let displaySize = CGSize(width: 200.0, height: 200.0) let telegramFile = self.telegramFile let layoutConstants = self.layoutConstants let imageLayout = self.imageNode.asyncLayout() - return { item, width, mergedTop, mergedBottom in + return { item, width, mergedTop, mergedBottom, dateHeaderAtBottom in let incoming = item.message.effectivelyIncoming var imageSize: CGSize = CGSize(width: 100.0, height: 100.0) if let telegramFile = telegramFile { @@ -66,7 +66,10 @@ class ChatMessageStickerItemNode: ChatMessageItemView { 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) + var layoutInsets = UIEdgeInsets(top: mergedTop ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, left: 0.0, bottom: mergedBottom ? layoutConstants.bubble.mergedSpacing : layoutConstants.bubble.defaultSpacing, right: 0.0) + if dateHeaderAtBottom { + layoutInsets.top += layoutConstants.timestampHeaderHeight + } let imageFrame = CGRect(origin: CGPoint(x: (incoming ? (layoutConstants.bubble.edgeInset + avatarInset) : (width - imageSize.width - layoutConstants.bubble.edgeInset)), y: 0.0), size: imageSize) diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 7682f5c1d5..524bb7ec7d 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -2,6 +2,7 @@ import Foundation import AsyncDisplayKit import Display import TelegramCore +import Postbox private let messageFont: UIFont = UIFont.systemFont(ofSize: 17.0) private let messageBoldFont: UIFont = UIFont.boldSystemFont(ofSize: 17.0) @@ -45,7 +46,19 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var timeinfo = tm() localtime_r(&t, &timeinfo) - let dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + var edited = false + for attribute in message.attributes { + if let attribute = attribute as? EditedMessageAttribute { + edited = true + break + } + } + let dateText: String + if edited { + dateText = String(format: "edited %02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + } else { + dateText = String(format: "%02d:%02d", arguments: [Int(timeinfo.tm_hour), Int(timeinfo.tm_min)]) + } //let dateText = "\(message.id.id)" let statusType: ChatMessageDateAndStatusType? @@ -83,19 +96,46 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } if let entities = entities { + var nsString: NSString? let string = NSMutableAttributedString(string: message.text, attributes: [NSFontAttributeName: messageFont, NSForegroundColorAttributeName: UIColor.black]) for entity in entities.entities { + let range = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound) switch entity.type { case .Url: - string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: range) + if nsString == nil { + nsString = message.text as NSString + } + string.addAttribute(TextNode.UrlAttribute, value: nsString!.substring(with: range), range: range) case .Email: string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) - case .TextUrl: + if nsString == nil { + nsString = message.text as NSString + } + string.addAttribute(TextNode.UrlAttribute, value: "mailto:\(nsString!.substring(with: range))", range: range) + case let .TextUrl(url): string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + if nsString == nil { + nsString = message.text as NSString + } + string.addAttribute(TextNode.UrlAttribute, value: url, range: range) case .Bold: string.addAttribute(NSFontAttributeName, value: messageBoldFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) case .Mention: string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + if nsString == nil { + nsString = message.text as NSString + } + string.addAttribute(TextNode.TelegramPeerTextMentionAttribute, value: nsString!.substring(with: range), range: range) + case let .TextMention(peerId): + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + string.addAttribute(TextNode.TelegramPeerMentionAttribute, value: peerId.toInt64() as NSNumber, range: range) + case .BotCommand: + string.addAttribute(NSForegroundColorAttributeName, value: UIColor(0x004bad), range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) + if nsString == nil { + nsString = message.text as NSString + } + string.addAttribute(TextNode.TelegramBotCommandAttribute, value: nsString!.substring(with: range), range: range) case .Code, .Pre: string.addAttribute(NSFontAttributeName, value: messageFixedFont, range: NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)) default: @@ -147,9 +187,31 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return (boundingSize, { [weak self] animation in if let strongSelf = self { + let cachedLayout = strongSelf.textNode.cachedLayout + + if case .System = animation { + if let cachedLayout = cachedLayout { + if cachedLayout != textLayout { + if let textContents = strongSelf.textNode.contents { + let fadeNode = ASDisplayNode() + fadeNode.displaysAsynchronously = false + fadeNode.contents = textContents + fadeNode.frame = strongSelf.textNode.frame + fadeNode.isLayerBacked = true + strongSelf.addSubnode(fadeNode) + fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in + fadeNode?.removeFromSupernode() + }) + strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) + } + } + } + } + let _ = textApply() if let statusApply = statusApply, let adjustedStatusFrame = adjustedStatusFrame { + let previousStatusFrame = strongSelf.statusNode.frame strongSelf.statusNode.frame = adjustedStatusFrame var hasAnimation = true if case .None = animation { @@ -158,6 +220,13 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { statusApply(hasAnimation) if strongSelf.statusNode.supernode == nil { strongSelf.addSubnode(strongSelf.statusNode) + } else { + if case let .System(duration) = animation { + let delta = CGPoint(x: previousStatusFrame.minX - adjustedStatusFrame.minX, y: previousStatusFrame.minY - adjustedStatusFrame.minY) + let statusPosition = strongSelf.statusNode.layer.position + let previousPosition = CGPoint(x: statusPosition.x + delta.x, y: statusPosition.y + delta.y) + strongSelf.statusNode.layer.animatePosition(from: previousPosition, to: statusPosition, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } } } else if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() @@ -185,4 +254,20 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } + + override func tapActionAtPoint(_ point: CGPoint) -> ChatMessageBubbleContentTapAction { + let textNodeFrame = self.textNode.frame + let attributes = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) + if let url = attributes[TextNode.UrlAttribute] as? String { + return .url(url) + } else if let peerId = attributes[TextNode.TelegramPeerMentionAttribute] as? NSNumber { + return .peerMention(PeerId(peerId.int64Value)) + } else if let peerName = attributes[TextNode.TelegramPeerTextMentionAttribute] as? String { + return .textMention(peerName) + } else if let botCommand = attributes[TextNode.TelegramBotCommandAttribute] as? String { + return .botCommand(botCommand) + } else { + return .none + } + } } diff --git a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift index 23916f108a..4522fe8cb2 100644 --- a/TelegramUI/ChatMessageWebpageBubbleContentNode.swift +++ b/TelegramUI/ChatMessageWebpageBubbleContentNode.swift @@ -123,7 +123,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { if let file = webpage.file { if file.isVideo { - let (initialImageWidth, refineLayout) = contentImageLayout(item.account, item.message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (initialImageWidth, refineLayout) = contentImageLayout(item.account, item.message, file, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else { @@ -132,7 +132,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { } } else if let image = webpage.image { if let type = webpage.type, ["photo"].contains(type) { - let (initialImageWidth, refineLayout) = contentImageLayout(item.account, item.message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height)) + let (initialImageWidth, refineLayout) = contentImageLayout(item.account, item.message, image, ImageCorners(radius: 4.0), true, CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height), layoutConstants) initialWidth = initialImageWidth + insets.left + insets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { diff --git a/TelegramUI/ChatPanelInterfaceInteraction.swift b/TelegramUI/ChatPanelInterfaceInteraction.swift index f8854d6e13..0de1a3b745 100644 --- a/TelegramUI/ChatPanelInterfaceInteraction.swift +++ b/TelegramUI/ChatPanelInterfaceInteraction.swift @@ -1,20 +1,35 @@ import Foundation import Postbox +import SwiftSignalKit + +final class ChatPanelInterfaceInteractionStatuses { + let editingMessage: Signal + + init(editingMessage: Signal) { + self.editingMessage = editingMessage + } +} final class ChatPanelInterfaceInteraction { let setupReplyMessage: (MessageId) -> Void + let setupEditMessage: (MessageId) -> Void let beginMessageSelection: (MessageId) -> Void let deleteSelectedMessages: () -> Void let forwardSelectedMessages: () -> Void let updateTextInputState: (ChatTextInputState) -> Void let updateInputMode: ((ChatInputMode) -> ChatInputMode) -> Void + let editMessage: (MessageId, String) -> Void + let statuses: ChatPanelInterfaceInteractionStatuses? - init(setupReplyMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping (ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void) { + init(setupReplyMessage: @escaping (MessageId) -> Void, setupEditMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping (ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, editMessage: @escaping (MessageId, String) -> Void, statuses: ChatPanelInterfaceInteractionStatuses?) { self.setupReplyMessage = setupReplyMessage + self.setupEditMessage = setupEditMessage self.beginMessageSelection = beginMessageSelection self.deleteSelectedMessages = deleteSelectedMessages self.forwardSelectedMessages = forwardSelectedMessages self.updateTextInputState = updateTextInputState self.updateInputMode = updateInputMode + self.editMessage = editMessage + self.statuses = statuses } } diff --git a/TelegramUI/ChatUnreadItem.swift b/TelegramUI/ChatUnreadItem.swift index 244cd9dea1..6809f8ff12 100644 --- a/TelegramUI/ChatUnreadItem.swift +++ b/TelegramUI/ChatUnreadItem.swift @@ -18,6 +18,14 @@ private func backgroundImage() -> UIImage? { private let titleFont = UIFont.systemFont(ofSize: 13.0) class ChatUnreadItem: ListViewItem { + let index: MessageIndex + let header: ChatMessageDateHeader + + init(index: MessageIndex) { + self.index = index + self.header = ChatMessageDateHeader(timestamp: index.timestamp) + } + func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> Void) -> Void) { async { @@ -29,9 +37,12 @@ class ChatUnreadItem: ListViewItem { } class ChatUnreadItemNode: ListViewItemNode { + var item: ChatUnreadItem? let backgroundNode: ASImageNode let labelNode: TextNode + private let layoutConstants = ChatMessageItemLayoutConstants() + init() { self.backgroundNode = ASImageNode() self.backgroundNode.isLayerBacked = true @@ -40,7 +51,7 @@ class ChatUnreadItemNode: ListViewItemNode { self.labelNode = TextNode() self.labelNode.isLayerBacked = true - super.init(layerBacked: true) + super.init(layerBacked: true, dynamicBounce: true, rotated: true) self.backgroundNode.image = backgroundImage() self.addSubnode(self.backgroundNode) @@ -69,21 +80,26 @@ class ChatUnreadItemNode: ListViewItemNode { } override func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { - let (layout, apply) = self.asyncLayout()(width) - apply() - self.contentSize = layout.contentSize - self.insets = layout.insets + if let item = item as? ChatUnreadItem { + let dateAtBottom = !chatItemsHaveCommonDateHeader(item, nextItem) + let (layout, apply) = self.asyncLayout()(item, width, dateAtBottom) + apply() + self.contentSize = layout.contentSize + self.insets = layout.insets + } } - func asyncLayout() -> (_ width: CGFloat) -> (ListViewItemNodeLayout, () -> Void) { + func asyncLayout() -> (_ item: ChatUnreadItem, _ width: CGFloat, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, () -> Void) { let labelLayout = TextNode.asyncLayout(self.labelNode) - return { width in + let layoutConstants = self.layoutConstants + return { item, width, dateAtBottom in let (size, apply) = labelLayout(NSAttributedString(string: "Unread", font: titleFont, textColor: UIColor(0x86868d)), nil, 1, .end, CGSize(width: width, height: CGFloat.greatestFiniteMagnitude), nil) let backgroundSize = CGSize(width: width, height: 25.0) - return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 25.0), insets: UIEdgeInsets(top: 5.0, left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in + return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 25.0), insets: UIEdgeInsets(top: 5.0 + (dateAtBottom ? layoutConstants.timestampHeaderHeight : 0.0), left: 0.0, bottom: 5.0, right: 0.0)), { [weak self] in if let strongSelf = self { + strongSelf.item = item let _ = apply() strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: backgroundSize) @@ -92,4 +108,18 @@ class ChatUnreadItemNode: ListViewItemNode { }) } } + + override public func header() -> ListViewItemHeader? { + if let item = self.item { + return item.header + } else { + return nil + } + } + + override func animateRemoved(_ currentTimestamp: Double, duration: Double) { + super.animateRemoved(currentTimestamp, duration: duration) + + self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + } } diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift new file mode 100644 index 0000000000..b1014e1450 --- /dev/null +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -0,0 +1,149 @@ +import Foundation +import AsyncDisplayKit +import TelegramCore +import Postbox +import SwiftSignalKit +import Display + +private let lineImage = generateVerticallyStretchableFilledCircleImage(radius: 1.0, color: UIColor(0x007ee5)) +private let closeButtonImage = generateImage(CGSize(width: 12.0, height: 12.0), contextGenerator: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + context.setStrokeColor(UIColor(0x9099A2).cgColor) + context.setLineWidth(2.0) + context.setLineCap(.round) + context.move(to: CGPoint(x: 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: size.width - 1.0, y: size.height - 1.0)) + context.strokePath() + context.move(to: CGPoint(x: size.width - 1.0, y: 1.0)) + context.addLine(to: CGPoint(x: 1.0, y: size.height - 1.0)) + context.strokePath() +}) + +final class EditAccessoryPanelNode: AccessoryPanelNode { + let messageId: MessageId + + let closeButton: ASButtonNode + let lineNode: ASImageNode + let titleNode: ASTextNode + let textNode: ASTextNode + var activityIndicator: UIActivityIndicatorView? + + private let messageDisposable = MetaDisposable() + private let editingMessageDisposable = MetaDisposable() + + override var interfaceInteraction: ChatPanelInterfaceInteraction? { + didSet { + if let statuses = self.interfaceInteraction?.statuses { + self.editingMessageDisposable.set(statuses.editingMessage.start(next: { [weak self] value in + if let strongSelf = self, let activityIndicator = strongSelf.activityIndicator { + if value { + activityIndicator.isHidden = false + activityIndicator.startAnimating() + } else { + activityIndicator.isHidden = true + activityIndicator.stopAnimating() + } + } + })) + } + } + } + + init(account: Account, messageId: MessageId) { + self.messageId = messageId + + self.closeButton = ASButtonNode() + self.closeButton.setImage(closeButtonImage, for: []) + self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.displaysAsynchronously = false + + self.lineNode = ASImageNode() + self.lineNode.displayWithoutProcessing = true + self.lineNode.displaysAsynchronously = false + self.lineNode.image = lineImage + + 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 text = "" + if let messageText = message?.text { + text = messageText + } + + strongSelf.titleNode.attributedText = NSAttributedString(string: "Edit Message", font: Font.medium(15.0), textColor: UIColor(0x007ee5)) + strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: UIColor.black) + + strongSelf.setNeedsLayout() + } + })) + } + + deinit { + self.messageDisposable.dispose() + self.editingMessageDisposable.dispose() + } + + override func didLoad() { + super.didLoad() + + let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + self.activityIndicator = activityIndicator + self.view.addSubview(activityIndicator) + activityIndicator.isHidden = true + } + + override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { + return CGSize(width: constrainedSize.width, height: 45.0) + } + + override func layout() { + super.layout() + + let bounds = self.bounds + let leftInset: CGFloat = 55.0 + let textLineInset: CGFloat = 10.0 + let rightInset: CGFloat = 55.0 + let textRightInset: CGFloat = 20.0 + + if let activityIndicator = self.activityIndicator { + let indicatorSize = activityIndicator.bounds.size + activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) + } + + let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) + self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) + + self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) + + let titleSize = self.titleNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) + self.titleNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 7.0), size: titleSize) + + let textSize = self.textNode.measure(CGSize(width: bounds.size.width - leftInset - textLineInset - rightInset - textRightInset, height: bounds.size.height)) + self.textNode.frame = CGRect(origin: CGPoint(x: leftInset + textLineInset, y: 25.0), size: textSize) + } + + @objc func closePressed() { + if let dismiss = self.dismiss { + dismiss() + } + } +} diff --git a/TelegramUI/GeoLocation.swift b/TelegramUI/GeoLocation.swift new file mode 100644 index 0000000000..5c159ce0e0 --- /dev/null +++ b/TelegramUI/GeoLocation.swift @@ -0,0 +1,85 @@ +import Foundation +import CoreLocation +import SwiftSignalKit + +enum GeoLocation { + case location(CLLocation) + case unavailable +} + +private final class LocationHelper: NSObject, CLLocationManagerDelegate { + private let queue: Queue + private var locationManager: CLLocationManager? + let location = Promise() + private var startedUpdating = false + + init(queue: Queue) { + self.queue = queue + + super.init() + + queue.async { + let locationManager = CLLocationManager() + self.locationManager = locationManager + locationManager.delegate = self + switch CLLocationManager.authorizationStatus() { + case .authorizedAlways, .authorizedWhenInUse: + locationManager.startUpdatingLocation() + case .denied, .restricted: + self.location.set(.single(.unavailable)) + case .notDetermined: + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + } + } + } + + deinit { + if let locationManager = self.locationManager { + self.queue.async { + locationManager.stopUpdatingLocation() + } + } + } + + func stop() { + if let locationManager = self.locationManager { + self.queue.async { + locationManager.stopUpdatingLocation() + } + } + } + + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + self.queue.async { + if !locations.isEmpty { + self.location.set(.single(.location(locations[locations.count - 1]))) + } + } + } + + func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { + self.queue.async { + switch status { + case .denied, .restricted: + self.location.set(.single(.unavailable)) + default: + break + } + } + } +} + +func currentGeoLocation() -> Signal { + return Signal { subscriber in + let queue = Queue() + let helper = LocationHelper(queue: queue) + let disposable = (helper.location.get() |> deliverOn(queue)).start(next: { location in + subscriber.putNext(location) + }) + return ActionDisposable { + helper.stop() + } + } + return .complete() +} diff --git a/TelegramUI/ImageContainingNode.swift b/TelegramUI/ImageContainingNode.swift new file mode 100644 index 0000000000..5e1982e222 --- /dev/null +++ b/TelegramUI/ImageContainingNode.swift @@ -0,0 +1,6 @@ +import Foundation +import AsyncDisplayKit + +protocol ImageContainingNode { + +} diff --git a/TelegramUI/LegacyAttachmentMenu.swift b/TelegramUI/LegacyAttachmentMenu.swift new file mode 100644 index 0000000000..847d7ee085 --- /dev/null +++ b/TelegramUI/LegacyAttachmentMenu.swift @@ -0,0 +1,259 @@ +import Foundation +import UIKit +import TelegramLegacyComponents +import Display +import SwiftSignalKit + +func legacyAttachmentMenu(parentController: LegacyController, presentOverlayController: @escaping (UIViewController) -> (() -> Void), openGallery: @escaping () -> Void, openCamera: @escaping (TGAttachmentCameraView?, TGMenuSheetController?) -> Void, sendMessagesWithSignals: @escaping ([Any]?) -> Void) -> TGMenuSheetController { + let controller = TGMenuSheetController() + controller.applicationInterface = parentController.applicationInterface + controller.dismissesByOutsideTap = true + controller.hasSwipeGesture = true + //controller.maxHeight = 445.0 - TGMenuSheetButtonItemViewHeight + + var itemViews: [Any] = [] + + let carouselItem = TGAttachmentCarouselItemView(camera: PGCamera.cameraAvailable(), selfPortrait: false, forProfilePhoto: false, assetType: TGMediaAssetAnyType)! + carouselItem.presentOverlayController = { controller in + return presentOverlayController(controller!) + } + carouselItem.cameraPressed = { [weak controller] cameraView in + if let controller = controller { + openCamera(cameraView, controller) + } + } + carouselItem.sendPressed = { [weak controller, weak carouselItem] currentItem, asFiles in + if let controller = controller, let carouselItem = carouselItem { + controller.dismiss(animated: true) + let intent: TGMediaAssetsControllerIntent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent + let signals = TGMediaAssetsController.resultSignals(for: carouselItem.selectionContext, editingContext: carouselItem.editingContext, intent: intent, currentItem: currentItem, storeAssets: true, useMediaCache: false, descriptionGenerator: legacyAssetPickerItemGenerator()) + sendMessagesWithSignals(signals) + } + }; + carouselItem.allowCaptions = false + itemViews.append(carouselItem) + + let galleryItem = TGMenuSheetButtonItemView(title: "Photo or Video", type: TGMenuSheetButtonTypeDefault, action: { [weak controller] in + controller?.dismiss(animated: true) + openGallery() + }) + itemViews.append(galleryItem) + + let fileItem = TGMenuSheetButtonItemView(title: "File", type: TGMenuSheetButtonTypeDefault, action: { + }) + itemViews.append(fileItem) + + let locationItem = TGMenuSheetButtonItemView(title: "Location", type: TGMenuSheetButtonTypeDefault, action: { + }) + itemViews.append(locationItem) + + let contactItem = TGMenuSheetButtonItemView(title: "Contact", type: TGMenuSheetButtonTypeDefault, action: { + }) + itemViews.append(contactItem) + + carouselItem.underlyingViews = [galleryItem, fileItem] + + carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * CGFloat(itemViews.count - 1) + + let cancelItem = TGMenuSheetButtonItemView(title: "Cancel", type: TGMenuSheetButtonTypeCancel, action: { [weak controller] in + controller?.dismiss(animated: true) + }) + itemViews.append(cancelItem) + + controller.setItemViews(itemViews) + + return controller + + /* + carouselItem.condensed = !hasContactItem; + carouselItem.parentController = self; + carouselItem.allowCaptions = [_companion allowCaptionedMedia]; + carouselItem.inhibitDocumentCaptions = [_companion encryptUploads]; + + __weak TGAttachmentCarouselItemView *weakCarouselItem = carouselItem; + carouselItem.suggestionContext = [self _suggestionContext]; + carouselItem.cameraPressed = ^(TGAttachmentCameraView *cameraView) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongSelf _displayCameraWithView:cameraView menuController:strongController]; + }; + carouselItem.sendPressed = ^(TGMediaAsset *currentItem, bool asFiles) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + __strong TGAttachmentCarouselItemView *strongCarouselItem = weakCarouselItem; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + + TGMediaAssetsControllerIntent intent = asFiles ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent; + [strongSelf _asyncProcessMediaAssetSignals:[TGMediaAssetsController resultSignalsForSelectionContext:strongCarouselItem.selectionContext editingContext:strongCarouselItem.editingContext intent:intent currentItem:currentItem storeAssets:[strongSelf->_companion controllerShouldStoreCapturedAssets] useMediaCache:[strongSelf->_companion controllerShouldCacheServerAssets] descriptionGenerator:^id(id result, NSString *caption, NSString *hash) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return nil; + + return [strongSelf _descriptionForItem:result caption:caption hash:hash]; + }]]; + }; + carouselItem.editorOpened = ^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf _updateCanReadHistory:TGModernConversationActivityChangeInactive]; + }; + carouselItem.editorClosed = ^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + [strongSelf _updateCanReadHistory:TGModernConversationActivityChangeActive]; + }; + [itemViews addObject:carouselItem]; + + TGMenuSheetButtonItemView *galleryItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.PhotoOrVideo") type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + [strongSelf _displayMediaPicker:false fromFileMenu:false]; + }]; + galleryItem.longPressAction = ^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + [strongSelf _displayWebImagePicker]; + }; + [itemViews addObject:galleryItem]; + + TGMenuSheetButtonItemView *fileItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"AttachmentMenu.File") type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongSelf _displayFileMenuWithController:strongController]; + }]; + [itemViews addObject:fileItem]; + + carouselItem.underlyingViews = @[ galleryItem, fileItem ]; + + TGMenuSheetButtonItemView *locationItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Location") type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + [strongSelf _displayLocationPicker]; + }]; + [itemViews addObject:locationItem]; + + if (hasContactItem) + { + TGMenuSheetButtonItemView *contactItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Conversation.Contact") type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + [strongSelf _displayContactPicker]; + }]; + [itemViews addObject:contactItem]; + } + + if (!TGIsPad()) { + NSArray *inlineBots = [TGDatabaseInstance() _syncCachedRecentInlineBots]; + NSUInteger counter = 0; + for (TGUser *user in inlineBots) { + if (user.userName.length == 0) + continue; + + TGMenuSheetButtonItemView *botItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:[@"@" stringByAppendingString:user.userName] type:TGMenuSheetButtonTypeDefault action:^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + strongSelf->_inputTextPanel.inputField.userInteractionEnabled = true; + [strongSelf->_inputTextPanel.inputField setText:[NSString stringWithFormat:@"@%@ ", user.userName]]; + [strongSelf openKeyboard]; + }]; + botItem.overflow = true; + [itemViews addObject:botItem]; + counter++; + if (counter == 20) { + break; + } + } + } + + carouselItem.remainingHeight = TGMenuSheetButtonItemViewHeight * (itemViews.count - 1); + + TGMenuSheetButtonItemView *cancelItem = [[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel action:^ + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __strong TGMenuSheetController *strongController = weakController; + if (strongController == nil) + return; + + [strongController dismissAnimated:true]; + }]; + [itemViews addObject:cancelItem]; + + [controller setItemViews:itemViews]; + + [self.view endEditing:true]; + [controller presentInViewController:self sourceView:_inputTextPanel.attachButton animated:true];*/ +} diff --git a/TelegramUI/LegacyCamera.swift b/TelegramUI/LegacyCamera.swift new file mode 100644 index 0000000000..645265c093 --- /dev/null +++ b/TelegramUI/LegacyCamera.swift @@ -0,0 +1,165 @@ +import Foundation +import TelegramLegacyComponents +import Display +import UIKit + +func presentedLegacyCamera(cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, sendMessagesWithSignals: @escaping ([Any]?) -> Void) { + let controller: TGCameraController + if let cameraView = cameraView, let previewView = cameraView.previewView() { + controller = TGCameraController(camera: previewView.camera, previewView: previewView, intent: TGCameraControllerGenericIntent) + } else { + controller = TGCameraController() + } + + controller.isImportant = true + controller.shouldStoreCapturedAssets = true + controller.allowCaptions = false//true + controller.inhibitDocumentCaptions = false + controller.suggestionContext = nil + + let screenSize = parentController.view.bounds.size + var standalone = true + var startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height) + if let cameraView = cameraView, let menuController = menuController { + standalone = false + startFrame = menuController.view.convert(cameraView.previewView()!.frame, from: cameraView) + } + + let legacyController = LegacyController(legacyController: controller, presentation: .custom) + legacyController.controllerLoaded = { [weak controller, weak legacyController] in + if let controller = controller, let legacyController = legacyController { + cameraView?.detachPreviewView() + controller.beginTransitionIn(from: startFrame) + } + } + + controller.presentOverlayController = { [weak legacyController] controller in + if let legacyController = legacyController { + let childController = LegacyController(legacyController: controller!, presentation: .custom) + legacyController.present(childController, in: .window) + return { [weak childController] in + childController?.dismiss() + } + } else { + return { + + } + } + } + + controller.beginTransitionOut = { [weak controller, weak cameraView] in + if let controller = controller, let cameraView = cameraView { + cameraView.willAttachPreviewView() + return controller.view.convert(cameraView.frame, from: cameraView.superview) + } else { + return CGRect() + } + } + + controller.finishedTransitionOut = { [weak cameraView] in + if let cameraView = cameraView { + cameraView.attachPreviewView(animated: true) + } + } + + controller.customDismiss = { [weak legacyController] in + legacyController?.dismiss() + } + + controller.finishedWithPhoto = { [weak menuController] image, caption, stickers in + if let image = image { + let description = NSMutableDictionary() + description["type"] = "capturedPhoto" + description["image"] = image + if let item = legacyAssetPickerItemGenerator()(description, caption, nil) { + sendMessagesWithSignals([SSignal.single(item)]) + } + } + + menuController?.dismiss(animated: false) + } + + controller.finishedWithVideo = { [weak menuController] videoURL, previewImage, duration, dimensions, adjustments, caption, stickers in + menuController?.dismiss(animated: false) + } + + parentController.present(legacyController, in: .window) + + /* + + if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone) + controllerWindow.frame = CGRectMake(0, 0, screenSize.width, screenSize.height); + + __weak TGModernConversationController *weakSelf = self; + __weak TGCameraController *weakCameraController = controller; + __weak TGAttachmentCameraView *weakCameraView = cameraView; + + controller.beginTransitionOut = ^CGRect + { + __strong TGCameraController *strongCameraController = weakCameraController; + if (strongCameraController == nil) + return CGRectZero; + + __strong TGAttachmentCameraView *strongCameraView = weakCameraView; + if (strongCameraView != nil) + { + [strongCameraView willAttachPreviewView]; + if (TGIsPad()) + return CGRectZero; + + return [strongCameraController.view convertRect:strongCameraView.frame fromView:strongCameraView.superview]; + } + + return CGRectZero; + }; + + controller.finishedTransitionOut = ^ + { + __strong TGAttachmentCameraView *strongCameraView = weakCameraView; + if (strongCameraView == nil) + return; + + [strongCameraView attachPreviewViewAnimated:true]; + }; + + controller.finishedWithPhoto = ^(UIImage *resultImage, NSString *caption, NSArray *stickers) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __autoreleasing NSString *disabledMessage = nil; + if (![TGApplicationFeatures isPhotoUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) + { + [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; + return; + } + + NSDictionary *imageDescription = [strongSelf->_companion imageDescriptionFromImage:resultImage stickers:stickers caption:caption optionalAssetUrl:nil]; + NSMutableArray *descriptions = [[NSMutableArray alloc] init]; + if (imageDescription != nil) + [descriptions addObject:imageDescription]; + [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:descriptions asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; + + [menuController dismissAnimated:false]; + }; + + controller.finishedWithVideo = ^(NSURL *videoURL, UIImage *previewImage, NSTimeInterval duration, CGSize dimensions, TGVideoEditAdjustments *adjustments, NSString *caption, NSArray *stickers) + { + __strong TGModernConversationController *strongSelf = weakSelf; + if (strongSelf == nil) + return; + + __autoreleasing NSString *disabledMessage = nil; + if (![TGApplicationFeatures isFileUploadEnabledForPeerType:[_companion applicationFeaturePeerType] disabledMessage:&disabledMessage]) + { + [[[TGAlertView alloc] initWithTitle:TGLocalized(@"FeatureDisabled.Oops") message:disabledMessage cancelButtonTitle:TGLocalized(@"Common.OK") okButtonTitle:nil completionBlock:nil] show]; + return; + } + + NSDictionary *desc = [strongSelf->_companion videoDescriptionFromVideoURL:videoURL previewImage:previewImage dimensions:dimensions duration:duration adjustments:adjustments stickers:stickers caption:caption]; + [strongSelf->_companion controllerWantsToSendImagesWithDescriptions:@[ desc ] asReplyToMessageId:[strongSelf currentReplyMessageId] botReplyMarkup:nil]; + + [menuController dismissAnimated:true]; + };*/ +} diff --git a/TelegramUI/LegacyController.swift b/TelegramUI/LegacyController.swift new file mode 100644 index 0000000000..cdcb0f9cbf --- /dev/null +++ b/TelegramUI/LegacyController.swift @@ -0,0 +1,175 @@ +import Foundation +import Display +import TelegramLegacyComponents + +enum LegacyControllerPresentation { + case custom + case modal +} + +private func passControllerAppearanceAnimated(presentation: LegacyControllerPresentation) -> Bool { + switch presentation { + case .custom: + return false + case .modal: + return true + } +} + +private final class LegacyControllerApplicationInterface: NSObject, TGLegacyApplicationInterface { + private weak var controller: ViewController? + + init(controller: ViewController?) { + self.controller = controller + + super.init() + } + + @available(iOS 8.0, *) + public func currentSizeClass() -> UIUserInterfaceSizeClass { + return .compact + } + + @available(iOS 8.0, *) + public func currentHorizontalSizeClass() -> UIUserInterfaceSizeClass { + return .compact + } + + public func forceSetStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation) { + if let controller = self.controller { + controller.statusBar.isHidden = hidden + } + } + + public func applicationBounds() -> CGRect { + if let controller = controller { + return controller.view.bounds; + } else { + return CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: 320.0, height: 480.0)) + } + } + + public func applicationStatusBarAlpha() -> CGFloat { + return controller?.statusBar.alpha ?? 1.0 + } + + public func setApplicationStatusBarAlpha(_ alpha: CGFloat) { + controller?.statusBar.alpha = alpha + } + + public func applicationStatusBarOffset() -> CGFloat { + return 0.0 + } + + public func setApplicationStatusBarOffset(_ offset: CGFloat) { + + } + + public func animateApplicationStatusBarAppearance(_ statusBarAnimation: Int32, delay: TimeInterval, duration: TimeInterval, completion: (() -> Swift.Void)!) { + if let completion = completion { + completion() + } + } + + public func animateApplicationStatusBarAppearance(_ statusBarAnimation: Int32, duration: TimeInterval, completion: (() -> Swift.Void)!) { + if let completion = completion { + completion() + } + } + + public func animateApplicationStatusBarStyleTransition(withDuration duration: TimeInterval) { + + } +} + +class LegacyController: ViewController { + private let legacyController: UIViewController + private let presentation: LegacyControllerPresentation + + private var controllerNode: LegacyControllerNode { + return self.displayNode as! LegacyControllerNode + } + + var applicationInterface: TGLegacyApplicationInterface { + return LegacyControllerApplicationInterface(controller: self) + } + + var controllerLoaded: (() -> Void)? + + init(legacyController: UIViewController, presentation: LegacyControllerPresentation) { + self.legacyController = legacyController + self.presentation = presentation + + super.init() + + if let legacyController = legacyController as? TGLegacyApplicationInterfaceHolder { + legacyController.applicationInterface = self.applicationInterface + } + + self.navigationBar.isHidden = true + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadDisplayNode() { + self.displayNode = LegacyControllerNode() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if !self.legacyController.isViewLoaded { + self.controllerNode.controllerView = self.legacyController.view + self.controllerNode.view.addSubview(self.legacyController.view) + + if let controllerLoaded = self.controllerLoaded { + controllerLoaded() + } + } + + self.legacyController.viewWillAppear(animated && passControllerAppearanceAnimated(presentation: self.presentation)) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + self.legacyController.viewWillDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation)) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + switch self.presentation { + case .modal: + self.controllerNode.animateModalIn() + self.legacyController.viewDidAppear(true) + case .custom: + self.legacyController.viewDidAppear(animated) + } + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.legacyController.viewDidDisappear(animated && passControllerAppearanceAnimated(presentation: self.presentation)) + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, transition: transition) + + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationBar.frame.maxY, transition: transition) + } + + func dismiss() { + switch self.presentation { + case .modal: + self.controllerNode.animateModalOut { [weak self] in + self?.presentingViewController?.dismiss(animated: false, completion: nil) + } + case .custom: + self.presentingViewController?.dismiss(animated: false, completion: nil) + } + } +} diff --git a/TelegramUI/LegacyControllerNode.swift b/TelegramUI/LegacyControllerNode.swift new file mode 100644 index 0000000000..413680ddba --- /dev/null +++ b/TelegramUI/LegacyControllerNode.swift @@ -0,0 +1,38 @@ +import Foundation +import AsyncDisplayKit +import Display + +final class LegacyControllerNode: ASDisplayNode { + private var containerLayout: ContainerViewLayout? + + var controllerView: UIView? { + didSet { + if let controllerView = self.controllerView, let containerLayout = self.containerLayout { + controllerView.frame = CGRect(origin: CGPoint(), size: containerLayout.size) + } + } + } + + override init() { + super.init(viewBlock: { + return UITracingLayerView() + }, didLoad: nil) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + self.containerLayout = layout + if let controllerView = self.controllerView { + controllerView.frame = CGRect(origin: CGPoint(), size: layout.size) + } + } + + func animateModalIn() { + self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) + } + + func animateModalOut(completion: @escaping () -> Void) { + self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: kCAMediaTimingFunctionEaseInEaseOut, removeOnCompletion: false, completion: { _ in + completion() + }) + } +} diff --git a/TelegramUI/LegacyEmptyController.swift b/TelegramUI/LegacyEmptyController.swift new file mode 100644 index 0000000000..c67a65a8ca --- /dev/null +++ b/TelegramUI/LegacyEmptyController.swift @@ -0,0 +1,9 @@ +import Foundation +import TelegramLegacyComponents + +final class LegacyEmptyController: TGViewController { + override func viewDidLoad() { + self.view.backgroundColor = nil + self.view.isOpaque = false + } +} diff --git a/TelegramUI/LegacyMediaPickers.swift b/TelegramUI/LegacyMediaPickers.swift new file mode 100644 index 0000000000..e4c2b8108c --- /dev/null +++ b/TelegramUI/LegacyMediaPickers.swift @@ -0,0 +1,137 @@ +import Foundation +import TelegramLegacyComponents +import SwiftSignalKit +import TelegramCore +import Postbox +import SSignalKit +import UIKit +import Display + +func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false) { + controller.captionsEnabled = false//captionsEnabled + controller.inhibitDocumentCaptions = false + controller.suggestionContext = nil + controller.dismissalBlock = { + + } + controller.localMediaCacheEnabled = false + controller.shouldStoreAssets = storeCreatedAssets + controller.shouldShowFileTipIfNeeded = showFileTooltip +} + +func legacyAssetPicker() -> Signal<(@escaping (UIViewController) -> (() -> Void)) -> TGMediaAssetsController, NoError> { + return Signal { subscriber in + let intent = TGMediaAssetsControllerSendMediaIntent + + if TGMediaAssetsLibrary.authorizationStatus() == TGMediaLibraryAuthorizationStatusNotDetermined { + TGMediaAssetsLibrary.requestAuthorization(for: TGMediaAssetAnyType, completion: { (status, group) in + if !TGLegacyComponentsAccessChecker().checkPhotoAuthorizationStatus(for: TGPhotoAccessIntentRead, alertDismissCompletion: nil) { + subscriber.putError(NoError()) + } else { + Queue.mainQueue().async { + subscriber.putNext({ present in + let controller = TGMediaAssetsController(assetGroup: group, intent: intent, presentOverlayController: { controller in + return present(controller!) + }) + return controller! + }) + subscriber.putCompletion() + } + } + }) + } else { + subscriber.putNext({ present in + let controller = TGMediaAssetsController(assetGroup: nil, intent: intent, presentOverlayController: { controller in + return present(controller!) + }) + return controller! + }) + subscriber.putCompletion() + } + + return ActionDisposable { + + } + } +} + +private enum LegacyAssetItem { + case image(UIImage) + case asset(PHAsset) +} + +private final class LegacyAssetItemWrapper: NSObject { + let item: LegacyAssetItem + + init(item: LegacyAssetItem) { + self.item = item + + super.init() + } +} + +func legacyAssetPickerItemGenerator() -> ((Any?, String?, String?) -> [AnyHashable : Any]?) { + return { anyDict, caption, hash in + let dict = anyDict as! NSDictionary + if (dict["type"] as! NSString) == "editedPhoto" || (dict["type"] as! NSString) == "capturedPhoto" { + let image = dict["image"] as! UIImage + var result: [AnyHashable : Any] = [:] + result["item" as NSString] = LegacyAssetItemWrapper(item: .image(image)) + return result + } else if (dict["type"] as! NSString) == "cloudPhoto" { + let asset = dict["asset"] as! TGMediaAsset + var result: [AnyHashable : Any] = [:] + result["item" as NSString] = LegacyAssetItemWrapper(item: .asset(asset.backingAsset)) + return result + } + return nil + } +} + +func legacyAssetPickerEnqueueMessages(account: Account, peerId: PeerId, signals: [Any]) -> Signal<[EnqueueMessage], NoError> { + return Signal { subscriber in + let disposable = SSignal.combineSignals(signals).start(next: { anyValues in + var messages: [EnqueueMessage] = [] + + for item in (anyValues as! NSArray) { + if let item = (item as? NSDictionary)?.object(forKey: "item") as? LegacyAssetItemWrapper { + switch item.item { + case let .image(image): + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpeg" + let scaledSize = image.size.aspectFitted(CGSize(width: 1280.0, height: 1280.0)) + if let scaledImage = generateImage(scaledSize, contextGenerator: { size, context in + context.draw(image.cgImage!, in: CGRect(origin: CGPoint(), size: size)) + }, opaque: true) { + if let scaledImageData = UIImageJPEGRepresentation(image, 0.52) { + let _ = try? scaledImageData.write(to: URL(fileURLWithPath: tempFilePath)) + let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) + messages.append(.message(text: "", media: media, replyToMessageId: nil)) + } + } + case let .asset(asset): + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let size = CGSize(width: CGFloat(asset.pixelWidth), height: CGFloat(asset.pixelHeight)) + let scaledSize = size.aspectFitted(CGSize(width: 1280.0, height: 1280.0)) + let resource = PhotoLibraryMediaResource(localIdentifier: asset.localIdentifier) + + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: [TelegramMediaImageRepresentation(dimensions: scaledSize, resource: resource)]) + messages.append(.message(text: "", media: media, replyToMessageId: nil)) + } + } + } + + subscriber.putNext(messages) + subscriber.putCompletion() + }, error: { _ in + subscriber.putError(NoError()) + }, completed: nil) + + return ActionDisposable { + disposable?.dispose() + } + } +} diff --git a/TelegramUI/LegacyNavigationController.swift b/TelegramUI/LegacyNavigationController.swift new file mode 100644 index 0000000000..836c374e09 --- /dev/null +++ b/TelegramUI/LegacyNavigationController.swift @@ -0,0 +1,8 @@ +import Foundation +import UIKit +import TelegramLegacyComponents + +func makeLegacyNavigationController(rootController: UIViewController) -> TGNavigationController { + return TGNavigationController.make(withRootController: rootController) +} + diff --git a/TelegramUI/ListMessageFileItemNode.swift b/TelegramUI/ListMessageFileItemNode.swift index 645938cb8b..e025785495 100644 --- a/TelegramUI/ListMessageFileItemNode.swift +++ b/TelegramUI/ListMessageFileItemNode.swift @@ -205,8 +205,8 @@ final class ListMessageFileItemNode: ListMessageNode { override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ListMessageItem { let doLayout = self.asyncLayout() - let merged = (top: false, bottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let merged = (top: false, bottom: false, dateAtBottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) + let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) @@ -221,7 +221,7 @@ final class ListMessageFileItemNode: ListMessageNode { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } - override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) let extensionIconTextMakeLayout = TextNode.asyncLayout(self.extensionIconText) @@ -230,7 +230,7 @@ final class ListMessageFileItemNode: ListMessageNode { let currentMedia = self.currentMedia let currentIconImageRepresentation = self.currentIconImageRepresentation - return { [weak self] item, width, _, _ in + return { [weak self] item, width, _, _, _ in let leftInset: CGFloat = 65.0 var extensionIconImage: UIImage? diff --git a/TelegramUI/ListMessageItem.swift b/TelegramUI/ListMessageItem.swift index 0f09b6d665..c610101d5c 100644 --- a/TelegramUI/ListMessageItem.swift +++ b/TelegramUI/ListMessageItem.swift @@ -36,8 +36,8 @@ final class ListMessageItem: ListViewItem { node.setupItem(self) let nodeLayout = node.asyncLayout() - let (top, bottom) = (false, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (top, bottom, dateAtBottom) = (false, false, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) + let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) node.updateSelectionState(animated: false) @@ -67,9 +67,9 @@ final class ListMessageItem: ListViewItem { let nodeLayout = node.asyncLayout() async { - let (top, bottom) = (false, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) + let (top, bottom, dateAtBottom) = (false, false, false) //self.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = nodeLayout(self, width, top, bottom) + let (layout, apply) = nodeLayout(self, width, top, bottom, dateAtBottom) Queue.mainQueue().async { completion(layout, { apply(animation) diff --git a/TelegramUI/ListMessageNode.swift b/TelegramUI/ListMessageNode.swift index 67f99dcf31..9fccd56ccf 100644 --- a/TelegramUI/ListMessageNode.swift +++ b/TelegramUI/ListMessageNode.swift @@ -18,8 +18,8 @@ class ListMessageNode: ListViewItemNode { override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { } - func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { - return { _, width, _, _ in + func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + return { _, width, _, _, _ in return (ListViewItemNodeLayout(contentSize: CGSize(width: width, height: 1.0), insets: UIEdgeInsets()), { _ in }) diff --git a/TelegramUI/ListMessageSnippetItemNode.swift b/TelegramUI/ListMessageSnippetItemNode.swift index de18155d8d..815009ea82 100644 --- a/TelegramUI/ListMessageSnippetItemNode.swift +++ b/TelegramUI/ListMessageSnippetItemNode.swift @@ -71,8 +71,8 @@ final class ListMessageSnippetItemNode: ListMessageNode { override public func layoutForWidth(_ width: CGFloat, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ListMessageItem { let doLayout = self.asyncLayout() - let merged = (top: false, bottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) - let (layout, apply) = doLayout(item, width, merged.top, merged.bottom) + let merged = (top: false, bottom: false, dateAtBottom: false)//item.mergedWithItems(top: previousItem, bottom: nextItem) + let (layout, apply) = doLayout(item, width, merged.top, merged.bottom, merged.dateAtBottom) self.contentSize = layout.contentSize self.insets = layout.insets apply(.None) @@ -87,7 +87,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { //self.layer.animateBoundsOriginYAdditive(from: -self.bounds.size.height * 1.4, to: 0.0, duration: duration) } - override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { + override func asyncLayout() -> (_ item: ListMessageItem, _ width: CGFloat, _ mergedTop: Bool, _ mergedBottom: Bool, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation) -> Void) { let titleNodeMakeLayout = TextNode.asyncLayout(self.titleNode) let descriptionNodeMakeLayout = TextNode.asyncLayout(self.descriptionNode) let iconTextMakeLayout = TextNode.asyncLayout(self.iconTextNode) @@ -96,7 +96,7 @@ final class ListMessageSnippetItemNode: ListMessageNode { let currentMedia = self.currentMedia let currentIconImageRepresentation = self.currentIconImageRepresentation - return { [weak self] item, width, _, _ in + return { [weak self] item, width, _, _, _ in let leftInset: CGFloat = 65.0 var extensionIconImage: UIImage? diff --git a/TelegramUI/PeerMediaCollectionController.swift b/TelegramUI/PeerMediaCollectionController.swift index 5afc614fdc..cf47295f66 100644 --- a/TelegramUI/PeerMediaCollectionController.swift +++ b/TelegramUI/PeerMediaCollectionController.swift @@ -130,6 +130,7 @@ public class PeerMediaCollectionController: ViewController { if let strongSelf = self { (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: id, messageId: nil)) } + }, openPeerMention: { _ in }, openMessageContextMenu: { [weak self] id, node, frame in if let strongSelf = self, strongSelf.isNodeLoaded { if let message = strongSelf.mediaCollectionDisplayNode.historyNode.messageInCurrentHistoryView(id) { @@ -176,17 +177,25 @@ public class PeerMediaCollectionController: ViewController { strongSelf.updateInterfaceState(animated: true, { $0.withToggledSelectedMessage(id) }) } } - }, sendSticker: { _ in }) + }, sendMessage: { _ in + },sendSticker: { _ in + }, requestMessageActionCallback: { _, _ in + }, openUrl: { _ in + }, shareCurrentLocation: { + }, shareAccountContact: { + }, sendBotCommand: { _, _ in + }) self.controllerInteraction = controllerInteraction - self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, beginMessageSelection: { _ in }, deleteSelectedMessages: { + self.interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { _ in }, setupEditMessage: { _ in }, beginMessageSelection: { _ in }, deleteSelectedMessages: { }, forwardSelectedMessages: { }, updateTextInputState: { _ in }, updateInputMode: { _ in - }) + }, editMessage: { _, _ in + }, statuses: nil) self.updateInterfaceState(animated: false, { return $0 }) diff --git a/TelegramUI/PerformanceSpinner.swift b/TelegramUI/PerformanceSpinner.swift new file mode 100644 index 0000000000..db941d1098 --- /dev/null +++ b/TelegramUI/PerformanceSpinner.swift @@ -0,0 +1,40 @@ +import Foundation +import SwiftSignalKit + +private final class SpinnerThread: NSObject { + private var thread: Thread? + private let condition: NSCondition + private var workValue: CGFloat = 0 + + override init() { + self.condition = NSCondition() + + super.init() + + let thread = Thread(target: self, selector: #selector(self.entryPoint), object: nil) + thread.name = "Spinner" + self.thread = thread + thread.start() + } + + @objc func entryPoint() { + while true { + workValue = workValue + CGFloat(sin(Double(workValue))) + usleep(100) + } + } + + func aquire() -> Int { + return 0 + } +} + +//private let atomicSpinner = SpinnerThread() + +func performanceSpinnerAcquire() -> Int { + //return atomicSpinner.aquire() + return 0 +} + +func performanceSpinnerRelease(_ index: Int) -> Void { +} diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 9dc8221cbd..148b0652b4 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -130,7 +130,7 @@ private enum Corner: Hashable { case let .BottomLeft(radius): return radius | (3 << 24) case let .BottomRight(radius): - return radius | (2 << 24) + return radius | (4 << 24) } } @@ -282,19 +282,31 @@ private func tailContext(_ tail: Tail) -> DrawingContext { case let .BottomLeft(radius): rect = CGRect(origin: CGPoint(x: 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) - c.move(to: CGPoint(x: 3.0, y: 0.0)) - c.addLine(to: CGPoint(x: 3.0, y: 8.7)) - c.addLine(to: CGPoint(x: 2.0, y: 11.7)) - c.addLine(to: CGPoint(x: 1.5, y: 12.7)) - c.addLine(to: CGPoint(x: 0.8, y: 13.7)) - c.addLine(to: CGPoint(x: 0.2, y: 14.4)) - c.addLine(to: CGPoint(x: 3.5, y: 13.8)) - c.addLine(to: CGPoint(x: 5.0, y: 13.2)) - c.addLine(to: CGPoint(x: 3.0 + CGFloat(radius) - 9.5, y: 11.5)) + c.move(to: CGPoint(x: 3.0, y: 1.0)) + c.addLine(to: CGPoint(x: 3.0, y: 11.0)) + c.addLine(to: CGPoint(x: 2.3, y: 13.0)) + c.addLine(to: CGPoint(x: 0.0, y: 16.6)) + c.addLine(to: CGPoint(x: 4.5, y: 15.5)) + c.addLine(to: CGPoint(x: 6.5, y: 14.3)) + c.addLine(to: CGPoint(x: 9.0, y: 12.5)) c.closePath() c.fillPath() case let .BottomRight(radius): - rect = CGRect(origin: CGPoint(x: -CGFloat(radius) + 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) + rect = CGRect(origin: CGPoint(x: 3.0, y: -CGFloat(radius)), size: CGSize(width: CGFloat(radius << 1), height: CGFloat(radius << 1))) + + c.translateBy(x: context.size.width / 2.0, y: context.size.height / 2.0) + c.scaleBy(x: -1.0, y: 1.0) + c.translateBy(x: -context.size.width / 2.0, y: -context.size.height / 2.0) + + c.move(to: CGPoint(x: 3.0, y: 1.0)) + c.addLine(to: CGPoint(x: 3.0, y: 11.0)) + c.addLine(to: CGPoint(x: 2.3, y: 13.0)) + c.addLine(to: CGPoint(x: 0.0, y: 16.6)) + c.addLine(to: CGPoint(x: 4.5, y: 15.5)) + c.addLine(to: CGPoint(x: 6.5, y: 14.3)) + c.addLine(to: CGPoint(x: 9.0, y: 12.5)) + c.closePath() + c.fillPath() /*CGContextMoveToPoint(c, 3.0, 0.0) CGContextAddLineToPoint(c, 3.0, 8.7) @@ -360,7 +372,12 @@ private func addCorners(_ context: DrawingContext, arguments: TransformImageArgu case let .Tail(radius): if radius > CGFloat(FLT_EPSILON) { let tail = tailContext(.BottomRight(Int(radius))) - context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius - 3.0, y: drawingRect.maxY - radius)) + let color = context.colorAt(CGPoint(x: drawingRect.maxX - 1.0, y: drawingRect.maxY - 1.0)) + context.withContext { c in + c.setFillColor(color.cgColor) + c.fill(CGRect(x: drawingRect.maxX, y: drawingRect.maxY - 6.0, width: 3.0, height: 6.0)) + } + context.blt(tail, at: CGPoint(x: drawingRect.maxX - radius, y: drawingRect.maxY - radius)) } } } @@ -370,24 +387,17 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr return signal |> map { (thumbnailData, fullSizeData, fullSizeComplete) in return { arguments in - var debugTiming = false - var startTime = 0.0 - if arguments.imageSize.equalTo(CGSize(width: 640.0, height: 853.0)) { - print("begin draw \(CFAbsoluteTimeGetCurrent() * 1000.0)") - debugTiming = true - startTime = CFAbsoluteTimeGetCurrent() - } - let context = DrawingContext(size: arguments.drawingSize, clear: true) - if debugTiming { - let currentTime = CFAbsoluteTimeGetCurrent() - print("create context: \((currentTime - startTime) * 1000.0) ms") - startTime = currentTime + let drawingRect = arguments.drawingRect + var fittedSize = arguments.imageSize + if abs(fittedSize.width - arguments.boundingSize.width).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.width = arguments.boundingSize.width + } + if abs(fittedSize.height - arguments.boundingSize.height).isLessThanOrEqualTo(CGFloat(1.0)) { + fittedSize.height = arguments.boundingSize.height } - let drawingRect = arguments.drawingRect - let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedRect = CGRect(origin: CGPoint(x: drawingRect.origin.x + (drawingRect.size.width - fittedSize.width) / 2.0, y: drawingRect.origin.y + (drawingRect.size.height - fittedSize.height) / 2.0), size: fittedSize) var fullSizeImage: CGImage? @@ -416,12 +426,6 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr } } - if debugTiming { - let currentTime = CFAbsoluteTimeGetCurrent() - print("decode full: \((currentTime - startTime) * 1000.0) ms") - startTime = currentTime - } - var thumbnailImage: CGImage? if let thumbnailData = thumbnailData, let imageSource = CGImageSourceCreateWithData(thumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) { thumbnailImage = image @@ -441,12 +445,6 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr blurredThumbnailImage = thumbnailContext.generateImage() } - if debugTiming { - let currentTime = CFAbsoluteTimeGetCurrent() - print("decode thumbnail: \((currentTime - startTime) * 1000.0) ms") - startTime = currentTime - } - context.withFlippedContext { c in c.setBlendMode(.copy) if arguments.boundingSize != arguments.imageSize { @@ -466,20 +464,8 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr } } - if debugTiming { - let currentTime = CFAbsoluteTimeGetCurrent() - print("draw: \((currentTime - startTime) * 1000.0) ms") - startTime = currentTime - } - addCorners(context, arguments: arguments) - if debugTiming { - let currentTime = CFAbsoluteTimeGetCurrent() - print("add corners: \((currentTime - startTime) * 1000.0) ms") - startTime = currentTime - } - return context } } diff --git a/TelegramUI/PreparedChatHistoryViewTransition.swift b/TelegramUI/PreparedChatHistoryViewTransition.swift index 8f642c1c7b..c14a2d494a 100644 --- a/TelegramUI/PreparedChatHistoryViewTransition.swift +++ b/TelegramUI/PreparedChatHistoryViewTransition.swift @@ -53,27 +53,27 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie case let .HoleChanges(filledHoleDirections, removeHoleDirections): if let (_, removeDirection) = removeHoleDirections.first { switch removeDirection { - case .LowerToUpper: - var holeIndex: MessageIndex? - for (index, _) in filledHoleDirections { - if holeIndex == nil || index < holeIndex! { - holeIndex = index - } - } - - if let holeIndex = holeIndex { - for i in 0 ..< toView.filteredEntries.count { - if toView.filteredEntries[i].index >= holeIndex { - let index = toView.filteredEntries.count - 1 - (i - 1) - stationaryItemRange = (index, Int.max) - break + case .LowerToUpper: + var holeIndex: MessageIndex? + for (index, _) in filledHoleDirections { + if holeIndex == nil || index < holeIndex! { + holeIndex = index } } - } - case .UpperToLower: - break - case .AroundIndex: - break + + if let holeIndex = holeIndex { + for i in 0 ..< toView.filteredEntries.count { + if toView.filteredEntries[i].index >= holeIndex { + let index = toView.filteredEntries.count - 1 - (i - 1) + stationaryItemRange = (index, Int.max) + break + } + } + } + case .UpperToLower: + break + case .AroundIndex: + break } } } @@ -94,15 +94,6 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie } adjustedIndicesAndItems.append(ChatHistoryViewTransitionInsertEntry(index: adjustedIndex, previousIndex: adjustedPrevousIndex, entry: entry, directionHint: directionHint)) - - /*switch entry { - case let .MessageEntry(message): - adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatMessageItem(account: account, peerId: peerId, controllerInteraction: controllerInteraction, message: message), directionHint: directionHint)) - case .HoleEntry: - adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatHoleItem(), directionHint: directionHint)) - case .UnreadEntry: - adjustedIndicesAndItems.append(ListViewInsertItem(index: adjustedIndex, previousIndex: adjustedPrevousIndex, item: ChatUnreadItem(), directionHint: directionHint)) - }*/ } for (index, entry, previousIndex) in updateIndices { diff --git a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift index 747c883218..ffef2c7833 100644 --- a/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift +++ b/TelegramUI/TapLongTapOrDoubleTapGestureRecognizer.swift @@ -82,6 +82,11 @@ final class TapLongTapOrDoubleTapGestureRecognizer: UIGestureRecognizer, UIGestu super.touchesBegan(touches, with: event) if let touch = touches.first { + if let hitResult = self.view?.hitTest(touch.location(in: self.view), with: event), let _ = hitResult as? UIButton { + self.state = .failed + return + } + self.tapCount += 1 if self.tapCount == 2 { self.timer?.invalidate() diff --git a/TelegramUI/TelegramApplicationContext.swift b/TelegramUI/TelegramApplicationContext.swift new file mode 100644 index 0000000000..580c5d3f6e --- /dev/null +++ b/TelegramUI/TelegramApplicationContext.swift @@ -0,0 +1,9 @@ +import Foundation + +public final class TelegramApplicationContext { + public let openUrl: (String) -> Void + + public init(openUrl: @escaping (String) -> Void) { + self.openUrl = openUrl + } +} diff --git a/TelegramUI/TelegramInitializeLegacyComponents.swift b/TelegramUI/TelegramInitializeLegacyComponents.swift new file mode 100644 index 0000000000..eaf1ecf5c9 --- /dev/null +++ b/TelegramUI/TelegramInitializeLegacyComponents.swift @@ -0,0 +1,60 @@ +import Foundation +import TelegramLegacyComponents +import UIKit + +/* + [TGHacks setApplication:application]; + [TGHacks setCurrentSizeClassGetter:^UIUserInterfaceSizeClass{ + return TGAppDelegateInstance.rootController.currentSizeClass; + }]; + [TGHacks setCurrenHorizontalClassGetter:^UIUserInterfaceSizeClass{ + return TGAppDelegateInstance.rootController.traitCollection.horizontalSizeClass; + }]; + TGLegacyComponentsSetDocumentsPath([TGAppDelegate documentsPath]); + [TGHacks setForceSetStatusBarHidden:^(BOOL hidden, UIStatusBarAnimation animation) { + [(TGApplication *)[UIApplication sharedApplication] forceSetStatusBarHidden:hidden withAnimation:animation]; + }]; + [TGHacks setApplicationBounds:^CGRect { + return TGAppDelegateInstance.rootController.applicationBounds; + }]; + [TGHacks setPauseMusicPlayer:^{ + [TGTelegraphInstance.musicPlayer controlPause]; + }]; + TGLegacyComponentsSetAccessChecker([[TGAccessCheckerImpl alloc] init]); + */ + +private final class AccessCheckerImpl: NSObject, TGAccessCheckerProtocol { + func checkAddressBookAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + return true + } + + func checkPhotoAuthorizationStatus(for intent: TGPhotoAccessIntent, alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + return true + } + + func checkMicrophoneAuthorizationStatus(for intent: TGMicrophoneAccessIntent, alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + return true + } + + func checkCameraAuthorizationStatus(alertDismissComlpetion alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + return true + } + + func checkLocationAuthorizationStatus(for intent: TGLocationAccessIntent, alertDismissComlpetion alertDismissCompletion: (() -> Swift.Void)!) -> Bool { + return true + } +} + +public func initializeLegacyComponents(application: UIApplication, currentSizeClassGetter: @escaping () -> UIUserInterfaceSizeClass, currentHorizontalClassGetter: @escaping () -> UIUserInterfaceSizeClass, documentsPath: String, currentApplicationBounds: @escaping () -> CGRect) { + freedomInit() + //freedomUIKitInit(); + TGHacks.setApplication(application) + TGLegacyComponentsSetAccessChecker(AccessCheckerImpl()) + TGHacks.setPauseMusicPlayer { + + } + TGViewController.setSizeClassSignal { + return SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber) + } + TGLegacyComponentsSetDocumentsPath(documentsPath) +} diff --git a/TelegramUI/TextNode.swift b/TelegramUI/TextNode.swift index 3fb80573e7..d61783ae1b 100644 --- a/TelegramUI/TextNode.swift +++ b/TelegramUI/TextNode.swift @@ -7,10 +7,12 @@ private let defaultFont = UIFont.systemFont(ofSize: 15.0) private final class TextNodeLine { let line: CTLine let frame: CGRect + let range: NSRange - init(line: CTLine, frame: CGRect) { + init(line: CTLine, frame: CGRect, range: NSRange) { self.line = line self.frame = frame + self.range = range } } @@ -60,10 +62,41 @@ final class TextNodeLayout: NSObject { return 0.0 } } + + func attributesAtPoint(_ point: CGPoint) -> [String: Any] { + if let attributedString = self.attributedString { + for line in self.lines { + let lineFrame = line.frame.offsetBy(dx: 0.0, dy: -line.frame.size.height) + if lineFrame.contains(point) { + let index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: point.x - lineFrame.minX, y: point.y - lineFrame.minY)) + if index >= 0 && index < attributedString.length { + return attributedString.attributes(at: index, effectiveRange: nil) + } + break + } + } + for line in self.lines { + let lineFrame = line.frame.offsetBy(dx: 0.0, dy: -line.frame.size.height) + if lineFrame.offsetBy(dx: 0.0, dy: -lineFrame.size.height).insetBy(dx: -3.0, dy: -3.0).contains(point) { + let index = CTLineGetStringIndexForPosition(line.line, CGPoint(x: point.x - lineFrame.minX, y: point.y - lineFrame.minY)) + if index >= 0 && index < attributedString.length { + return attributedString.attributes(at: index, effectiveRange: nil) + } + break + } + } + } + return [:] + } } final class TextNode: ASDisplayNode { - private var cachedLayout: TextNodeLayout? + static let UrlAttribute = "UrlAttributeT" + static let TelegramPeerMentionAttribute = "TelegramPeerMention" + static let TelegramPeerTextMentionAttribute = "TelegramPeerTextMention" + static let TelegramBotCommandAttribute = "TelegramBotCommand" + + private(set) var cachedLayout: TextNodeLayout? override init() { super.init() @@ -73,10 +106,20 @@ final class TextNode: ASDisplayNode { self.clipsToBounds = false } + func attributesAtPoint(_ point: CGPoint) -> [String: Any] { + if let cachedLayout = self.cachedLayout { + return cachedLayout.attributesAtPoint(point) + } else { + return [:] + } + } + private class func calculateLayout(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, cutout: TextNodeCutout?) -> TextNodeLayout { if let attributedString = attributedString { + let stringLength = attributedString.length + let font: CTFont - if attributedString.length != 0 { + if stringLength != 0 { if let stringFont = attributedString.attribute(kCTFontAttributeName as String, at: 0, effectiveRange: nil) { font = stringFont as! CTFont } else { @@ -148,7 +191,8 @@ final class TextNode: ASDisplayNode { let coreTextLine: CTLine - let originalLine = CTTypesetterCreateLineWithOffset(typesetter, CFRange(location: lastLineCharacterIndex, length: attributedString.length - lastLineCharacterIndex), 0.0) + let lineRange = CFRange(location: lastLineCharacterIndex, length: stringLength - lastLineCharacterIndex) + let originalLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 0.0) if CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) < Double(constrainedSize.width) { coreTextLine = originalLine @@ -168,7 +212,7 @@ final class TextNode: ASDisplayNode { layoutSize.height += fontLineHeight + fontLineSpacing layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length))) break } else { @@ -179,7 +223,8 @@ final class TextNode: ASDisplayNode { layoutSize.height += fontLineSpacing } - let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, CFRangeMake(lastLineCharacterIndex, lineCharacterCount), 100.0) + let lineRange = CFRangeMake(lastLineCharacterIndex, lineCharacterCount) + let coreTextLine = CTTypesetterCreateLineWithOffset(typesetter, lineRange, 100.0) lastLineCharacterIndex += lineCharacterCount let lineWidth = ceil(CGFloat(CTLineGetTypographicBounds(coreTextLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(coreTextLine))) @@ -187,7 +232,7 @@ final class TextNode: ASDisplayNode { layoutSize.height += fontLineHeight layoutSize.width = max(layoutSize.width, lineWidth + lineAdditionalWidth) - lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame)) + lines.append(TextNodeLine(line: coreTextLine, frame: lineFrame, range: NSMakeRange(lineRange.location, lineRange.length))) } else { if !lines.isEmpty { layoutSize.height += fontLineSpacing