no message

This commit is contained in:
Peter 2017-02-22 21:32:39 +03:00
parent a0ccb729be
commit 22d81e15c3
21 changed files with 1186 additions and 898 deletions

View File

@ -232,6 +232,7 @@
D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */; }; D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */; };
D099EA2D1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */; }; D099EA2D1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */; };
D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */; }; D099EA2F1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */; };
D09AEFD41E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */; };
D0A749971E3AA25200AD786E /* NotificationSoundSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */; }; D0A749971E3AA25200AD786E /* NotificationSoundSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */; };
D0AB0BB11D6718DA002C78E7 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */; }; D0AB0BB11D6718DA002C78E7 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */; };
D0AB0BB31D6718EB002C78E7 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB21D6718EB002C78E7 /* libz.tbd */; }; D0AB0BB31D6718EB002C78E7 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB21D6718EB002C78E7 /* libz.tbd */; };
@ -245,7 +246,6 @@
D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */; }; D0B843CF1DA922AD005F29E1 /* PeerInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */; };
D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */; }; D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */; };
D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */; }; D0B843D31DA922E3005F29E1 /* ChannelInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */; };
D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */; };
D0B843D91DAAAA0C005F29E1 /* ItemListPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */; }; D0B843D91DAAAA0C005F29E1 /* ItemListPeerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */; };
D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */; }; D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */; };
D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */; }; D0B844561DAC3AEE005F29E1 /* PresenceStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */; };
@ -321,6 +321,9 @@
D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */; }; D0DF0C9E1D82141F008AEB01 /* ChatInterfaceInputContexts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */; };
D0DF0CA11D821B28008AEB01 /* HashtagChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */; }; D0DF0CA11D821B28008AEB01 /* HashtagChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */; };
D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */; }; D0DF0CA41D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */; };
D0E305A51E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */; };
D0E305AD1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */; };
D0E305AF1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */; };
D0E35A071DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E35A061DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift */; }; D0E35A071DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E35A061DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift */; };
D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift */; }; D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift */; };
D0E7A1BD1D8C246D00C37A6F /* ChatHistoryListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */; }; D0E7A1BD1D8C246D00C37A6F /* ChatHistoryListNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */; };
@ -693,6 +696,7 @@
D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedVideoNode.swift; sourceTree = "<group>"; }; D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManagedVideoNode.swift; sourceTree = "<group>"; };
D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = "<group>"; }; D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = "<group>"; };
D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = "<group>"; }; D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = "<group>"; };
D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextEmptyStateItem.swift; sourceTree = "<group>"; };
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSoundSelection.swift; sourceTree = "<group>"; }; D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSoundSelection.swift; sourceTree = "<group>"; };
D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; }; D0AB0BB01D6718DA002C78E7 /* libiconv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libiconv.tbd; path = usr/lib/libiconv.tbd; sourceTree = SDKROOT; };
D0AB0BB21D6718EB002C78E7 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; D0AB0BB21D6718EB002C78E7 /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
@ -708,7 +712,6 @@
D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoEntries.swift; sourceTree = "<group>"; }; D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoEntries.swift; sourceTree = "<group>"; };
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = "<group>"; }; D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = "<group>"; };
D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = "<group>"; }; D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = "<group>"; };
D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoEntries.swift; sourceTree = "<group>"; };
D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerItem.swift; sourceTree = "<group>"; }; D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerItem.swift; sourceTree = "<group>"; };
D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = "<group>"; }; D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = "<group>"; };
D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = "<group>"; }; D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = "<group>"; };
@ -784,6 +787,9 @@
D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = "<group>"; }; D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = "<group>"; };
D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputPanelItem.swift; sourceTree = "<group>"; }; D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputPanelItem.swift; sourceTree = "<group>"; };
D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = "<group>"; }; D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = "<group>"; };
D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidateAddressNameInteractive.swift; sourceTree = "<group>"; };
D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerEmptyStateItem.swift; sourceTree = "<group>"; };
D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListLoadingIndicatorEmptyStateItem.swift; sourceTree = "<group>"; };
D0E35A061DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputContextPanelNode.swift; sourceTree = "<group>"; }; D0E35A061DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputContextPanelNode.swift; sourceTree = "<group>"; };
D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputPanelItem.swift; sourceTree = "<group>"; }; D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputPanelItem.swift; sourceTree = "<group>"; };
D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryListNode.swift; sourceTree = "<group>"; }; D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryListNode.swift; sourceTree = "<group>"; };
@ -981,6 +987,7 @@
children = ( children = (
D0E6521D1E3A2305004EEA91 /* Items */, D0E6521D1E3A2305004EEA91 /* Items */,
D01B27981E39144C0022A4C0 /* ItemListController.swift */, D01B27981E39144C0022A4C0 /* ItemListController.swift */,
D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */,
D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */, D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */,
); );
name = "Item List"; name = "Item List";
@ -1688,6 +1695,8 @@
D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */, D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */,
D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */, D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */,
D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */, D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */,
D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */,
D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */,
); );
name = Items; name = Items;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1720,7 +1729,6 @@
children = ( children = (
D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */, D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */,
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */, D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */,
D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */,
D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */, D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */,
D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */, D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */,
D0486F091E523C8500091F0C /* GroupInfoController.swift */, D0486F091E523C8500091F0C /* GroupInfoController.swift */,
@ -2118,6 +2126,7 @@
D087750B1E3E7B7600A97350 /* PreferencesKeys.swift */, D087750B1E3E7B7600A97350 /* PreferencesKeys.swift */,
D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */, D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */,
D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */, D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */,
D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */,
); );
name = Utils; name = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2444,6 +2453,7 @@
D04BB2B91E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */, D04BB2B91E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */,
D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */, D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */,
D003702E1DA43052004308D3 /* ItemListAvatarAndNameItem.swift in Sources */, D003702E1DA43052004308D3 /* ItemListAvatarAndNameItem.swift in Sources */,
D0E305A51E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift in Sources */,
D0D03B1C1DECB0FE00220C46 /* internal.c in Sources */, D0D03B1C1DECB0FE00220C46 /* internal.c in Sources */,
D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */, D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */,
D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */, D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */,
@ -2568,6 +2578,7 @@
D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */,
D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */, D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */,
D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */, D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */,
D0E305AF1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift in Sources */,
D087750C1E3E7B7600A97350 /* PreferencesKeys.swift in Sources */, D087750C1E3E7B7600A97350 /* PreferencesKeys.swift in Sources */,
D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */,
D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */,
@ -2575,6 +2586,7 @@
D07827BD1E004A3400071108 /* ChatListSearchItemHeader.swift in Sources */, D07827BD1E004A3400071108 /* ChatListSearchItemHeader.swift in Sources */,
D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */, D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */,
D08774F81E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift in Sources */, D08774F81E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift in Sources */,
D0E305AD1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift in Sources */,
D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */, D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */,
D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */, D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */,
D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */, D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */,
@ -2647,7 +2659,6 @@
D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */, D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */,
D0561DE81E574C3200E6B9E9 /* ChannelAdminsController.swift in Sources */, D0561DE81E574C3200E6B9E9 /* ChannelAdminsController.swift in Sources */,
D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */,
D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */,
D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */,
D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */, D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */,
D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */, D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */,
@ -2721,6 +2732,7 @@
D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */, D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */,
D00C7CD71E3664070080C3D5 /* ItemListMultilineInputItem.swift in Sources */, D00C7CD71E3664070080C3D5 /* ItemListMultilineInputItem.swift in Sources */,
D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */, D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */,
D09AEFD41E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift in Sources */,
D0F69D6D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift in Sources */, D0F69D6D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift in Sources */,
D0D03B201DECB0FE00220C46 /* stream.c in Sources */, D0D03B201DECB0FE00220C46 /* stream.c in Sources */,
); );

View File

