no message

This commit is contained in:
Peter
2016-09-10 18:00:50 +03:00
parent 12068fd805
commit cb5a81670d
39 changed files with 1033 additions and 319 deletions

View File

@@ -7,6 +7,7 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */; };
D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */; }; D02958021D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */; };
D03ADB481D703268005A521C /* ChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB471D703268005A521C /* ChatInterfaceState.swift */; }; D03ADB481D703268005A521C /* ChatInterfaceState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB471D703268005A521C /* ChatInterfaceState.swift */; };
D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; }; D03ADB4B1D70443F005A521C /* ReplyAccessoryPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */; };
@@ -30,6 +31,14 @@
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */; }; D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */; };
D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */; }; D0D2689A1D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */; };
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */; }; D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */; };
D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */; };
D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */; };
D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */; };
D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */; };
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */; };
D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA01D821B28008AEB01 /* HashtagsTableCell.swift */; };
D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */; };
D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA51D82BCE0008AEB01 /* MentionsTableCell.swift */; };
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */; }; D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */; };
D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */; }; D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */; };
D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */; }; D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */; };
@@ -75,7 +84,7 @@
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */; }; D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */; };
D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */; }; D0F69DF41D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */; };
D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */; }; D0F69DF51D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */; };
D0F69DFE1D6B8A880046BCD6 /* ChatListAvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */; }; D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */; };
D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */; }; D0F69DFF1D6B8A880046BCD6 /* ChatListController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */; };
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */; }; D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */; };
D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */; }; D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */; };
@@ -145,7 +154,6 @@
D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E851D6B8C850046BCD6 /* RingBuffer.h */; }; D0F69E8E1D6B8C850046BCD6 /* RingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E851D6B8C850046BCD6 /* RingBuffer.h */; };
D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E861D6B8C850046BCD6 /* RingBuffer.m */; }; D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E861D6B8C850046BCD6 /* RingBuffer.m */; };
D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */; }; D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */; };
D0F69E951D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */; };
D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */; }; D0F69E961D6B8C9B0046BCD6 /* ProgressiveImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */; };
D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E941D6B8C9B0046BCD6 /* WebP.swift */; }; D0F69E971D6B8C9B0046BCD6 /* WebP.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F69E941D6B8C9B0046BCD6 /* WebP.swift */; };
D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */; }; D0F69E9A1D6B8D200046BCD6 /* UIImage+WebP.h in Headers */ = {isa = PBXBuildFile; fileRef = D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */; };
@@ -175,6 +183,7 @@
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatChannelSubscriberInputPanelNode.swift; sourceTree = "<group>"; };
D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = "<group>"; }; D02958011D6F0D5F00360E5E /* TapLongTapOrDoubleTapGestureRecognizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TapLongTapOrDoubleTapGestureRecognizer.swift; sourceTree = "<group>"; };
D03ADB471D703268005A521C /* ChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceState.swift; sourceTree = "<group>"; }; D03ADB471D703268005A521C /* ChatInterfaceState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceState.swift; sourceTree = "<group>"; };
D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; }; D03ADB4A1D70443F005A521C /* ReplyAccessoryPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyAccessoryPanelNode.swift; sourceTree = "<group>"; };
@@ -201,6 +210,14 @@
D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionNode.swift; sourceTree = "<group>"; }; D0D2686D1D7898A900C422DA /* ChatMessageSelectionNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatMessageSelectionNode.swift; sourceTree = "<group>"; };
D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanelInterfaceInteraction.swift; sourceTree = "<group>"; }; D0D268991D79CF9F00C422DA /* ChatPanelInterfaceInteraction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatPanelInterfaceInteraction.swift; sourceTree = "<group>"; };
D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareRecipientsActionSheetController.swift; sourceTree = "<group>"; }; D0D2689C1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareRecipientsActionSheetController.swift; sourceTree = "<group>"; };
D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceStateContextMenus.swift; sourceTree = "<group>"; };
D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputContextPanelNode.swift; sourceTree = "<group>"; };
D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInputContextPanelNode.swift; sourceTree = "<group>"; };
D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContextPanels.swift; sourceTree = "<group>"; };
D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = "<group>"; };
D0DF0CA01D821B28008AEB01 /* HashtagsTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagsTableCell.swift; sourceTree = "<group>"; };
D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = "<group>"; };
D0DF0CA51D82BCE0008AEB01 /* MentionsTableCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionsTableCell.swift; sourceTree = "<group>"; };
D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = "<group>"; }; D0F69CD31D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FFMpegMediaFrameSourceContext.swift; sourceTree = "<group>"; };
D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = "<group>"; }; D0F69CD41D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaPlayerAudioRenderer.swift; sourceTree = "<group>"; };
D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = "<group>"; }; D0F69CD61D6B87D30046BCD6 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = "<group>"; };
@@ -246,7 +263,7 @@
D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordControllerNode.swift; sourceTree = "<group>"; }; D0F69DEC1D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPasswordControllerNode.swift; sourceTree = "<group>"; };
D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneController.swift; sourceTree = "<group>"; }; D0F69DED1D6B8A6C0046BCD6 /* AuthorizationPhoneController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneController.swift; sourceTree = "<group>"; };
D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneControllerNode.swift; sourceTree = "<group>"; }; D0F69DEE1D6B8A6C0046BCD6 /* AuthorizationPhoneControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationPhoneControllerNode.swift; sourceTree = "<group>"; };
D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListAvatarNode.swift; sourceTree = "<group>"; }; D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AvatarNode.swift; sourceTree = "<group>"; };
D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = "<group>"; }; D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListController.swift; sourceTree = "<group>"; };
D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = "<group>"; }; D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListControllerNode.swift; sourceTree = "<group>"; };
D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListEmptyItem.swift; sourceTree = "<group>"; }; D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatListEmptyItem.swift; sourceTree = "<group>"; };
@@ -316,7 +333,6 @@
D0F69E851D6B8C850046BCD6 /* RingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RingBuffer.h; sourceTree = "<group>"; }; D0F69E851D6B8C850046BCD6 /* RingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RingBuffer.h; sourceTree = "<group>"; };
D0F69E861D6B8C850046BCD6 /* RingBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RingBuffer.m; sourceTree = "<group>"; }; D0F69E861D6B8C850046BCD6 /* RingBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RingBuffer.m; sourceTree = "<group>"; };
D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingByteBuffer.swift; sourceTree = "<group>"; }; D0F69E871D6B8C850046BCD6 /* RingByteBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RingByteBuffer.swift; sourceTree = "<group>"; };
D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRepresentationsUtils.swift; sourceTree = "<group>"; };
D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressiveImage.swift; sourceTree = "<group>"; }; D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressiveImage.swift; sourceTree = "<group>"; };
D0F69E941D6B8C9B0046BCD6 /* WebP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebP.swift; sourceTree = "<group>"; }; D0F69E941D6B8C9B0046BCD6 /* WebP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebP.swift; sourceTree = "<group>"; };
D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+WebP.h"; sourceTree = "<group>"; }; D0F69E981D6B8D200046BCD6 /* UIImage+WebP.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+WebP.h"; sourceTree = "<group>"; };
@@ -379,6 +395,9 @@
D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */, D03ADB4C1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift */,
D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */, D0BA6F841D784ECD0034826E /* ChatInterfaceStateInputPanels.swift */,
D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */, D0D268661D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift */,
D0DF0C941D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift */,
D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */,
D0DF0C9B1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift */,
); );
name = "Interface State"; name = "Interface State";
sourceTree = "<group>"; sourceTree = "<group>";
@@ -419,19 +438,12 @@
children = ( children = (
D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */, D0BA6F821D784C520034826E /* ChatInputPanelNode.swift */,
D0F69E3F1D6B8B6B0046BCD6 /* Text Input */, D0F69E3F1D6B8B6B0046BCD6 /* Text Input */,
D0BA6F861D784F700034826E /* Message Selection */, D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */,
D0105D591D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift */,
); );
name = "Input Panels"; name = "Input Panels";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D0BA6F861D784F700034826E /* Message Selection */ = {
isa = PBXGroup;
children = (
D0BA6F871D784F880034826E /* ChatMessageSelectionInputPanelNode.swift */,
);
name = "Message Selection";
sourceTree = "<group>";
};
D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */ = { D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -448,6 +460,34 @@
name = "Share Recipients"; name = "Share Recipients";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
D0DF0C961D81FD87008AEB01 /* Input Context Panels */ = {
isa = PBXGroup;
children = (
D0DF0C991D81FF3F008AEB01 /* ChatInputContextPanelNode.swift */,
D0DF0C9F1D8219C7008AEB01 /* Hashtags */,
D0DF0CA21D82BCBC008AEB01 /* Mentions */,
);
name = "Input Context Panels";
sourceTree = "<group>";
};
D0DF0C9F1D8219C7008AEB01 /* Hashtags */ = {
isa = PBXGroup;
children = (
D0DF0C971D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift */,
D0DF0CA01D821B28008AEB01 /* HashtagsTableCell.swift */,
);
name = Hashtags;
sourceTree = "<group>";
};
D0DF0CA21D82BCBC008AEB01 /* Mentions */ = {
isa = PBXGroup;
children = (
D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */,
D0DF0CA51D82BCE0008AEB01 /* MentionsTableCell.swift */,
);
name = Mentions;
sourceTree = "<group>";
};
D0F69CCE1D6B87950046BCD6 /* Files */ = { D0F69CCE1D6B87950046BCD6 /* Files */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -517,6 +557,7 @@
D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */, D0F69DC41D6B89E10046BCD6 /* RadialProgressNode.swift */,
D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */, D0F69DC21D6B89DA0046BCD6 /* TextNode.swift */,
D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */, D0F69DC01D6B89D30046BCD6 /* ListSectionHeaderNode.swift */,
D0F69DF71D6B8A880046BCD6 /* AvatarNode.swift */,
D0F69DCA1D6B89F20046BCD6 /* Search */, D0F69DCA1D6B89F20046BCD6 /* Search */,
); );
name = Nodes; name = Nodes;
@@ -595,7 +636,6 @@
D0F69DF61D6B8A720046BCD6 /* Chat List */ = { D0F69DF61D6B8A720046BCD6 /* Chat List */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D0F69DF71D6B8A880046BCD6 /* ChatListAvatarNode.swift */,
D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */, D0F69DF81D6B8A880046BCD6 /* ChatListController.swift */,
D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */, D0F69DF91D6B8A880046BCD6 /* ChatListControllerNode.swift */,
D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */, D0F69DFA1D6B8A880046BCD6 /* ChatListEmptyItem.swift */,
@@ -637,6 +677,7 @@
D0F69E181D6B8AD10046BCD6 /* Items */, D0F69E181D6B8AD10046BCD6 /* Items */,
D03ADB461D703250005A521C /* Interface State */, D03ADB461D703250005A521C /* Interface State */,
D03ADB491D704427005A521C /* Accessory Panels */, D03ADB491D704427005A521C /* Accessory Panels */,
D0DF0C961D81FD87008AEB01 /* Input Context Panels */,
D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */, D0D2686A1D788F6600C422DA /* Navigation Accessory Panels */,
D0BA6F811D784C3A0034826E /* Input Panels */, D0BA6F811D784C3A0034826E /* Input Panels */,
D0F69E441D6B8B850046BCD6 /* History Navigation */, D0F69E441D6B8B850046BCD6 /* History Navigation */,
@@ -787,7 +828,6 @@
D0F69E911D6B8C8E0046BCD6 /* Utils */ = { D0F69E911D6B8C8E0046BCD6 /* Utils */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D0F69E921D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift */,
D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */, D0F69E931D6B8C9B0046BCD6 /* ProgressiveImage.swift */,
D0F69E941D6B8C9B0046BCD6 /* WebP.swift */, D0F69E941D6B8C9B0046BCD6 /* WebP.swift */,
); );
@@ -971,6 +1011,7 @@
D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */, D0B417C31D7DE54E004562A4 /* ChatPresentationInterfaceState.swift in Sources */,
D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */, D0F69E3C1D6B8B030046BCD6 /* ChatMessageTextBubbleContentNode.swift in Sources */,
D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */, D03ADB4D1D7045C9005A521C /* ChatInterfaceStateAccessoryPanels.swift in Sources */,
D0DF0C9A1D81FF3F008AEB01 /* ChatInputContextPanelNode.swift in Sources */,
D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */, D0D2689D1D79D33E00C422DA /* ShareRecipientsActionSheetController.swift in Sources */,
D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */, D0F69E171D6B8ACF0046BCD6 /* ChatHistoryLocation.swift in Sources */,
D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */, D0F69E741D6B8C340046BCD6 /* ContactsControllerNode.swift in Sources */,
@@ -995,7 +1036,6 @@
D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */, D0F69E0C1D6B8AB10046BCD6 /* HorizontalPeerItem.swift in Sources */,
D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */, D0F69E551D6B8BDA0046BCD6 /* GalleryController.swift in Sources */,
D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */, D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */,
D0F69E951D6B8C9B0046BCD6 /* ImageRepresentationsUtils.swift in Sources */,
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */, D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */,
D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */, D0F69E431D6B8B7E0046BCD6 /* ResizeableTextInputView.swift in Sources */,
D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */, D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */,
@@ -1006,20 +1046,26 @@
D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */, D0F69E1A1D6B8AE60046BCD6 /* ChatHoleItem.swift in Sources */,
D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */, D0F69D9C1D6B87EC0046BCD6 /* MediaPlaybackData.swift in Sources */,
D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */, D0F69D241D6B87D30046BCD6 /* MediaPlayerAudioRenderer.swift in Sources */,
D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */,
D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */, D0F69D4B1D6B87D30046BCD6 /* TouchDownGestureRecognizer.swift in Sources */,
D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */, D0F69E3D1D6B8B030046BCD6 /* ChatMessageWebpageBubbleContentNode.swift in Sources */,
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */,
D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */, D0D2686E1D7898A900C422DA /* ChatMessageSelectionNode.swift in Sources */,
D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */, D0F69E8B1D6B8C850046BCD6 /* FFMpegSwResample.m in Sources */,
D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */, D0F69DD21D6B8A0D0046BCD6 /* SearchDisplayControllerContentNode.swift in Sources */,
D0105D5A1D80B957008755D8 /* ChatChannelSubscriberInputPanelNode.swift in Sources */,
D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */, D0F69DC51D6B89E10046BCD6 /* RadialProgressNode.swift in Sources */,
D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */, D0F69E491D6B8BAC0046BCD6 /* ActionSheetRollImageItem.swift in Sources */,
D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */, D0F69E761D6B8C340046BCD6 /* ContactsSearchContainerNode.swift in Sources */,
D0DF0CA61D82BCE0008AEB01 /* MentionsTableCell.swift in Sources */,
D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */, D0F69E011D6B8A880046BCD6 /* ChatListEmptyItem.swift in Sources */,
D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */, D0F69E591D6B8BDA0046BCD6 /* GalleryPagerNode.swift in Sources */,
D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */, D0F69E391D6B8B030046BCD6 /* ChatMessageMediaBubbleContentNode.swift in Sources */,
D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */, D0F69D351D6B87D30046BCD6 /* MediaFrameSource.swift in Sources */,
D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */, D0F69E371D6B8B030046BCD6 /* ChatMessageItem.swift in Sources */,
D0DF0C9C1D81FFB2008AEB01 /* ChatInterfaceInputContextPanels.swift in Sources */,
D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */, D0F69E641D6B8BF90046BCD6 /* ChatVideoGalleryItem.swift in Sources */,
D0DF0CA11D821B28008AEB01 /* HashtagsTableCell.swift in Sources */,
D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */, D0F69E351D6B8B030046BCD6 /* ChatMessageInteractiveFileNode.swift in Sources */,
D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */, D0F69E151D6B8ACF0046BCD6 /* ChatControllerNode.swift in Sources */,
D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */, D0F69E001D6B8A880046BCD6 /* ChatListControllerNode.swift in Sources */,
@@ -1036,7 +1082,7 @@
D0F69E0A1D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift in Sources */, D0F69E0A1D6B8AA60046BCD6 /* ChatListSearchRecentPeersNode.swift in Sources */,
D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */, D0F69E3E1D6B8B030046BCD6 /* ChatUnreadItem.swift in Sources */,
D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */, D0F69D771D6B87DF0046BCD6 /* FFMpegMediaPassthroughVideoFrameDecoder.swift in Sources */,
D0F69DFE1D6B8A880046BCD6 /* ChatListAvatarNode.swift in Sources */, D0F69DFE1D6B8A880046BCD6 /* AvatarNode.swift in Sources */,
D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */, D0F69E9B1D6B8D200046BCD6 /* UIImage+WebP.m in Sources */,
D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */, D0F69E581D6B8BDA0046BCD6 /* GalleryItemNode.swift in Sources */,
D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */, D0F69DAD1D6B87EC0046BCD6 /* Cache.swift in Sources */,
@@ -1046,6 +1092,7 @@
D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */,
D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */,
D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */, D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */,
D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */,
D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */, D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */,
D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */, D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */,
D0F69D2C1D6B87D30046BCD6 /* MediaPlayerNode.swift in Sources */, D0F69D2C1D6B87D30046BCD6 /* MediaPlayerNode.swift in Sources */,
@@ -1058,6 +1105,7 @@
D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */, D0F69E8C1D6B8C850046BCD6 /* FrameworkBundle.swift in Sources */,
D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */, D0F69D661D6B87D30046BCD6 /* FFMpegMediaFrameSourceContextHelpers.swift in Sources */,
D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */, D0F69DD11D6B8A0D0046BCD6 /* SearchDisplayController.swift in Sources */,
D0DF0C951D81B063008AEB01 /* ChatInterfaceStateContextMenus.swift in Sources */,
D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */, D0F69DF21D6B8A6C0046BCD6 /* AuthorizationPasswordController.swift in Sources */,
D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */, D0F69E8F1D6B8C850046BCD6 /* RingBuffer.m in Sources */,
D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */, D0F69DF31D6B8A6C0046BCD6 /* AuthorizationPasswordControllerNode.swift in Sources */,

