mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-02 04:38:33 +00:00
no message
This commit is contained in:
parent
a0ccb729be
commit
22d81e15c3
@ -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 */,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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
|
||||||
}) {
|
}) {
|
||||||
|
if !state.removedPeerIds.contains(participant.peer.id) {
|
||||||
var editable = true
|
var editable = true
|
||||||
if case .creator = participant.participant {
|
if case .creator = participant.participant {
|
||||||
editable = false
|
editable = false
|
||||||
}
|
}
|
||||||
entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: false, revealed: 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
|
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 {
|
||||||
|
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: {
|
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
|
||||||
updateState { state in
|
updateState { state in
|
||||||
return state
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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 let .invalidFormat(error):
|
||||||
|
switch error {
|
||||||
|
case .startsWithDigit:
|
||||||
|
text = NSAttributedString(string: "Names can't start with a digit.", textColor: UIColor(0xcf3030))
|
||||||
|
case .startsWithUnderscore:
|
||||||
|
text = NSAttributedString(string: "Names can't start with an underscore.", textColor: UIColor(0xcf3030))
|
||||||
|
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))
|
||||||
|
case .invalidCharacters:
|
||||||
|
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
|
||||||
|
}
|
||||||
|
case let .availability(availability):
|
||||||
|
switch availability {
|
||||||
case .available:
|
case .available:
|
||||||
text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c))
|
text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c))
|
||||||
case .checking:
|
case .invalid:
|
||||||
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))
|
|
||||||
case .invalid, .underscopeEnd, .underscopeStart:
|
|
||||||
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
|
text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030))
|
||||||
case .short:
|
case .taken:
|
||||||
text = NSAttributedString(string: "Names must have at least 5 characters.", textColor: UIColor(0xcf3030))
|
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 {
|
||||||
|
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(.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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
if completed {
|
||||||
|
if let strongSelf = self {
|
||||||
|
if strongSelf.inputNode !== dismissedInputNode {
|
||||||
dismissedInputNode?.removeFromSupernode()
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -100,11 +100,20 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if entities == nil {
|
if entities == nil {
|
||||||
|
var generateEntities = false
|
||||||
|
for media in message.media {
|
||||||
|
if media is TelegramMediaImage || media is TelegramMediaFile {
|
||||||
|
generateEntities = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if generateEntities {
|
||||||
let parsedEntities = generateTextEntities(message.text)
|
let parsedEntities = generateTextEntities(message.text)
|
||||||
if !parsedEntities.isEmpty {
|
if !parsedEntities.isEmpty {
|
||||||
entities = TextEntitiesMessageAttribute(entities: parsedEntities)
|
entities = TextEntitiesMessageAttribute(entities: parsedEntities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let entities = entities {
|
if let entities = entities {
|
||||||
attributedText = stringWithAppliedEntities(message.text, entities: entities.entities, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont)
|
attributedText = stringWithAppliedEntities(message.text, entities: entities.entities, baseFont: messageFont, boldFont: messageBoldFont, fixedFont: messageFixedFont)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
@ -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
|
||||||
|
|||||||
14
TelegramUI/ItemListControllerEmptyStateItem.swift
Normal file
14
TelegramUI/ItemListControllerEmptyStateItem.swift
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
47
TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift
Normal file
47
TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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))
|
||||||
|
|
||||||
|
|||||||
65
TelegramUI/ItemListTextEmptyStateItem.swift
Normal file
65
TelegramUI/ItemListTextEmptyStateItem.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
if animated {
|
||||||
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in
|
contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in
|
||||||
contentNode?.removeFromSupernode()
|
contentNode?.removeFromSupernode()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
contentNode.removeFromSupernode()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
TelegramUI/ValidateAddressNameInteractive.swift
Normal file
43
TelegramUI/ValidateAddressNameInteractive.swift
Normal 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) })
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user