@ -6,11 +6,21 @@ import TelegramCore
private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed()
private struct ChannelAdminsControllerArguments { private final class ChannelAdminsControllerArguments {
let account: Account let account: Account
let updateCurrentAdministrationType: () -> Void let updateCurrentAdministrationType: () -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let removeAdmin: (PeerId) -> Void
let addAdmin: () -> Void let addAdmin: () -> Void
init(account: Account, updateCurrentAdministrationType: @escaping () -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removeAdmin: @escaping (PeerId) -> Void, addAdmin: @escaping () -> Void) {
self.account = account
self.updateCurrentAdministrationType = updateCurrentAdministrationType
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removeAdmin = removeAdmin
self.addAdmin = addAdmin
}
} }
private enum ChannelAdminsSection: Int32 { private enum ChannelAdminsSection: Int32 {
@ -54,7 +64,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
case administrationInfo(String) case administrationInfo(String)
case adminsHeader(String) case adminsHeader(String)
case adminPeerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing) case adminPeerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool)
case addAdmin(Bool) case addAdmin(Bool)
case adminsInfo(String) case adminsInfo(String)
@ -79,7 +89,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
return .index(3) return .index(3)
case .adminsInfo: case .adminsInfo:
return .index(4) return .index(4)
case let .adminPeerItem(_, participant, _): case let .adminPeerItem(_, participant, _, _):
return .peer(participant.peer.id) return .peer(participant.peer.id)
} }
} }
@ -104,8 +114,8 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .adminPeerItem(lhsIndex, lhsParticipant, lhsEditing): case let .adminPeerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled):
if case let .adminPeerItem(rhsIndex, rhsParticipant, rhsEditing) = rhs { if case let .adminPeerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex { if lhsIndex != rhsIndex {
return false return false
} }
@ -115,6 +125,9 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
if lhsEditing != rhsEditing { if lhsEditing != rhsEditing {
return false return false
} }
if lhsEnabled != rhsEnabled {
return false
}
return true return true
} else { } else {
return false return false
@ -152,11 +165,11 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
default: default:
return true return true
} }
case let .adminPeerItem(index, _, _): case let .adminPeerItem(index, _, _, _):
switch rhs { switch rhs {
case .administrationType, .administrationInfo, .adminsHeader: case .administrationType, .administrationInfo, .adminsHeader:
return false return false
case let .adminPeerItem(rhsIndex, _, _): case let .adminPeerItem(rhsIndex, _, _, _):
return index < rhsIndex return index < rhsIndex
default: default:
return true return true
@ -184,13 +197,13 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
label = "All Members" label = "All Members"
} }
return ItemListDisclosureItem(title: "Who can add members", label: label, sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(title: "Who can add members", label: label, sectionId: self.section, style: .blocks, action: {
arguments.updateCurrentAdministrationType()
}) })
case let .administrationInfo(text): case let .administrationInfo(text):
return ItemListTextItem(text: text, sectionId: self.section) return ItemListTextItem(text: text, sectionId: self.section)
case let .adminsHeader(title): case let .adminsHeader(title):
return ItemListSectionHeaderItem(text: title, sectionId: self.section) return ItemListSectionHeaderItem(text: title, sectionId: self.section)
case let .adminPeerItem(_, participant, editing): case let .adminPeerItem(_, participant, editing, enabled):
let peerText: String let peerText: String
switch participant.participant { switch participant.participant {
case .creator: case .creator:
@ -198,10 +211,10 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
default: default:
peerText = "Moderator" peerText = "Moderator"
} }
return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: nil, editing: editing, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .text(peerText), label: nil, editing: editing, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { _ in }, removePeer: { peerId in
arguments.removeAdmin(peerId)
}) })
case let .addAdmin(editing): case let .addAdmin(editing):
return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Admin", sectionId: self.section, editing: editing, action: { return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Admin", sectionId: self.section, editing: editing, action: {
@ -220,25 +233,75 @@ private enum CurrentAdministrationType {
private struct ChannelAdminsControllerState: Equatable { private struct ChannelAdminsControllerState: Equatable {
let selectedType: CurrentAdministrationType? let selectedType: CurrentAdministrationType?
let editing: Bool
let peerIdWithRevealedOptions: PeerId?
let removingPeerId: PeerId?
let removedPeerIds: Set<PeerId>
let temporaryAdmins: [RenderedChannelParticipant]
init() { init() {
self.selectedType = nil self.selectedType = nil
self.editing = false
self.peerIdWithRevealedOptions = nil
self.removingPeerId = nil
self.removedPeerIds = Set()
self.temporaryAdmins = []
} }
init(selectedType: CurrentAdministrationType?) { init(selectedType: CurrentAdministrationType?, editing: Bool, peerIdWithRevealedOptions: PeerId?, removingPeerId: PeerId?, removedPeerIds: Set<PeerId>, temporaryAdmins: [RenderedChannelParticipant]) {
self.selectedType = selectedType self.selectedType = selectedType
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
self.removingPeerId = removingPeerId
self.removedPeerIds = removedPeerIds
self.temporaryAdmins = temporaryAdmins
} }
static func ==(lhs: ChannelAdminsControllerState, rhs: ChannelAdminsControllerState) -> Bool { static func ==(lhs: ChannelAdminsControllerState, rhs: ChannelAdminsControllerState) -> Bool {
if lhs.selectedType != rhs.selectedType { if lhs.selectedType != rhs.selectedType {
return false return false
} }
if lhs.editing != rhs.editing {
return false
}
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
return false
}
if lhs.removingPeerId != rhs.removingPeerId {
return false
}
if lhs.removedPeerIds != rhs.removedPeerIds {
return false
}
if lhs.temporaryAdmins != rhs.temporaryAdmins {
return false
}
return true return true
} }
func withUpdatedSelectedType(_ selectedType: CurrentAdministrationType?) -> ChannelAdminsControllerState { func withUpdatedSelectedType(_ selectedType: CurrentAdministrationType?) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: selectedType) return ChannelAdminsControllerState(selectedType: selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins)
}
func withUpdatedEditing(_ editing: Bool) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins)
}
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins)
}
func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: self.temporaryAdmins)
}
func withUpdatedRemovedPeerIds(_ removedPeerIds: Set<PeerId>) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: removedPeerIds, temporaryAdmins: self.temporaryAdmins)
}
func withUpdatedTemporaryAdmins(_ temporaryAdmins: [RenderedChannelParticipant]) -> ChannelAdminsControllerState {
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: temporaryAdmins)
} }
} }
@ -275,8 +338,20 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins
if let participants = participants { if let participants = participants {
entries.append(.adminsHeader(isGroup ? "GROUP ADMINS" : "CHANNEL ADMINS")) entries.append(.adminsHeader(isGroup ? "GROUP ADMINS" : "CHANNEL ADMINS"))
var combinedParticipants: [RenderedChannelParticipant] = participants
var existingParticipantIds = Set<PeerId>()
for participant in participants {
existingParticipantIds.insert(participant.peer.id)
}
for participant in state.temporaryAdmins {
if !existingParticipantIds.contains(participant.peer.id) {
combinedParticipants.append(participant)
}
}
var index: Int32 = 0 var index: Int32 = 0
for participant in participants.sorted(by: { lhs, rhs in for participant in combinedParticipants.sorted(by: { lhs, rhs in
let lhsInvitedAt: Int32 let lhsInvitedAt: Int32
switch lhs.participant { switch lhs.participant {
case .creator: case .creator:
@ -301,15 +376,17 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins
} }
return lhsInvitedAt < rhsInvitedAt return lhsInvitedAt < rhsInvitedAt
}) { }) {
var editable = true if !state.removedPeerIds.contains(participant.peer.id) {
if case .creator = participant.participant { var editable = true
editable = false if case .creator = participant.participant {
editable = false
}
entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), existingParticipantIds.contains(participant.peer.id)))
index += 1
} }
entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: false, revealed: false)))
index += 1
} }
entries.append(.addAdmin(false)) entries.append(.addAdmin(state.editing))
entries.append(.adminsInfo(isGroup ? "You can add admins to help you manage your group" : "You can add admins to help you manage your channel")) entries.append(.adminsInfo(isGroup ? "You can add admins to help you manage your group" : "You can add admins to help you manage your channel"))
} }
} }
@ -317,62 +394,261 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins
return entries return entries
} }
/*private func effectiveAdministrationType(state: ChannelAdminsControllerState, peer: TelegramChannel) -> CurrentAdministrationType { public func channelAdminsController(account: Account, peerId: PeerId) -> ViewController {
let selectedType: CurrentAdministrationType
if let current = state.selectedType {
selectedType = current
} else {
if let addressName = peer.addressName, !addressName.isEmpty {
selectedType = .publicChannel
} else {
selectedType = .privateChannel
}
}
return selectedType
}*/
public func ChannelAdminsController(account: Account, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(ChannelAdminsControllerState(), ignoreRepeated: true) let statePromise = ValuePromise(ChannelAdminsControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelAdminsControllerState()) let stateValue = Atomic(value: ChannelAdminsControllerState())
let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { f in let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let updateAdministrationDisposable = MetaDisposable() let updateAdministrationDisposable = MetaDisposable()
actionsDisposable.add(updateAdministrationDisposable) actionsDisposable.add(updateAdministrationDisposable)
let removeAdminDisposable = MetaDisposable()
actionsDisposable.add(removeAdminDisposable)
let addAdminDisposable = MetaDisposable() let addAdminDisposable = MetaDisposable()
actionsDisposable.add(addAdminDisposable) actionsDisposable.add(addAdminDisposable)
let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil)
let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: { let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: {
let actionSheet = ActionSheetController()
let result = ValuePromise<Bool>()
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "All Members", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
result.set(true)
}),
ActionSheetButtonItem(title: "Only Admins", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
result.set(false)
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
let updateSignal = result.get()
|> take(1)
|> mapToSignal { value -> Signal<Void, NoError> in
updateState { state in
return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers)
}
return account.postbox.loadedPeerWithId(peerId)
|> mapToSignal { peer -> Signal<Void, NoError> in
if let peer = peer as? TelegramChannel, case let .group(info) = peer.info {
var updatedValue: Bool?
if value && !info.flags.contains(.everyMemberCanInviteMembers) {
updatedValue = true
} else if !value && info.flags.contains(.everyMemberCanInviteMembers) {
updatedValue = false
}
if let updatedValue = updatedValue {
return updateGroupManagementType(account: account, peerId: peerId, type: updatedValue ? .unrestricted : .restrictedToAdmins)
} else {
return .complete()
}
} else {
return .complete()
}
}
}
updateAdministrationDisposable.set(updateSignal.start())
presentControllerImpl?(actionSheet, nil)
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, removeAdmin: { adminId in
updateState {
return $0.withUpdatedRemovingPeerId(adminId)
}
let applyPeers: Signal<Void, NoError> = adminsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { peers -> Signal<Void, NoError> in
if let peers = peers {
var updatedPeers = peers
for i in 0 ..< updatedPeers.count {
if updatedPeers[i].peer.id == adminId {
updatedPeers.remove(at: i)
break
}
}
adminsPromise.set(.single(updatedPeers))
}
return .complete()
}
removeAdminDisposable.set((removePeerAdmin(account: account, peerId: peerId, adminId: adminId)
|> then(applyPeers |> mapError { _ -> RemovePeerAdminError in return .generic }) |> deliverOnMainQueue).start(error: { _ in
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}, completed: {
updateState { state in
var updatedTemporaryAdmins = state.temporaryAdmins
for i in 0 ..< updatedTemporaryAdmins.count {
if updatedTemporaryAdmins[i].peer.id == adminId {
updatedTemporaryAdmins.remove(at: i)
break
}
}
return state.withUpdatedRemovingPeerId(nil).withUpdatedTemporaryAdmins(updatedTemporaryAdmins)
}
}))
}, addAdmin: { }, addAdmin: {
var confirmationImpl: ((PeerId) -> Signal<Bool, NoError>)?
let contactsController = ContactSelectionController(account: account, title: "Add admin", confirmation: { peerId in
if let confirmationImpl = confirmationImpl {
return confirmationImpl(peerId)
} else {
return .single(false)
}
})
confirmationImpl = { [weak contactsController] peerId in
return account.postbox.loadedPeerWithId(peerId)
|> deliverOnMainQueue
|> mapToSignal { peer in
let result = ValuePromise<Bool>()
if let contactsController = contactsController {
let alertController = standardTextAlertController(title: nil, text: "Add \(peer.displayTitle) as admin?", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
result.set(false)
}),
TextAlertAction(type: .defaultAction, title: "OK", action: {
result.set(true)
})
])
contactsController.present(alertController, in: .window)
}
return result.get()
}
}
let addAdmin = contactsController.result
|> deliverOnMainQueue
|> mapToSignal { memberId -> Signal<Void, NoError> in
if let memberId = memberId {
return account.postbox.peerView(id: memberId)
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { view -> Signal<Void, NoError> in
if let peer = view.peers[memberId] {
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
updateState { state in
var found = false
for participant in state.temporaryAdmins {
if participant.peer.id == memberId {
found = true
break
}
}
var removedPeerIds = state.removedPeerIds
removedPeerIds.remove(memberId)
if !found {
var temporaryAdmins = state.temporaryAdmins
temporaryAdmins.append(RenderedChannelParticipant(participant: ChannelParticipant.moderator(id: peer.id, invitedBy: account.peerId, invitedAt: timestamp), peer: peer))
return state.withUpdatedTemporaryAdmins(temporaryAdmins).withUpdatedRemovedPeerIds(removedPeerIds)
} else {
return state.withUpdatedRemovedPeerIds(removedPeerIds)
}
}
let applyAdmin: Signal<Void, AddPeerAdminError> = adminsPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapError { _ -> AddPeerAdminError in return .generic }
|> mapToSignal { admins -> Signal<Void, AddPeerAdminError> in
if let admins = admins {
var updatedAdmins = admins
var found = false
for i in 0 ..< updatedAdmins.count {
if updatedAdmins[i].peer.id == memberId {
found = true
break
}
}
if !found {
updatedAdmins.append(RenderedChannelParticipant(participant: ChannelParticipant.moderator(id: peer.id, invitedBy: account.peerId, invitedAt: timestamp), peer: peer))
adminsPromise.set(.single(updatedAdmins))
}
}
return .complete()
}
return addPeerAdmin(account: account, peerId: peerId, adminId: memberId)
|> deliverOnMainQueue
|> then(applyAdmin)
|> `catch` { _ -> Signal<Void, NoError> in
updateState { state in
var temporaryAdmins = state.temporaryAdmins
for i in 0 ..< temporaryAdmins.count {
if temporaryAdmins[i].peer.id == memberId {
temporaryAdmins.remove(at: i)
break
}
}
return state.withUpdatedTemporaryAdmins(temporaryAdmins)
}
return .complete()
}
} else {
return .complete()
}
}
} else {
return .complete()
}
}
presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
addAdminDisposable.set(addAdmin.start())
}) })
let peerView = account.viewTracker.peerView(peerId) let peerView = account.viewTracker.peerView(peerId) |> deliverOnMainQueue
let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil)
let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peerId) |> map { Optional($0) }) let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peerId) |> map { Optional($0) })
adminsPromise.set(adminsSignal) adminsPromise.set(adminsSignal)
let signal = combineLatest(statePromise.get(), peerView, adminsPromise.get()) let signal = combineLatest(statePromise.get(), peerView, adminsPromise.get() |> deliverOnMainQueue)
|> deliverOnMainQueue
|> map { state, view, admins -> (ItemListControllerState, (ItemListNodeState<ChannelAdminsEntry>, ChannelAdminsEntry.ItemGenerationArguments)) in |> map { state, view, admins -> (ItemListControllerState, (ItemListNodeState<ChannelAdminsEntry>, ChannelAdminsEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view)
var rightNavigationButton: ItemListNavigationButton? var rightNavigationButton: ItemListNavigationButton?
if let admins = admins, admins.count > 1 { if let admins = admins, admins.count > 1 {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { if state.editing {
updateState { state in rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
return state updateState { state in
} return state.withUpdatedEditing(false)
}) }
})
} else {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(true)
}
})
}
} }
let controllerState = ItemListControllerState(title: "Admins", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true) let controllerState = ItemListControllerState(title: "Admins", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
let listState = ItemListNodeState(entries: ChannelAdminsControllerEntries(view: view, state: state, participants: admins), style: .blocks, animateChanges: false) let listState = ItemListNodeState(entries: ChannelAdminsControllerEntries(view: view, state: state, participants: admins), style: .blocks, animateChanges: true)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} |> afterDisposed { } |> afterDisposed {
@ -380,5 +656,10 @@ public func ChannelAdminsController(account: Account, peerId: PeerId) -> ViewCon
} }
let controller = ItemListController(signal) let controller = ItemListController(signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window, with: p)
}
}
return controller return controller
} }