View File

@@ -19,50 +19,50 @@ public class AuthorizationController: NavigationController {
self.pushViewController(phoneController, animated: false) self.pushViewController(phoneController, animated: false)
let authorizationSequence = phoneController.result |> mapToSignal { (account, sentCode, phone) -> Signal<Api.auth.Authorization, NoError> in let authorizationSequence = phoneController.result |> mapToSignal { (account, sentCode, phone) -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in
return deferred { [weak self] in return deferred { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.account = account strongSelf.account = account
let codeController = AuthorizationCodeController(account: account, phone: phone, sentCode: sentCode) let codeController = AuthorizationCodeController(account: account, phone: phone, sentCode: sentCode)
strongSelf.pushViewController(codeController, animated: true) strongSelf.pushViewController(codeController, animated: true)
return codeController.result |> mapToSignal { result -> Signal<Api.auth.Authorization, NoError> in return codeController.result |> mapToSignal { result -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in
switch result { switch result {
case let .Authorization(authorization): case let .Authorization(authorization):
return single(authorization, NoError.self) return single((authorization, account), NoError.self)
case .Password: case .Password:
return deferred { [weak self] () -> Signal<Api.auth.Authorization, NoError> in return deferred { [weak self] () -> Signal<(Api.auth.Authorization, UnauthorizedAccount), NoError> in
if let strongSelf = self { if let strongSelf = self {
let passwordController = AuthorizationPasswordController(account: account) let passwordController = AuthorizationPasswordController(account: account)
strongSelf.pushViewController(passwordController, animated: true) strongSelf.pushViewController(passwordController, animated: true)
return passwordController.result return passwordController.result |> map { ($0, account) }
} else { } else {
return complete(Api.auth.Authorization.self, NoError.self) return .complete()
} }
} |> runOn(Queue.mainQueue()) } |> runOn(Queue.mainQueue())
} }
} }
} else { } else {
return complete(Api.auth.Authorization.self, NoError.self) return .complete()
} }
} |> runOn(Queue.mainQueue()) } |> runOn(Queue.mainQueue())
} }
let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization -> Signal<Account, NoError> in let accountSignal = authorizationSequence |> mapToSignal { [weak self] authorization, account -> Signal<Account, NoError> in
if let strongSelf = self { if let strongSelf = self {
switch authorization { switch authorization {
case let .authorization(user): case let .authorization(user):
let user = TelegramUser(user: user) let user = TelegramUser(user: user)
return account.postbox.modify { modifier -> AccountState in return account.postbox.modify { modifier -> AccountState in
let state = AuthorizedAccountState(masterDatacenterId: strongSelf.account.masterDatacenterId, peerId: user.id, state: nil) let state = AuthorizedAccountState(masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil)
modifier.setState(state) modifier.setState(state)
return state return state
} |> map { state -> Account in } |> map { state -> Account in
return Account(id: account.id, postbox: account.postbox, network: account.network, peerId: user.id) return Account(id: account.id, postbox: account.postbox, network: account.network, peerId: user.id)
}
} }
}
} else { } else {
return .complete() return .complete()
} }

View File

@@ -5,7 +5,7 @@ import UIKit
import Display import Display
import TelegramCore import TelegramCore
private class ChatListAvatarNodeParameters: NSObject { private class AvatarNodeParameters: NSObject {
let account: Account let account: Account
let peerId: PeerId let peerId: PeerId
let letters: [String] let letters: [String]
@@ -30,28 +30,28 @@ let gradientColors: [NSArray] = [
[UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor] [UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor]
] ]
private enum ChatListAvatarNodeState: Equatable { private enum AvatarNodeState: Equatable {
case Empty case Empty
case PeerAvatar(Peer) case PeerAvatar(PeerId, [String], TelegramMediaImageRepresentation?)
} }
private func ==(lhs: ChatListAvatarNodeState, rhs: ChatListAvatarNodeState) -> Bool { private func ==(lhs: AvatarNodeState, rhs: AvatarNodeState) -> Bool {
switch (lhs, rhs) { switch (lhs, rhs) {
case (.Empty, .Empty): case (.Empty, .Empty):
return true return true
case let (.PeerAvatar(lhsPeer), .PeerAvatar(rhsPeer)) where lhsPeer.isEqual(rhsPeer): case let (.PeerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .PeerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)):
return true return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations
default: default:
return false return false
} }
} }
public final class ChatListAvatarNode: ASDisplayNode { public final class AvatarNode: ASDisplayNode {
var font: UIFont { var font: UIFont {
didSet { didSet {
if oldValue !== font { if oldValue !== font {
if let parameters = self.parameters { if let parameters = self.parameters {
self.parameters = ChatListAvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font) self.parameters = AvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font)
} }
if !self.displaySuspended { if !self.displaySuspended {
@@ -60,10 +60,10 @@ public final class ChatListAvatarNode: ASDisplayNode {
} }
} }
} }
private var parameters: ChatListAvatarNodeParameters? private var parameters: AvatarNodeParameters?
let imageNode: ImageNode let imageNode: ImageNode
private var state: ChatListAvatarNodeState = .Empty private var state: AvatarNodeState = .Empty
public init(font: UIFont) { public init(font: UIFont) {
self.font = font self.font = font
@@ -92,11 +92,11 @@ public final class ChatListAvatarNode: ASDisplayNode {
} }
public func setPeer(account: Account, peer: Peer) { public func setPeer(account: Account, peer: Peer) {
let updatedState = ChatListAvatarNodeState.PeerAvatar(peer) let updatedState = AvatarNodeState.PeerAvatar(peer.id, peer.displayLetters, peer.smallProfileImage)
if updatedState != self.state { if updatedState != self.state {
self.state = updatedState self.state = updatedState
let parameters = ChatListAvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font) let parameters = AvatarNodeParameters(account: account, peerId: peer.id, letters: peer.displayLetters, font: self.font)
self.displaySuspended = true self.displaySuspended = true
self.contents = nil self.contents = nil
@@ -134,7 +134,7 @@ public final class ChatListAvatarNode: ASDisplayNode {
context.clip() context.clip()
let colorIndex: Int let colorIndex: Int
if let parameters = parameters as? ChatListAvatarNodeParameters { if let parameters = parameters as? AvatarNodeParameters {
colorIndex = Int(parameters.account.peerId.id + parameters.peerId.id) colorIndex = Int(parameters.account.peerId.id + parameters.peerId.id)
} else { } else {
colorIndex = 0 colorIndex = 0
@@ -149,11 +149,9 @@ public final class ChatListAvatarNode: ASDisplayNode {
context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions()) context.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: bounds.size.height), options: CGGradientDrawingOptions())
//CGContextDrawRadialGradient(context, gradient, CGPoint(x: bounds.size.width * 0.5, y: -bounds.size.width * 0.2), 0.0, CGPoint(x: bounds.midX, y: bounds.midY), bounds.width, CGGradientDrawingOptions())
context.setBlendMode(.normal) context.setBlendMode(.normal)
if let parameters = parameters as? ChatListAvatarNodeParameters { if let parameters = parameters as? AvatarNodeParameters {
let letters = parameters.letters let letters = parameters.letters
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
let attributedString = NSAttributedString(string: string, attributes: [NSFontAttributeName: parameters.font, NSForegroundColorAttributeName: UIColor.white]) let attributedString = NSAttributedString(string: string, attributes: [NSFontAttributeName: parameters.font, NSForegroundColorAttributeName: UIColor.white])
@@ -161,19 +159,9 @@ public final class ChatListAvatarNode: ASDisplayNode {
let line = CTLineCreateWithAttributedString(attributedString) let line = CTLineCreateWithAttributedString(attributedString)
let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
/*var ascent: CGFloat = 0.0
var descent: CGFloat = 0.0
var leading: CGFloat = 0.0
let lineWidth = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading))
let opticalBounds = CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: ascent + descent + leading))*/
//let opticalBounds = CTLineGetImageBounds(line, context)
let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0)
let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0)) let lineOrigin = CGPoint(x: floorToScreenPixels(-lineBounds.origin.x + (bounds.size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floorToScreenPixels(-lineBounds.origin.y + (bounds.size.height - lineBounds.size.height) / 2.0))
//let lineOrigin = CGPoint(x: floorToScreenPixels(-opticalBounds.origin.x + (bounds.size.width - opticalBounds.size.width) / 2.0), y: floorToScreenPixels(-opticalBounds.origin.y + (bounds.size.height - opticalBounds.size.height) / 2.0))
context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) context.translateBy(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
context.scaleBy(x: 1.0, y: -1.0) context.scaleBy(x: 1.0, y: -1.0)
context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0) context.translateBy(x: -bounds.size.width / 2.0, y: -bounds.size.height / 2.0)
@@ -181,13 +169,6 @@ public final class ChatListAvatarNode: ASDisplayNode {
context.translateBy(x: lineOrigin.x, y: lineOrigin.y) context.translateBy(x: lineOrigin.x, y: lineOrigin.y)
CTLineDraw(line, context) CTLineDraw(line, context)
context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) context.translateBy(x: -lineOrigin.x, y: -lineOrigin.y)
/*var attributes: [String : AnyObject] = [:]
attributes[NSFontAttributeName] = parameters.font
attributes[NSForegroundColorAttributeName] = UIColor.whiteColor()
let lettersSize = string.sizeWithAttributes(attributes)
string.drawAtPoint(CGPoint(x: floor((bounds.size.width - lettersSize.width) / 2.0), y: floor((bounds.size.height - lettersSize.height) / 2.0)), withAttributes: attributes)*/
} }
} }
} }

View File

@@ -6,10 +6,10 @@ private let normalFont = Font.medium(16.0)
private let smallFont = Font.medium(12.0) private let smallFont = Font.medium(12.0)
final class ChatAvatarNavigationNode: ASDisplayNode { final class ChatAvatarNavigationNode: ASDisplayNode {
let avatarNode: ChatListAvatarNode let avatarNode: AvatarNode
override init() { override init() {
self.avatarNode = ChatListAvatarNode(font: normalFont) self.avatarNode = AvatarNode(font: normalFont)
super.init() super.init()

View File

@@ -0,0 +1,138 @@
import Foundation
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
private enum SubscriberAction {
case join
case kicked
case muteNotifications
case unmuteNotifications
}
private func titleAndColorForAction(_ action: SubscriberAction) -> (String, UIColor) {
switch action {
case .join:
return ("Join", UIColor(0x1195f2))
case .kicked:
return ("Join", UIColor.gray)
case .muteNotifications:
return ("Mute", UIColor(0x1195f2))
case .unmuteNotifications:
return ("Unmute", UIColor(0x1195f2))
}
}
private func actionForPeer(_ peer: Peer) -> SubscriberAction? {
if let channel = peer as? TelegramChannel {
switch channel.participationStatus {
case .kicked:
return .kicked
case .left:
return .join
case .member:
return .muteNotifications
}
return .join
} else {
return nil
}
}
final class ChatChannelSubscriberInputPanelNode: ChatInputPanelNode {
private let button: UIButton
private let activityIndicator: UIActivityIndicatorView
private var action: SubscriberAction?
private let actionDisposable = MetaDisposable()
override var peer: Peer? {
didSet {
if let peer = self.peer, oldValue == nil || !peer.isEqual(oldValue!) {
if let action = actionForPeer(peer) {
self.action = action
let (title, color) = titleAndColorForAction(action)
self.button.setTitle(title, for: [])
self.button.setTitleColor(color, for: [.normal])
self.button.setTitleColor(color.withAlphaComponent(0.5), for: [.highlighted])
self.button.sizeToFit()
self.setNeedsLayout()
} else {
self.action = nil
}
}
}
}
override init() {
self.button = UIButton()
self.activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
self.activityIndicator.isHidden = true
super.init()
self.view.addSubview(self.button)
self.view.addSubview(self.activityIndicator)
button.addTarget(self, action: #selector(self.buttonPressed), for: [.touchUpInside])
}
deinit {
self.actionDisposable.dispose()
}
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
return CGSize(width: constrainedSize.width, height: 45.0)
}
override func layout() {
super.layout()
let bounds = self.bounds
let buttonSize = self.button.bounds.size
self.button.frame = CGRect(origin: CGPoint(x: floor((bounds.size.width - buttonSize.width) / 2.0), y: floor((bounds.size.height - buttonSize.height) / 2.0)), size: buttonSize)
//_activityIndicator.frame = CGRectMake(self.frame.size.width - _activityIndicator.frame.size.width - 12.0f, CGFloor((self.frame.size.height - _activityIndicator.frame.size.height) / 2.0f), _activityIndicator.frame.size.width, _activityIndicator.frame.size.height);
let indicatorSize = self.activityIndicator.bounds.size
self.activityIndicator.frame = CGRect(origin: CGPoint(x: bounds.size.width - indicatorSize.width - 12.0, y: floor((bounds.size.height - indicatorSize.height) / 2.0)), size: indicatorSize)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.bounds.contains(point) {
return self.button
} else {
return nil
}
}
@objc func buttonPressed() {
guard let account = self.account, let action = self.action, let peer = self.peer else {
return
}
switch action {
case .join:
self.activityIndicator.isHidden = false
self.activityIndicator.startAnimating()
self.actionDisposable.set((joinChannel(account: account, peerId: peer.id)
|> afterDisposed { [weak self] in
Queue.mainQueue().async {
if let strongSelf = self {
strongSelf.activityIndicator.isHidden = true
strongSelf.activityIndicator.stopAnimating()
}
}
}).start())
case .kicked:
break
case .muteNotifications:
break
case .unmuteNotifications:
break
}
}
}

View File

@@ -383,11 +383,10 @@ public class ChatController: ViewController {
private var enqueuedHistoryViewTransition: (ChatHistoryViewTransition, () -> Void)? private var enqueuedHistoryViewTransition: (ChatHistoryViewTransition, () -> Void)?
private var layoutActionOnViewTransition: (@escaping () -> Void)? private var layoutActionOnViewTransition: (@escaping () -> Void)?
private let _ready = Promise<Bool>() private let _historyReady = Promise<Bool>()
override public var ready: Promise<Bool> { private var didSetHistoryReady = false
return self._ready private let _peerReady = Promise<Bool>()
} private var didSetPeerReady = false
private var didSetReady = false
private let maxVisibleIncomingMessageId = Promise<MessageId>() private let maxVisibleIncomingMessageId = Promise<MessageId>()
private let canReadHistory = Promise<Bool>() private let canReadHistory = Promise<Bool>()
@@ -397,7 +396,7 @@ public class ChatController: ViewController {
return self._chatHistoryLocation.get() return self._chatHistoryLocation.get()
} }
private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil) private var presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: ChatInterfaceState(), peer: nil, inputContext: nil)
private let chatInterfaceStatePromise = Promise<ChatInterfaceState>() private let chatInterfaceStatePromise = Promise<ChatInterfaceState>()
private var leftNavigationButton: ChatNavigationButton? private var leftNavigationButton: ChatNavigationButton?
@@ -407,6 +406,7 @@ public class ChatController: ViewController {
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable() private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
private var controllerInteraction: ChatControllerInteraction? private var controllerInteraction: ChatControllerInteraction?
private var interfaceInteraction: ChatPanelInterfaceInteraction?
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) { public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) {
self.account = account self.account = account
@@ -415,6 +415,8 @@ public class ChatController: ViewController {
super.init() super.init()
self.ready.set(combineLatest(self._historyReady.get(), self._peerReady.get()) |> map { $0 && $1 })
self.setupThemeWithDarkMode(useDarkMode) self.setupThemeWithDarkMode(useDarkMode)
self.scrollToTop = { [weak self] in self.scrollToTop = { [weak self] in
@@ -487,47 +489,21 @@ public class ChatController: ViewController {
} }
}, openMessageContextMenu: { [weak self] id, node, frame in }, openMessageContextMenu: { [weak self] id, node, frame in
if let strongSelf = self, let historyView = strongSelf.historyView { if let strongSelf = self, let historyView = strongSelf.historyView {
let contextMenuController = ContextMenuController(actions: [ if let strongSelf = self, let historyView = strongSelf.historyView {
ContextMenuAction(content: .text("Reply"), action: { [weak strongSelf] in for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if let strongSelf = strongSelf, let historyView = strongSelf.historyView { if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id { strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in
strongSelf.updateChatInterfaceState(animated: true, { $0.withUpdatedReplyMessageId(message.id) }) if let node = node {
strongSelf.chatDisplayNode.ensureInputViewFocused() return (node, frame)
break
}
}
}),
ContextMenuAction(content: .text("Copy"), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if !message.text.isEmpty {
UIPasteboard.general.string = message.text
}
break
}
}
}),
ContextMenuAction(content: .text("More..."), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
strongSelf.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() })
} else { } else {
strongSelf.updateChatInterfaceState(animated: true, { $0.withUpdatedSelectedMessage(message.id) }) return nil
} }
}))
break
}
} }
})
]) break
strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in
if let node = node {
return (node, frame)
} else {
return nil
} }
})) }
} }
}, navigateToMessage: { [weak self] fromId, id in }, navigateToMessage: { [weak self] fromId, id in
if let strongSelf = self, let historyView = strongSelf.historyView { if let strongSelf = self, let historyView = strongSelf.historyView {
@@ -564,7 +540,7 @@ public class ChatController: ViewController {
}, toggleMessageSelection: { [weak self] messageId in }, toggleMessageSelection: { [weak self] messageId in
if let strongSelf = self, let historyView = strongSelf.historyView { if let strongSelf = self, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId { for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId {
strongSelf.updateChatInterfaceState(animated: false, { $0.withToggledSelectedMessage(messageId) }) strongSelf.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(messageId) } })
break break
} }
} }
@@ -576,13 +552,20 @@ public class ChatController: ViewController {
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode())) self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: UIBarButtonItem(customDisplayNode: ChatAvatarNavigationNode()))
self.updateChatInterfaceState(animated: false, { return $0 }) self.updateChatPresentationInterfaceState(animated: false, { return $0 })
peerDisposable.set((account.postbox.peerWithId(peerId) peerDisposable.set((account.postbox.peerWithId(peerId)
|> deliverOnMainQueue).start(next: { [weak self] peer in |> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self { if let strongSelf = self {
strongSelf.title = peer.displayTitle if let peer = peer {
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer) strongSelf.title = peer.displayTitle
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
}
strongSelf.updateChatPresentationInterfaceState(animated: false, { return $0.updatedPeer { _ in return peer } })
if !strongSelf.didSetPeerReady {
strongSelf.didSetPeerReady = true
strongSelf._peerReady.set(.single(true))
}
} }
})) }))
@@ -608,9 +591,9 @@ public class ChatController: ViewController {
case .Loading: case .Loading:
Queue.mainQueue().async { [weak self] in Queue.mainQueue().async { [weak self] in
if let strongSelf = self { if let strongSelf = self {
if !strongSelf.didSetReady { if !strongSelf.didSetHistoryReady {
strongSelf.didSetReady = true strongSelf.didSetHistoryReady = true
strongSelf._ready.set(.single(true)) strongSelf._historyReady.set(.single(true))
} }
} }
} }
@@ -775,8 +758,8 @@ public class ChatController: ViewController {
self?.layoutActionOnViewTransition = f self?.layoutActionOnViewTransition = f
} }
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] state, animated in self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in
self?.updateChatInterfaceState(animated: animated, { _ in return state }) self?.updateChatPresentationInterfaceState(animated: animated, { $0.updatedInterfaceState(f) })
} }
self.chatDisplayNode.displayAttachmentMenu = { [weak self] in self.chatDisplayNode.displayAttachmentMenu = { [weak self] in
@@ -804,7 +787,22 @@ public class ChatController: ViewController {
} }
} }
self.chatDisplayNode.interfaceInteraction = ChatPanelInterfaceInteraction(deleteSelectedMessages: { [weak self] in let interfaceInteraction = ChatPanelInterfaceInteraction(setupReplyMessage: { [weak self] messageId in
if let strongSelf = self, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId {
strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withUpdatedReplyMessageId(message.id) } })
strongSelf.chatDisplayNode.ensureInputViewFocused()
break
}
}
}, beginMessageSelection: { [weak self] messageId in
if let strongSelf = self, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId {
strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withUpdatedSelectedMessage(message.id) } })
break
}
}
}, deleteSelectedMessages: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty { if let messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
@@ -812,15 +810,22 @@ public class ChatController: ViewController {
modifier.deleteMessages(Array(messageIds)) modifier.deleteMessages(Array(messageIds))
}).start() }).start()
} }
strongSelf.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() }) strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
} }
}, forwardSelectedMessages: { [weak self] in }, forwardSelectedMessages: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
let controller = ShareRecipientsActionSheetController() let controller = ShareRecipientsActionSheetController()
strongSelf.present(controller, in: .window) strongSelf.present(controller, in: .window)
} }
}, updateTextInputState: { [weak self] textInputState in
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState { $0.updatedInterfaceState { $0.withUpdatedInputState(textInputState) } }
}
}) })
self.interfaceInteraction = interfaceInteraction
self.chatDisplayNode.interfaceInteraction = interfaceInteraction
self.displayNodeDidLoad() self.displayNodeDidLoad()
self.dequeueHistoryViewTransition() self.dequeueHistoryViewTransition()
@@ -851,9 +856,9 @@ public class ChatController: ViewController {
if strongSelf.isNodeLoaded { if strongSelf.isNodeLoaded {
strongSelf.dequeueHistoryViewTransition() strongSelf.dequeueHistoryViewTransition()
} else { } else {
if !strongSelf.didSetReady { if !strongSelf.didSetHistoryReady {
strongSelf.didSetReady = true strongSelf.didSetHistoryReady = true
strongSelf._ready.set(.single(true)) strongSelf._historyReady.set(.single(true))
} }
} }
} else { } else {
@@ -886,9 +891,9 @@ public class ChatController: ViewController {
} }
} }
if !strongSelf.didSetReady { if !strongSelf.didSetHistoryReady {
strongSelf.didSetReady = true strongSelf.didSetHistoryReady = true
strongSelf._ready.set(.single(true)) strongSelf._historyReady.set(.single(true))
} }
completion() completion()
@@ -944,16 +949,18 @@ public class ChatController: ViewController {
}) })
} }
func updateChatInterfaceState(animated: Bool = true, _ f: (ChatInterfaceState) -> ChatInterfaceState) { func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) {
let updatedChatInterfaceState = f(self.presentationInterfaceState.interfaceState) let temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState)
let inputContext = inputContextForChatPresentationIntefaceState(temporaryChatPresentationInterfaceState, account: self.account)
let updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputContext { _ in return inputContext }
if self.isNodeLoaded { if self.isNodeLoaded {
self.chatDisplayNode.updateChatInterfaceState(updatedChatInterfaceState, animated: animated) self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated)
} }
self.presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: updatedChatInterfaceState, peer: nil) self.presentationInterfaceState = updatedChatPresentationInterfaceState
self.chatInterfaceStatePromise.set(.single(updatedChatInterfaceState)) self.chatInterfaceStatePromise.set(.single(updatedChatPresentationInterfaceState.interfaceState))
if let button = leftNavigationButtonForChatInterfaceState(updatedChatInterfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) { if let button = leftNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.leftNavigationButton, target: self, selector: #selector(self.leftNavigationButtonAction)) {
self.navigationItem.setLeftBarButton(button.buttonItem, animated: true) self.navigationItem.setLeftBarButton(button.buttonItem, animated: true)
self.leftNavigationButton = button self.leftNavigationButton = button
} else if let _ = self.leftNavigationButton { } else if let _ = self.leftNavigationButton {
@@ -961,7 +968,7 @@ public class ChatController: ViewController {
self.leftNavigationButton = nil self.leftNavigationButton = nil
} }
if let button = rightNavigationButtonForChatInterfaceState(updatedChatInterfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) { if let button = rightNavigationButtonForChatInterfaceState(updatedChatPresentationInterfaceState.interfaceState, currentButton: self.rightNavigationButton, target: self, selector: #selector(self.rightNavigationButtonAction), chatInfoNavigationButton: self.chatInfoNavigationButton) {
self.navigationItem.setRightBarButton(button.buttonItem, animated: true) self.navigationItem.setRightBarButton(button.buttonItem, animated: true)
self.rightNavigationButton = button self.rightNavigationButton = button
} else if let _ = self.rightNavigationButton { } else if let _ = self.rightNavigationButton {
@@ -970,9 +977,9 @@ public class ChatController: ViewController {
} }
if let controllerInteraction = self.controllerInteraction { if let controllerInteraction = self.controllerInteraction {
if updatedChatInterfaceState.selectionState != controllerInteraction.selectionState { if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState {
let animated = controllerInteraction.selectionState == nil || updatedChatInterfaceState.selectionState == nil let animated = controllerInteraction.selectionState == nil || updatedChatPresentationInterfaceState.interfaceState.selectionState == nil
controllerInteraction.selectionState = updatedChatInterfaceState.selectionState controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState
self.chatDisplayNode.listView.forEachItemNode { itemNode in self.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView { if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated) itemNode.updateSelectionState(animated: animated)
@@ -997,7 +1004,7 @@ public class ChatController: ViewController {
private func navigationButtonAction(_ action: ChatNavigationButtonAction) { private func navigationButtonAction(_ action: ChatNavigationButtonAction) {
switch action { switch action {
case .cancelMessageSelection: case .cancelMessageSelection:
self.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() }) self.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
case .clearHistory: case .clearHistory:
let actionSheet = ActionSheetController() let actionSheet = ActionSheetController()
actionSheet.setItemGroups([ActionSheetItemGroup(items: [ actionSheet.setItemGroups([ActionSheetItemGroup(items: [

View File

@@ -7,6 +7,11 @@ import TelegramCore
private let backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0") private let backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0")
private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: ChatPresentationInterfaceState, _ rhs: ChatPresentationInterfaceState) -> Bool {
return false
}
enum ChatMessageViewPosition: Equatable { enum ChatMessageViewPosition: Equatable {
case AroundUnread(count: Int) case AroundUnread(count: Int)
case Around(index: MessageIndex, anchorIndex: MessageIndex) case Around(index: MessageIndex, anchorIndex: MessageIndex)
@@ -46,20 +51,22 @@ class ChatControllerNode: ASDisplayNode {
let backgroundNode: ASDisplayNode let backgroundNode: ASDisplayNode
let listView: ListView let listView: ListView
let inputPanelBackgroundNode: ASDisplayNode private let inputPanelBackgroundNode: ASDisplayNode
private let inputPanelBackgroundSeparatorNode: ASDisplayNode
var inputPanelNode: ChatInputPanelNode? private var inputPanelNode: ChatInputPanelNode?
var accessoryPanelNode: AccessoryPanelNode? private var accessoryPanelNode: AccessoryPanelNode?
private var inputContextPanelNode: ChatInputContextPanelNode?
var textInputPanelNode: ChatTextInputPanelNode? private var textInputPanelNode: ChatTextInputPanelNode?
let navigateToLatestButton: ChatHistoryNavigationButtonNode let navigateToLatestButton: ChatHistoryNavigationButtonNode
private var ignoreUpdateHeight = false private var ignoreUpdateHeight = false
var chatInterfaceState = ChatInterfaceState() var chatPresentationInterfaceState = ChatPresentationInterfaceState()
var requestUpdateChatInterfaceState: (ChatInterfaceState, Bool) -> Void = { _ in } var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _ in }
var displayAttachmentMenu: () -> Void = { } var displayAttachmentMenu: () -> Void = { }
var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in }
var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
@@ -81,6 +88,11 @@ class ChatControllerNode: ASDisplayNode {
self.inputPanelBackgroundNode = ASDisplayNode() self.inputPanelBackgroundNode = ASDisplayNode()
self.inputPanelBackgroundNode.backgroundColor = UIColor(0xfafafa) self.inputPanelBackgroundNode.backgroundColor = UIColor(0xfafafa)
self.inputPanelBackgroundNode.isLayerBacked = true
self.inputPanelBackgroundSeparatorNode = ASDisplayNode()
self.inputPanelBackgroundSeparatorNode.backgroundColor = UIColor(0xcdccd3)
self.inputPanelBackgroundSeparatorNode.isLayerBacked = true
self.navigateToLatestButton = ChatHistoryNavigationButtonNode() self.navigateToLatestButton = ChatHistoryNavigationButtonNode()
self.navigateToLatestButton.alpha = 0.0 self.navigateToLatestButton.alpha = 0.0
@@ -97,10 +109,11 @@ class ChatControllerNode: ASDisplayNode {
self.addSubnode(self.listView) self.addSubnode(self.listView)
self.addSubnode(self.inputPanelBackgroundNode) self.addSubnode(self.inputPanelBackgroundNode)
self.addSubnode(self.inputPanelBackgroundSeparatorNode)
self.addSubnode(self.navigateToLatestButton) self.addSubnode(self.navigateToLatestButton)
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.listView.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.textInputPanelNode = ChatTextInputPanelNode() self.textInputPanelNode = ChatTextInputPanelNode()
self.textInputPanelNode?.updateHeight = { [weak self] in self.textInputPanelNode?.updateHeight = { [weak self] in
@@ -119,12 +132,12 @@ class ChatControllerNode: ASDisplayNode {
if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode {
strongSelf.ignoreUpdateHeight = true strongSelf.ignoreUpdateHeight = true
textInputPanelNode.text = "" textInputPanelNode.text = ""
strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), false) strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil) })
strongSelf.ignoreUpdateHeight = false strongSelf.ignoreUpdateHeight = false
} }
}) })
let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatInterfaceState.replyMessageId).start() let _ = enqueueMessage(account: strongSelf.account, peerId: strongSelf.peerId, text: text, replyMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId).start()
} }
} }
@@ -171,10 +184,11 @@ class ChatControllerNode: ASDisplayNode {
var dismissedInputPanelNode: ASDisplayNode? var dismissedInputPanelNode: ASDisplayNode?
var dismissedAccessoryPanelNode: ASDisplayNode? var dismissedAccessoryPanelNode: ASDisplayNode?
var dismissedInputContextPanelNode: ChatInputContextPanelNode?
var inputPanelSize: CGSize? var inputPanelSize: CGSize?
var immediatelyLayoutInputPanelAndAnimateAppearance = false var immediatelyLayoutInputPanelAndAnimateAppearance = false
if let inputPanelNode = inputPanelForChatIntefaceState(self.chatInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) { if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) {
inputPanelSize = inputPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) inputPanelSize = inputPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height))
if inputPanelNode !== self.inputPanelNode { if inputPanelNode !== self.inputPanelNode {
@@ -193,7 +207,7 @@ class ChatControllerNode: ASDisplayNode {
var accessoryPanelSize: CGSize? var accessoryPanelSize: CGSize?
var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false
if let accessoryPanelNode = accessoryPanelForChatIntefaceState(self.chatInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) {
accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height))
if accessoryPanelNode !== self.accessoryPanelNode { if accessoryPanelNode !== self.accessoryPanelNode {
@@ -208,7 +222,7 @@ class ChatControllerNode: ASDisplayNode {
accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in
if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode {
strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), true) strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) })
} }
} }
@@ -220,6 +234,21 @@ class ChatControllerNode: ASDisplayNode {
self.accessoryPanelNode = nil self.accessoryPanelNode = nil
} }
var immediatelyLayoutInputContextPanelAndAnimateAppearance = false
if let inputContextPanelNode = inputContextPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputContextPanelNode, interfaceInteraction: self.interfaceInteraction) {
if inputContextPanelNode !== self.inputContextPanelNode {
dismissedInputContextPanelNode = self.inputContextPanelNode
self.inputContextPanelNode = inputContextPanelNode
self.insertSubnode(inputContextPanelNode, aboveSubnode: self.navigateToLatestButton)
immediatelyLayoutInputContextPanelAndAnimateAppearance = true
}
} else if let inputContextPanelNode = self.inputContextPanelNode {
dismissedInputContextPanelNode = inputContextPanelNode
self.inputContextPanelNode = nil
}
var inputPanelsHeight: CGFloat = 0.0 var inputPanelsHeight: CGFloat = 0.0
var inputPanelFrame: CGRect? var inputPanelFrame: CGRect?
@@ -244,6 +273,7 @@ class ChatControllerNode: ASDisplayNode {
let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize) let navigateToLatestButtonFrame = CGRect(origin: CGPoint(x: layout.size.width - navigateToLatestButtonSize.width - 6.0, y: layout.size.height - insets.bottom - inputPanelsHeight - navigateToLatestButtonSize.height - 6.0), size: navigateToLatestButtonSize)
transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame) transition.updateFrame(node: self.inputPanelBackgroundNode, frame: inputBackgroundFrame)
transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: inputBackgroundFrame.origin, size: CGSize(width: inputBackgroundFrame.size.width, height: UIScreenPixel)))
transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame) transition.updateFrame(node: self.navigateToLatestButton, frame: navigateToLatestButtonFrame)
if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) { if let inputPanelNode = self.inputPanelNode, let inputPanelFrame = inputPanelFrame, !inputPanelNode.frame.equalTo(inputPanelFrame) {
@@ -267,6 +297,19 @@ class ChatControllerNode: ASDisplayNode {
transition.updateAlpha(node: accessoryPanelNode, alpha: 1.0) transition.updateAlpha(node: accessoryPanelNode, alpha: 1.0)
} }
let inputContextPanelsFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - insets.bottom - inputPanelsHeight - insets.top)))
if let inputContextPanelNode = self.inputContextPanelNode {
if immediatelyLayoutInputContextPanelAndAnimateAppearance {
inputContextPanelNode.frame = inputContextPanelsFrame
inputContextPanelNode.updateFrames(transition: .immediate)
inputContextPanelNode.animateIn()
} else if !inputContextPanelNode.frame.equalTo(inputContextPanelsFrame) {
transition.updateFrame(node: inputContextPanelNode, frame: inputContextPanelsFrame)
inputContextPanelNode.updateFrames(transition: transition)
}
}
if let dismissedInputPanelNode = dismissedInputPanelNode { if let dismissedInputPanelNode = dismissedInputPanelNode {
var frameCompleted = false var frameCompleted = false
var alphaCompleted = false var alphaCompleted = false
@@ -312,11 +355,43 @@ class ChatControllerNode: ASDisplayNode {
completed() completed()
}) })
} }
if let dismissedInputContextPanelNode = dismissedInputContextPanelNode {
var frameCompleted = false
var animationCompleted = false
var completed = { [weak dismissedInputContextPanelNode] in
if let dismissedInputContextPanelNode = dismissedInputContextPanelNode, frameCompleted, animationCompleted {
dismissedInputContextPanelNode.removeFromSupernode()
}
}
if !dismissedInputContextPanelNode.frame.equalTo(inputContextPanelsFrame) {
transition.updateFrame(node: dismissedInputContextPanelNode, frame: inputContextPanelsFrame, completion: { _ in
frameCompleted = true
completed()
})
} else {
frameCompleted = true
}
dismissedInputContextPanelNode.animateOut(completion: {
animationCompleted = true
completed()
})
}
} }
func updateChatInterfaceState(_ chatInterfaceState: ChatInterfaceState, animated: Bool) { func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool) {
if self.chatInterfaceState != chatInterfaceState { if let textInputPanelNode = self.textInputPanelNode {
self.chatInterfaceState = chatInterfaceState self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedInputState(textInputPanelNode.inputTextState) }
}
if self.chatPresentationInterfaceState != chatPresentationInterfaceState {
var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.inputState != chatPresentationInterfaceState.interfaceState.inputState
self.chatPresentationInterfaceState = chatPresentationInterfaceState
if let textInputPanelNode = self.textInputPanelNode, updateInputTextState {
textInputPanelNode.inputTextState = chatPresentationInterfaceState.interfaceState.inputState
}
if !self.ignoreUpdateHeight { if !self.ignoreUpdateHeight {
self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate) self.requestLayout(animated ? .animated(duration: 0.4, curve: .spring) : .immediate)

View File

@@ -0,0 +1,18 @@
import Foundation
import AsyncDisplayKit
import Display
class ChatInputContextPanelNode: ASDisplayNode {
var interfaceInteraction: ChatPanelInterfaceInteraction?
func updateFrames(transition: ContainedViewLayoutTransition) {
}
func animateIn() {
}
func animateOut(completion: @escaping () -> Void) {
completion()
}
}

View File

@@ -1,9 +1,13 @@
import Foundation import Foundation
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore
class ChatInputPanelNode: ASDisplayNode { class ChatInputPanelNode: ASDisplayNode {
var account: Account?
var interfaceInteraction: ChatPanelInterfaceInteraction? var interfaceInteraction: ChatPanelInterfaceInteraction?
var peer: Peer?
func updateFrames(transition: ContainedViewLayoutTransition) { func updateFrames(transition: ContainedViewLayoutTransition) {
} }

View File

@@ -0,0 +1,30 @@
import Foundation
import TelegramCore
func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputContextPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputContextPanelNode? {
guard let inputContext = chatPresentationInterfaceState.inputContext, let peer = chatPresentationInterfaceState.peer else {
return nil
}
switch inputContext {
case .hashtag:
if let currentPanel = currentPanel as? HashtagChatInputContextPanelNode {
return currentPanel
} else {
let panel = HashtagChatInputContextPanelNode()
panel.interfaceInteraction = interfaceInteraction
return panel
}
case .mention:
if let currentPanel = currentPanel as? MentionChatInputContextPanelNode {
return currentPanel
} else {
let panel = MentionChatInputContextPanelNode()
panel.interfaceInteraction = interfaceInteraction
panel.setup(account: account, peerId: peer.id, query: "")
return panel
}
}
return nil
}

View File

@@ -0,0 +1,12 @@
import Foundation
import TelegramCore
import Postbox
func inputContextForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account) -> ChatPresentationInputContext? {
if chatPresentationInterfaceState.interfaceState.inputState.inputText == "#" {
return .hashtag
} else if chatPresentationInterfaceState.interfaceState.inputState.inputText == "@" {
return .mention
}
return nil
}

View File

@@ -9,29 +9,52 @@ struct ChatInterfaceSelectionState: Equatable {
} }
} }
struct ChatTextInputState: Equatable {
let inputText: String
let selectionRange: Range<Int>
static func ==(lhs: ChatTextInputState, rhs: ChatTextInputState) -> Bool {
return lhs.inputText == rhs.inputText && lhs.selectionRange == rhs.selectionRange
}
init() {
self.inputText = ""
self.selectionRange = 0 ..< 0
}
init(inputText: String, selectionRange: Range<Int>) {
self.inputText = inputText
self.selectionRange = selectionRange
}
}
final class ChatInterfaceState: Equatable { final class ChatInterfaceState: Equatable {
let inputText: String? let inputState: ChatTextInputState
let replyMessageId: MessageId? let replyMessageId: MessageId?
let selectionState: ChatInterfaceSelectionState? let selectionState: ChatInterfaceSelectionState?
init() { init() {
self.inputText = nil self.inputState = ChatTextInputState()
self.replyMessageId = nil self.replyMessageId = nil
self.selectionState = nil self.selectionState = nil
} }
init(inputText: String?, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) { init(inputState: ChatTextInputState, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) {
self.inputText = inputText self.inputState = inputState
self.replyMessageId = replyMessageId self.replyMessageId = replyMessageId
self.selectionState = selectionState self.selectionState = selectionState
} }
static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool { static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool {
return lhs.inputText == rhs.inputText && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState return lhs.inputState == rhs.inputState && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState
}
func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
return ChatInterfaceState(inputState: inputState, replyMessageId: self.replyMessageId, selectionState: self.selectionState)
} }
func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState { func withUpdatedReplyMessageId(_ replyMessageId: MessageId?) -> ChatInterfaceState {
return ChatInterfaceState(inputText: self.inputText, replyMessageId: replyMessageId, selectionState: self.selectionState) return ChatInterfaceState(inputState: self.inputState, replyMessageId: replyMessageId, selectionState: self.selectionState)
} }
func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { func withUpdatedSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState {
@@ -40,7 +63,7 @@ final class ChatInterfaceState: Equatable {
selectedIds.formUnion(selectionState.selectedIds) selectedIds.formUnion(selectionState.selectedIds)
} }
selectedIds.insert(messageId) selectedIds.insert(messageId)
return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds))
} }
func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState { func withToggledSelectedMessage(_ messageId: MessageId) -> ChatInterfaceState {
@@ -53,10 +76,10 @@ final class ChatInterfaceState: Equatable {
} else { } else {
selectedIds.insert(messageId) selectedIds.insert(messageId)
} }
return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds)) return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds))
} }
func withoutSelectionState() -> ChatInterfaceState { func withoutSelectionState() -> ChatInterfaceState {
return ChatInterfaceState(inputText: self.inputText, replyMessageId: self.replyMessageId, selectionState: nil) return ChatInterfaceState(inputState: self.inputState, replyMessageId: self.replyMessageId, selectionState: nil)
} }
} }

