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

View File

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

View File

@@ -5,7 +5,7 @@ import UIKit
import Display
import TelegramCore
private class ChatListAvatarNodeParameters: NSObject {
private class AvatarNodeParameters: NSObject {
let account: Account
let peerId: PeerId
let letters: [String]
@@ -30,28 +30,28 @@ let gradientColors: [NSArray] = [
[UIColor(0xd669ed).cgColor, UIColor(0xe0a2f3).cgColor]
]
private enum ChatListAvatarNodeState: Equatable {
private enum AvatarNodeState: Equatable {
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) {
case (.Empty, .Empty):
return true
case let (.PeerAvatar(lhsPeer), .PeerAvatar(rhsPeer)) where lhsPeer.isEqual(rhsPeer):
return true
case let (.PeerAvatar(lhsPeerId, lhsLetters, lhsPhotoRepresentations), .PeerAvatar(rhsPeerId, rhsLetters, rhsPhotoRepresentations)):
return lhsPeerId == rhsPeerId && lhsLetters == rhsLetters && lhsPhotoRepresentations == rhsPhotoRepresentations
default:
return false
}
}
public final class ChatListAvatarNode: ASDisplayNode {
public final class AvatarNode: ASDisplayNode {
var font: UIFont {
didSet {
if oldValue !== font {
if let parameters = self.parameters {
self.parameters = ChatListAvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font)
self.parameters = AvatarNodeParameters(account: parameters.account, peerId: parameters.peerId, letters: parameters.letters, font: self.font)
}
if !self.displaySuspended {
@@ -60,10 +60,10 @@ public final class ChatListAvatarNode: ASDisplayNode {
}
}
}
private var parameters: ChatListAvatarNodeParameters?
private var parameters: AvatarNodeParameters?
let imageNode: ImageNode
private var state: ChatListAvatarNodeState = .Empty
private var state: AvatarNodeState = .Empty
public init(font: UIFont) {
self.font = font
@@ -92,11 +92,11 @@ public final class ChatListAvatarNode: ASDisplayNode {
}
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 {
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.contents = nil
@@ -134,7 +134,7 @@ public final class ChatListAvatarNode: ASDisplayNode {
context.clip()
let colorIndex: Int
if let parameters = parameters as? ChatListAvatarNodeParameters {
if let parameters = parameters as? AvatarNodeParameters {
colorIndex = Int(parameters.account.peerId.id + parameters.peerId.id)
} else {
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())
//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)
if let parameters = parameters as? ChatListAvatarNodeParameters {
if let parameters = parameters as? AvatarNodeParameters {
let letters = parameters.letters
let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1]))
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 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 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.scaleBy(x: 1.0, y: -1.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)
CTLineDraw(line, context)
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)
final class ChatAvatarNavigationNode: ASDisplayNode {
let avatarNode: ChatListAvatarNode
let avatarNode: AvatarNode
override init() {
self.avatarNode = ChatListAvatarNode(font: normalFont)
self.avatarNode = AvatarNode(font: normalFont)
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 layoutActionOnViewTransition: (@escaping () -> Void)?
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
private var didSetReady = false
private let _historyReady = Promise<Bool>()
private var didSetHistoryReady = false
private let _peerReady = Promise<Bool>()
private var didSetPeerReady = false
private let maxVisibleIncomingMessageId = Promise<MessageId>()
private let canReadHistory = Promise<Bool>()
@@ -397,7 +396,7 @@ public class ChatController: ViewController {
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 var leftNavigationButton: ChatNavigationButton?
@@ -407,6 +406,7 @@ public class ChatController: ViewController {
private let galleryHiddenMesageAndMediaDisposable = MetaDisposable()
private var controllerInteraction: ChatControllerInteraction?
private var interfaceInteraction: ChatPanelInterfaceInteraction?
public init(account: Account, peerId: PeerId, messageId: MessageId? = nil) {
self.account = account
@@ -415,6 +415,8 @@ public class ChatController: ViewController {
super.init()
self.ready.set(combineLatest(self._historyReady.get(), self._peerReady.get()) |> map { $0 && $1 })
self.setupThemeWithDarkMode(useDarkMode)
self.scrollToTop = { [weak self] in
@@ -487,47 +489,21 @@ public class ChatController: ViewController {
}
}, openMessageContextMenu: { [weak self] id, node, frame in
if let strongSelf = self, let historyView = strongSelf.historyView {
let contextMenuController = ContextMenuController(actions: [
ContextMenuAction(content: .text("Reply"), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
strongSelf.updateChatInterfaceState(animated: true, { $0.withUpdatedReplyMessageId(message.id) })
strongSelf.chatDisplayNode.ensureInputViewFocused()
break
}
}
}),
ContextMenuAction(content: .text("Copy"), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if !message.text.isEmpty {
UIPasteboard.general.string = message.text
}
break
}
}
}),
ContextMenuAction(content: .text("More..."), action: { [weak strongSelf] in
if let strongSelf = strongSelf, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if strongSelf.presentationInterfaceState.interfaceState.selectionState != nil {
strongSelf.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() })
if let strongSelf = self, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == id {
if let contextMenuController = contextMenuForChatPresentationIntefaceState(strongSelf.presentationInterfaceState, account: strongSelf.account, message: message, interfaceInteraction: strongSelf.interfaceInteraction) {
strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in
if let node = node {
return (node, frame)
} else {
strongSelf.updateChatInterfaceState(animated: true, { $0.withUpdatedSelectedMessage(message.id) })
return nil
}
break
}
}))
}
})
])
strongSelf.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak strongSelf, weak node] in
if let node = node {
return (node, frame)
} else {
return nil
break
}
}))
}
}
}, navigateToMessage: { [weak self] fromId, id in
if let strongSelf = self, let historyView = strongSelf.historyView {
@@ -564,7 +540,7 @@ public class ChatController: ViewController {
}, toggleMessageSelection: { [weak self] messageId in
if let strongSelf = self, let historyView = strongSelf.historyView {
for case let .MessageEntry(message) in historyView.filteredEntries where message.id == messageId {
strongSelf.updateChatInterfaceState(animated: false, { $0.withToggledSelectedMessage(messageId) })
strongSelf.updateChatPresentationInterfaceState(animated: false, { $0.updatedInterfaceState { $0.withToggledSelectedMessage(messageId) } })
break
}
}
@@ -576,13 +552,20 @@ public class ChatController: ViewController {
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)
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
strongSelf.title = peer.displayTitle
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(account: strongSelf.account, peer: peer)
if let 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:
Queue.mainQueue().async { [weak self] in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
if !strongSelf.didSetHistoryReady {
strongSelf.didSetHistoryReady = true
strongSelf._historyReady.set(.single(true))
}
}
}
@@ -775,8 +758,8 @@ public class ChatController: ViewController {
self?.layoutActionOnViewTransition = f
}
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] state, animated in
self?.updateChatInterfaceState(animated: animated, { _ in return state })
self.chatDisplayNode.requestUpdateChatInterfaceState = { [weak self] animated, f in
self?.updateChatPresentationInterfaceState(animated: animated, { $0.updatedInterfaceState(f) })
}
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 messageIds = strongSelf.presentationInterfaceState.interfaceState.selectionState?.selectedIds, !messageIds.isEmpty {
@@ -812,15 +810,22 @@ public class ChatController: ViewController {
modifier.deleteMessages(Array(messageIds))
}).start()
}
strongSelf.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() })
strongSelf.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
}
}, forwardSelectedMessages: { [weak self] in
if let strongSelf = self {
let controller = ShareRecipientsActionSheetController()
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.dequeueHistoryViewTransition()
@@ -851,9 +856,9 @@ public class ChatController: ViewController {
if strongSelf.isNodeLoaded {
strongSelf.dequeueHistoryViewTransition()
} else {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
if !strongSelf.didSetHistoryReady {
strongSelf.didSetHistoryReady = true
strongSelf._historyReady.set(.single(true))
}
}
} else {
@@ -886,9 +891,9 @@ public class ChatController: ViewController {
}
}
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
if !strongSelf.didSetHistoryReady {
strongSelf.didSetHistoryReady = true
strongSelf._historyReady.set(.single(true))
}
completion()
@@ -944,16 +949,18 @@ public class ChatController: ViewController {
})
}
func updateChatInterfaceState(animated: Bool = true, _ f: (ChatInterfaceState) -> ChatInterfaceState) {
let updatedChatInterfaceState = f(self.presentationInterfaceState.interfaceState)
func updateChatPresentationInterfaceState(animated: Bool = true, _ f: (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) {
let temporaryChatPresentationInterfaceState = f(self.presentationInterfaceState)
let inputContext = inputContextForChatPresentationIntefaceState(temporaryChatPresentationInterfaceState, account: self.account)
let updatedChatPresentationInterfaceState = temporaryChatPresentationInterfaceState.updatedInputContext { _ in return inputContext }
if self.isNodeLoaded {
self.chatDisplayNode.updateChatInterfaceState(updatedChatInterfaceState, animated: animated)
self.chatDisplayNode.updateChatPresentationInterfaceState(updatedChatPresentationInterfaceState, animated: animated)
}
self.presentationInterfaceState = ChatPresentationInterfaceState(interfaceState: updatedChatInterfaceState, peer: nil)
self.chatInterfaceStatePromise.set(.single(updatedChatInterfaceState))
self.presentationInterfaceState = updatedChatPresentationInterfaceState
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.leftNavigationButton = button
} else if let _ = self.leftNavigationButton {
@@ -961,7 +968,7 @@ public class ChatController: ViewController {
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.rightNavigationButton = button
} else if let _ = self.rightNavigationButton {
@@ -970,9 +977,9 @@ public class ChatController: ViewController {
}
if let controllerInteraction = self.controllerInteraction {
if updatedChatInterfaceState.selectionState != controllerInteraction.selectionState {
let animated = controllerInteraction.selectionState == nil || updatedChatInterfaceState.selectionState == nil
controllerInteraction.selectionState = updatedChatInterfaceState.selectionState
if updatedChatPresentationInterfaceState.interfaceState.selectionState != controllerInteraction.selectionState {
let animated = controllerInteraction.selectionState == nil || updatedChatPresentationInterfaceState.interfaceState.selectionState == nil
controllerInteraction.selectionState = updatedChatPresentationInterfaceState.interfaceState.selectionState
self.chatDisplayNode.listView.forEachItemNode { itemNode in
if let itemNode = itemNode as? ChatMessageItemView {
itemNode.updateSelectionState(animated: animated)
@@ -997,7 +1004,7 @@ public class ChatController: ViewController {
private func navigationButtonAction(_ action: ChatNavigationButtonAction) {
switch action {
case .cancelMessageSelection:
self.updateChatInterfaceState(animated: true, { $0.withoutSelectionState() })
self.updateChatPresentationInterfaceState(animated: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
case .clearHistory:
let actionSheet = ActionSheetController()
actionSheet.setItemGroups([ActionSheetItemGroup(items: [

View File

@@ -7,6 +7,11 @@ import TelegramCore
private let backgroundImage = UIImage(bundleImageName: "Chat/Wallpapers/Builtin0")
private func shouldRequestLayoutOnPresentationInterfaceStateTransition(_ lhs: ChatPresentationInterfaceState, _ rhs: ChatPresentationInterfaceState) -> Bool {
return false
}
enum ChatMessageViewPosition: Equatable {
case AroundUnread(count: Int)
case Around(index: MessageIndex, anchorIndex: MessageIndex)
@@ -46,20 +51,22 @@ class ChatControllerNode: ASDisplayNode {
let backgroundNode: ASDisplayNode
let listView: ListView
let inputPanelBackgroundNode: ASDisplayNode
private let inputPanelBackgroundNode: ASDisplayNode
private let inputPanelBackgroundSeparatorNode: ASDisplayNode
var inputPanelNode: ChatInputPanelNode?
var accessoryPanelNode: AccessoryPanelNode?
private var inputPanelNode: ChatInputPanelNode?
private var accessoryPanelNode: AccessoryPanelNode?
private var inputContextPanelNode: ChatInputContextPanelNode?
var textInputPanelNode: ChatTextInputPanelNode?
private var textInputPanelNode: ChatTextInputPanelNode?
let navigateToLatestButton: ChatHistoryNavigationButtonNode
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 setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in }
var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in }
@@ -81,6 +88,11 @@ class ChatControllerNode: ASDisplayNode {
self.inputPanelBackgroundNode = ASDisplayNode()
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.alpha = 0.0
@@ -97,10 +109,11 @@ class ChatControllerNode: ASDisplayNode {
self.addSubnode(self.listView)
self.addSubnode(self.inputPanelBackgroundNode)
self.addSubnode(self.inputPanelBackgroundSeparatorNode)
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?.updateHeight = { [weak self] in
@@ -119,12 +132,12 @@ class ChatControllerNode: ASDisplayNode {
if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode {
strongSelf.ignoreUpdateHeight = true
textInputPanelNode.text = ""
strongSelf.requestUpdateChatInterfaceState(strongSelf.chatInterfaceState.withUpdatedReplyMessageId(nil), false)
strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil) })
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 dismissedAccessoryPanelNode: ASDisplayNode?
var dismissedInputContextPanelNode: ChatInputContextPanelNode?
var inputPanelSize: CGSize?
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))
if inputPanelNode !== self.inputPanelNode {
@@ -193,7 +207,7 @@ class ChatControllerNode: ASDisplayNode {
var accessoryPanelSize: CGSize?
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))
if accessoryPanelNode !== self.accessoryPanelNode {
@@ -208,7 +222,7 @@ class ChatControllerNode: ASDisplayNode {
accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in
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
}
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 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)
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)
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)
}
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 {
var frameCompleted = false
var alphaCompleted = false
@@ -312,11 +355,43 @@ class ChatControllerNode: ASDisplayNode {
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) {
if self.chatInterfaceState != chatInterfaceState {
self.chatInterfaceState = chatInterfaceState
func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, animated: Bool) {
if let textInputPanelNode = self.textInputPanelNode {
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 {
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 AsyncDisplayKit
import Display
import Postbox
import TelegramCore
class ChatInputPanelNode: ASDisplayNode {
var account: Account?
var interfaceInteraction: ChatPanelInterfaceInteraction?
var peer: Peer?
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 {
let inputText: String?
let inputState: ChatTextInputState
let replyMessageId: MessageId?
let selectionState: ChatInterfaceSelectionState?
init() {
self.inputText = nil
self.inputState = ChatTextInputState()
self.replyMessageId = nil
self.selectionState = nil
}
init(inputText: String?, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) {
self.inputText = inputText
init(inputState: ChatTextInputState, replyMessageId: MessageId?, selectionState: ChatInterfaceSelectionState?) {
self.inputState = inputState
self.replyMessageId = replyMessageId
self.selectionState = selectionState
}
static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool {
return lhs.inputText == rhs.inputText && lhs.replyMessageId == rhs.replyMessageId && lhs.selectionState == rhs.selectionState
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 {
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 {
@@ -40,7 +63,7 @@ final class ChatInterfaceState: Equatable {
selectedIds.formUnion(selectionState.selectedIds)
}
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 {
@@ -53,10 +76,10 @@ final class ChatInterfaceState: Equatable {
} else {
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 {
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 TelegramCore
func accessoryPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? {
if let _ = chatInterfaceState.selectionState {
func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: AccessoryPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> AccessoryPanelNode? {
if let _ = chatPresentationInterfaceState.interfaceState.selectionState {
return nil
}
if let replyMessageId = chatInterfaceState.replyMessageId {
if let replyMessageId = chatPresentationInterfaceState.interfaceState.replyMessageId {
if let replyPanelNode = currentPanel as? ReplyAccessoryPanelNode, replyPanelNode.messageId == replyMessageId {
replyPanelNode.interfaceInteraction = interfaceInteraction
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 TelegramCore
func inputPanelForChatIntefaceState(_ chatInterfaceState: ChatInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? {
if let selectionState = chatInterfaceState.selectionState {
func inputPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, account: Account, currentPanel: ChatInputPanelNode?, textInputPanelNode: ChatTextInputPanelNode?, interfaceInteraction: ChatPanelInterfaceInteraction?) -> ChatInputPanelNode? {
if let selectionState = chatPresentationInterfaceState.interfaceState.selectionState {
if let currentPanel = currentPanel as? ChatMessageSelectionInputPanelNode {
currentPanel.selectedMessageCount = selectionState.selectedIds.count
currentPanel.interfaceInteraction = interfaceInteraction
currentPanel.peer = chatPresentationInterfaceState.peer
return currentPanel
} else {
let panel = ChatMessageSelectionInputPanelNode()
panel.account = account
panel.peer = chatPresentationInterfaceState.peer
panel.selectedMessageCount = selectionState.selectedIds.count
panel.interfaceInteraction = interfaceInteraction
return panel
}
} else {
if let currentPanel = currentPanel as? ChatTextInputPanelNode {
currentPanel.interfaceInteraction = interfaceInteraction
return currentPanel
} else {
if let textInputPanelNode = textInputPanelNode {
textInputPanelNode.interfaceInteraction = interfaceInteraction
return textInputPanelNode
} else {
let panel = ChatTextInputPanelNode()
panel.interfaceInteraction = interfaceInteraction
return panel
if let peer = chatPresentationInterfaceState.peer {
if let channel = peer as? TelegramChannel {
switch channel.info {
case .broadcast:
switch channel.role {
case .creator, .editor, .moderator:
break
case .member:
if let currentPanel = currentPanel as? ChatChannelSubscriberInputPanelNode {
currentPanel.peer = peer
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
let avatarNode: ChatListAvatarNode
let avatarNode: AvatarNode
let contentNode: ASDisplayNode
let titleNode: TextNode
let textNode: TextNode
@@ -121,7 +121,7 @@ class ChatListItemNode: ListViewItemNode {
var relativePosition: (first: Bool, last: Bool) = (false, false)
required init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(24.0))
self.avatarNode = AvatarNode(font: Font.regular(24.0))
self.avatarNode.isLayerBacked = true
self.highlightedBackgroundNode = ASDisplayNode()

View File

@@ -35,10 +35,10 @@ final class ChatMessageAvatarAccessoryItem: ListViewAccessoryItem {
}
final class ChatMessageAvatarAccessoryItemNode: ListViewAccessoryItemNode {
let avatarNode: ChatListAvatarNode
let avatarNode: AvatarNode
override init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(14.0))
self.avatarNode = AvatarNode(font: Font.regular(14.0))
self.avatarNode.isLayerBacked = true
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
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 avatarInset: CGFloat = (item.peerId.isGroupOrChannel && item.message.author != nil) ? layoutConstants.avatarDiameter : 0.0
@@ -718,7 +719,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView {
var incoming = true
if let item = self.item {
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

View File

@@ -51,7 +51,7 @@ public class ChatMessageItem: ListViewItem, CustomStringConvertible {
self.message = message
var accessoryItem: ListViewAccessoryItem?
let incoming = account.peerId != message.author?.id
let incoming = message.effectivelyIncoming
let displayAuthorInfo = incoming && message.author != nil && peerId.isGroupOrChannel
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.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))
}
}

View File

@@ -1,11 +1,40 @@
import Foundation
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode {
private let deleteButton: 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 {
didSet {
self.deleteButton.isEnabled = self.selectedMessageCount != 0

View File

@@ -56,7 +56,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
let imageLayout = self.imageNode.asyncLayout()
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)
if let telegramFile = telegramFile {
if let thumbnailSize = telegramFile.previewRepresentations.first?.dimensions {

View File

@@ -35,7 +35,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
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 textConstrainedSize = CGSize(width: constrainedSize.width - horizontalInset, height: constrainedSize.height)

View File

@@ -1,11 +1,18 @@
import Foundation
import Postbox
final class ChatPanelInterfaceInteraction {
let setupReplyMessage: (MessageId) -> Void
let beginMessageSelection: (MessageId) -> Void
let deleteSelectedMessages: () -> 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.forwardSelectedMessages = forwardSelectedMessages
self.updateTextInputState = updateTextInputState
}
}

View File

@@ -1,7 +1,56 @@
import Foundation
import Postbox
struct ChatPresentationInterfaceState {
enum ChatPresentationInputContext {
case hashtag
case mention
}
struct ChatPresentationInterfaceState: Equatable {
let interfaceState: ChatInterfaceState
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 Display
import AsyncDisplayKit
import WebKit
import Postbox
import TelegramCore
private let textInputViewBackground: UIImage = {
let diameter: CGFloat = 10.0
@@ -36,6 +37,48 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
var sendMessage: () -> 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 {
get {
return self.textInputNode?.attributedText?.string ?? ""
@@ -81,7 +124,13 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.view.addSubview(self.sendButton)
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
}
@@ -94,6 +143,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
textInputNode.typingAttributes = [NSFontAttributeName: Font.regular(16.0)]
textInputNode.clipsToBounds = true
textInputNode.delegate = self
textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
self.addSubnode(textInputNode)
self.textInputNode = textInputNode
@@ -103,6 +153,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.textInputBackgroundView.isUserInteractionEnabled = false
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 {
@@ -155,6 +213,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
self.invalidateCalculatedLayout()
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 highlightedBackgroundNode: ASDisplayNode
private let avatarNode: ChatListAvatarNode
private let avatarNode: AvatarNode
private let titleNode: TextNode
private let statusNode: TextNode
@@ -140,7 +140,7 @@ class ContactsPeerItemNode: ListViewItemNode {
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
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.titleNode = TextNode()

View File

@@ -65,7 +65,7 @@ class ContactsVCardItemNode: ListViewItemNode {
private let separatorNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let avatarNode: ChatListAvatarNode
private let avatarNode: AvatarNode
private let titleNode: TextNode
private let statusNode: TextNode
@@ -82,7 +82,7 @@ class ContactsVCardItemNode: ListViewItemNode {
self.highlightedBackgroundNode.backgroundColor = UIColor(0xd9d9d9)
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.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 let avatarNode: ChatListAvatarNode
private let avatarNode: AvatarNode
private let titleNode: ASTextNode
private var peer: Peer?
fileprivate var action: ((PeerId) -> Void)?
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.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>? {
var location: TelegramCloudMediaLocation?
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 {
if let location = peer.smallProfileImage?.location.cloudLocation {
return deferred { () -> Signal<UIImage, NoError> in
return cachedCloudFileLocation(location)
|> `catch` { _ in

View File

@@ -412,10 +412,10 @@ func chatMessagePhoto(account: Account, photo: TelegramMediaImage) -> Signal<(Tr
if let blurredThumbnailImage = blurredThumbnailImage, let cgImage = blurredThumbnailImage.cgImage {
c.interpolationQuality = .low
c.draw(cgImage, in: fittedRect)
c.setBlendMode(.normal)
}
if let fullSizeImage = fullSizeImage {
c.setBlendMode(.normal)
c.interpolationQuality = .medium
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)
class SettingsAccountInfoItemNode: ListControllerGroupableItemNode {
let avatarNode: ChatListAvatarNode
let avatarNode: AvatarNode
let nameNode: TextNode
let statusNode: TextNode
override init() {
self.avatarNode = ChatListAvatarNode(font: Font.regular(20.0))
self.avatarNode = AvatarNode(font: Font.regular(20.0))
self.nameNode = TextNode()
self.nameNode.isLayerBacked = true

View File

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

View File

@@ -1,23 +1,8 @@
import Foundation
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 {
private var touchLocation = CGPoint()
private var timer: Foundation.Timer?
var touchDown: (() -> Void)?
override init(target: Any?, action: Selector?) {
super.init(target: target, action: action)
@@ -26,53 +11,14 @@ class TouchDownGestureRecognizer: UIGestureRecognizer, UIGestureRecognizerDelega
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPanGestureRecognizer {
return true
}
return false
}
override func reset() {
self.timer?.invalidate()
self.timer = nil
super.reset()
}
func timerEvent() {
self.state = .began
return true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
self.touchLocation = touch.location(in: self.view)
if let touchDown = self.touchDown {
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
}
}