View File

@ -1,2 +1,307 @@
import Foundation import Foundation
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
private final class ChannelBlacklistControllerArguments {
let account: Account
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let removePeer: (PeerId) -> Void
init(account: Account, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void) {
self.account = account
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.removePeer = removePeer
}
}
private enum ChannelBlacklistSection: Int32 {
case peers
}
private enum ChannelBlacklistEntryStableId: Hashable {
case peer(PeerId)
var hashValue: Int {
switch self {
case let .peer(peerId):
return peerId.hashValue
}
}
static func ==(lhs: ChannelBlacklistEntryStableId, rhs: ChannelBlacklistEntryStableId) -> Bool {
switch lhs {
case let .peer(peerId):
if case .peer(peerId) = rhs {
return true
} else {
return false
}
}
}
}
private enum ChannelBlacklistEntry: ItemListNodeEntry {
case peerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool)
var section: ItemListSectionId {
switch self {
case .peerItem:
return ChannelBlacklistSection.peers.rawValue
}
}
var stableId: ChannelBlacklistEntryStableId {
switch self {
case let .peerItem(_, participant, _, _):
return .peer(participant.peer.id)
}
}
static func ==(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool {
switch lhs {
case let .peerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled):
if case let .peerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex {
return false
}
if lhsParticipant != rhsParticipant {
return false
}
if lhsEditing != rhsEditing {
return false
}
if lhsEnabled != rhsEnabled {
return false
}
return true
} else {
return false
}
}
}
static func <(lhs: ChannelBlacklistEntry, rhs: ChannelBlacklistEntry) -> Bool {
switch lhs {
case let .peerItem(index, _, _, _):
switch rhs {
case let .peerItem(rhsIndex, _, _, _):
return index < rhsIndex
}
}
}
func item(_ arguments: ChannelBlacklistControllerArguments) -> ListViewItem {
switch self {
case let .peerItem(_, participant, editing, enabled):
return ItemListPeerItem(account: arguments.account, peer: participant.peer, presence: nil, text: .none, label: nil, editing: editing, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removePeer(peerId)
})
}
}
}
private struct ChannelBlacklistControllerState: Equatable {
let editing: Bool
let peerIdWithRevealedOptions: PeerId?
let removingPeerId: PeerId?
init() {
self.editing = false
self.peerIdWithRevealedOptions = nil
self.removingPeerId = nil
}
init(editing: Bool, peerIdWithRevealedOptions: PeerId?, removingPeerId: PeerId?) {
self.editing = editing
self.peerIdWithRevealedOptions = peerIdWithRevealedOptions
self.removingPeerId = removingPeerId
}
static func ==(lhs: ChannelBlacklistControllerState, rhs: ChannelBlacklistControllerState) -> Bool {
if lhs.editing != rhs.editing {
return false
}
if lhs.peerIdWithRevealedOptions != rhs.peerIdWithRevealedOptions {
return false
}
if lhs.removingPeerId != rhs.removingPeerId {
return false
}
return true
}
func withUpdatedEditing(_ editing: Bool) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(editing: editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId)
}
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(editing: self.editing, peerIdWithRevealedOptions: peerIdWithRevealedOptions, removingPeerId: self.removingPeerId)
}
func withUpdatedRemovingPeerId(_ removingPeerId: PeerId?) -> ChannelBlacklistControllerState {
return ChannelBlacklistControllerState(editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: removingPeerId)
}
}
private func channelBlacklistControllerEntries(view: PeerView, state: ChannelBlacklistControllerState, participants: [RenderedChannelParticipant]?) -> [ChannelBlacklistEntry] {
var entries: [ChannelBlacklistEntry] = []
if let participants = participants {
var index: Int32 = 0
for participant in participants.sorted(by: { lhs, rhs in
let lhsInvitedAt: Int32
switch lhs.participant {
case .creator:
lhsInvitedAt = Int32.min
case let .editor(_, _, invitedAt):
lhsInvitedAt = invitedAt
case let .moderator(_, _, invitedAt):
lhsInvitedAt = invitedAt
case let .member(_, invitedAt):
lhsInvitedAt = invitedAt
}
let rhsInvitedAt: Int32
switch rhs.participant {
case .creator:
rhsInvitedAt = Int32.min
case let .editor(_, _, invitedAt):
rhsInvitedAt = invitedAt
case let .moderator(_, _, invitedAt):
rhsInvitedAt = invitedAt
case let .member(_, invitedAt):
rhsInvitedAt = invitedAt
}
return lhsInvitedAt < rhsInvitedAt
}) {
var editable = true
if case .creator = participant.participant {
editable = false
}
entries.append(.peerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), state.removingPeerId != participant.peer.id))
index += 1
}
}
return entries
}
public func channelBlacklistController(account: Account, peerId: PeerId) -> ViewController {
let statePromise = ValuePromise(ChannelBlacklistControllerState(), ignoreRepeated: true)
let stateValue = Atomic(value: ChannelBlacklistControllerState())
let updateState: ((ChannelBlacklistControllerState) -> ChannelBlacklistControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let actionsDisposable = DisposableSet()
let updateAdministrationDisposable = MetaDisposable()
actionsDisposable.add(updateAdministrationDisposable)
let removePeerDisposable = MetaDisposable()
actionsDisposable.add(removePeerDisposable)
let peersPromise = Promise<[RenderedChannelParticipant]?>(nil)
let arguments = ChannelBlacklistControllerArguments(account: account, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
}, removePeer: { memberId in
updateState {
return $0.withUpdatedRemovingPeerId(memberId)
}
let applyPeers: Signal<Void, NoError> = peersPromise.get()
|> filter { $0 != nil }
|> take(1)
|> deliverOnMainQueue
|> mapToSignal { peers -> Signal<Void, NoError> in
if let peers = peers {
var updatedPeers = peers
for i in 0 ..< updatedPeers.count {
if updatedPeers[i].peer.id == memberId {
updatedPeers.remove(at: i)
break
}
}
peersPromise.set(.single(updatedPeers))
}
return .complete()
}
removePeerDisposable.set((removeChannelBlacklistedPeer(account: account, peerId: peerId, memberId: memberId) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}, completed: {
updateState {
return $0.withUpdatedRemovingPeerId(nil)
}
}))
})
let peerView = account.viewTracker.peerView(peerId)
let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelBlacklist(account: account, peerId: peerId) |> map { Optional($0) })
peersPromise.set(peersSignal)
let signal = combineLatest(statePromise.get(), peerView, peersPromise.get())
|> deliverOnMainQueue
|> map { state, view, peers -> (ItemListControllerState, (ItemListNodeState<ChannelBlacklistEntry>, ChannelBlacklistEntry.ItemGenerationArguments)) in
var rightNavigationButton: ItemListNavigationButton?
if let peers = peers, !peers.isEmpty {
if state.editing {
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(false)
}
})
} else {
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
updateState { state in
return state.withUpdatedEditing(true)
}
})
}
}
var emptyStateItem: ItemListControllerEmptyStateItem?
if let peers = peers {
if peers.isEmpty {
emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the group and can only come back if invited by an admin. Invite links don't work for them.")
}
} else if peers == nil {
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem()
}
let controllerState = ItemListControllerState(title: "Blacklist", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true)
return (controllerState, (listState, arguments))
} |> afterDisposed {
actionsDisposable.dispose()
}
let controller = ItemListController(signal)
presentControllerImpl = { [weak controller] c, p in
if let controller = controller {
controller.present(c, in: .window, with: p)
}
}
return controller
}

View File