View File

@@ -2,12 +2,12 @@ import Foundation
import AsyncDisplayKit import AsyncDisplayKit
import TelegramCore import TelegramCore
func accessoryPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? { func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? {
if let _ = chatInterfaceState.selectionState { if let _ = chatPresentationInterfaceState.interfaceState.selectionState {
return nil return nil
} }
if let replyMessageId = chatInterfaceState.replyMessageId { if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId {
if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId { if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId {
replyPanelNode.interfaceInteraction = interfaceInteraction replyPanelNode.interfaceInteraction = interfaceInteraction
return replyPanelNode return replyPanelNode

View File

@@ -0,0 +1,48 @@
import Foundation
import Postbox
import TelegramCore
import Display
import UIKit
func contextMenuForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, message: Message, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ContextMenuController? {
guard let peer = chatPresentationInterfaceState.peer, let interfaceInteraction = interfaceInteraction else {
return nil
}
var actions: [ContextMenuAction] = []
var canReply = false
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
switch channel.role {
case .creator, .editor, .moderator:
canReply = true
case .member:
canReply = false
}
} else {
canReply = true
}
if canReply {
actions.append(ContextMenuAction(content: .text("Reply"), action: {
interfaceInteraction.setupReplyMessage(message.id)
}))
}
actions.append(ContextMenuAction(content: .text("Copy"), action: {
if !message.text.isEmpty {
UIPasteboard.general.string = message.text
}
}))
actions.append(ContextMenuAction(content: .text("More..."), action: {
interfaceInteraction.beginMessageSelection(message.id)
}))
if !actions.isEmpty {
let contextMenuController = ContextMenuController(actions: actions)
return contextMenuController
} else {
return nil
}
}

View File

@@ -2,31 +2,78 @@ import Foundation
import AsyncDisplayKit import AsyncDisplayKit
import TelegramCore import TelegramCore
func inputPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? { func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? {
if let selectionState = chatInterfaceState.selectionState { if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState {
if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode { if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode {
currentPanel.selectedMessageCount = selectionState.selectedIds.count currentPanel.selectedMessageCount = selectionState.selectedIds.count
currentPanel.interfaceInteraction = interfaceInteraction currentPanel.interfaceInteraction = interfaceInteraction
currentPanel.peer = chatPresentationInterfaceState.peer
return currentPanel return currentPanel
} else { } else {
let panel = ChatMessageSelectionInputPanelNode() let panel = ChatMessageSelectionInputPanelNode()
panel.account = account
panel.peer = chatPresentationInterfaceState.peer
panel.selectedMessageCount = selectionState.selectedIds.count panel.selectedMessageCount = selectionState.selectedIds.count
panel.interfaceInteraction = interfaceInteraction panel.interfaceInteraction = interfaceInteraction
return panel return panel
} }
} else { } else {
if let currentPanel = currentPanel as? ChatTextInputPanelNode { if let peer = chatPresentationInterfaceState.peer {
currentPanel.interfaceInteraction = interfaceInteraction if let channel = peer as? TelegramChannel {
return currentPanel switch channel.info {
} else { case .broadcast:
if let textInputPanelNode = textInputPanelNode { switch channel.role {
textInputPanelNode.interfaceInteraction = interfaceInteraction case .creator, .editor, .moderator:
return textInputPanelNode break
} else { case .member:
let panel = ChatTextInputPanelNode() if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
panel.interfaceInteraction = interfaceInteraction currentPanel.peer = peer
return panel return currentPanel
} else {
let panel = ChatChannelSubscriberInputPanelNode()
panel.account = account
panel.peer = peer
return panel
}
}
case .group:
switch channel.participationStatus {
case .kicked, .left:
if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
currentPanel.peer = peer
return currentPanel
} else {
let panel = ChatChannelSubscriberInputPanelNode()
panel.account = account
panel.peer = peer
return panel
}
case .member:
break
}
}
} }
if let currentPanel = currentPanel as? ChatTextInputPanelNode {
currentPanel.interfaceInteraction = interfaceInteraction
currentPanel.peer = peer
return currentPanel
} else {
if let textInputPanelNode = textInputPanelNode {
textInputPanelNode.interfaceInteraction = interfaceInteraction
textInputPanelNode.account = account
textInputPanelNode.peer = peer
return textInputPanelNode
} else {
let panel = ChatTextInputPanelNode()
panel.interfaceInteraction = interfaceInteraction
panel.account = account
panel.peer = peer
return panel
}
}
} else {
return nil
} }
} }
} }

View File

@@ -108,7 +108,7 @@ class ChatListItemNode: ListViewItemNode {
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
let avatarNode: ChatListAvatarNode let avatarNode: AvatarNode
let contentNode: ASDisplayNode let contentNode: ASDisplayNode
let titleNode: TextNode let titleNode: TextNode
let textNode: TextNode let textNode: TextNode
@@ -121,7 +121,7 @@ class ChatListItemNode: ListViewItemNode {
var relativePosition: (first: Bool, last: Bool) = (false, false) var relativePosition: (first: Bool, last: Bool) = (false, false)
required init() { required init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(24.0)) self.avatarNode = AvatarNode(font: Font.regular(24.0))
self.avatarNode.isLayerBacked = true self.avatarNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode = ASDisplayNode()

View File

@@ -35,10 +35,10 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem {
} }
final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode { final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
let avatarNode: ChatListAvatarNode let avatarNode: AvatarNode
override init() { override init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(14.0)) self.avatarNode = AvatarNode(font: Font.regular(14.0))
self.avatarNode.isLayerBacked = true self.avatarNode.isLayerBacked = true
self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0)) self.avatarNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 38.0, height: 38.0))

View File

@@ -234,7 +234,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
return { item, width, mergedTop, mergedBottom in return { item, width, mergedTop, mergedBottom in
let message = item.message let message = item.message
let incoming = item.account.peerId != message.author?.id let incoming = item.message.effectivelyIncoming
let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil let displayAuthorInfo = !mergedTop && incoming && item.peerId.isGroupOrChannel && item.message.author != nil
let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0 let avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0
@@ -718,7 +719,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
var incoming = true var incoming = true
if let item = self.item { if let item = self.item {
selected = selectionState.selectedIds.contains(item.message.id) selected = selectionState.selectedIds.contains(item.message.id)
incoming = item.message.flags.contains(.Incoming) incoming = item.message.effectivelyIncoming
} }
let offset: CGFloat = incoming ? 42.0 : 0.0 let offset: CGFloat = incoming ? 42.0 : 0.0

View File

@@ -51,7 +51,7 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible {
self.message = message self.message = message
var accessoryItem: ListViewAccessoryItem? var accessoryItem: ListViewAccessoryItem?
let incoming = account.peerId != message.author?.id let incoming = message.effectivelyIncoming
let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel
if displayAuthorInfo { if displayAuthorInfo {

View File

@@ -40,7 +40,7 @@ struct ChatMessageItemLayoutConstants {
self.bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 4.0, defaultSpacing: 2.5, mergedSpacing: 0.0, maximumWidthFillFactor: 0.9, minimumSize: CGSize(width: 40.0, height: 33.0), contentInsets: UIEdgeInsets(top: 1.0, left: 6.0, bottom: 1.0, right: 1.0)) self.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.text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 5.0, left: 9.0, bottom: 4.0, right: 9.0))
self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0), defaultCornerRadius: 15.0, mergedCornerRadius: 4.0, contentMergedCornerRadius: 2.0) self.image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0), defaultCornerRadius: 15.0, mergedCornerRadius: 4.0, contentMergedCornerRadius: 5.0)
self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0)) self.file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
} }
} }

