From 22d81e15c3eeff264ee3a25f9ce54adef942fa06 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 22 Feb 2017 21:32:39 +0300 Subject: [PATCH] no message --- TelegramUI.xcodeproj/project.pbxproj | 20 +- TelegramUI/ChannelAdminsController.swift | 379 ++++++++-- TelegramUI/ChannelBlacklistController.swift | 305 ++++++++ TelegramUI/ChannelVisibilityController.swift | 349 ++++++--- TelegramUI/ChatControllerNode.swift | 37 +- TelegramUI/ChatListController.swift | 25 +- TelegramUI/ChatListControllerNode.swift | 4 +- TelegramUI/ChatListSearchContainerNode.swift | 4 +- .../ChatMessageTextBubbleContentNode.swift | 15 +- TelegramUI/GroupInfoController.swift | 8 +- TelegramUI/GroupInfoEntries.swift | 677 ------------------ TelegramUI/ItemListActionItem.swift | 12 +- .../ItemListControllerEmptyStateItem.swift | 14 + TelegramUI/ItemListControllerNode.swift | 49 +- TelegramUI/ItemListEditableItem.swift | 2 +- ...emListLoadingIndicatorEmptyStateItem.swift | 47 ++ TelegramUI/ItemListPeerItem.swift | 11 +- TelegramUI/ItemListTextEmptyStateItem.swift | 65 ++ TelegramUI/SearchBarNode.swift | 3 +- TelegramUI/SearchDisplayController.swift | 15 +- .../ValidateAddressNameInteractive.swift | 43 ++ 21 files changed, 1186 insertions(+), 898 deletions(-) delete mode 100644 TelegramUI/GroupInfoEntries.swift create mode 100644 TelegramUI/ItemListControllerEmptyStateItem.swift create mode 100644 TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift create mode 100644 TelegramUI/ItemListTextEmptyStateItem.swift create mode 100644 TelegramUI/ValidateAddressNameInteractive.swift diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 096e4f1745..2f267b64c1 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -232,6 +232,7 @@ D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */; }; D099EA2D1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.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 */; }; D0AB0BB11D6718DA002C78E7 /* libiconv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D0AB0BB01D6718DA002C78E7 /* libiconv.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 */; }; D0B843D11DA922D7005F29E1 /* UserInfoEntries.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843D01DA922D7005F29E1 /* UserInfoEntries.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 */; }; D0B843DB1DAAB138005F29E1 /* ItemListPeerActionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.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 */; }; D0DF0CA11D821B28008AEB01 /* HashtagChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.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 */; }; D0E35A091DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.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 = ""; }; D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = ""; }; D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = ""; }; + D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextEmptyStateItem.swift; sourceTree = ""; }; D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSoundSelection.swift; sourceTree = ""; }; 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; }; @@ -708,7 +712,6 @@ D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerInfoEntries.swift; sourceTree = ""; }; D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = ""; }; D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = ""; }; - D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoEntries.swift; sourceTree = ""; }; D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerItem.swift; sourceTree = ""; }; D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = ""; }; D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = ""; }; @@ -784,6 +787,9 @@ D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = ""; }; D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputPanelItem.swift; sourceTree = ""; }; D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = ""; }; + D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidateAddressNameInteractive.swift; sourceTree = ""; }; + D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerEmptyStateItem.swift; sourceTree = ""; }; + D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListLoadingIndicatorEmptyStateItem.swift; sourceTree = ""; }; D0E35A061DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputContextPanelNode.swift; sourceTree = ""; }; D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputPanelItem.swift; sourceTree = ""; }; D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryListNode.swift; sourceTree = ""; }; @@ -981,6 +987,7 @@ children = ( D0E6521D1E3A2305004EEA91 /* Items */, D01B27981E39144C0022A4C0 /* ItemListController.swift */, + D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */, D01B27941E38F3BF0022A4C0 /* ItemListControllerNode.swift */, ); name = "Item List"; @@ -1688,6 +1695,8 @@ D08774F71E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift */, D0561DDE1E56FE8200E6B9E9 /* ItemListSingleLineInputItem.swift */, D0561DE51E57424700E6B9E9 /* ItemListMultilineTextItem.swift */, + D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */, + D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */, ); name = Items; sourceTree = ""; @@ -1720,7 +1729,6 @@ children = ( D0B843CE1DA922AD005F29E1 /* PeerInfoEntries.swift */, D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */, - D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */, D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */, D0B843CC1DA903BB005F29E1 /* PeerInfoController.swift */, D0486F091E523C8500091F0C /* GroupInfoController.swift */, @@ -2118,6 +2126,7 @@ D087750B1E3E7B7600A97350 /* PreferencesKeys.swift */, D01D6BFB1E42AB3C006151C6 /* EmojiUtils.swift */, D0DA44551E4E7F43005FDCA7 /* ShakeAnimation.swift */, + D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */, ); name = Utils; sourceTree = ""; @@ -2444,6 +2453,7 @@ D04BB2B91E44E5E400650E93 /* AuthorizationSequencePhoneEntryControllerNode.swift in Sources */, D0F69E571D6B8BDA0046BCD6 /* GalleryItem.swift in Sources */, D003702E1DA43052004308D3 /* ItemListAvatarAndNameItem.swift in Sources */, + D0E305A51E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift in Sources */, D0D03B1C1DECB0FE00220C46 /* internal.c in Sources */, D0F69D231D6B87D30046BCD6 /* FFMpegMediaFrameSourceContext.swift in Sources */, D0F69E8D1D6B8C850046BCD6 /* Localizable.swift in Sources */, @@ -2568,6 +2578,7 @@ D0F69E361D6B8B030046BCD6 /* ChatMessageInteractiveMediaNode.swift in Sources */, D08775191E3F53FC00A97350 /* ContactMultiselectionController.swift in Sources */, D04BB2B51E44E58E00650E93 /* AuthorizationSequencePhoneEntryController.swift in Sources */, + D0E305AF1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift in Sources */, D087750C1E3E7B7600A97350 /* PreferencesKeys.swift in Sources */, D0F69E381D6B8B030046BCD6 /* ChatMessageItemView.swift in Sources */, D0D268671D78793B00C422DA /* ChatInterfaceStateNavigationButtons.swift in Sources */, @@ -2575,6 +2586,7 @@ D07827BD1E004A3400071108 /* ChatListSearchItemHeader.swift in Sources */, D0F69E901D6B8C850046BCD6 /* RingByteBuffer.swift in Sources */, D08774F81E3DE7BF00A97350 /* ItemListEditableDeleteControlNode.swift in Sources */, + D0E305AD1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift in Sources */, D0DF0C981D81FF28008AEB01 /* HashtagChatInputContextPanelNode.swift in Sources */, D0F69E731D6B8C340046BCD6 /* ContactsController.swift in Sources */, D0F69D261D6B87D30046BCD6 /* MediaManager.swift in Sources */, @@ -2647,7 +2659,6 @@ D0215D521E0423EE001A0B1E /* InstantPageShapeItem.swift in Sources */, D0561DE81E574C3200E6B9E9 /* ChannelAdminsController.swift in Sources */, D0F69DE51D6B8A420046BCD6 /* ListControllerSpacerItem.swift in Sources */, - D0B843D51DA95427005F29E1 /* GroupInfoEntries.swift in Sources */, D0BA6F851D784ECD0034826E /* ChatInterfaceStateInputPanels.swift in Sources */, D0F69DE11D6B8A420046BCD6 /* ListControllerDisclosureActionItem.swift in Sources */, D0F69E301D6B8B030046BCD6 /* ChatMessageBubbleContentNode.swift in Sources */, @@ -2721,6 +2732,7 @@ D0F69D2E1D6B87D30046BCD6 /* PeerAvatar.swift in Sources */, D00C7CD71E3664070080C3D5 /* ItemListMultilineInputItem.swift in Sources */, D0F69E141D6B8ACF0046BCD6 /* ChatControllerInteraction.swift in Sources */, + D09AEFD41E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift in Sources */, D0F69D6D1D6B87D30046BCD6 /* MediaTrackDecodableFrame.swift in Sources */, D0D03B201DECB0FE00220C46 /* stream.c in Sources */, ); diff --git a/TelegramUI/ChannelAdminsController.swift b/TelegramUI/ChannelAdminsController.swift index 09ca114d47..d606fc2f89 100644 --- a/TelegramUI/ChannelAdminsController.swift +++ b/TelegramUI/ChannelAdminsController.swift @@ -6,11 +6,21 @@ import TelegramCore private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed() -private struct ChannelAdminsControllerArguments { +private final class ChannelAdminsControllerArguments { let account: Account let updateCurrentAdministrationType: () -> Void + let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void + let removeAdmin: (PeerId) -> 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 { @@ -54,7 +64,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { case administrationInfo(String) case adminsHeader(String) - case adminPeerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing) + case adminPeerItem(Int32, RenderedChannelParticipant, ItemListPeerItemEditing, Bool) case addAdmin(Bool) case adminsInfo(String) @@ -79,7 +89,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { return .index(3) case .adminsInfo: return .index(4) - case let .adminPeerItem(_, participant, _): + case let .adminPeerItem(_, participant, _, _): return .peer(participant.peer.id) } } @@ -104,8 +114,8 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { } else { return false } - case let .adminPeerItem(lhsIndex, lhsParticipant, lhsEditing): - if case let .adminPeerItem(rhsIndex, rhsParticipant, rhsEditing) = rhs { + case let .adminPeerItem(lhsIndex, lhsParticipant, lhsEditing, lhsEnabled): + if case let .adminPeerItem(rhsIndex, rhsParticipant, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } @@ -115,6 +125,9 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { if lhsEditing != rhsEditing { return false } + if lhsEnabled != rhsEnabled { + return false + } return true } else { return false @@ -152,11 +165,11 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { default: return true } - case let .adminPeerItem(index, _, _): + case let .adminPeerItem(index, _, _, _): switch rhs { case .administrationType, .administrationInfo, .adminsHeader: return false - case let .adminPeerItem(rhsIndex, _, _): + case let .adminPeerItem(rhsIndex, _, _, _): return index < rhsIndex default: return true @@ -184,13 +197,13 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { label = "All Members" } return ItemListDisclosureItem(title: "Who can add members", label: label, sectionId: self.section, style: .blocks, action: { - + arguments.updateCurrentAdministrationType() }) case let .administrationInfo(text): return ItemListTextItem(text: text, sectionId: self.section) case let .adminsHeader(title): return ItemListSectionHeaderItem(text: title, sectionId: self.section) - case let .adminPeerItem(_, participant, editing): + case let .adminPeerItem(_, participant, editing, enabled): let peerText: String switch participant.participant { case .creator: @@ -198,10 +211,10 @@ private enum ChannelAdminsEntry: ItemListNodeEntry { default: 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 - - }, removePeer: { _ 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: { peerId in + arguments.removeAdmin(peerId) }) case let .addAdmin(editing): return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Admin", sectionId: self.section, editing: editing, action: { @@ -220,25 +233,75 @@ private enum CurrentAdministrationType { private struct ChannelAdminsControllerState: Equatable { let selectedType: CurrentAdministrationType? + let editing: Bool + let peerIdWithRevealedOptions: PeerId? + let removingPeerId: PeerId? + let removedPeerIds: Set + let temporaryAdmins: [RenderedChannelParticipant] init() { 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, temporaryAdmins: [RenderedChannelParticipant]) { 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 { if lhs.selectedType != rhs.selectedType { 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 } 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) -> 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 { entries.append(.adminsHeader(isGroup ? "GROUP ADMINS" : "CHANNEL ADMINS")) + var combinedParticipants: [RenderedChannelParticipant] = participants + var existingParticipantIds = Set() + 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 - for participant in participants.sorted(by: { lhs, rhs in + for participant in combinedParticipants.sorted(by: { lhs, rhs in let lhsInvitedAt: Int32 switch lhs.participant { case .creator: @@ -301,15 +376,17 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins } return lhsInvitedAt < rhsInvitedAt }) { - var editable = true - if case .creator = participant.participant { - editable = false + if !state.removedPeerIds.contains(participant.peer.id) { + var editable = true + if case .creator = participant.participant { + editable = false + } + entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: state.editing, revealed: participant.peer.id == state.peerIdWithRevealedOptions), existingParticipantIds.contains(participant.peer.id))) + index += 1 } - entries.append(.adminPeerItem(index, participant, ItemListPeerItemEditing(editable: editable, editing: false, revealed: false))) - index += 1 } - entries.append(.addAdmin(false)) + entries.append(.addAdmin(state.editing)) entries.append(.adminsInfo(isGroup ? "You can add admins to help you manage your group" : "You can add admins to help you manage your channel")) } } @@ -317,62 +394,261 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins return entries } -/*private func effectiveAdministrationType(state: ChannelAdminsControllerState, peer: TelegramChannel) -> CurrentAdministrationType { - 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 { +public func channelAdminsController(account: Account, peerId: PeerId) -> ViewController { let statePromise = ValuePromise(ChannelAdminsControllerState(), ignoreRepeated: true) let stateValue = Atomic(value: ChannelAdminsControllerState()) let updateState: ((ChannelAdminsControllerState) -> ChannelAdminsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + let actionsDisposable = DisposableSet() let updateAdministrationDisposable = MetaDisposable() actionsDisposable.add(updateAdministrationDisposable) + + let removeAdminDisposable = MetaDisposable() + actionsDisposable.add(removeAdminDisposable) let addAdminDisposable = MetaDisposable() actionsDisposable.add(addAdminDisposable) + let adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) + let arguments = ChannelAdminsControllerArguments(account: account, updateCurrentAdministrationType: { + let actionSheet = ActionSheetController() + let result = ValuePromise() + 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 in + updateState { state in + return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers) + } + + return account.postbox.loadedPeerWithId(peerId) + |> mapToSignal { peer -> Signal 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 = adminsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { peers -> Signal 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: { + var confirmationImpl: ((PeerId) -> Signal)? + 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() + 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 in + if let memberId = memberId { + return account.postbox.peerView(id: memberId) + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { view -> Signal 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 = adminsPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapError { _ -> AddPeerAdminError in return .generic } + |> mapToSignal { admins -> Signal 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 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 adminsPromise = Promise<[RenderedChannelParticipant]?>(nil) + let peerView = account.viewTracker.peerView(peerId) |> deliverOnMainQueue let adminsSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelAdmins(account: account, peerId: peerId) |> map { Optional($0) }) 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.ItemGenerationArguments)) in - let peer = peerViewMainPeer(view) - var rightNavigationButton: ItemListNavigationButton? if let admins = admins, admins.count > 1 { - rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: { - updateState { state in - return state - } - }) + 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) + } + }) + } } 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)) } |> afterDisposed { @@ -380,5 +656,10 @@ public func ChannelAdminsController(account: Account, peerId: PeerId) -> ViewCon } let controller = ItemListController(signal) + presentControllerImpl = { [weak controller] c, p in + if let controller = controller { + controller.present(c, in: .window, with: p) + } + } return controller } diff --git a/TelegramUI/ChannelBlacklistController.swift b/TelegramUI/ChannelBlacklistController.swift index fbf287572c..8658e54b78 100644 --- a/TelegramUI/ChannelBlacklistController.swift +++ b/TelegramUI/ChannelBlacklistController.swift @@ -1,2 +1,307 @@ 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 = peersPromise.get() + |> filter { $0 != nil } + |> take(1) + |> deliverOnMainQueue + |> mapToSignal { peers -> Signal 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.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 +} diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index d25a1ddcdb..9c7278c308 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -4,18 +4,32 @@ import SwiftSignalKit import Postbox import TelegramCore -private struct ChannelVisibilityControllerArguments { +private final class ChannelVisibilityControllerArguments { let account: Account let updateCurrentType: (CurrentChannelType) -> Void - let updatePublicLinkText: (String) -> Void - let displayPrivateLinkMenu: () -> Void + let updatePublicLinkText: (String?, String) -> 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 { case type case link - case existingPublicLinks +} + +private enum ChannelVisibilityEntryTag { + case privateLink } private enum ChannelVisibilityEntry: ItemListNodeEntry { @@ -24,23 +38,24 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case typePrivate(Bool) case typeInfo(String) + case publicLinkAvailability(Bool) case privateLink(String?) - case editablePublicLink(String) + case editablePublicLink(String?, String) case privateLinkInfo(String) case publicLinkInfo(String) - case publicLinkStatus(String, AddressNameStatus) + case publicLinkStatus(String, AddressNameValidationStatus) case existingLinksInfo(String) - case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing) + case existingLinkPeerItem(Int32, Peer, ItemListPeerItemEditing, Bool) var section: ItemListSectionId { switch self { case .typeHeader, .typePublic, .typePrivate, .typeInfo: return ChannelVisibilitySection.type.rawValue - case .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus: + case .publicLinkAvailability, .privateLink, .editablePublicLink, .privateLinkInfo, .publicLinkInfo, .publicLinkStatus: return ChannelVisibilitySection.link.rawValue case .existingLinksInfo, .existingLinkPeerItem: - return ChannelVisibilitySection.existingPublicLinks.rawValue + return ChannelVisibilitySection.link.rawValue } } @@ -55,21 +70,23 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { case .typeInfo: return 3 - case .privateLink: + case .publicLinkAvailability: return 4 - case .editablePublicLink: + case .privateLink: return 5 - case .privateLinkInfo: + case .editablePublicLink: return 6 - case .publicLinkStatus: + case .privateLinkInfo: return 7 - case .publicLinkInfo: + case .publicLinkStatus: return 8 + case .publicLinkInfo: + return 9 case .existingLinksInfo: - return 9 - case let .existingLinkPeerItem(index, _, _): - return 10 + index + return 10 + case let .existingLinkPeerItem(index, _, _, _): + return 11 + index } } @@ -99,14 +116,20 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } + case let .publicLinkAvailability(value): + if case .publicLinkAvailability(value) = rhs { + return true + } else { + return false + } case let .privateLink(lhsLink): if case let .privateLink(rhsLink) = rhs, lhsLink == rhsLink { return true } else { return false } - case let .editablePublicLink(text): - if case .editablePublicLink(text) = rhs { + case let .editablePublicLink(lhsCurrentText, lhsText): + if case let .editablePublicLink(rhsCurrentText, rhsText) = rhs, lhsCurrentText == rhsCurrentText, lhsText == rhsText { return true } else { return false @@ -135,8 +158,8 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { } else { return false } - case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing): - if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing) = rhs { + case let .existingLinkPeerItem(lhsIndex, lhsPeer, lhsEditing, lhsEnabled): + if case let .existingLinkPeerItem(rhsIndex, rhsPeer, rhsEditing, rhsEnabled) = rhs { if lhsIndex != rhsIndex { return false } @@ -146,6 +169,9 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { if lhsEditing != rhsEditing { return false } + if lhsEnabled != rhsEnabled { + return false + } return true } else { return false @@ -171,13 +197,21 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { }) case let .typeInfo(text): 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): - return ItemListActionItem(title: link ?? "Loading", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { - - }) - case let .editablePublicLink(text): + return ItemListActionItem(title: link ?? "Loading", kind: .neutral, alignment: .natural, sectionId: self.section, style: .blocks, action: { + if let link = link { + arguments.displayPrivateLinkMenu(link) + } + }, 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 - arguments.updatePublicLinkText(updatedText) + arguments.updatePublicLinkText(currentText, updatedText) }, action: { }) @@ -189,31 +223,44 @@ private enum ChannelVisibilityEntry: ItemListNodeEntry { var displayActivity = false let text: NSAttributedString switch status { - case .available: - text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c)) - case .checking: - 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: + case let .invalidFormat(error): + switch error { + case .startsWithDigit: 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)) - case .short: + 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: + text = NSAttributedString(string: "\(addressName) is available.", textColor: UIColor(0x26972c)) + case .invalid: + text = NSAttributedString(string: "Sorry, this name is invalid.", textColor: UIColor(0xcf3030)) + case .taken: + text = NSAttributedString(string: "\(addressName) is already taken.", textColor: UIColor(0xcf3030)) + } + case .checking: + text = NSAttributedString(string: "Checking name...", textColor: UIColor(0x6d6d72)) + displayActivity = true } return ItemListActivityTextItem(displayActivity: displayActivity, text: text, sectionId: self.section) case let .existingLinksInfo(text): return ItemListTextItem(text: text, sectionId: self.section) - case let .existingLinkPeerItem(_, peer, editing): - 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 - - }, removePeer: { _ in - + case let .existingLinkPeerItem(_, peer, editing, enabled): + var label = "" + if let addressName = peer.addressName { + 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 } -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 { let selectedType: CurrentChannelType? let editingPublicLinkText: String? - let addressNameStatus: AddressNameStatus? + let addressNameValidationStatus: AddressNameValidationStatus? let updatingAddressName: Bool + let revealedRevokePeerId: PeerId? + let revokingPeerId: PeerId? init() { self.selectedType = nil self.editingPublicLinkText = nil - self.addressNameStatus = nil + self.addressNameValidationStatus = nil 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.editingPublicLinkText = editingPublicLinkText - self.addressNameStatus = addressNameStatus + self.addressNameValidationStatus = addressNameValidationStatus self.updatingAddressName = updatingAddressName + self.revealedRevokePeerId = revealedRevokePeerId + self.revokingPeerId = revokingPeerId } static func ==(lhs: ChannelVisibilityControllerState, rhs: ChannelVisibilityControllerState) -> Bool { @@ -280,34 +304,48 @@ private struct ChannelVisibilityControllerState: Equatable { if lhs.editingPublicLinkText != rhs.editingPublicLinkText { return false } - if lhs.addressNameStatus != rhs.addressNameStatus { + if lhs.addressNameValidationStatus != rhs.addressNameValidationStatus { return false } if lhs.updatingAddressName != rhs.updatingAddressName { return false } + if lhs.revealedRevokePeerId != rhs.revealedRevokePeerId { + return false + } + if lhs.revokingPeerId != rhs.revokingPeerId { + return false + } return true } 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 { - 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 { - return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameStatus: addressNameStatus, updatingAddressName: self.updatingAddressName) + func withUpdatedAddressNameValidationStatus(_ addressNameValidationStatus: AddressNameValidationStatus?) -> ChannelVisibilityControllerState { + return ChannelVisibilityControllerState(selectedType: self.selectedType, editingPublicLinkText: self.editingPublicLinkText, addressNameValidationStatus: addressNameValidationStatus, updatingAddressName: self.updatingAddressName, revealedRevokePeerId: self.revealedRevokePeerId, revokingPeerId: self.revokingPeerId) } 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] = [] if let peer = view.peers[view.peerId] as? TelegramChannel { @@ -359,11 +397,39 @@ private func channelVisibilityControllerEntries(view: PeerView, state: ChannelVi switch selectedType { case .publicChannel: - entries.append(.editablePublicLink(currentAddressName)) - if let status = state.addressNameStatus { - entries.append(.publicLinkStatus(currentAddressName, status)) + var displayAvailability = false + 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(.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: 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.")) @@ -426,7 +492,18 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie 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 displayPrivateLinkMenuImpl: ((String) -> Void)? let actionsDisposable = DisposableSet() @@ -436,46 +513,67 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie let updateAddressNameDisposable = MetaDisposable() actionsDisposable.add(updateAddressNameDisposable) + let revokeAddressNameDisposable = MetaDisposable() + actionsDisposable.add(revokeAddressNameDisposable) + let arguments = ChannelVisibilityControllerArguments(account: account, updateCurrentType: { type in updateState { state in return state.withUpdatedSelectedType(type) } - }, updatePublicLinkText: { text in + }, updatePublicLinkText: { currentText, text in if text.isEmpty { checkAddressNameDisposable.set(nil) 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 { updateState { state in 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 updateState { state in - let status: AddressNameStatus - 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) + return state.withUpdatedAddressNameValidationStatus(result) } })) } - }, 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 signal = combineLatest(statePromise.get(), peerView) - |> map { state, view -> (ItemListControllerState, (ItemListNodeState, ChannelVisibilityEntry.ItemGenerationArguments)) in + let signal = combineLatest(statePromise.get(), peerView, peersDisablingAddressNameAssignment.get()) + |> map { state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState, ChannelVisibilityEntry.ItemGenerationArguments)) in let peer = peerViewMainPeer(view) var rightNavigationButton: ItemListNavigationButton? @@ -486,9 +584,9 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie case .privateChannel: break case .publicChannel: - if let addressNameStatus = state.addressNameStatus { - switch addressNameStatus { - case .available: + if let addressNameValidationStatus = state.addressNameValidationStatus { + switch addressNameValidationStatus { + case .availability(.available): break default: doneEnabled = false @@ -510,7 +608,7 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie } 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 updateState { state in 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 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)) } |> afterDisposed { @@ -551,5 +649,34 @@ public func channelVisibilityController(account: Account, peerId: PeerId) -> Vie dismissImpl = { [weak controller] in 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 } diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index fb4074cbb1..71c12e17b6 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -213,7 +213,7 @@ class ChatControllerNode: ASDisplayNode { } 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 @@ -236,10 +236,9 @@ class ChatControllerNode: ASDisplayNode { var duration: Double = 0.0 var curve: UInt = 0 - var animated = true switch transition { case .immediate: - animated = false + break case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { @@ -340,14 +339,14 @@ class ChatControllerNode: ASDisplayNode { var inputPanelsHeight: CGFloat = 0.0 var inputPanelFrame: CGRect? - if let inputPanelNode = self.inputPanelNode { + if self.inputPanelNode != 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)) inputPanelsHeight += inputPanelSize!.height } var accessoryPanelFrame: CGRect? - if let accessoryPanelNode = self.accessoryPanelNode { + if self.accessoryPanelNode != 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)) inputPanelsHeight += accessoryPanelSize!.height @@ -429,7 +428,7 @@ class ChatControllerNode: ASDisplayNode { if let dismissedInputPanelNode = dismissedInputPanelNode { var frameCompleted = 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 { return } @@ -452,7 +451,7 @@ class ChatControllerNode: ASDisplayNode { if let dismissedAccessoryPanelNode = dismissedAccessoryPanelNode { var frameCompleted = false var alphaCompleted = false - var completed = { [weak dismissedAccessoryPanelNode] in + let completed = { [weak dismissedAccessoryPanelNode] in if frameCompleted && alphaCompleted { dismissedAccessoryPanelNode?.removeFromSupernode() } @@ -475,7 +474,7 @@ class ChatControllerNode: ASDisplayNode { if let dismissedInputContextPanelNode = dismissedInputContextPanelNode { var frameCompleted = false var animationCompleted = false - var completed = { [weak dismissedInputContextPanelNode] in + let completed = { [weak dismissedInputContextPanelNode] in if let dismissedInputContextPanelNode = dismissedInputContextPanelNode, frameCompleted, animationCompleted { dismissedInputContextPanelNode.removeFromSupernode() } @@ -496,8 +495,16 @@ class ChatControllerNode: ASDisplayNode { } 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 - dismissedInputNode?.removeFromSupernode() + 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() + } + } else { + dismissedInputNode?.removeFromSupernode() + } + } }) } } @@ -521,14 +528,14 @@ class ChatControllerNode: ASDisplayNode { } if self.chatPresentationInterfaceState != chatPresentationInterfaceState { - var updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) - var updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState + let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) + let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil var extendedSearchLayout = false if let inputQueryResult = chatPresentationInterfaceState.inputQueryResult { - if case let .contextRequestResult(peer, _) = inputQueryResult { + if case .contextRequestResult = inputQueryResult { extendedSearchLayout = true } } @@ -612,7 +619,7 @@ class ChatControllerNode: ASDisplayNode { self.scheduledLayoutTransitionRequest = (requestId, transition) (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in if let strongSelf = self { - if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest { + if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { strongSelf.scheduledLayoutTransitionRequest = nil strongSelf.requestLayout(currentRequestTransition) } @@ -626,7 +633,7 @@ class ChatControllerNode: ASDisplayNode { let inputNode = ChatMediaInputNode(account: self.account, controllerInteraction: self.controllerInteraction) inputNode.interfaceInteraction = interfaceInteraction 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) } } } diff --git a/TelegramUI/ChatListController.swift b/TelegramUI/ChatListController.swift index c035068cea..32836cb7fd 100644 --- a/TelegramUI/ChatListController.swift +++ b/TelegramUI/ChatListController.swift @@ -5,15 +5,6 @@ import Display import TelegramCore private let composeButtonImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in - /* - - - - Created with Sketch. - - - - */ context.clear(CGRect(origin: CGPoint(), size: size)) 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 ") @@ -32,6 +23,8 @@ public class ChatListController: TelegramController { private var titleDisposable: Disposable? private var badgeDisposable: Disposable? + private var dismissSearchOnDisappear = false + public override init(account: Account) { self.account = account @@ -106,7 +99,7 @@ public class ChatListController: TelegramController { self.chatListDisplayNode.navigationBar = self.navigationBar self.chatListDisplayNode.requestDeactivateSearch = { [weak self] in - self?.deactivateSearch() + self?.deactivateSearch(animated: true) } 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 if let strongSelf = strongSelf { + strongSelf.dismissSearchOnDisappear = true (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) { super.viewDidDisappear(animated) + + if self.dismissSearchOnDisappear { + self.dismissSearchOnDisappear = false + self.deactivateSearch(animated: false) + } } 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 { - self.chatListDisplayNode.deactivateSearch() - self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) + self.chatListDisplayNode.deactivateSearch(animated: animated) + self.setDisplayNavigationBar(true, transition: animated ? .animated(duration: 0.5, curve: .spring) : .immediate) } } diff --git a/TelegramUI/ChatListControllerNode.swift b/TelegramUI/ChatListControllerNode.swift index ef9570595f..27c8ed5718 100644 --- a/TelegramUI/ChatListControllerNode.swift +++ b/TelegramUI/ChatListControllerNode.swift @@ -107,7 +107,7 @@ class ChatListControllerNode: ASDisplayNode { } } - func deactivateSearch() { + func deactivateSearch(animated: Bool) { if let searchDisplayController = self.searchDisplayController { var maybePlaceholderNode: SearchBarPlaceholderNode? 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 } } diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index 2ac1015adc..f94784f7fb 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -422,7 +422,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.enqueuedRecentTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) if firstTime { } else { } @@ -447,7 +447,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { self.enqueuedTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() - options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) if firstTime { } else { //options.insert(.AnimateAlpha) diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index cbb4912767..d969ac2113 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -100,9 +100,18 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } if entities == nil { - let parsedEntities = generateTextEntities(message.text) - if !parsedEntities.isEmpty { - entities = TextEntitiesMessageAttribute(entities: parsedEntities) + 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) + if !parsedEntities.isEmpty { + entities = TextEntitiesMessageAttribute(entities: parsedEntities) + } } } if let entities = entities { diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 95fab34a70..46eded33e0 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -322,11 +322,11 @@ private enum GroupInfoEntry: ItemListNodeEntry { }) case let .membersAdmins(count): 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): 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): let label: String? @@ -524,10 +524,10 @@ private func groupInfoEntries(account: Account, view: PeerView, state: GroupInfo entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText)) 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 { - entries.append(GroupInfoEntry.membersBlacklist(count: bannedCount)) + entries.append(GroupInfoEntry.membersBlacklist(count: Int(bannedCount))) } } } else { diff --git a/TelegramUI/GroupInfoEntries.swift b/TelegramUI/GroupInfoEntries.swift deleted file mode 100644 index 6f676fc079..0000000000 --- a/TelegramUI/GroupInfoEntries.swift +++ /dev/null @@ -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) -} -*/ diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index c4e7e76137..ca21f6a460 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -21,14 +21,16 @@ class ItemListActionItem: ListViewItem, ItemListItem { let sectionId: ItemListSectionId let style: ItemListStyle 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.kind = kind self.alignment = alignment self.sectionId = sectionId self.style = style 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)) -> Void) { @@ -80,6 +82,12 @@ class ItemListActionItemNode: ListViewItemNode { private let titleNode: TextNode + private var item: ItemListActionItem? + + var tag: Any? { + return self.item?.tag + } + init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true @@ -141,6 +149,8 @@ class ItemListActionItemNode: ListViewItemNode { return (layout, { [weak self] in if let strongSelf = self { + strongSelf.item = item + let _ = titleApply() let leftInset: CGFloat diff --git a/TelegramUI/ItemListControllerEmptyStateItem.swift b/TelegramUI/ItemListControllerEmptyStateItem.swift new file mode 100644 index 0000000000..0333cf9916 --- /dev/null +++ b/TelegramUI/ItemListControllerEmptyStateItem.swift @@ -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) { + + } +} diff --git a/TelegramUI/ItemListControllerNode.swift b/TelegramUI/ItemListControllerNode.swift index cfbd2e8cbc..6c4ba1711b 100644 --- a/TelegramUI/ItemListControllerNode.swift +++ b/TelegramUI/ItemListControllerNode.swift @@ -37,6 +37,7 @@ enum ItemListStyle { private struct ItemListNodeTransition { let entries: ItemListNodeEntryTransition let updateStyle: ItemListStyle? + let emptyStateItem: ItemListControllerEmptyStateItem? let firstTime: Bool let animated: Bool let animateAlpha: Bool @@ -45,11 +46,13 @@ private struct ItemListNodeTransition { struct ItemListNodeState { let entries: [Entry] let style: ItemListStyle + let emptyStateItem: ItemListControllerEmptyStateItem? 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.style = style + self.emptyStateItem = emptyStateItem self.animateChanges = animateChanges } } @@ -62,10 +65,13 @@ final class ItemListNode: ASDisplayNode { private var didSetReady = false let listNode: ListView + private var emptyStateItem: ItemListControllerEmptyStateItem? + private var emptyStateNode: ItemListControllerEmptyStateItemNode? + private let transitionDisposable = MetaDisposable() private var enqueuedTransitions: [ItemListNodeTransition] = [] - private var hadValidLayout = false + private var validLayout: (ContainerViewLayout, CGFloat)? var dismiss: (() -> Void)? @@ -89,7 +95,7 @@ final class ItemListNode: ASDisplayNode { if previous?.style != 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 if let strongSelf = self { strongSelf.enqueueTransition(transition) @@ -144,15 +150,20 @@ final class ItemListNode: 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 }) - if !self.hadValidLayout { - self.hadValidLayout = true + if let emptyStateNode = self.emptyStateNode { + emptyStateNode.updateLayout(layout: layout, navigationBarHeight: navigationBarHeight, transition: transition) + } + + let dequeue = self.validLayout == nil + self.validLayout = (layout, navigationBarHeight) + if dequeue { self.dequeueTransitions() } } private func enqueueTransition(_ transition: ItemListNodeTransition) { self.enqueuedTransitions.append(transition) - if self.hadValidLayout { + if self.validLayout != nil { self.dequeueTransitions() } } @@ -177,6 +188,7 @@ final class ItemListNode: ASDisplayNode { options.insert(.AnimateInsertion) } else if transition.animateAlpha { options.insert(.PreferSynchronousResourceLoading) + options.insert(.PreferSynchronousDrawing) 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 @@ -187,6 +199,31 @@ final class ItemListNode: 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 + } + } } } } diff --git a/TelegramUI/ItemListEditableItem.swift b/TelegramUI/ItemListEditableItem.swift index 47dcb392d9..1a22091d84 100644 --- a/TelegramUI/ItemListEditableItem.swift +++ b/TelegramUI/ItemListEditableItem.swift @@ -176,7 +176,7 @@ class ItemListRevealOptionsItemNode: ListViewItemNode { override func layout() { 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)) revealNode.frame = CGRect(origin: CGPoint(x: self.bounds.size.width + max(self.revealOffset, -revealSize.width), y: 0.0), size: revealSize) } diff --git a/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift b/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift new file mode 100644 index 0000000000..471283f4e2 --- /dev/null +++ b/TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift @@ -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) + } + } +} diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 16d5274ac7..c75d5be7c2 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -27,6 +27,7 @@ struct ItemListPeerItemEditing: Equatable { enum ItemListPeerItemText { case activity case text(String) + case none } final class ItemListPeerItem: ListViewItem, ItemListItem { @@ -65,7 +66,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { node.insets = layout.insets completion(node, { - return (nil, { apply(false) }) + return (node.avatarNode.ready, { apply(false) }) }) } } @@ -113,7 +114,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { private let highlightedBackgroundNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? - private let avatarNode: AvatarNode + fileprivate let avatarNode: AvatarNode private let titleNode: TextNode private let labelNode: TextNode private let statusNode: TextNode @@ -235,6 +236,8 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { } case let .text(text): statusAttributedString = NSAttributedString(string: text, font: statusFont, textColor: UIColor(0xa6a6a6)) + case .none: + break } 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.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.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 } - 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.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)) diff --git a/TelegramUI/ItemListTextEmptyStateItem.swift b/TelegramUI/ItemListTextEmptyStateItem.swift new file mode 100644 index 0000000000..fb88b7af71 --- /dev/null +++ b/TelegramUI/ItemListTextEmptyStateItem.swift @@ -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) + } +} diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index aad36d25b3..7ea6450c03 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -91,6 +91,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { self.textField = SearchBarTextField() self.textField.font = Font.regular(15.0) + self.textField.autocorrectionType = .no self.textField.returnKeyType = .done self.cancelButton = ASButtonNode() @@ -166,7 +167,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { 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 completion() } diff --git a/TelegramUI/SearchDisplayController.swift b/TelegramUI/SearchDisplayController.swift index 6f84717c10..f58a306187 100644 --- a/TelegramUI/SearchDisplayController.swift +++ b/TelegramUI/SearchDisplayController.swift @@ -54,19 +54,24 @@ final class SearchDisplayController { self.searchBar.animateIn(from: placeholder, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring) } - func deactivate(placeholder: SearchBarPlaceholderNode?) { + func deactivate(placeholder: SearchBarPlaceholderNode?, animated: Bool = true) { searchBar.deactivate() if let placeholder = placeholder { 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() }) } let contentNode = self.contentNode - contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in - contentNode?.removeFromSupernode() - }) + if animated { + contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak contentNode] _ in + contentNode?.removeFromSupernode() + }) + } else { + contentNode.removeFromSupernode() + } } } diff --git a/TelegramUI/ValidateAddressNameInteractive.swift b/TelegramUI/ValidateAddressNameInteractive.swift new file mode 100644 index 0000000000..ed3acb349b --- /dev/null +++ b/TelegramUI/ValidateAddressNameInteractive.swift @@ -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 { + 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) }) + } +}