@ -4,18 +4,32 @@ import SwiftSignalKit
import Postbox import Postbox
import TelegramCore import TelegramCore
private struct ChannelVisibilityControllerArguments { private final class ChannelVisibilityControllerArguments {
let account: Account let account: Account
let updateCurrentType: (CurrentChannelType) -> Void let updateCurrentType: (CurrentChannelType) -> Void
let updatePublicLinkText: (String) -> Void let updatePublicLinkText: (String?, String) -> Void
let displayPrivateLinkMenu: () -> Void let displayPrivateLinkMenu: (String) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let revokePeerId: (PeerId) -> Void
init(account: Account, updateCurrentType: @escaping (CurrentChannelType) -> Void, updatePublicLinkText: @escaping (String?, String) -> Void, displayPrivateLinkMenu: @escaping (String) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, revokePeerId: @escaping (PeerId) -> Void) {
self.account = account
self.updateCurrentType = updateCurrentType
self.updatePublicLinkText = updatePublicLinkText
self.displayPrivateLinkMenu = displayPrivateLinkMenu
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.revokePeerId = revokePeerId
}
} }
private enum ChannelVisibilitySection: Int32 { private enum ChannelVisibilitySection: Int32 {
case type case type
case link case link
case existingPublicLinks }
private enum ChannelVisibilityEntryTag {
case privateLink
} }
private enum ChannelVisibilityEntry: ItemListNodeEntry { private enum ChannelVisibilityEntry: ItemListNodeEntry {
@ -24,23 +38,24 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case typePrivate(Bool) case typePrivate(Bool)
case typeInfo(String) case typeInfo(String)
case publicLinkAvailability(Bool)
case privateLink(String?) case privateLink(String?)
case editablePublicLink(String) case editablePublicLink(String?, String)
case privateLinkInfo(String) case privateLinkInfo(String)
case publicLinkInfo(String) case publicLinkInfo(String)
case publicLinkStatus(String, AddressNameStatus) case publicLinkStatus(String, AddressNameValidationStatus)
case existingLinksInfo(String) case existingLinksInfo(String)
case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing) case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing, Bool)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .typeHeader, .typePublic, .typePrivate, .typeInfo: case .typeHeader, .typePublic, .typePrivate, .typeInfo:
return ChannelVisibilitySection.type.rawValue return ChannelVisibilitySection.type.rawValue
case .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus: case .publicLinkAvailability, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus:
return ChannelVisibilitySection.link.rawValue return ChannelVisibilitySection.link.rawValue
case .existingLinksInfo, .existingLinkPeerItem: case .existingLinksInfo, .existingLinkPeerItem:
return ChannelVisibilitySection.existingPublicLinks.rawValue return ChannelVisibilitySection.link.rawValue
} }
} }
@ -55,21 +70,23 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
case .typeInfo: case .typeInfo:
return 3 return 3
case .privateLink: case .publicLinkAvailability:
return 4 return 4
case .editablePublicLink: case .privateLink:
return 5 return 5
case .privateLinkInfo: case .editablePublicLink:
return 6 return 6
case .publicLinkStatus: case .privateLinkInfo:
return 7 return 7
case .publicLinkInfo: case .publicLinkStatus:
return 8 return 8
case .publicLinkInfo:
return 9
case .existingLinksInfo: case .existingLinksInfo:
return 9 return 10
case let .existingLinkPeerItem(index, _, _): case let .existingLinkPeerItem(index, _, _, _):
return 10 + index return 11 + index
} }
} }
@ -99,14 +116,20 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .publicLinkAvailability(value):
if case .publicLinkAvailability(value) = rhs {
return true
} else {
return false
}
case let .privateLink(lhsLink): case let .privateLink(lhsLink):
if case let .privateLink(rhsLink) = rhs, lhsLink == rhsLink { if case let .privateLink(rhsLink) = rhs, lhsLink == rhsLink {
return true return true
} else { } else {
return false return false
} }
case let .editablePublicLink(text): case let .editablePublicLink(lhsCurrentText, lhsText):
if case .editablePublicLink(text) = rhs { if case let .editablePublicLink(rhsCurrentText, rhsText) = rhs, lhsCurrentText == rhsCurrentText, lhsText == rhsText {
return true return true
} else { } else {
return false return false
@ -135,8 +158,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing): case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled):
if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing) = rhs { if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs {
if lhsIndex != rhsIndex { if lhsIndex != rhsIndex {
return false return false
} }
@ -146,6 +169,9 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
if lhsEditing != rhsEditing { if lhsEditing != rhsEditing {
return false return false
} }
if lhsEnabled != rhsEnabled {
return false
}
return true return true
} else { } else {
return false return false
@ -171,13 +197,21 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
}) })
case let .typeInfo(text): case let .typeInfo(text):
return ItemListTextItem(text: text, sectionId: self.section) return ItemListTextItem(text: text, sectionId: self.section)
case let .publicLinkAvailability(value):
if value {
return ItemListActivityTextItem(displayActivity: true, text: NSAttributedString(string: "Checking", textColor: UIColor(0x6d6d72)), sectionId: self.section)
} else {
return ItemListActivityTextItem(displayActivity: false, text: NSAttributedString(string: "Sorry, you have reserved too many public usernames. You can revoke the link from one of your older groups or channels, or create a private entity instead.", textColor: UIColor(0xcf3030)), sectionId: self.section)
}
case let .privateLink(link): case let .privateLink(link):
return ItemListActionItem(title: link ?? "Loading", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(title: link ?? "Loading", kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: {
if let link = link {
}) arguments.displayPrivateLinkMenu(link)
case let .editablePublicLink(text): }
}, tag: ChannelVisibilityEntryTag.privateLink)
case let .editablePublicLink(currentText, text):
return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in return ItemListSingleLineInputItem(title: NSAttributedString(string: "t.me/", textColor: .black), text: text, placeholder: "", sectionId: self.section, textUpdated: { updatedText in
arguments.updatePublicLinkText(updatedText) arguments.updatePublicLinkText(currentText, updatedText)
}, action: { }, action: {
}) })
@ -189,31 +223,44 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry {
var displayActivity = false var displayActivity = false
let text: NSAttributedString let text: NSAttributedString
switch status { switch status {
case .available: case let .invalidFormat(error):
text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c)) switch error {
case .checking: case .startsWithDigit:
text = NSAttributedString(string: "Checking name...", textColor: .gray)
displayActivity = true
case let .invalid(reason):
switch reason {
case .alreadyTaken:
text = NSAttributedString(string: "\(addressName) is already taken.", textColor: .red)
case .digitStart:
text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030)) text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030))
case .invalid, .underscopeEnd, .underscopeStart: case .startsWithUnderscore:
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) text = NSAttributedString(string: "Names can't start with an underscore.", textColor: UIColor(0xcf3030))
case .short: case .endsWithUnderscore:
text = NSAttributedString(string: "Names can't end with an underscore.", textColor: UIColor(0xcf3030))
case .tooShort:
text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030)) text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030))
case .invalidCharacters:
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
} }
case let .availability(availability):
switch availability {
case .available:
text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c))
case .invalid:
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
case .taken:
text = NSAttributedString(string: "\(addressName) is already taken.", textColor: UIColor(0xcf3030))
}
case .checking:
text = NSAttributedString(string: "Checking name...", textColor: UIColor(0x6d6d72))
displayActivity = true
} }
return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section) return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section)
case let .existingLinksInfo(text): case let .existingLinksInfo(text):
return ItemListTextItem(text: text, sectionId: self.section) return ItemListTextItem(text: text, sectionId: self.section)
case let .existingLinkPeerItem(_, peer, editing): case let .existingLinkPeerItem(_, peer, editing, enabled):
return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .activity, label: nil, editing: editing, enabled: true, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in var label = ""
if let addressName = peer.addressName {
}, removePeer: { _ in label = "t.me/" + addressName
}
return ItemListPeerItem(account: arguments.account, peer: peer, presence: nil, text: .text(label), label: nil, editing: editing, enabled: enabled, sectionId: self.section, action: nil, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.revokePeerId(peerId)
}) })
} }
} }
@ -224,53 +271,30 @@ private enum CurrentChannelType {
case privateChannel case privateChannel
} }
private enum AddressNameStatus: Equatable {
case available
case checking
case invalid(UsernameAvailabilityError)
static func ==(lhs: AddressNameStatus, rhs: AddressNameStatus) -> Bool {
switch lhs {
case .available:
if case .available = rhs {
return true
} else {
return false
}
case .checking:
if case .checking = rhs {
return true
} else {
return false
}
case let .invalid(reason):
if case .invalid(reason) = rhs {
return true
} else {
return false
}
}
}
}
private struct ChannelVisibilityControllerState: Equatable { private struct ChannelVisibilityControllerState: Equatable {
let selectedType: CurrentChannelType? let selectedType: CurrentChannelType?
let editingPublicLinkText: String? let editingPublicLinkText: String?
let addressNameStatus: AddressNameStatus? let addressNameValidationStatus: AddressNameValidationStatus?
let updatingAddressName: Bool let updatingAddressName: Bool
let revealedRevokePeerId: PeerId?
let revokingPeerId: PeerId?
init() { init() {
self.selectedType = nil self.selectedType = nil
self.editingPublicLinkText = nil self.editingPublicLinkText = nil
self.addressNameStatus = nil self.addressNameValidationStatus = nil
self.updatingAddressName = false self.updatingAddressName = false
self.revealedRevokePeerId = nil
self.revokingPeerId = nil
} }
init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameStatus: AddressNameStatus?, updatingAddressName: Bool) { init(selectedType: CurrentChannelType?, editingPublicLinkText: String?, addressNameValidationStatus: AddressNameValidationStatus?, updatingAddressName: Bool, revealedRevokePeerId: PeerId?, revokingPeerId: PeerId?) {
self.selectedType = selectedType self.selectedType = selectedType
self.editingPublicLinkText = editingPublicLinkText self.editingPublicLinkText = editingPublicLinkText
self.addressNameStatus = addressNameStatus self.addressNameValidationStatus = addressNameValidationStatus
self.updatingAddressName = updatingAddressName self.updatingAddressName = updatingAddressName
self.revealedRevokePeerId = revealedRevokePeerId
self.revokingPeerId = revokingPeerId
} }
static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool { static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool {
@ -280,34 +304,48 @@ private struct ChannelVisibilityControllerState: Equatable {
if lhs.editingPublicLinkText != rhs.editingPublicLinkText { if lhs.editingPublicLinkText != rhs.editingPublicLinkText {
return false return false
} }
if lhs.addressNameStatus != rhs.addressNameStatus { if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus {
return false return false
} }
if lhs.updatingAddressName != rhs.updatingAddressName { if lhs.updatingAddressName != rhs.updatingAddressName {
return false return false
} }
if lhs.revealedRevokePeerId != rhs.revealedRevokePeerId {
return false
}
if lhs.revokingPeerId != rhs.revokingPeerId {
return false
}
return true return true
} }
func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState { func withUpdatedSelectedType(_ selectedType: CurrentChannelType?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: self.updatingAddressName) return ChannelVisibilityControllerState(selectedType: selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId)
} }
func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState { func withUpdatedEditingPublicLinkText(_ editingPublicLinkText: String?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: self.updatingAddressName) return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId)
} }
func withUpdatedAddressNameStatus(_ addressNameStatus: AddressNameStatus?) -> ChannelVisibilityControllerState { func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: addressNameStatus, updatingAddressName: self.updatingAddressName) return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId)
} }
func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState { func withUpdatedUpdatingAddressName(_ updatingAddressName: Bool) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: self.addressNameStatus, updatingAddressName: updatingAddressName) return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId)
}
func withUpdatedRevealedRevokePeerId(_ revealedRevokePeerId: PeerId?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: revealedRevokePeerId, revokingPeerId: self.revokingPeerId)
}
func withUpdatedRevokingPeerId(_ revokingPeerId: PeerId?) -> ChannelVisibilityControllerState {
return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: self.addressNameValidationStatus, updatingAddressName: updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: revokingPeerId)
} }
} }
private func channelVisibilityControllerEntries(view: PeerView, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] { private func channelVisibilityControllerEntries(view: PeerView, publicChannelsToRevoke: [Peer]?, state: ChannelVisibilityControllerState) -> [ChannelVisibilityEntry] {
var entries: [ChannelVisibilityEntry] = [] var entries: [ChannelVisibilityEntry] = []
if let peer = view.peers[view.peerId] as? TelegramChannel { if let peer = view.peers[view.peerId] as? TelegramChannel {
@ -359,11 +397,39 @@ private func channelVisibilityControllerEntries(view: PeerView, state: ChannelVi
switch selectedType { switch selectedType {
case .publicChannel: case .publicChannel:
entries.append(.editablePublicLink(currentAddressName)) var displayAvailability = false
if let status = state.addressNameStatus { if peer.addressName == nil {
entries.append(.publicLinkStatus(currentAddressName, status)) displayAvailability = publicChannelsToRevoke == nil || !(publicChannelsToRevoke!.isEmpty)
}
if displayAvailability {
if let publicChannelsToRevoke = publicChannelsToRevoke {
entries.append(.publicLinkAvailability(false))
var index: Int32 = 0
for peer in publicChannelsToRevoke.sorted(by: { lhs, rhs in
var lhsDate: Int32 = 0
var rhsDate: Int32 = 0
if let lhs = lhs as? TelegramChannel {
lhsDate = lhs.creationDate
}
if let rhs = rhs as? TelegramChannel {
rhsDate = rhs.creationDate
}
return lhsDate > rhsDate
}) {
entries.append(.existingLinkPeerItem(index, peer, ItemListPeerItemEditing(editable: true, editing: true, revealed: state.revealedRevokePeerId == peer.id), state.revokingPeerId == nil))
index += 1
}
} else {
entries.append(.publicLinkAvailability(true))
}
} else {
entries.append(.editablePublicLink(peer.addressName, currentAddressName))
if let status = state.addressNameValidationStatus {
entries.append(.publicLinkStatus(currentAddressName, status))
}
entries.append(.publicLinkInfo("People can share this link with others and find your group using Telegram search."))
} }
entries.append(.publicLinkInfo("People can share this link with others and find your group using Telegram search."))
case .privateChannel: case .privateChannel:
entries.append(.privateLink((view.cachedData as? CachedChannelData)?.exportedInvitation?.link)) entries.append(.privateLink((view.cachedData as? CachedChannelData)?.exportedInvitation?.link))
entries.append(.publicLinkInfo("People can join your group by following this link. You can revoke the link at any time.")) entries.append(.publicLinkInfo("People can join your group by following this link. You can revoke the link at any time."))
@ -426,7 +492,18 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie
statePromise.set(stateValue.modify { f($0) }) statePromise.set(stateValue.modify { f($0) })
} }
let peersDisablingAddressNameAssignment = Promise<[Peer]?>()
peersDisablingAddressNameAssignment.set(.single(nil) |> then(channelAddressNameAssignmentAvailability(account: account, peerId: peerId) |> mapToSignal { result -> Signal<[Peer]?, NoError> in
if case .addressNameLimitReached = result {
return adminedPublicChannels(account: account)
|> map { Optional($0) }
} else {
return .single([])
}
}))
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var displayPrivateLinkMenuImpl: ((String) -> Void)?
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
@ -436,46 +513,67 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie
let updateAddressNameDisposable = MetaDisposable() let updateAddressNameDisposable = MetaDisposable()
actionsDisposable.add(updateAddressNameDisposable) actionsDisposable.add(updateAddressNameDisposable)
let revokeAddressNameDisposable = MetaDisposable()
actionsDisposable.add(revokeAddressNameDisposable)
let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in
updateState { state in updateState { state in
return state.withUpdatedSelectedType(type) return state.withUpdatedSelectedType(type)
} }
}, updatePublicLinkText: { text in }, updatePublicLinkText: { currentText, text in
if text.isEmpty { if text.isEmpty {
checkAddressNameDisposable.set(nil) checkAddressNameDisposable.set(nil)
updateState { state in updateState { state in
return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameStatus(nil) return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil)
}
} else if currentText == text {
checkAddressNameDisposable.set(nil)
updateState { state in
return state.withUpdatedEditingPublicLinkText(text).withUpdatedAddressNameValidationStatus(nil).withUpdatedAddressNameValidationStatus(nil)
} }
} else { } else {
updateState { state in updateState { state in
return state.withUpdatedEditingPublicLinkText(text) return state.withUpdatedEditingPublicLinkText(text)
} }
checkAddressNameDisposable.set((addressNameAvailability(account: account, domain: .peer(peerId), def: nil, current: text)
checkAddressNameDisposable.set((validateAddressNameInteractive(account: account, domain: .peer(peerId), name: text)
|> deliverOnMainQueue).start(next: { result in |> deliverOnMainQueue).start(next: { result in
updateState { state in updateState { state in
let status: AddressNameStatus return state.withUpdatedAddressNameValidationStatus(result)
switch result {
case let .fail(_, error):
status = .invalid(error)
case .none:
status = .available
case .success:
status = .available
case .progress:
status = .checking
}
return state.withUpdatedAddressNameStatus(status)
} }
})) }))
} }
}, displayPrivateLinkMenu: { }, displayPrivateLinkMenu: { text in
displayPrivateLinkMenuImpl?(text)
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.revealedRevokePeerId) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedRevealedRevokePeerId(peerId)
} else {
return state
}
}
}, revokePeerId: { peerId in
updateState { state in
return state.withUpdatedRevokingPeerId(peerId)
}
revokeAddressNameDisposable.set((updateAddressName(account: account, domain: .peer(peerId), name: nil) |> deliverOnMainQueue).start(error: { _ in
updateState { state in
return state.withUpdatedRevokingPeerId(nil)
}
}, completed: {
updateState { state in
return state.withUpdatedRevokingPeerId(nil)
}
peersDisablingAddressNameAssignment.set(.single([]))
}))
}) })
let peerView = account.viewTracker.peerView(peerId) let peerView = account.viewTracker.peerView(peerId)
let signal = combineLatest(statePromise.get(), peerView) let signal = combineLatest(statePromise.get(), peerView, peersDisablingAddressNameAssignment.get())
|> map { state, view -> (ItemListControllerState, (ItemListNodeState<ChannelVisibilityEntry>, ChannelVisibilityEntry.ItemGenerationArguments)) in |> map { state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState<ChannelVisibilityEntry>, ChannelVisibilityEntry.ItemGenerationArguments)) in
let peer = peerViewMainPeer(view) let peer = peerViewMainPeer(view)
var rightNavigationButton: ItemListNavigationButton? var rightNavigationButton: ItemListNavigationButton?
@ -486,9 +584,9 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie
case .privateChannel: case .privateChannel:
break break
case .publicChannel: case .publicChannel:
if let addressNameStatus = state.addressNameStatus { if let addressNameValidationStatus = state.addressNameValidationStatus {
switch addressNameStatus { switch addressNameValidationStatus {
case .available: case .availability(.available):
break break
default: default:
doneEnabled = false doneEnabled = false
@ -510,7 +608,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie
} }
if let updatedAddressNameValue = updatedAddressNameValue { if let updatedAddressNameValue = updatedAddressNameValue {
updateAddressNameDisposable.set((updatePeerAddressName(account: account, peerId: peerId, username: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue) updateAddressNameDisposable.set((updateAddressName(account: account, domain: .peer(peerId), name: updatedAddressNameValue.isEmpty ? nil : updatedAddressNameValue)
|> deliverOnMainQueue).start(error: { _ in |> deliverOnMainQueue).start(error: { _ in
updateState { state in updateState { state in
return state.withUpdatedUpdatingAddressName(false) return state.withUpdatedUpdatingAddressName(false)
@ -540,7 +638,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie
}) })
let controllerState = ItemListControllerState(title: isGroup ? "Group Type" : "Channel Link", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false) let controllerState = ItemListControllerState(title: isGroup ? "Group Type" : "Channel Link", leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, animateChanges: false)
let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(view: view, state: state), style: .blocks, animateChanges: false) let listState = ItemListNodeState(entries: channelVisibilityControllerEntries(view: view, publicChannelsToRevoke: publicChannelsToRevoke, state: state), style: .blocks, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} |> afterDisposed { } |> afterDisposed {
@ -551,5 +649,34 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in
controller?.dismiss() controller?.dismiss()
} }
displayPrivateLinkMenuImpl = { [weak controller] text in
if let strongController = controller {
var resultItemNode: ListViewItemNode?
let _ = strongController.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListActionItemNode {
if let tag = itemNode.tag as? ChannelVisibilityEntryTag {
if tag == .privateLink {
resultItemNode = itemNode
return true
}
}
}
return false
})
if let resultItemNode = resultItemNode {
let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text("Copy"), action: {
UIPasteboard.general.string = text
})])
strongController.present(contextMenuController, in: .window, with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in
if let resultItemNode = resultItemNode {
return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0))
} else {
return nil
}
}))
}
}
}
return controller return controller
} }