View File

@@ -1,11 +1,40 @@
import Foundation import Foundation
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
import Postbox
import TelegramCore
final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
private let deleteButton: UIButton private let deleteButton: UIButton
private let forwardButton: UIButton private let forwardButton: UIButton
override var peer: Peer? {
didSet {
var canDelete = false
if let channel = self.peer as? TelegramChannel {
switch channel.info {
case .broadcast:
switch channel.role {
case .creator, .editor, .moderator:
canDelete = true
case .member:
canDelete = false
}
case .group:
switch channel.role {
case .creator, .editor, .moderator:
canDelete = true
case .member:
canDelete = false
}
}
} else {
canDelete = true
}
self.deleteButton.isHidden = !canDelete
}
}
var selectedMessageCount: Int = 0 { var selectedMessageCount: Int = 0 {
didSet { didSet {
self.deleteButton.isEnabled = self.selectedMessageCount != 0 self.deleteButton.isEnabled = self.selectedMessageCount != 0

View File

@@ -56,7 +56,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
let imageLayout = self.imageNode.asyncLayout() let imageLayout = self.imageNode.asyncLayout()
return { item, width, mergedTop, mergedBottom in return { item, width, mergedTop, mergedBottom in
let incoming = item.account.peerId != item.message.author?.id let incoming = item.message.effectivelyIncoming
var imageSize: CGSize = CGSize(width: 100.0, height: 100.0) var imageSize: CGSize = CGSize(width: 100.0, height: 100.0)
if let telegramFile = telegramFile { if let telegramFile = telegramFile {
if let thumbnailSize = telegramFile.previewRepresentations.first?.dimensions { if let thumbnailSize = telegramFile.previewRepresentations.first?.dimensions {

View File

@@ -35,7 +35,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
let message = item.message let message = item.message
let incoming = item.account.peerId != message.author?.id let incoming = item.message.effectivelyIncoming
let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right let horizontalInset = layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height) let textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)

View File

@@ -1,11 +1,18 @@
import Foundation import Foundation
import Postbox
final class ChatPanelInterfaceInteraction { final class ChatPanelInterfaceInteraction {
let setupReplyMessage: (MessageId) -> Void
let beginMessageSelection: (MessageId) -> Void
let deleteSelectedMessages: () -> Void let deleteSelectedMessages: () -> Void
let forwardSelectedMessages: () -> Void let forwardSelectedMessages: () -> Void
let updateTextInputState: (ChatTextInputState) -> Void
init(deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void) { init(setupReplyMessage: @escaping (MessageId) -> Void, beginMessageSelection: @escaping (MessageId) -> Void, deleteSelectedMessages: @escaping () -> Void, forwardSelectedMessages: @escaping () -> Void, updateTextInputState: @escaping (ChatTextInputState) -> Void) {
self.setupReplyMessage = setupReplyMessage
self.beginMessageSelection = beginMessageSelection
self.deleteSelectedMessages = deleteSelectedMessages self.deleteSelectedMessages = deleteSelectedMessages
self.forwardSelectedMessages = forwardSelectedMessages self.forwardSelectedMessages = forwardSelectedMessages
self.updateTextInputState = updateTextInputState
} }
} }

View File

@@ -1,7 +1,56 @@
import Foundation import Foundation
import Postbox import Postbox
struct ChatPresentationInterfaceState { enum ChatPresentationInputContext {
case hashtag
case mention
}
struct ChatPresentationInterfaceState: Equatable {
let interfaceState: ChatInterfaceState let interfaceState: ChatInterfaceState
let peer: Peer? let peer: Peer?
let inputContext: ChatPresentationInputContext?
init() {
self.interfaceState = ChatInterfaceState()
self.peer = nil
self.inputContext = nil
}
init(interfaceState: ChatInterfaceState, peer: Peer?, inputContext: ChatPresentationInputContext?) {
self.interfaceState = interfaceState
self.peer = peer
self.inputContext = inputContext
}
static func ==(lhs: ChatPresentationInterfaceState, rhs: ChatPresentationInterfaceState) -> Bool {
if lhs.interfaceState != rhs.interfaceState {
return false
}
if let lhsPeer = lhs.peer, let rhsPeer = rhs.peer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhs.peer == nil) != (rhs.peer == nil) {
return false
}
if lhs.inputContext != rhs.inputContext {
return false
}
return true
}
func updatedInterfaceState(_ f: (ChatInterfaceState) -> ChatInterfaceState) -> ChatPresentationInterfaceState {
return ChatPresentationInterfaceState(interfaceState: f(self.interfaceState), peer: self.peer, inputContext: self.inputContext)
}
func updatedPeer(_ f: (Peer?) -> Peer?) -> ChatPresentationInterfaceState {
return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: f(self.peer), inputContext: self.inputContext)
}
func updatedInputContext(_ f: (ChatPresentationInputContext?) -> ChatPresentationInputContext?) -> ChatPresentationInterfaceState {
return ChatPresentationInterfaceState(interfaceState: self.interfaceState, peer: self.peer, inputContext: f(self.inputContext))
}
} }

View File

@@ -2,7 +2,8 @@ import Foundation
import UIKit import UIKit
import Display import Display
import AsyncDisplayKit import AsyncDisplayKit
import WebKit import Postbox
import TelegramCore
private let textInputViewBackground: UIImage = { private let textInputViewBackground: UIImage = {
let diameter: CGFloat = 10.0 let diameter: CGFloat = 10.0
@@ -36,6 +37,48 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
var sendMessage: () -> Void = { } var sendMessage: () -> Void = { }
var updateHeight: () -> Void = { } var updateHeight: () -> Void = { }
private var updatingInputState = false
private var currentPlaceholder: String?
override var peer: Peer? {
didSet {
if let peer = self.peer, oldValue == nil || !peer.isEqual(oldValue!) {
let placeholder: String
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
placeholder = "Broadcast"
} else {
placeholder = "Message"
}
if self.currentPlaceholder != placeholder {
self.currentPlaceholder = placeholder
let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode)
let (placeholderSize, placeholderApply) = placeholderLayout(NSAttributedString(string: placeholder, font: Font.regular(16.0), textColor: UIColor(0xbebec0)), nil, 1, .end, CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), nil)
self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size)
let _ = placeholderApply()
}
}
}
}
var inputTextState: ChatTextInputState {
get {
if let textInputNode = self.textInputNode {
let text = textInputNode.attributedText?.string ?? ""
let selectionRange: Range<Int> = textInputNode.selectedRange.location ..< (textInputNode.selectedRange.location + textInputNode.selectedRange.length)
return ChatTextInputState(inputText: text, selectionRange: selectionRange)
} else {
return ChatTextInputState()
}
} set(value) {
if let textInputNode = self.textInputNode {
self.updatingInputState = true
textInputNode.attributedText = NSAttributedString(string: value.inputText, font: Font.regular(16.0), textColor: UIColor.black)
textInputNode.selectedRange = NSMakeRange(value.selectionRange.lowerBound, value.selectionRange.count)
self.updatingInputState = false
}
}
}
var text: String { var text: String {
get { get {
return self.textInputNode?.attributedText?.string ?? "" return self.textInputNode?.attributedText?.string ?? ""
@@ -81,7 +124,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.view.addSubview(self.sendButton) self.view.addSubview(self.sendButton)
self.textInputBackgroundView.clipsToBounds = true self.textInputBackgroundView.clipsToBounds = true
self.textInputBackgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))) let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
strongSelf.ensureFocused()
}
}
self.textInputBackgroundView.addGestureRecognizer(recognizer)
self.textInputBackgroundView.isUserInteractionEnabled = true self.textInputBackgroundView.isUserInteractionEnabled = true
} }
@@ -94,6 +143,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)] textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)]
textInputNode.clipsToBounds = true textInputNode.clipsToBounds = true
textInputNode.delegate = self textInputNode.delegate = self
textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.addSubnode(textInputNode) self.addSubnode(textInputNode)
self.textInputNode = textInputNode self.textInputNode = textInputNode
@@ -103,6 +153,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.textInputBackgroundView.isUserInteractionEnabled = false self.textInputBackgroundView.isUserInteractionEnabled = false
self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0]) self.textInputBackgroundView.removeGestureRecognizer(self.textInputBackgroundView.gestureRecognizers![0])
let recognizer = TouchDownGestureRecognizer(target: self, action: #selector(self.textInputBackgroundViewTap(_:)))
recognizer.touchDown = { [weak self] in
if let strongSelf = self {
strongSelf.ensureFocused()
}
}
textInputNode.view.addGestureRecognizer(recognizer)
} }
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize { override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
@@ -155,6 +213,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.invalidateCalculatedLayout() self.invalidateCalculatedLayout()
self.updateHeight() self.updateHeight()
} }
self.interfaceInteraction?.updateTextInputState(self.inputTextState)
}
}
@objc func editableTextNodeDidChangeSelection(_ editableTextNode: ASEditableTextNode, fromSelectedRange: NSRange, toSelectedRange: NSRange, dueToEditing: Bool) {
if !dueToEditing && !updatingInputState {
self.interfaceInteraction?.updateTextInputState(self.inputTextState)
} }
} }

