no message

This commit is contained in:
Peter
2016-11-17 22:17:44 +03:00
parent d55d3eecc6
commit 46882a10dc
53 changed files with 2898 additions and 357 deletions

View File

@@ -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;
};

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
LastUpgradeVersion = "0820"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -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

View File

@@ -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

View File

@@ -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()
}
}))
}
}

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}
}
}

View File

@@ -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):

View File

@@ -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
}
}
}

View File

@@ -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: [])
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View 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
})
}
}
}

View File

@@ -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()

View File

@@ -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
}
}

View 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))
}

View File

@@ -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
}
}
}
}

View 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)
}
}
}
}

View File

@@ -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)

View File

@@ -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)\")"
}
}

View File

@@ -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
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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)
}
}

View 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()
}
}
}

View 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()
}

View File

@@ -0,0 +1,6 @@
import Foundation
import AsyncDisplayKit
protocol ImageContainingNode {
}

View 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];*/
}

View 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];
};*/
}

View 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)
}
}
}

View 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()
})
}
}

View File

@@ -0,0 +1,9 @@
import Foundation
import TelegramLegacyComponents
final class LegacyEmptyController: TGViewController {
override func viewDidLoad() {
self.view.backgroundColor = nil
self.view.isOpaque = false
}
}

View 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()
}
}
}

View File

@@ -0,0 +1,8 @@
import Foundation
import UIKit
import TelegramLegacyComponents
func makeLegacyNavigationController(rootController: UIViewController) -> TGNavigationController {
return TGNavigationController.make(withRootController: rootController)
}

View File

@@ -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?

View File

@@ -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)

View File

@@ -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
})

View File

@@ -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?

View File

@@ -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 })

View 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 {
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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()

View 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
}
}

View 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)
}

View File

@@ -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