View File

@ -213,7 +213,7 @@ class ChatControllerNode: ASDisplayNode {
} }
if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode {
inputMediaNode.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) let _ = inputMediaNode.updateLayout(width: layout.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState)
} }
var insets: UIEdgeInsets var insets: UIEdgeInsets
@ -236,10 +236,9 @@ class ChatControllerNode: ASDisplayNode {
var duration: Double = 0.0 var duration: Double = 0.0
var curve: UInt = 0 var curve: UInt = 0
var animated = true
switch transition { switch transition {
case .immediate: case .immediate:
animated = false break
case let .animated(animationDuration, animationCurve): case let .animated(animationDuration, animationCurve):
duration = animationDuration duration = animationDuration
switch animationCurve { switch animationCurve {
@ -340,14 +339,14 @@ class ChatControllerNode: ASDisplayNode {
var inputPanelsHeight: CGFloat = 0.0 var inputPanelsHeight: CGFloat = 0.0
var inputPanelFrame: CGRect? var inputPanelFrame: CGRect?
if let inputPanelNode = self.inputPanelNode { if self.inputPanelNode != nil {
assert(inputPanelSize != nil) assert(inputPanelSize != nil)
inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height))
inputPanelsHeight += inputPanelSize!.height inputPanelsHeight += inputPanelSize!.height
} }
var accessoryPanelFrame: CGRect? var accessoryPanelFrame: CGRect?
if let accessoryPanelNode = self.accessoryPanelNode { if self.accessoryPanelNode != nil {
assert(accessoryPanelSize != nil) assert(accessoryPanelSize != nil)
accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height))
inputPanelsHeight += accessoryPanelSize!.height inputPanelsHeight += accessoryPanelSize!.height
@ -429,7 +428,7 @@ class ChatControllerNode: ASDisplayNode {
if let dismissedInputPanelNode = dismissedInputPanelNode { if let dismissedInputPanelNode = dismissedInputPanelNode {
var frameCompleted = false var frameCompleted = false
var alphaCompleted = false var alphaCompleted = false
var completed = { [weak self, weak dismissedInputPanelNode] in let completed = { [weak self, weak dismissedInputPanelNode] in
if let strongSelf = self, let dismissedInputPanelNode = dismissedInputPanelNode, strongSelf.inputPanelNode === dismissedInputPanelNode { if let strongSelf = self, let dismissedInputPanelNode = dismissedInputPanelNode, strongSelf.inputPanelNode === dismissedInputPanelNode {
return return
} }
@ -452,7 +451,7 @@ class ChatControllerNode: ASDisplayNode {
if let dismissedAccessoryPanelNode = dismissedAccessoryPanelNode { if let dismissedAccessoryPanelNode = dismissedAccessoryPanelNode {
var frameCompleted = false var frameCompleted = false
var alphaCompleted = false var alphaCompleted = false
var completed = { [weak dismissedAccessoryPanelNode] in let completed = { [weak dismissedAccessoryPanelNode] in
if frameCompleted && alphaCompleted { if frameCompleted && alphaCompleted {
dismissedAccessoryPanelNode?.removeFromSupernode() dismissedAccessoryPanelNode?.removeFromSupernode()
} }
@ -475,7 +474,7 @@ class ChatControllerNode: ASDisplayNode {
if let dismissedInputContextPanelNode = dismissedInputContextPanelNode { if let dismissedInputContextPanelNode = dismissedInputContextPanelNode {
var frameCompleted = false var frameCompleted = false
var animationCompleted = false var animationCompleted = false
var completed = { [weak dismissedInputContextPanelNode] in let completed = { [weak dismissedInputContextPanelNode] in
if let dismissedInputContextPanelNode = dismissedInputContextPanelNode, frameCompleted, animationCompleted { if let dismissedInputContextPanelNode = dismissedInputContextPanelNode, frameCompleted, animationCompleted {
dismissedInputContextPanelNode.removeFromSupernode() dismissedInputContextPanelNode.removeFromSupernode()
} }
@ -496,8 +495,16 @@ class ChatControllerNode: ASDisplayNode {
} }
if let dismissedInputNode = dismissedInputNode { if let dismissedInputNode = dismissedInputNode {
transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - CGFloat(FLT_EPSILON)), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), completion: { [weak dismissedInputNode] _ in transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - CGFloat(FLT_EPSILON)), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), completion: { [weak self, weak dismissedInputNode] completed in
dismissedInputNode?.removeFromSupernode() if completed {
if let strongSelf = self {
if strongSelf.inputNode !== dismissedInputNode {
dismissedInputNode?.removeFromSupernode()
}
} else {
dismissedInputNode?.removeFromSupernode()
}
}
}) })
} }
} }
@ -521,14 +528,14 @@ class ChatControllerNode: ASDisplayNode {
} }
if self.chatPresentationInterfaceState != chatPresentationInterfaceState { if self.chatPresentationInterfaceState != chatPresentationInterfaceState {
var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState)
var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState
self.chatPresentationInterfaceState = chatPresentationInterfaceState self.chatPresentationInterfaceState = chatPresentationInterfaceState
let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil
var extendedSearchLayout = false var extendedSearchLayout = false
if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult { if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult {
if case let .contextRequestResult(peer, _) = inputQueryResult { if case .contextRequestResult = inputQueryResult {
extendedSearchLayout = true extendedSearchLayout = true
} }
} }
@ -612,7 +619,7 @@ class ChatControllerNode: ASDisplayNode {
self.scheduledLayoutTransitionRequest = (requestId, transition) self.scheduledLayoutTransitionRequest = (requestId, transition)
(self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in
if let strongSelf = self { if let strongSelf = self {
if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest { if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId {
strongSelf.scheduledLayoutTransitionRequest = nil strongSelf.scheduledLayoutTransitionRequest = nil
strongSelf.requestLayout(currentRequestTransition) strongSelf.requestLayout(currentRequestTransition)
} }
@ -626,7 +633,7 @@ class ChatControllerNode: ASDisplayNode {
let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction) let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction)
inputNode.interfaceInteraction = interfaceInteraction inputNode.interfaceInteraction = interfaceInteraction
self.inputMediaNode = inputNode self.inputMediaNode = inputNode
inputNode.updateLayout(width: self.bounds.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) let _ = inputNode.updateLayout(width: self.bounds.size.width, transition: .immediate, interfaceState: self.chatPresentationInterfaceState)
} }
} }
} }

View File

@ -5,15 +5,6 @@ import Display
import TelegramCore import TelegramCore
private let composeButtonImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in private let composeButtonImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
/*
<svg width="24px" height="24px" viewBox="0 -1 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs></defs>
<path d="M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z" id="Edit" stroke="none" fill="#000000" fill-rule="evenodd"></path>
</svg>
*/
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor(0x007ee5).cgColor) context.setFillColor(UIColor(0x007ee5).cgColor)
try? drawSvgPath(context, path: "M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z ") try? drawSvgPath(context, path: "M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z ")
@ -32,6 +23,8 @@ public class ChatListController: TelegramController {
private var titleDisposable: Disposable? private var titleDisposable: Disposable?
private var badgeDisposable: Disposable? private var badgeDisposable: Disposable?
private var dismissSearchOnDisappear = false
public override init(account: Account) { public override init(account: Account) {
self.account = account self.account = account
@ -106,7 +99,7 @@ public class ChatListController: TelegramController {
self.chatListDisplayNode.navigationBar = self.navigationBar self.chatListDisplayNode.navigationBar = self.navigationBar
self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in
self?.deactivateSearch() self?.deactivateSearch(animated: true)
} }
self.chatListDisplayNode.chatListNode.activateSearch = { [weak self] in self.chatListDisplayNode.chatListNode.activateSearch = { [weak self] in
@ -161,6 +154,7 @@ public class ChatListController: TelegramController {
} }
strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in strongSelf.openMessageFromSearchDisposable.set((storedPeer |> deliverOnMainQueue).start(completed: { [weak strongSelf] in
if let strongSelf = strongSelf { if let strongSelf = strongSelf {
strongSelf.dismissSearchOnDisappear = true
(strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peer.id)) (strongSelf.navigationController as? NavigationController)?.pushViewController(ChatController(account: strongSelf.account, peerId: peer.id))
} }
})) }))
@ -176,6 +170,11 @@ public class ChatListController: TelegramController {
override public func viewDidDisappear(_ animated: Bool) { override public func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated) super.viewDidDisappear(animated)
if self.dismissSearchOnDisappear {
self.dismissSearchOnDisappear = false
self.deactivateSearch(animated: false)
}
} }
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -208,10 +207,10 @@ public class ChatListController: TelegramController {
} }
} }
private func deactivateSearch() { private func deactivateSearch(animated: Bool) {
if !self.displayNavigationBar { if !self.displayNavigationBar {
self.chatListDisplayNode.deactivateSearch() self.chatListDisplayNode.deactivateSearch(animated: animated)
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate)
} }
} }