View File

@@ -123,7 +123,7 @@ class ContactsPeerItemNode: ListViewItemNode {
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private let avatarNode: ChatListAvatarNode private let avatarNode: AvatarNode
private let titleNode: TextNode private let titleNode: TextNode
private let statusNode: TextNode private let statusNode: TextNode
@@ -140,7 +140,7 @@ class ContactsPeerItemNode: ListViewItemNode {
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
self.avatarNode = ChatListAvatarNode(font: Font.regular(15.0)) self.avatarNode = AvatarNode(font: Font.regular(15.0))
self.avatarNode.isLayerBacked = true self.avatarNode.isLayerBacked = true
self.titleNode = TextNode() self.titleNode = TextNode()

View File

@@ -65,7 +65,7 @@ class ContactsVCardItemNode: ListViewItemNode {
private let separatorNode: ASDisplayNode private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private let avatarNode: ChatListAvatarNode private let avatarNode: AvatarNode
private let titleNode: TextNode private let titleNode: TextNode
private let statusNode: TextNode private let statusNode: TextNode
@@ -82,7 +82,7 @@ class ContactsVCardItemNode: ListViewItemNode {
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9) self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
self.highlightedBackgroundNode.isLayerBacked = true self.highlightedBackgroundNode.isLayerBacked = true
self.avatarNode = ChatListAvatarNode(font: Font.regular(15.0)) self.avatarNode = AvatarNode(font: Font.regular(15.0))
self.avatarNode.isLayerBacked = true self.avatarNode.isLayerBacked = true
self.titleNode = TextNode() self.titleNode = TextNode()

View File

@@ -0,0 +1,76 @@
import Foundation
import AsyncDisplayKit
import Postbox
import TelegramCore
import Display
final class HashtagChatInputContextPanelNode: ChatInputContextPanelNode, UITableViewDelegate, UITableViewDataSource {
private let tableView: UITableView
private let tableBackgroundView: UIView
private var results: [String] = []
override init() {
self.tableView = UITableView(frame: CGRect(), style: .plain)
self.tableBackgroundView = UIView()
self.tableBackgroundView.backgroundColor = UIColor.white
super.init()
self.clipsToBounds = true
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.rowHeight = 42.0
self.tableView.showsVerticalScrollIndicator = false
self.tableView.backgroundColor = nil
self.tableView.isOpaque = false
self.view.addSubview(self.tableBackgroundView)
self.view.addSubview(self.tableView)
self.results = (0 ..< 50).map { "#tag \($0)" }
}
func setup(account: Account, peerId: PeerId, query: String) {
}
private func updateTable() {
let itemsHeight = CGFloat(self.results.count) * self.tableView.rowHeight
let minimalDisplayedItemsHeight = floor(self.tableView.rowHeight * 3.5)
let topInset = max(0.0, self.bounds.size.height - min(itemsHeight, minimalDisplayedItemsHeight))
self.tableView.contentInset = UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0)
self.tableView.contentOffset = CGPoint(x: 0.0, y: -topInset)
self.tableView.setNeedsLayout()
}
override func updateFrames(transition: ContainedViewLayoutTransition) {
self.tableView.frame = self.bounds
self.updateTable()
}
override func animateIn() {
self.layer.animateBounds(from: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), to: self.layer.bounds, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring)
}
override func animateOut(completion: @escaping () -> Void) {
self.layer.animateBounds(from: self.layer.bounds, to: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { _ in
completion()
})
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.results.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = (tableView.dequeueReusableCell(withIdentifier: "C") as? HashtagsTableCell) ?? HashtagsTableCell()
cell.textLabel?.text = self.results[indexPath.row]
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.tableBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -self.tableView.contentOffset.y)), size: self.bounds.size)
}
}

