mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 22:55:00 +00:00
no message
This commit is contained in:
@@ -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 = "<group>"; };
|
||||
D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = "<group>"; };
|
||||
D003702D1DA43052004308D3 /* PeerInfoAvatarAndNameItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoAvatarAndNameItem.swift; sourceTree = "<group>"; };
|
||||
D003702F1DA43077004308D3 /* PeerInfoItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoItem.swift; sourceTree = "<group>"; };
|
||||
D00370311DA46C06004308D3 /* PeerInfoTextWithLabelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoTextWithLabelItem.swift; sourceTree = "<group>"; };
|
||||
D00E15251DDBD4E700ACF65C /* LegacyCamera.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCamera.swift; sourceTree = "<group>"; };
|
||||
D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = "<group>"; };
|
||||
D01AC9171DD5033100E8160F /* ChatMessageActionButtonsNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageActionButtonsNode.swift; sourceTree = "<group>"; };
|
||||
D01AC91E1DD5E09000E8160F /* EditAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditAccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||
D021E0CD1DB4135500C6B04F /* ChatMediaInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputNode.swift; sourceTree = "<group>"; };
|
||||
D021E0CF1DB413BC00C6B04F /* ChatInputNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputNode.swift; sourceTree = "<group>"; };
|
||||
D021E0D11DB4147500C6B04F /* ChatInterfaceInputNodes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputNodes.swift; sourceTree = "<group>"; };
|
||||
D021E0E41DB55D0A00C6B04F /* ChatMediaInputStickerPackItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMediaInputStickerPackItem.swift; sourceTree = "<group>"; };
|
||||
D023EBB11DDA800700BD496D /* LegacyMediaPickers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyMediaPickers.swift; sourceTree = "<group>"; };
|
||||
D023ED2D1DDB5BEC00BD496D /* LegacyAttachmentMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyAttachmentMenu.swift; sourceTree = "<group>"; };
|
||||
D023ED2F1DDB605D00BD496D /* LegacyEmptyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyEmptyController.swift; sourceTree = "<group>"; };
|
||||
D023ED311DDB60CF00BD496D /* LegacyNavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyNavigationController.swift; sourceTree = "<group>"; };
|
||||
D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = "<group>"; };
|
||||
D02BE0701D91814C000889C2 /* ChatHistoryGridNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryGridNode.swift; sourceTree = "<group>"; };
|
||||
D02BE0761D9190EF000889C2 /* GridMessageItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GridMessageItem.swift; sourceTree = "<group>"; };
|
||||
@@ -253,11 +281,19 @@
|
||||
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||
D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateAccessoryPanels.swift; sourceTree = "<group>"; };
|
||||
D03ADB4E1D70546B005A521C /* AccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessoryPanelNode.swift; sourceTree = "<group>"; };
|
||||
D04B66B71DD672D00049C3D2 /* GeoLocation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeoLocation.swift; sourceTree = "<group>"; };
|
||||
D05811931DD5F9380057C769 /* TelegramApplicationContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelegramApplicationContext.swift; sourceTree = "<group>"; };
|
||||
D06879541DB8F1FC00424BBD /* CachedResourceRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CachedResourceRepresentations.swift; sourceTree = "<group>"; };
|
||||
D06879561DB8F22200424BBD /* FetchCachedRepresentations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchCachedRepresentations.swift; sourceTree = "<group>"; };
|
||||
D073CE621DCBBE5D007511FD /* MessageSent.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = MessageSent.caf; path = TelegramUI/Sounds/MessageSent.caf; sourceTree = "<group>"; };
|
||||
D073CE641DCBC26B007511FD /* ServiceSoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceSoundManager.swift; sourceTree = "<group>"; };
|
||||
D073CE701DCBF23F007511FD /* DeclareEncodables.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeclareEncodables.swift; sourceTree = "<group>"; };
|
||||
D07551871DDA4BB50073E051 /* TelegramLegacyComponents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TelegramLegacyComponents.framework; path = "../TelegramLegacyComponents/build/Debug-iphoneos/TelegramLegacyComponents.framework"; sourceTree = "<group>"; };
|
||||
D075518A1DDA4D7D0073E051 /* LegacyController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyController.swift; sourceTree = "<group>"; };
|
||||
D075518C1DDA4E0B0073E051 /* LegacyControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyControllerNode.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
D07A7DA21D957671005BCD27 /* ListMessageSnippetItemNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageSnippetItemNode.swift; sourceTree = "<group>"; };
|
||||
D07A7DA41D95783C005BCD27 /* ListMessageNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListMessageNode.swift; sourceTree = "<group>"; };
|
||||
D07CFF731DCA207200761F81 /* PeerSelectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerSelectionController.swift; sourceTree = "<group>"; };
|
||||
@@ -454,6 +490,8 @@
|
||||
D0F69EA91D6B9BCB0046BCD6 /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = "third-party/FFmpeg-iOS/lib/libavformat.a"; sourceTree = "<group>"; };
|
||||
D0F69EAA1D6B9BCB0046BCD6 /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = "third-party/FFmpeg-iOS/lib/libavutil.a"; sourceTree = "<group>"; };
|
||||
D0F69EAB1D6B9BCB0046BCD6 /* libswresample.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libswresample.a; path = "third-party/FFmpeg-iOS/lib/libswresample.a"; sourceTree = "<group>"; };
|
||||
D0F7AB341DCFADCD009AD9A1 /* ChatMessageBubbleImages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageBubbleImages.swift; sourceTree = "<group>"; };
|
||||
D0F7AB381DCFF87B009AD9A1 /* ChatMessageDateHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageDateHeader.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
D0FC40831D5B8E7400261D9D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
@@ -574,6 +616,21 @@
|
||||
name = Sounds;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
D0B844541DAC3ADF005F29E1 /* Strings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */,
|
||||
);
|
||||
name = Strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D0BA6F811D784C3A0034826E /* Input Panels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -832,6 +884,7 @@
|
||||
D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */,
|
||||
D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */,
|
||||
D0F69DCA1D6B89F20046BCD6 /* Search */,
|
||||
D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */,
|
||||
);
|
||||
name = Nodes;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0800"
|
||||
LastUpgradeVersion = "0820"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -52,7 +52,7 @@ public class AuthorizationController: NavigationController {
|
||||
let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization, account -> Signal<Account, NoError> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Bool>(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()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,20 @@ class ChatControllerNode: ASDisplayNode {
|
||||
if textInputPanelNode.textInputNode?.isFirstResponder() ?? false {
|
||||
applyKeyboardAutocorrection()
|
||||
}
|
||||
let text = textInputPanelNode.text
|
||||
|
||||
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
|
||||
|
||||
if let interfaceInteraction = strongSelf.interfaceInteraction, !text.isEmpty {
|
||||
interfaceInteraction.editMessage(editMessage.messageId, editMessage.inputState.inputText)
|
||||
}
|
||||
} else {
|
||||
let text = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText
|
||||
|
||||
if !text.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
|
||||
strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in
|
||||
@@ -128,6 +141,7 @@ class ChatControllerNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.textInputPanelNode?.displayAttachmentMenu = { [weak self] in
|
||||
self?.displayAttachmentMenu()
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<MessageId>()
|
||||
private let maxVisibleIncomingMessageIndex = ValuePromise<MessageIndex>(ignoreRepeated: true)
|
||||
let canReadHistory = ValuePromise<Bool>()
|
||||
|
||||
private let _chatHistoryLocation = ValuePromise<ChatHistoryLocation>()
|
||||
@@ -264,22 +264,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
|
||||
|
||||
self.historyDisposable.set(appliedTransition.start())
|
||||
|
||||
let previousMaxIncomingMessageId = Atomic<MessageId?>(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<Void, NoError> {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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: .Unread(index: maxReadIndex), initialData: initialData)
|
||||
} else {
|
||||
preloaded = true
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: nil, initialData: initialData)
|
||||
}
|
||||
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, initialData: initialData)
|
||||
}
|
||||
}
|
||||
case let .InitialSearch(messageId, count):
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ import TelegramCore
|
||||
import Postbox
|
||||
|
||||
func inputContextForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatPresentationInputContext? {
|
||||
if chatPresentationInterfaceState.interfaceState.inputState.inputText == "#" {
|
||||
if let _ = chatPresentationInterfaceState.interfaceState.editMessage {
|
||||
return nil
|
||||
} else {
|
||||
if chatPresentationInterfaceState.interfaceState.composeInputState.inputText == "#" {
|
||||
return .hashtag
|
||||
} else if chatPresentationInterfaceState.interfaceState.inputState.inputText == "@" {
|
||||
} else if chatPresentationInterfaceState.interfaceState.composeInputState.inputText == "@" {
|
||||
return .mention
|
||||
}
|
||||
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 {
|
||||
if let _ = chatPresentationInterfaceState.interfaceState.editMessage {
|
||||
return ChatTextInputPanelState(accessoryItems: [])
|
||||
} else {
|
||||
if chatPresentationInterfaceState.interfaceState.composeInputState.inputText.isEmpty {
|
||||
return ChatTextInputPanelState(accessoryItems: [.stickers])
|
||||
} else {
|
||||
return ChatTextInputPanelState(accessoryItems: [])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
240
TelegramUI/ChatMessageActionButtonsNode.swift
Normal file
240
TelegramUI/ChatMessageActionButtonsNode.swift
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
117
TelegramUI/ChatMessageBubbleImages.swift
Normal file
117
TelegramUI/ChatMessageBubbleImages.swift
Normal file
@@ -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))
|
||||
}
|
||||
@@ -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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
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,8 +828,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
|
||||
}
|
||||
|
||||
if !self.backgroundNode.frame.contains(point) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
173
TelegramUI/ChatMessageDateHeader.swift
Normal file
173
TelegramUI/ChatMessageDateHeader.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,33 +82,26 @@ 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)
|
||||
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<MediaResourceStatus, NoError>?
|
||||
@@ -175,27 +168,19 @@ final class ChatMessageInteractiveMediaNode: ASTransformNode {
|
||||
}
|
||||
}
|
||||
|
||||
let arguments = TransformImageArguments(corners: corners, imageSize: boundingSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets())
|
||||
let arguments = TransformImageArguments(corners: corners, imageSize: drawingSize, 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 imageApply = imageLayout(arguments)
|
||||
|
||||
let adjustedArguments = TransformImageArguments(corners: corners, imageSize: nativeSize, boundingSize: adjustedImageSize, intrinsicInsets: UIEdgeInsets())
|
||||
|
||||
let adjustedImageFrame = CGRect(origin: imageFrame.origin, size: adjustedArguments.drawingSize)
|
||||
let imageApply = imageLayout(adjustedArguments)
|
||||
|
||||
return (CGSize(width: adjustedImageSize.width, height: adjustedImageSize.height), { [weak self] in
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
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)\")"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
|
||||
final class ChatPanelInterfaceInteractionStatuses {
|
||||
let editingMessage: Signal<Bool, NoError>
|
||||
|
||||
init(editingMessage: Signal<Bool, NoError>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
149
TelegramUI/EditAccessoryPanelNode.swift
Normal file
149
TelegramUI/EditAccessoryPanelNode.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
85
TelegramUI/GeoLocation.swift
Normal file
85
TelegramUI/GeoLocation.swift
Normal file
@@ -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<GeoLocation>()
|
||||
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<GeoLocation, NoError> {
|
||||
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()
|
||||
}
|
||||
6
TelegramUI/ImageContainingNode.swift
Normal file
6
TelegramUI/ImageContainingNode.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
|
||||
protocol ImageContainingNode {
|
||||
|
||||
}
|
||||
259
TelegramUI/LegacyAttachmentMenu.swift
Normal file
259
TelegramUI/LegacyAttachmentMenu.swift
Normal file
@@ -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<TGUser *> *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];*/
|
||||
}
|
||||
165
TelegramUI/LegacyCamera.swift
Normal file
165
TelegramUI/LegacyCamera.swift
Normal file
@@ -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];
|
||||
};*/
|
||||
}
|
||||
175
TelegramUI/LegacyController.swift
Normal file
175
TelegramUI/LegacyController.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
TelegramUI/LegacyControllerNode.swift
Normal file
38
TelegramUI/LegacyControllerNode.swift
Normal file
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
9
TelegramUI/LegacyEmptyController.swift
Normal file
9
TelegramUI/LegacyEmptyController.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
import TelegramLegacyComponents
|
||||
|
||||
final class LegacyEmptyController: TGViewController {
|
||||
override func viewDidLoad() {
|
||||
self.view.backgroundColor = nil
|
||||
self.view.isOpaque = false
|
||||
}
|
||||
}
|
||||
137
TelegramUI/LegacyMediaPickers.swift
Normal file
137
TelegramUI/LegacyMediaPickers.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
8
TelegramUI/LegacyNavigationController.swift
Normal file
8
TelegramUI/LegacyNavigationController.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import TelegramLegacyComponents
|
||||
|
||||
func makeLegacyNavigationController(rootController: UIViewController) -> TGNavigationController {
|
||||
return TGNavigationController.make(withRootController: rootController)
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
})
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
40
TelegramUI/PerformanceSpinner.swift
Normal file
40
TelegramUI/PerformanceSpinner.swift
Normal file
@@ -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 {
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
9
TelegramUI/TelegramApplicationContext.swift
Normal file
9
TelegramUI/TelegramApplicationContext.swift
Normal file
@@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
|
||||
public final class TelegramApplicationContext {
|
||||
public let openUrl: (String) -> Void
|
||||
|
||||
public init(openUrl: @escaping (String) -> Void) {
|
||||
self.openUrl = openUrl
|
||||
}
|
||||
}
|
||||
60
TelegramUI/TelegramInitializeLegacyComponents.swift
Normal file
60
TelegramUI/TelegramInitializeLegacyComponents.swift
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user