View File

@ -107,7 +107,7 @@ class ChatListControllerNode: ASDisplayNode {
} }
} }
func deactivateSearch() { func deactivateSearch(animated: Bool) {
if let searchDisplayController = self.searchDisplayController { if let searchDisplayController = self.searchDisplayController {
var maybePlaceholderNode: SearchBarPlaceholderNode? var maybePlaceholderNode: SearchBarPlaceholderNode?
self.chatListNode.forEachItemNode { node in self.chatListNode.forEachItemNode { node in
@ -116,7 +116,7 @@ class ChatListControllerNode: ASDisplayNode {
} }
} }
searchDisplayController.deactivate(placeholder: maybePlaceholderNode) searchDisplayController.deactivate(placeholder: maybePlaceholderNode, animated: animated)
self.searchDisplayController = nil self.searchDisplayController = nil
} }
} }

View File

@ -422,7 +422,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
self.enqueuedRecentTransitions.remove(at: 0) self.enqueuedRecentTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions() var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing)
if firstTime { if firstTime {
} else { } else {
} }
@ -447,7 +447,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode {
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions() var options = ListViewDeleteAndInsertOptions()
options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousDrawing)
if firstTime { if firstTime {
} else { } else {
//options.insert(.AnimateAlpha) //options.insert(.AnimateAlpha)

View File

@ -100,9 +100,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
} }
} }
if entities == nil { if entities == nil {
let parsedEntities = generateTextEntities(message.text) var generateEntities = false
if !parsedEntities.isEmpty { for media in message.media {
entities = TextEntitiesMessageAttribute(entities: parsedEntities) if media is TelegramMediaImage || media is TelegramMediaFile {
generateEntities = true
break
}
}
if generateEntities {
let parsedEntities = generateTextEntities(message.text)
if !parsedEntities.isEmpty {
entities = TextEntitiesMessageAttribute(entities: parsedEntities)
}
} }
} }
if let entities = entities { if let entities = entities {

View File

@ -322,11 +322,11 @@ private enum GroupInfoEntry: ItemListNodeEntry {
}) })
case let .membersAdmins(count): case let .membersAdmins(count):
return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: {
arguments.pushController(ChannelAdminsController(account: arguments.account, peerId: arguments.peerId)) arguments.pushController(channelAdminsController(account: arguments.account, peerId: arguments.peerId))
}) })
case let .membersBlacklist(count): case let .membersBlacklist(count):
return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: { return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: {
arguments.pushController(channelBlacklistController(account: arguments.account, peerId: arguments.peerId))
}) })
case let .member(_, _, peer, presence, memberStatus, editing, enabled): case let .member(_, _, peer, presence, memberStatus, editing, enabled):
let label: String? let label: String?
@ -524,10 +524,10 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo
entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText)) entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText))
if let adminCount = cachedChannelData.participantsSummary.adminCount { if let adminCount = cachedChannelData.participantsSummary.adminCount {
entries.append(GroupInfoEntry.membersAdmins(count: adminCount)) entries.append(GroupInfoEntry.membersAdmins(count: Int(adminCount)))
} }
if let bannedCount = cachedChannelData.participantsSummary.bannedCount { if let bannedCount = cachedChannelData.participantsSummary.bannedCount {
entries.append(GroupInfoEntry.membersBlacklist(count: bannedCount)) entries.append(GroupInfoEntry.membersBlacklist(count: Int(bannedCount)))
} }
} }
} else { } else {

View File

@ -1,677 +0,0 @@
/*import Foundation
import Postbox
import TelegramCore
import SwiftSignalKit
import Display
private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed()
private enum GroupInfoSection: ItemListSectionId {
case info
case about
case sharedMediaAndNotifications
case infoManagement
case memberManagement
case members
case leave
}
enum GroupInfoMemberStatus {
case member
case admin
}
private struct GroupPeerEntryStableId: PeerInfoEntryStableId {
let peerId: PeerId
func isEqual(to: PeerInfoEntryStableId) -> Bool {
if let to = to as? GroupPeerEntryStableId, to.peerId == self.peerId {
return true
} else {
return false
}
}
var hashValue: Int {
return self.peerId.hashValue
}
}
enum GroupInfoEntry: PeerInfoEntry {
case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState)
case setGroupPhoto
case aboutHeader
case about(text: String)
case sharedMedia
case notifications(settings: PeerNotificationSettings?)
case groupTypeSetup(isPublic: Bool)
case groupDescriptionSetup(text: String)
case groupManagementInfoLabel(text: String)
case membersAdmins(count: Int)
case membersBlacklist(count: Int)
case usersHeader
case addMember
case member(index: Int, peerId: PeerId, peer: Peer?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus)
case leave
var section: ItemListSectionId {
switch self {
case .info, .setGroupPhoto:
return GroupInfoSection.info.rawValue
case .aboutHeader, .about:
return GroupInfoSection.about.rawValue
case .sharedMedia, .notifications:
return GroupInfoSection.sharedMediaAndNotifications.rawValue
case .groupTypeSetup, .groupDescriptionSetup, .groupManagementInfoLabel:
return GroupInfoSection.infoManagement.rawValue
case .membersAdmins, .membersBlacklist:
return GroupInfoSection.memberManagement.rawValue
case .usersHeader, .addMember, .member:
return GroupInfoSection.members.rawValue
case .leave:
return GroupInfoSection.leave.rawValue
}
}
func isEqual(to: PeerInfoEntry) -> Bool {
guard let entry = to as? GroupInfoEntry else {
return false
}
switch self {
case let .info(lhsPeer, lhsCachedData, lhsState):
if case let .info(rhsPeer, rhsCachedData, rhsState) = entry {
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer == nil) != (rhsPeer != nil) {
return false
}
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
if !lhsCachedData.isEqual(to: rhsCachedData) {
return false
}
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
return false
}
if lhsState != rhsState {
return false
}
return true
} else {
return false
}
case .setGroupPhoto:
if case .setGroupPhoto = entry {
return true
} else {
return false
}
case .aboutHeader:
if case .aboutHeader = entry {
return true
} else {
return false
}
case let .about(lhsText):
switch entry {
case .about(lhsText):
return true
default:
return false
}
case .sharedMedia:
switch entry {
case .sharedMedia:
return true
default:
return false
}
case let .notifications(lhsSettings):
switch entry {
case let .notifications(rhsSettings):
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
return lhsSettings.isEqual(to: rhsSettings)
} else if (lhsSettings != nil) != (rhsSettings != nil) {
return false
}
return true
default:
return false
}
case let .groupTypeSetup(isPublic):
if case .groupTypeSetup(isPublic) = entry {
return true
} else {
return false
}
case let .groupDescriptionSetup(text):
if case .groupDescriptionSetup(text) = entry {
return true
} else {
return false
}
case let .groupManagementInfoLabel(text):
if case .groupManagementInfoLabel(text) = entry {
return true
} else {
return false
}
case let .membersAdmins(lhsCount):
if case let .membersAdmins(rhsCount) = entry, lhsCount == rhsCount {
return true
} else {
return false
}
case let .membersBlacklist(lhsCount):
if case let .membersBlacklist(rhsCount) = entry, lhsCount == rhsCount {
return true
} else {
return false
}
case .usersHeader:
if case .usersHeader = entry {
return true
} else {
return false
}
case .addMember:
if case .addMember = entry {
return true
} else {
return false
}
case let .member(lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus):
if case let .member(rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus) = entry {
if lhsIndex != rhsIndex {
return false
}
if lhsMemberStatus != rhsMemberStatus {
return false
}
if lhsPeerId != rhsPeerId {
return false
}
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
if !lhsPeer.isEqual(rhsPeer) {
return false
}
} else if (lhsPeer != nil) != (rhsPeer != nil) {
return false
}
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if !lhsPresence.isEqual(to: rhsPresence) {
return false
}
} else if (lhsPresence != nil) != (rhsPresence != nil) {
return false
}
return true
} else {
return false
}
case .leave:
if case .leave = entry {
return true
} else {
return false
}
}
}
var stableId: PeerInfoEntryStableId {
switch self {
case let .member(_, peerId, _, _, _):
return GroupPeerEntryStableId(peerId: peerId)
default:
return IntPeerInfoEntryStableId(value: self.sortIndex)
}
}
private var sortIndex: Int {
switch self {
case .info:
return 0
case .setGroupPhoto:
return 1
case .aboutHeader:
return 2
case .about:
return 3
case .notifications:
return 4
case .sharedMedia:
return 5
case .groupTypeSetup:
return 6
case .groupDescriptionSetup:
return 7
case .groupManagementInfoLabel:
return 8
case .membersAdmins:
return 9
case .membersBlacklist:
return 10
case .usersHeader:
return 11
case .addMember:
return 12
case let .member(index, _, _, _, _):
return 20 + index
case .leave:
return 1000000
}
}
func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool {
guard let other = entry as? GroupInfoEntry else {
return false
}
return self.sortIndex < other.sortIndex
}
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
switch self {
case let .info(peer, cachedData, state):
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks, editingNameUpdated: { editingName in
})
case .setGroupPhoto:
return ItemListActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
})
case let .notifications(settings):
let label: String
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
label = "Disabled"
} else {
label = "Enabled"
}
return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: {
interaction.changeNotificationMuteSettings()
})
case .sharedMedia:
return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: {
interaction.openSharedMedia()
})
case .addMember:
return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section, action: {
})
case let .groupTypeSetup(isPublic):
return ItemListDisclosureItem(title: "Group Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .blocks, action: {
})
case let .groupDescriptionSetup(text):
return ItemListMultilineInputItem(text: text, sectionId: self.section, textUpdated: { updatedText in
interaction.updateState { state in
if let state = state as? GroupInfoState, let editingState = state.editingState {
return state.withUpdatedEditingState(editingState.withUpdatedEditingDescriptionText(updatedText))
}
return state
}
}, action: {
})
case let .membersAdmins(count):
return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: {
})
case let .membersBlacklist(count):
return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: {
})
case let .member(_, _, peer, presence, memberStatus):
let label: String?
switch memberStatus {
case .admin:
label = "admin"
case .member:
label = nil
}
return ItemListPeerItem(account: account, peer: peer, presence: presence, label: label, sectionId: self.section, action: {
if let peer = peer {
interaction.openPeerInfo(peer.id)
}
})
case .leave:
return ItemListActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
})
default:
preconditionFailure()
}
}
}
private struct GroupInfoState: PeerInfoState {
let editingState: GroupInfoEditingState?
let updatingName: ItemListAvatarAndNameInfoItemName?
func isEqual(to: PeerInfoState) -> Bool {
if let to = to as? GroupInfoState {
if self.editingState != to.editingState {
return false
}
if self.updatingName != to.updatingName {
return false
}
return true
} else {
return false
}
}
func withUpdatedEditingState(_ editingState: GroupInfoEditingState?) -> GroupInfoState {
return GroupInfoState(editingState: editingState, updatingName: self.updatingName)
}
func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> GroupInfoState {
return GroupInfoState(editingState: self.editingState, updatingName: updatingName)
}
}
private struct GroupInfoEditingState: Equatable {
let editingName: ItemListAvatarAndNameInfoItemName?
let editingDescriptionText: String
func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> GroupInfoEditingState {
return GroupInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText)
}
static func ==(lhs: GroupInfoEditingState, rhs: GroupInfoEditingState) -> Bool {
if lhs.editingName != rhs.editingName {
return false
}
if lhs.editingDescriptionText != rhs.editingDescriptionText {
return false
}
return true
}
}
func groupInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries {
var entries: [PeerInfoEntry] = []
if let peer = peerViewMainPeer(view) {
let infoState = ItemListAvatarAndNameInfoItemState(editingName: (state as? GroupInfoState)?.editingState?.editingName, updatingName: (state as? GroupInfoState)?.updatingName)
entries.append(GroupInfoEntry.info(peer: peer, cachedData: view.cachedData, state: infoState))
}
var highlightAdmins = false
var canManageGroup = false
if let group = view.peers[view.peerId] as? TelegramGroup {
if group.flags.contains(.adminsEnabled) {
highlightAdmins = true
switch group.role {
case .admin, .creator:
canManageGroup = true
case .member:
break
}
} else {
canManageGroup = true
}
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
highlightAdmins = true
switch channel.role {
case .creator:
canManageGroup = true
case .editor, .moderator, .member:
break
}
}
if canManageGroup {
entries.append(GroupInfoEntry.setGroupPhoto)
}
if let editingState = (state as? GroupInfoState)?.editingState {
if let cachedChannelData = view.cachedData as? CachedChannelData {
entries.append(GroupInfoEntry.groupTypeSetup(isPublic: cachedChannelData.exportedInvitation != nil))
entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText))
if let adminCount = cachedChannelData.participantsSummary.adminCount {
entries.append(GroupInfoEntry.membersAdmins(count: adminCount))
}
if let bannedCount = cachedChannelData.participantsSummary.bannedCount {
entries.append(GroupInfoEntry.membersBlacklist(count: bannedCount))
}
}
} else {
entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings))
entries.append(GroupInfoEntry.sharedMedia)
}
if canManageGroup {
entries.append(GroupInfoEntry.addMember)
}
if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants {
let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in
let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence
let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if lhsPresence.status < rhsPresence.status {
return false
} else if lhsPresence.status > rhsPresence.status {
return true
}
} else if let _ = lhsPresence {
return true
} else if let _ = rhsPresence {
return false
}
switch lhs {
case .creator:
return false
case let .admin(lhsId, _, lhsInvitedAt):
switch rhs {
case .creator:
return true
case let .admin(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .member(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
}
case let .member(lhsId, _, lhsInvitedAt):
switch rhs {
case .creator:
return true
case let .admin(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .member(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
}
}
return false
})
for i in 0 ..< sortedParticipants.count {
if let peer = view.peers[sortedParticipants[i].peerId] {
let memberStatus: GroupInfoMemberStatus
if highlightAdmins {
switch sortedParticipants[i] {
case .admin, .creator:
memberStatus = .admin
case .member:
memberStatus = .member
}
} else {
memberStatus = .member
}
entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus))
}
}
} else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants {
let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in
let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence
let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
if lhsPresence.status < rhsPresence.status {
return false
} else if lhsPresence.status > rhsPresence.status {
return true
}
} else if let _ = lhsPresence {
return true
} else if let _ = rhsPresence {
return false
}
switch lhs {
case .creator:
return false
case let .moderator(lhsId, _, lhsInvitedAt):
switch rhs {
case .creator:
return true
case let .moderator(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .editor(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .member(rhsId, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
}
case let .editor(lhsId, _, lhsInvitedAt):
switch rhs {
case .creator:
return true
case let .moderator(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .editor(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .member(rhsId, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
}
case let .member(lhsId, lhsInvitedAt):
switch rhs {
case .creator:
return true
case let .moderator(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .editor(rhsId, _, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
case let .member(rhsId, rhsInvitedAt):
if lhsInvitedAt == rhsInvitedAt {
return lhsId.id < rhsId.id
}
return lhsInvitedAt > rhsInvitedAt
}
}
return false
})
for i in 0 ..< sortedParticipants.count {
if let peer = view.peers[sortedParticipants[i].peerId] {
let memberStatus: GroupInfoMemberStatus
if highlightAdmins {
switch sortedParticipants[i] {
case .moderator, .editor, .creator:
memberStatus = .admin
case .member:
memberStatus = .member
}
} else {
memberStatus = .member
}
entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus))
}
}
}
if let group = view.peers[view.peerId] as? TelegramGroup {
if case .Member = group.membership {
entries.append(GroupInfoEntry.leave)
}
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
if case .member = channel.participationStatus {
entries.append(GroupInfoEntry.leave)
}
}
var leftNavigationButton: PeerInfoNavigationButton?
var rightNavigationButton: PeerInfoNavigationButton?
if canManageGroup {
if let state = state as? GroupInfoState, let _ = state.editingState {
leftNavigationButton = PeerInfoNavigationButton(title: "Cancel", action: { state in
if state == nil {
return GroupInfoState(editingState: nil, updatingName: nil)
} else if let state = state as? GroupInfoState {
return state.withUpdatedEditingState(nil)
} else {
return state
}
})
rightNavigationButton = PeerInfoNavigationButton(title: "Done", action: { state in
if state == nil {
return GroupInfoState(editingState: nil, updatingName: nil)
} else if let state = state as? GroupInfoState {
return state.withUpdatedEditingState(nil)
} else {
return state
}
})
} else {
var editingName: ItemListAvatarAndNameInfoItemName?
if let peer = peerViewMainPeer(view) {
editingName = ItemListAvatarAndNameInfoItemName(peer.indexName)
}
let editingDescriptionText: String
if let cachedChannelData = view.cachedData as? CachedChannelData, let about = cachedChannelData.about {
editingDescriptionText = about
} else {
editingDescriptionText = ""
}
rightNavigationButton = PeerInfoNavigationButton(title: "Edit", action: { state in
if state == nil {
return GroupInfoState(editingState: GroupInfoEditingState(editingName: editingName, editingDescriptionText: editingDescriptionText), updatingName: nil)
} else if let state = state as? GroupInfoState {
return state.withUpdatedEditingState(GroupInfoEditingState(editingName: editingName, editingDescriptionText: editingDescriptionText))
} else {
return state
}
})
}
}
return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton)
}
*/

View File

@ -21,14 +21,16 @@ class ItemListActionItem: ListViewItem, ItemListItem {
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let action: () -> Void let action: () -> Void
let tag: Any?
init(title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void) { init(title: String, kind: ItemListActionKind, alignment: ItemListActionAlignment, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, tag: Any? = nil) {
self.title = title self.title = title
self.kind = kind self.kind = kind
self.alignment = alignment self.alignment = alignment
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.action = action self.action = action
self.tag = tag
} }
func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) { func nodeConfiguredForWidth(async: @escaping (@escaping () -> Void) -> Void, width: CGFloat, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, () -> Void)) -> Void) {
@ -80,6 +82,12 @@ class ItemListActionItemNode: ListViewItemNode {
private let titleNode: TextNode private let titleNode: TextNode
private var item: ItemListActionItem?
var tag: Any? {
return self.item?.tag
}
init() { init() {
self.backgroundNode = ASDisplayNode() self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true self.backgroundNode.isLayerBacked = true
@ -141,6 +149,8 @@ class ItemListActionItemNode: ListViewItemNode {
return (layout, { [weak self] in return (layout, { [weak self] in
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item
let _ = titleApply() let _ = titleApply()
let leftInset: CGFloat let leftInset: CGFloat

View File

@ -0,0 +1,14 @@
import Foundation
import AsyncDisplayKit
import Display
protocol ItemListControllerEmptyStateItem {
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode
}
class ItemListControllerEmptyStateItemNode: ASDisplayNode {
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
}
}

View File

@ -37,6 +37,7 @@ enum ItemListStyle {
private struct ItemListNodeTransition { private struct ItemListNodeTransition {
let entries: ItemListNodeEntryTransition let entries: ItemListNodeEntryTransition
let updateStyle: ItemListStyle? let updateStyle: ItemListStyle?
let emptyStateItem: ItemListControllerEmptyStateItem?
let firstTime: Bool let firstTime: Bool
let animated: Bool let animated: Bool
let animateAlpha: Bool let animateAlpha: Bool
@ -45,11 +46,13 @@ private struct ItemListNodeTransition {
struct ItemListNodeState<Entry: ItemListNodeEntry> { struct ItemListNodeState<Entry: ItemListNodeEntry> {
let entries: [Entry] let entries: [Entry]
let style: ItemListStyle let style: ItemListStyle
let emptyStateItem: ItemListControllerEmptyStateItem?
let animateChanges: Bool let animateChanges: Bool
init(entries: [Entry], style: ItemListStyle, animateChanges: Bool = true) { init(entries: [Entry], style: ItemListStyle, emptyStateItem: ItemListControllerEmptyStateItem? = nil, animateChanges: Bool = true) {
self.entries = entries self.entries = entries
self.style = style self.style = style
self.emptyStateItem = emptyStateItem
self.animateChanges = animateChanges self.animateChanges = animateChanges
} }
} }
@ -62,10 +65,13 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
private var didSetReady = false private var didSetReady = false
let listNode: ListView let listNode: ListView
private var emptyStateItem: ItemListControllerEmptyStateItem?
private var emptyStateNode: ItemListControllerEmptyStateItemNode?
private let transitionDisposable = MetaDisposable() private let transitionDisposable = MetaDisposable()
private var enqueuedTransitions: [ItemListNodeTransition] = [] private var enqueuedTransitions: [ItemListNodeTransition] = []
private var hadValidLayout = false private var validLayout: (ContainerViewLayout, CGFloat)?
var dismiss: (() -> Void)? var dismiss: (() -> Void)?
@ -89,7 +95,7 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
if previous?.style != state.style { if previous?.style != state.style {
updatedStyle = state.style updatedStyle = state.style
} }
return ItemListNodeTransition(entries: transition, updateStyle: updatedStyle, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && !state.animateChanges) return ItemListNodeTransition(entries: transition, updateStyle: updatedStyle, emptyStateItem: state.emptyStateItem, firstTime: previous == nil, animated: previous != nil && state.animateChanges, animateAlpha: previous != nil && !state.animateChanges)
}) |> deliverOnMainQueue).start(next: { [weak self] transition in }) |> deliverOnMainQueue).start(next: { [weak self] transition in
if let strongSelf = self { if let strongSelf = self {
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
@ -144,15 +150,20 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if !self.hadValidLayout { if let emptyStateNode = self.emptyStateNode {
self.hadValidLayout = true emptyStateNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
let dequeue = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
if dequeue {
self.dequeueTransitions() self.dequeueTransitions()
} }
} }
private func enqueueTransition(_ transition: ItemListNodeTransition) { private func enqueueTransition(_ transition: ItemListNodeTransition) {
self.enqueuedTransitions.append(transition) self.enqueuedTransitions.append(transition)
if self.hadValidLayout { if self.validLayout != nil {
self.dequeueTransitions() self.dequeueTransitions()
} }
} }
@ -177,6 +188,7 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
options.insert(.AnimateInsertion) options.insert(.AnimateInsertion)
} else if transition.animateAlpha { } else if transition.animateAlpha {
options.insert(.PreferSynchronousResourceLoading) options.insert(.PreferSynchronousResourceLoading)
options.insert(.PreferSynchronousDrawing)
options.insert(.AnimateAlpha) options.insert(.AnimateAlpha)
} }
self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in self.listNode.transaction(deleteIndices: transition.entries.deletions, insertIndicesAndItems: transition.entries.insertions, updateIndicesAndItems: transition.entries.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
@ -187,6 +199,31 @@ final class ItemListNode<Entry: ItemListNodeEntry>: ASDisplayNode {
} }
} }
}) })
var updateEmptyStateItem = false
if let emptyStateItem = self.emptyStateItem, let updatedEmptyStateItem = transition.emptyStateItem {
updateEmptyStateItem = !emptyStateItem.isEqual(to: updatedEmptyStateItem)
} else if (self.emptyStateItem != nil) != (transition.emptyStateItem != nil) {
updateEmptyStateItem = true
}
if updateEmptyStateItem {
self.emptyStateItem = transition.emptyStateItem
if let emptyStateItem = transition.emptyStateItem {
let updatedNode = emptyStateItem.node(current: self.emptyStateNode)
if let emptyStateNode = self.emptyStateNode, updatedNode !== emptyStateNode {
emptyStateNode.removeFromSupernode()
}
if self.emptyStateNode !== updatedNode {
self.emptyStateNode = updatedNode
if let validLayout = self.validLayout {
updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
self.addSubnode(updatedNode)
}
} else if let emptyStateNode = self.emptyStateNode {
emptyStateNode.removeFromSupernode()
self.emptyStateNode = nil
}
}
} }
} }
} }