View File

@@ -0,0 +1,12 @@
import Foundation
import UIKit
final class HashtagsTableCell: UITableViewCell {
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

View File

@@ -34,13 +34,13 @@ final class HorizontalPeerItem: ListViewItem {
} }
private final class HorizontalPeerItemNode: ListViewItemNode { private final class HorizontalPeerItemNode: ListViewItemNode {
private let avatarNode: ChatListAvatarNode private let avatarNode: AvatarNode
private let titleNode: ASTextNode private let titleNode: ASTextNode
private var peer: Peer? private var peer: Peer?
fileprivate var action: ((PeerId) -> Void)? fileprivate var action: ((PeerId) -> Void)?
init() { init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(14.0)) self.avatarNode = AvatarNode(font: Font.regular(14.0))
//self.avatarNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0) //self.avatarNode.transform = CATransform3DMakeRotation(CGFloat(M_PI / 2.0), 0.0, 0.0, 1.0)
self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((92.0 - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0)) self.avatarNode.frame = CGRect(origin: CGPoint(x: floor((92.0 - 60.0) / 2.0), y: 4.0), size: CGSize(width: 60.0, height: 60.0))

View File

@@ -1,39 +0,0 @@
import TelegramCore
func smallestImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? {
if representations.count == 0 {
return nil
} else {
var dimensions = representations[0].dimensions
var index = 0
for i in 1 ..< representations.count {
let representationDimensions = representations[i].dimensions
if representationDimensions.width < dimensions.width && representationDimensions.height < dimensions.height {
dimensions = representationDimensions
index = i
}
}
return representations[index]
}
}
func largestImageRepresentation(_ representations: [TelegramMediaImageRepresentation]) -> TelegramMediaImageRepresentation? {
if representations.count == 0 {
return nil
} else {
var dimensions = representations[0].dimensions
var index = 0
for i in 1 ..< representations.count {
let representationDimensions = representations[i].dimensions
if representationDimensions.width > dimensions.width && representationDimensions.height > dimensions.height {
dimensions = representationDimensions
index = i
}
}
return representations[index]
}
}

View File

@@ -0,0 +1,107 @@
import Foundation
import AsyncDisplayKit
import Postbox
import TelegramCore
import Display
import SwiftSignalKit
final class MentionChatInputContextPanelNode: ChatInputContextPanelNode, UITableViewDelegate, UITableViewDataSource {
private let tableView: UITableView
private let tableBackgroundView: UIView
private var account: Account?
private var results: [Peer] = []
private let disposable = MetaDisposable()
override init() {
self.tableView = UITableView(frame: CGRect(), style: .plain)
self.tableBackgroundView = UIView()
self.tableBackgroundView.backgroundColor = UIColor.white
super.init()
self.clipsToBounds = true
self.tableView.dataSource = self
self.tableView.delegate = self
self.tableView.rowHeight = 42.0
self.tableView.showsVerticalScrollIndicator = false
self.tableView.backgroundColor = nil
self.tableView.isOpaque = false
self.tableView.separatorInset = UIEdgeInsets(top: 0.0, left: 61.0, bottom: 0.0, right: 0.0)
self.view.addSubview(self.tableBackgroundView)
self.view.addSubview(self.tableView)
}
deinit {
self.disposable.dispose()
}
func setup(account: Account, peerId: PeerId, query: String) {
self.account = account
let signal = peerParticipants(account: account, id: peerId)
|> deliverOnMainQueue
self.disposable.set(signal.start(next: { [weak self] peers in
if let strongSelf = self {
strongSelf.results = peers
strongSelf.tableView.reloadData()
strongSelf.updateTable(animated: true)
}
}))
}
private func updateTable(animated: Bool = false) {
let itemsHeight = CGFloat(self.results.count) * self.tableView.rowHeight
let minimalDisplayedItemsHeight = floor(self.tableView.rowHeight * 3.5)
let topInset = max(0.0, self.bounds.size.height - min(itemsHeight, minimalDisplayedItemsHeight))
if animated {
self.layer.animateBounds(from: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), to: self.layer.bounds, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring)
}
self.tableView.contentInset = UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0)
self.tableView.contentOffset = CGPoint(x: 0.0, y: -topInset)
self.tableView.setNeedsLayout()
}
override func updateFrames(transition: ContainedViewLayoutTransition) {
self.tableView.frame = self.bounds
self.updateTable()
}
override func animateIn() {
self.layer.animateBounds(from: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), to: self.layer.bounds, duration: 0.45, timingFunction: kCAMediaTimingFunctionSpring)
}
override func animateOut(completion: @escaping () -> Void) {
self.layer.animateBounds(from: self.layer.bounds, to: self.layer.bounds.offsetBy(dx: 0.0, dy: -self.layer.bounds.size.height), duration: 0.25, timingFunction: kCAMediaTimingFunctionEaseOut, removeOnCompletion: false, completion: { _ in
completion()
})
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.results.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell = (tableView.dequeueReusableCell(withIdentifier: "C") as? MentionsTableCell) ?? MentionsTableCell()
if let account = self.account {
cell.setupPeer(account: account, peer: self.results[indexPath.row])
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.tableBackgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, -self.tableView.contentOffset.y)), size: self.bounds.size)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let addressName = self.results[indexPath.row].addressName {
let string = "@" + addressName + " "
self.interfaceInteraction?.updateTextInputState(ChatTextInputState(inputText: string, selectionRange: string.characters.count ..< string.characters.count))
}
}
}

View File

@@ -0,0 +1,45 @@
import Foundation
import UIKit
import Display
import Postbox
import TelegramCore
final class MentionsTableCell: UITableViewCell {
private let avatarNode = AvatarNode(font: Font.regular(16.0))
private let labelNode = TextNode()
private var peer: Peer?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.contentView.addSubnode(self.avatarNode)
self.contentView.addSubnode(self.labelNode)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupPeer(account: Account, peer: Peer) {
self.peer = peer
self.avatarNode.setPeer(account: account, peer: peer)
self.setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
self.avatarNode.frame = CGRect(origin: CGPoint(x: 16.0, y: floor((self.bounds.size.height - 30.0) / 2.0)), size: CGSize(width: 30.0, height: 30.0))
if let peer = self.peer {
let makeLayout = TextNode.asyncLayout(self.labelNode)
let string = NSMutableAttributedString()
string.append(NSAttributedString(string: peer.displayTitle, font: Font.medium(15.0), textColor: UIColor.black))
if let addressName = peer.addressName {
string.append(NSAttributedString(string: " @" + addressName, font: Font.regular(15.0), textColor: UIColor(0x9099a2)))
}
let (layout, apply) = makeLayout(string, nil, 1, .end, CGSize(width: self.bounds.size.width - 61.0 - 10.0, height: self.bounds.size.height), nil)
self.labelNode.frame = CGRect(origin: CGPoint(x: 61.0, y: floor((self.bounds.size.height - layout.size.height) / 2.0) + 2.0), size: layout.size)
let _ = apply()
}
}
}