View File

@ -176,7 +176,7 @@ class ItemListRevealOptionsItemNode: ListViewItemNode {
override func layout() { override func layout() {
if let revealNode = self.revealNode { if let revealNode = self.revealNode {
let height = self.bounds.size.height let height = self.contentSize.height
let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height)) let revealSize = revealNode.measure(CGSize(width: CGFloat.greatestFiniteMagnitude, height: height))
revealNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) revealNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize)
} }

View File

@ -0,0 +1,47 @@
import Foundation
import AsyncDisplayKit
import Display
final class ItemListLoadingIndicatorEmptyStateItem: ItemListControllerEmptyStateItem {
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
return to is ItemListLoadingIndicatorEmptyStateItem
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
if let current = current as? ItemListLoadingIndicatorEmptyStateItemNode {
return current
} else {
return ItemListLoadingIndicatorEmptyStateItemNode()
}
}
}
final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private var indicator: UIActivityIndicatorView?
private var validLayout: (ContainerViewLayout, CGFloat)?
override func didLoad() {
super.didLoad()
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
self.indicator = indicator
self.view.addSubview(indicator)
if let layout = self.validLayout {
self.updateLayout(layout: layout.0, navigationBarHeight: layout.1, transition: .immediate)
}
indicator.startAnimating()
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
if let indicator = self.indicator {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar])
insets.top += navigationBarHeight
let size = indicator.bounds.size
indicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - size.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - size.height) / 2.0)), size: size)
}
}
}

View File

@ -27,6 +27,7 @@ struct ItemListPeerItemEditing: Equatable {
enum ItemListPeerItemText { enum ItemListPeerItemText {
case activity case activity
case text(String) case text(String)
case none
} }
final class ItemListPeerItem: ListViewItem, ItemListItem { final class ItemListPeerItem: ListViewItem, ItemListItem {
@ -65,7 +66,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem {
node.insets = layout.insets node.insets = layout.insets
completion(node, { completion(node, {
return (nil, { apply(false) }) return (node.avatarNode.ready, { apply(false) })
}) })
} }
} }
@ -113,7 +114,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
private let highlightedBackgroundNode: ASDisplayNode private let highlightedBackgroundNode: ASDisplayNode
private var disabledOverlayNode: ASDisplayNode? private var disabledOverlayNode: ASDisplayNode?
private let avatarNode: AvatarNode fileprivate let avatarNode: AvatarNode
private let titleNode: TextNode private let titleNode: TextNode
private let labelNode: TextNode private let labelNode: TextNode
private let statusNode: TextNode private let statusNode: TextNode
@ -235,6 +236,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
} }
case let .text(text): case let .text(text):
statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: UIColor(0xa6a6a6)) statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: UIColor(0xa6a6a6))
case .none:
break
} }
if let label = item.label { if let label = item.label {
@ -368,7 +371,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))) transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))) transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)))
transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 5.0), size: titleLayout.size)) transition.updateFrame(node: strongSelf.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: statusAttributedString == nil ? 13.0 : 5.0), size: titleLayout.size))
transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: statusLayout.size)) transition.updateFrame(node: strongSelf.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: statusLayout.size))
transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - 15.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0 - labelLayout.size.height / 10.0)), size: labelLayout.size)) transition.updateFrame(node: strongSelf.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - labelLayout.size.width - 15.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0 - labelLayout.size.height / 10.0)), size: labelLayout.size))
@ -450,7 +453,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode {
editingOffset = 0.0 editingOffset = 0.0
} }
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 5.0), size: self.titleNode.bounds.size)) transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: self.titleNode.frame.minY), size: self.titleNode.bounds.size))
transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: self.statusNode.bounds.size)) transition.updateFrame(node: self.statusNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: 25.0), size: self.statusNode.bounds.size))
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - self.labelNode.bounds.size.width - 15.0, y: floor((self.contentSize.height - self.labelNode.bounds.size.height) / 2.0 - self.labelNode.bounds.size.height / 10.0)), size: self.labelNode.bounds.size)) transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: revealOffset + width - self.labelNode.bounds.size.width - 15.0, y: floor((self.contentSize.height - self.labelNode.bounds.size.height) / 2.0 - self.labelNode.bounds.size.height / 10.0)), size: self.labelNode.bounds.size))

View File

@ -0,0 +1,65 @@
import Foundation
import AsyncDisplayKit
import Display
final class ItemListTextEmptyStateItem: ItemListControllerEmptyStateItem {
let text: String
init(text: String) {
self.text = text
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let to = to as? ItemListTextEmptyStateItem {
return self.text == to.text
} else {
return false
}
}
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
let result: ItemListTextEmptyStateItemNode
if let current = current as? ItemListTextEmptyStateItemNode {
result = current
} else {
result = ItemListTextEmptyStateItemNode()
}
result.updateText(text: self.text)
return result
}
}
final class ItemListTextEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
private let textNode: ASTextNode
private var validLayout: (ContainerViewLayout, CGFloat)?
private var text: String?
override init() {
self.textNode = ASTextNode()
self.textNode.isLayerBacked = true
super.init()
self.addSubnode(self.textNode)
}
func updateText(text: String) {
if self.text != text {
self.text = text
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .gray, paragraphAlignment: .center)
if let validLayout = self.validLayout {
self.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
}
}
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [.statusBar])
insets.top += navigationBarHeight
let textSize = self.textNode.measure(CGSize(width: layout.size.width - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
self.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - textSize.height) / 2.0)), size: textSize)
}
}

View File

@ -91,6 +91,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
self.textField = SearchBarTextField() self.textField = SearchBarTextField()
self.textField.font = Font.regular(15.0) self.textField.font = Font.regular(15.0)
self.textField.autocorrectionType = .no
self.textField.returnKeyType = .done self.textField.returnKeyType = .done
self.cancelButton = ASButtonNode() self.cancelButton = ASButtonNode()
@ -166,7 +167,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
self.textField.placeholderLabel.isHidden = false self.textField.placeholderLabel.isHidden = false
} }
func animateOut(to node: SearchBarPlaceholderNode, duration: Double, timingFunction: String, completion: () -> Void) { func transitionOut(to node: SearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: () -> Void) {
node.isHidden = false node.isHidden = false
completion() completion()
} }

View File

@ -54,19 +54,24 @@ final class SearchDisplayController {
self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
} }
func deactivate(placeholder: SearchBarPlaceholderNode?) { func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) {
searchBar.deactivate() searchBar.deactivate()
if let placeholder = placeholder { if let placeholder = placeholder {
let searchBar = self.searchBar let searchBar = self.searchBar
searchBar.animateOut(to: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak searchBar] in searchBar.transitionOut(to: placeholder, transition: animated ? ContainedViewLayoutTransition.animated(duration: 0.5, curve: .spring) : ContainedViewLayoutTransition.immediate, completion: {
[weak searchBar] in
searchBar?.removeFromSupernode() searchBar?.removeFromSupernode()
}) })
} }
let contentNode = self.contentNode let contentNode = self.contentNode
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in if animated {
contentNode?.removeFromSupernode() contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in
}) contentNode?.removeFromSupernode()
})
} else {
contentNode.removeFromSupernode()
}
} }
} }

View File

@ -0,0 +1,43 @@
import Foundation
import TelegramCore
import SwiftSignalKit
import Postbox
enum AddressNameValidationStatus: Equatable {
case checking
case invalidFormat(AddressNameFormatError)
case availability(AddressNameAvailability)
static func ==(lhs: AddressNameValidationStatus, rhs: AddressNameValidationStatus) -> Bool {
switch lhs {
case .checking:
if case .checking = rhs {
return true
} else {
return false
}
case let .invalidFormat(error):
if case .invalidFormat(error) = rhs {
return true
} else {
return false
}
case let .availability(availability):
if case .availability(availability) = rhs {
return true
} else {
return false
}
}
}
}
func validateAddressNameInteractive(account: Account, domain: AddressNameDomain, name: String) -> Signal<AddressNameValidationStatus, NoError> {
if let error = checkAddressNameFormat(name) {
return .single(.invalidFormat(error))
} else {
return .single(.checking) |> then(addressNameAvailability(account: account, domain: domain, name: name)
|> delay(0.3, queue: Queue.concurrentDefaultQueue())
|> map { result -> AddressNameValidationStatus in .availability(result) })
}
}