View File

@@ -20,23 +20,7 @@ private let roundCorners = { () -> UIImage in
}() }()
func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal<UIImage, NoError>? { func peerAvatarImage(account: Account, peer: Peer, displayDimensions: CGSize = CGSize(width: 60.0, height: 60.0)) -> Signal<UIImage, NoError>? {
var location: TelegramCloudMediaLocation? if let location = peer.smallProfileImage?.location.cloudLocation {
if let user = peer as? TelegramUser {
if let photo = user.photo.first {
location = photo.location.cloudLocation
}
} else if let group = peer as? TelegramGroup {
if let photo = group.photo.first {
location = photo.location.cloudLocation
}
} else if let channel = peer as? TelegramChannel {
if let photo = channel.photo.first {
location = photo.location.cloudLocation
}
}
if let location = location {
return deferred { () -> Signal<UIImage, NoError> in return deferred { () -> Signal<UIImage, NoError> in
return cachedCloudFileLocation(location) return cachedCloudFileLocation(location)
|> `catch` { _ in |> `catch` { _ in

View File

@@ -412,10 +412,10 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr
if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage { if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage {
c.interpolationQuality = .low c.interpolationQuality = .low
c.draw(cgImage, in: fittedRect) c.draw(cgImage, in: fittedRect)
c.setBlendMode(.normal)
} }
if let fullSizeImage = fullSizeImage { if let fullSizeImage = fullSizeImage {
c.setBlendMode(.normal)
c.interpolationQuality = .medium c.interpolationQuality = .medium
c.draw(fullSizeImage, in: fittedRect) c.draw(fullSizeImage, in: fittedRect)
} }

View File

@@ -28,13 +28,13 @@ private let nameFont = Font.medium(19.0)
private let statusFont = Font.regular(15.0) private let statusFont = Font.regular(15.0)
class SettingsAccountInfoItemNode: ListControllerGroupableItemNode { class SettingsAccountInfoItemNode: ListControllerGroupableItemNode {
let avatarNode: ChatListAvatarNode let avatarNode: AvatarNode
let nameNode: TextNode let nameNode: TextNode
let statusNode: TextNode let statusNode: TextNode
override init() { override init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(20.0)) self.avatarNode = AvatarNode(font: Font.regular(20.0))
self.nameNode = TextNode() self.nameNode = TextNode()
self.nameNode.isLayerBacked = true self.nameNode.isLayerBacked = true

View File

@@ -55,7 +55,7 @@ public class SettingsController: ListController {
} }
peerAndConnectionStatusDisposable.set(peerAndConnectionStatus.start()) peerAndConnectionStatusDisposable.set(peerAndConnectionStatus.start())
peer.set(account.postbox.peerWithId(account.peerId)) peer.set(account.postbox.loadedPeerWithId(account.peerId))
connectionStatus.set(account.network.connectionStatus) connectionStatus.set(account.network.connectionStatus)
} }

View File

@@ -1,23 +1,8 @@
import Foundation import Foundation
import UIKit.UIGestureRecognizerSubclass import UIKit.UIGestureRecognizerSubclass
private class TouchDownGestureRecognizerTimerTarget: NSObject {
weak var target: TouchDownGestureRecognizer?
init(target: TouchDownGestureRecognizer) {
self.target = target
super.init()
}
@objc func event() {
self.target?.timerEvent()
}
}
class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate { class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelegate {
private var touchLocation = CGPoint() var touchDown: (() -> Void)?
private var timer: Foundation.Timer?
override init(target: Any?, action: Selector?) { override init(target: Any?, action: Selector?) {
super.init(target: target, action: action) super.init(target: target, action: action)
@@ -26,53 +11,14 @@ class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelega
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPanGestureRecognizer { return true
return true
}
return false
}
override func reset() {
self.timer?.invalidate()
self.timer = nil
super.reset()
}
func timerEvent() {
self.state = .began
} }
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) { override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event) super.touchesBegan(touches, with: event)
if let touch = touches.first { if let touchDown = self.touchDown {
self.touchLocation = touch.location(in: self.view) touchDown()
} }
self.timer?.invalidate()
self.timer = Timer(timeInterval: 0.08, target: TouchDownGestureRecognizerTimerTarget(target: self), selector: #selector(TouchDownGestureRecognizerTimerTarget.event), userInfo: nil, repeats: false)
if let timer = self.timer {
RunLoop.main.add(timer, forMode: RunLoopMode.commonModes)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
if let touch = touches.first {
let location = touch.location(in: self.view)
let distance = CGPoint(x: location.x - self.touchLocation.x, y: location.y - self.touchLocation.y)
if distance.x * distance.x + distance.y * distance.y > 4.0 {
self.state = .cancelled
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesEnded(touches, with: event)
self.state = .ended
} }
} }