mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-30 19:58:39 +00:00
no message
This commit is contained in:
parent
a0ccb729be
commit
22d81e15c3
@ -232,6 +232,7 @@
|
||||
D099EA291DE76655001AF5A8 /* ManagedVideoNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D099EA281DE76655001AF5A8 /* ManagedVideoNode.swift */; };
|
||||
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 = "<group>"; };
|
||||
D099EA2C1DE76782001AF5A8 /* PeerMessageManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PeerMessageManagedMediaId.swift; sourceTree = "<group>"; };
|
||||
D099EA2E1DE775BB001AF5A8 /* ChatContextResultManagedMediaId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatContextResultManagedMediaId.swift; sourceTree = "<group>"; };
|
||||
D09AEFD31E5BAF67005C1A8B /* ItemListTextEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListTextEmptyStateItem.swift; sourceTree = "<group>"; };
|
||||
D0A749961E3AA25200AD786E /* NotificationSoundSelection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationSoundSelection.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
D0B843D01DA922D7005F29E1 /* UserInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D21DA922E3005F29E1 /* ChannelInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D41DA95427005F29E1 /* GroupInfoEntries.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupInfoEntries.swift; sourceTree = "<group>"; };
|
||||
D0B843D81DAAAA0C005F29E1 /* ItemListPeerItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerItem.swift; sourceTree = "<group>"; };
|
||||
D0B843DA1DAAB138005F29E1 /* ItemListPeerActionItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListPeerActionItem.swift; sourceTree = "<group>"; };
|
||||
D0B844551DAC3AEE005F29E1 /* PresenceStrings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresenceStrings.swift; sourceTree = "<group>"; };
|
||||
@ -784,6 +787,9 @@
|
||||
D0DF0C9D1D82141F008AEB01 /* ChatInterfaceInputContexts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatInterfaceInputContexts.swift; sourceTree = "<group>"; };
|
||||
D0DF0CA01D821B28008AEB01 /* HashtagChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HashtagChatInputPanelItem.swift; sourceTree = "<group>"; };
|
||||
D0DF0CA31D82BCD0008AEB01 /* MentionChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MentionChatInputContextPanelNode.swift; sourceTree = "<group>"; };
|
||||
D0E305A41E5B2BFB00D7A3A2 /* ValidateAddressNameInteractive.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValidateAddressNameInteractive.swift; sourceTree = "<group>"; };
|
||||
D0E305AC1E5BA3E700D7A3A2 /* ItemListControllerEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListControllerEmptyStateItem.swift; sourceTree = "<group>"; };
|
||||
D0E305AE1E5BA8E000D7A3A2 /* ItemListLoadingIndicatorEmptyStateItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemListLoadingIndicatorEmptyStateItem.swift; sourceTree = "<group>"; };
|
||||
D0E35A061DE4803400BC6096 /* VerticalListContextResultsChatInputContextPanelNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputContextPanelNode.swift; sourceTree = "<group>"; };
|
||||
D0E35A081DE4804900BC6096 /* VerticalListContextResultsChatInputPanelItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalListContextResultsChatInputPanelItem.swift; sourceTree = "<group>"; };
|
||||
D0E7A1BC1D8C246D00C37A6F /* ChatHistoryListNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatHistoryListNode.swift; sourceTree = "<group>"; };
|
||||
@ -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 = "<group>";
|
||||
@ -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 = "<group>";
|
||||
@ -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 */,
|
||||
);
|
||||
|
||||
@ -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<PeerId>
|
||||
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<PeerId>, 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<PeerId>) -> ChannelAdminsControllerState {
|
||||
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: removedPeerIds, temporaryAdmins: self.temporaryAdmins)
|
||||
}
|
||||
|
||||
func withUpdatedTemporaryAdmins(_ temporaryAdmins: [RenderedChannelParticipant]) -> ChannelAdminsControllerState {
|
||||
return ChannelAdminsControllerState(selectedType: self.selectedType, editing: self.editing, peerIdWithRevealedOptions: self.peerIdWithRevealedOptions, removingPeerId: self.removingPeerId, removedPeerIds: self.removedPeerIds, temporaryAdmins: temporaryAdmins)
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,8 +338,20 @@ private func ChannelAdminsControllerEntries(view: PeerView, state: ChannelAdmins
|
||||
if let participants = participants {
|
||||
entries.append(.adminsHeader(isGroup ? "GROUP ADMINS" : "CHANNEL ADMINS"))
|
||||
|
||||
var combinedParticipants: [RenderedChannelParticipant] = participants
|
||||
var existingParticipantIds = Set<PeerId>()
|
||||
for participant in participants {
|
||||
existingParticipantIds.insert(participant.peer.id)
|
||||
}
|
||||
|
||||
for participant in state.temporaryAdmins {
|
||||
if !existingParticipantIds.contains(participant.peer.id) {
|
||||
combinedParticipants.append(participant)
|
||||
}
|
||||
}
|
||||
|
||||
var index: Int32 = 0
|
||||
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<Bool>()
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: "All Members", color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
result.set(true)
|
||||
}),
|
||||
ActionSheetButtonItem(title: "Only Admins", color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
result.set(false)
|
||||
})
|
||||
]), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: "Cancel", color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
let updateSignal = result.get()
|
||||
|> take(1)
|
||||
|> mapToSignal { value -> Signal<Void, NoError> in
|
||||
updateState { state in
|
||||
return state.withUpdatedSelectedType(value ? .everyoneCanAddMembers : .adminsCanAddMembers)
|
||||
}
|
||||
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> mapToSignal { peer -> Signal<Void, NoError> in
|
||||
if let peer = peer as? TelegramChannel, case let .group(info) = peer.info {
|
||||
var updatedValue: Bool?
|
||||
if value && !info.flags.contains(.everyMemberCanInviteMembers) {
|
||||
updatedValue = true
|
||||
} else if !value && info.flags.contains(.everyMemberCanInviteMembers) {
|
||||
updatedValue = false
|
||||
}
|
||||
if let updatedValue = updatedValue {
|
||||
return updateGroupManagementType(account: account, peerId: peerId, type: updatedValue ? .unrestricted : .restrictedToAdmins)
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateAdministrationDisposable.set(updateSignal.start())
|
||||
presentControllerImpl?(actionSheet, nil)
|
||||
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
||||
updateState { state in
|
||||
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
|
||||
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
}
|
||||
}, removeAdmin: { adminId in
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(adminId)
|
||||
}
|
||||
let applyPeers: Signal<Void, NoError> = adminsPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { peers -> Signal<Void, NoError> in
|
||||
if let peers = peers {
|
||||
var updatedPeers = peers
|
||||
for i in 0 ..< updatedPeers.count {
|
||||
if updatedPeers[i].peer.id == adminId {
|
||||
updatedPeers.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
adminsPromise.set(.single(updatedPeers))
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}
|
||||
|
||||
removeAdminDisposable.set((removePeerAdmin(account: account, peerId: peerId, adminId: adminId)
|
||||
|> then(applyPeers |> mapError { _ -> RemovePeerAdminError in return .generic }) |> deliverOnMainQueue).start(error: { _ in
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(nil)
|
||||
}
|
||||
}, completed: {
|
||||
updateState { state in
|
||||
var updatedTemporaryAdmins = state.temporaryAdmins
|
||||
for i in 0 ..< updatedTemporaryAdmins.count {
|
||||
if updatedTemporaryAdmins[i].peer.id == adminId {
|
||||
updatedTemporaryAdmins.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
return state.withUpdatedRemovingPeerId(nil).withUpdatedTemporaryAdmins(updatedTemporaryAdmins)
|
||||
}
|
||||
}))
|
||||
}, addAdmin: {
|
||||
var confirmationImpl: ((PeerId) -> Signal<Bool, NoError>)?
|
||||
let contactsController = ContactSelectionController(account: account, title: "Add admin", confirmation: { peerId in
|
||||
if let confirmationImpl = confirmationImpl {
|
||||
return confirmationImpl(peerId)
|
||||
} else {
|
||||
return .single(false)
|
||||
}
|
||||
})
|
||||
confirmationImpl = { [weak contactsController] peerId in
|
||||
return account.postbox.loadedPeerWithId(peerId)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { peer in
|
||||
let result = ValuePromise<Bool>()
|
||||
if let contactsController = contactsController {
|
||||
let alertController = standardTextAlertController(title: nil, text: "Add \(peer.displayTitle) as admin?", actions: [
|
||||
TextAlertAction(type: .genericAction, title: "Cancel", action: {
|
||||
result.set(false)
|
||||
}),
|
||||
TextAlertAction(type: .defaultAction, title: "OK", action: {
|
||||
result.set(true)
|
||||
})
|
||||
])
|
||||
contactsController.present(alertController, in: .window)
|
||||
}
|
||||
|
||||
return result.get()
|
||||
}
|
||||
}
|
||||
let addAdmin = contactsController.result
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { memberId -> Signal<Void, NoError> in
|
||||
if let memberId = memberId {
|
||||
return account.postbox.peerView(id: memberId)
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { view -> Signal<Void, NoError> in
|
||||
if let peer = view.peers[memberId] {
|
||||
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
|
||||
|
||||
updateState { state in
|
||||
var found = false
|
||||
for participant in state.temporaryAdmins {
|
||||
if participant.peer.id == memberId {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
var removedPeerIds = state.removedPeerIds
|
||||
removedPeerIds.remove(memberId)
|
||||
if !found {
|
||||
var temporaryAdmins = state.temporaryAdmins
|
||||
temporaryAdmins.append(RenderedChannelParticipant(participant: ChannelParticipant.moderator(id: peer.id, invitedBy: account.peerId, invitedAt: timestamp), peer: peer))
|
||||
return state.withUpdatedTemporaryAdmins(temporaryAdmins).withUpdatedRemovedPeerIds(removedPeerIds)
|
||||
} else {
|
||||
return state.withUpdatedRemovedPeerIds(removedPeerIds)
|
||||
}
|
||||
}
|
||||
|
||||
let applyAdmin: Signal<Void, AddPeerAdminError> = adminsPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapError { _ -> AddPeerAdminError in return .generic }
|
||||
|> mapToSignal { admins -> Signal<Void, AddPeerAdminError> in
|
||||
if let admins = admins {
|
||||
var updatedAdmins = admins
|
||||
var found = false
|
||||
for i in 0 ..< updatedAdmins.count {
|
||||
if updatedAdmins[i].peer.id == memberId {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
updatedAdmins.append(RenderedChannelParticipant(participant: ChannelParticipant.moderator(id: peer.id, invitedBy: account.peerId, invitedAt: timestamp), peer: peer))
|
||||
adminsPromise.set(.single(updatedAdmins))
|
||||
}
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}
|
||||
|
||||
return addPeerAdmin(account: account, peerId: peerId, adminId: memberId)
|
||||
|> deliverOnMainQueue
|
||||
|> then(applyAdmin)
|
||||
|> `catch` { _ -> Signal<Void, NoError> in
|
||||
updateState { state in
|
||||
var temporaryAdmins = state.temporaryAdmins
|
||||
for i in 0 ..< temporaryAdmins.count {
|
||||
if temporaryAdmins[i].peer.id == memberId {
|
||||
temporaryAdmins.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return state.withUpdatedTemporaryAdmins(temporaryAdmins)
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
presentControllerImpl?(contactsController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
addAdminDisposable.set(addAdmin.start())
|
||||
})
|
||||
|
||||
let peerView = account.viewTracker.peerView(peerId)
|
||||
|
||||
let 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>, 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
|
||||
}
|
||||
|
||||
@ -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<Void, NoError> = peersPromise.get()
|
||||
|> filter { $0 != nil }
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue
|
||||
|> mapToSignal { peers -> Signal<Void, NoError> in
|
||||
if let peers = peers {
|
||||
var updatedPeers = peers
|
||||
for i in 0 ..< updatedPeers.count {
|
||||
if updatedPeers[i].peer.id == memberId {
|
||||
updatedPeers.remove(at: i)
|
||||
break
|
||||
}
|
||||
}
|
||||
peersPromise.set(.single(updatedPeers))
|
||||
}
|
||||
|
||||
return .complete()
|
||||
}
|
||||
|
||||
removePeerDisposable.set((removeChannelBlacklistedPeer(account: account, peerId: peerId, memberId: memberId) |> then(applyPeers) |> deliverOnMainQueue).start(error: { _ in
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(nil)
|
||||
}
|
||||
}, completed: {
|
||||
updateState {
|
||||
return $0.withUpdatedRemovingPeerId(nil)
|
||||
}
|
||||
|
||||
}))
|
||||
})
|
||||
|
||||
let peerView = account.viewTracker.peerView(peerId)
|
||||
|
||||
let peersSignal: Signal<[RenderedChannelParticipant]?, NoError> = .single(nil) |> then(channelBlacklist(account: account, peerId: peerId) |> map { Optional($0) })
|
||||
|
||||
peersPromise.set(peersSignal)
|
||||
|
||||
let signal = combineLatest(statePromise.get(), peerView, peersPromise.get())
|
||||
|> deliverOnMainQueue
|
||||
|> map { state, view, peers -> (ItemListControllerState, (ItemListNodeState<ChannelBlacklistEntry>, ChannelBlacklistEntry.ItemGenerationArguments)) in
|
||||
var rightNavigationButton: ItemListNavigationButton?
|
||||
if let peers = peers, !peers.isEmpty {
|
||||
if state.editing {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Done", style: .bold, enabled: true, action: {
|
||||
updateState { state in
|
||||
return state.withUpdatedEditing(false)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
rightNavigationButton = ItemListNavigationButton(title: "Edit", style: .regular, enabled: true, action: {
|
||||
updateState { state in
|
||||
return state.withUpdatedEditing(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
if let peers = peers {
|
||||
if peers.isEmpty {
|
||||
emptyStateItem = ItemListTextEmptyStateItem(text: "Blacklisted users are removed from the group and can only come back if invited by an admin. Invite links don't work for them.")
|
||||
}
|
||||
} else if peers == nil {
|
||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem()
|
||||
}
|
||||
|
||||
let controllerState = ItemListControllerState(title: "Blacklist", leftNavigationButton: nil, rightNavigationButton: rightNavigationButton, animateChanges: true)
|
||||
let listState = ItemListNodeState(entries: channelBlacklistControllerEntries(view: view, state: state, participants: peers), style: .blocks, emptyStateItem: emptyStateItem, animateChanges: true)
|
||||
|
||||
return (controllerState, (listState, arguments))
|
||||
} |> afterDisposed {
|
||||
actionsDisposable.dispose()
|
||||
}
|
||||
|
||||
let controller = ItemListController(signal)
|
||||
presentControllerImpl = { [weak controller] c, p in
|
||||
if let controller = controller {
|
||||
controller.present(c, in: .window, with: p)
|
||||
}
|
||||
}
|
||||
return controller
|
||||
}
|
||||
|
||||
@ -4,18 +4,32 @@ import SwiftSignalKit
|
||||
import Postbox
|
||||
import 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>, ChannelVisibilityEntry.ItemGenerationArguments)) in
|
||||
let signal = combineLatest(statePromise.get(), peerView, peersDisablingAddressNameAssignment.get())
|
||||
|> map { state, view, publicChannelsToRevoke -> (ItemListControllerState, (ItemListNodeState<ChannelVisibilityEntry>, 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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,15 +5,6 @@ import Display
|
||||
import TelegramCore
|
||||
|
||||
private let composeButtonImage = generateImage(CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
|
||||
/*
|
||||
|
||||
<svg width="24px" height="24px" viewBox="0 -1 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<path d="M0,4 L15,4 L14,5 L1,5 L1,22 L18,22 L18,9 L19,8 L19,23 L0,23 L0,4 Z M18.5944456,1.70209754 L19.5995507,2.70718758 L10.0510517,12.255543 L9.54849908,13.7631781 L11.0561568,13.2606331 L20.6046559,3.71227763 L21.6097611,4.71736767 L11.5587094,14.7682681 L7.53828874,15.7733582 L9.04594649,11.250453 L18.5944456,1.70209754 Z M19.0969982,1.19955251 L20.0773504,0.21921503 C20.3690844,-0.0725145755 20.8398084,-0.0729335627 21.1298838,0.217137419 L23.0947435,2.18196761 C23.3833646,2.47058439 23.3838887,2.94326675 23.0926659,3.23448517 L22.1123136,4.21482265 L19.0969982,1.19955251 Z" id="Edit" stroke="none" fill="#000000" fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
*/
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -1,677 +0,0 @@
|
||||
/*import Foundation
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
|
||||
private let addMemberPlusIcon = UIImage(bundleImageName: "Peer Info/PeerItemPlusIcon")?.precomposed()
|
||||
|
||||
private enum GroupInfoSection: ItemListSectionId {
|
||||
case info
|
||||
case about
|
||||
case sharedMediaAndNotifications
|
||||
case infoManagement
|
||||
case memberManagement
|
||||
case members
|
||||
case leave
|
||||
}
|
||||
|
||||
enum GroupInfoMemberStatus {
|
||||
case member
|
||||
case admin
|
||||
}
|
||||
|
||||
private struct GroupPeerEntryStableId: PeerInfoEntryStableId {
|
||||
let peerId: PeerId
|
||||
|
||||
func isEqual(to: PeerInfoEntryStableId) -> Bool {
|
||||
if let to = to as? GroupPeerEntryStableId, to.peerId == self.peerId {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var hashValue: Int {
|
||||
return self.peerId.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
enum GroupInfoEntry: PeerInfoEntry {
|
||||
case info(peer: Peer?, cachedData: CachedPeerData?, state: ItemListAvatarAndNameInfoItemState)
|
||||
case setGroupPhoto
|
||||
case aboutHeader
|
||||
case about(text: String)
|
||||
case sharedMedia
|
||||
case notifications(settings: PeerNotificationSettings?)
|
||||
case groupTypeSetup(isPublic: Bool)
|
||||
case groupDescriptionSetup(text: String)
|
||||
case groupManagementInfoLabel(text: String)
|
||||
case membersAdmins(count: Int)
|
||||
case membersBlacklist(count: Int)
|
||||
case usersHeader
|
||||
case addMember
|
||||
case member(index: Int, peerId: PeerId, peer: Peer?, presence: PeerPresence?, memberStatus: GroupInfoMemberStatus)
|
||||
case leave
|
||||
|
||||
var section: ItemListSectionId {
|
||||
switch self {
|
||||
case .info, .setGroupPhoto:
|
||||
return GroupInfoSection.info.rawValue
|
||||
case .aboutHeader, .about:
|
||||
return GroupInfoSection.about.rawValue
|
||||
case .sharedMedia, .notifications:
|
||||
return GroupInfoSection.sharedMediaAndNotifications.rawValue
|
||||
case .groupTypeSetup, .groupDescriptionSetup, .groupManagementInfoLabel:
|
||||
return GroupInfoSection.infoManagement.rawValue
|
||||
case .membersAdmins, .membersBlacklist:
|
||||
return GroupInfoSection.memberManagement.rawValue
|
||||
case .usersHeader, .addMember, .member:
|
||||
return GroupInfoSection.members.rawValue
|
||||
case .leave:
|
||||
return GroupInfoSection.leave.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
func isEqual(to: PeerInfoEntry) -> Bool {
|
||||
guard let entry = to as? GroupInfoEntry else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch self {
|
||||
case let .info(lhsPeer, lhsCachedData, lhsState):
|
||||
if case let .info(rhsPeer, rhsCachedData, rhsState) = entry {
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer == nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsCachedData = lhsCachedData, let rhsCachedData = rhsCachedData {
|
||||
if !lhsCachedData.isEqual(to: rhsCachedData) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsCachedData != nil) != (rhsCachedData != nil) {
|
||||
return false
|
||||
}
|
||||
if lhsState != rhsState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .setGroupPhoto:
|
||||
if case .setGroupPhoto = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .aboutHeader:
|
||||
if case .aboutHeader = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .about(lhsText):
|
||||
switch entry {
|
||||
case .about(lhsText):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case .sharedMedia:
|
||||
switch entry {
|
||||
case .sharedMedia:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .notifications(lhsSettings):
|
||||
switch entry {
|
||||
case let .notifications(rhsSettings):
|
||||
if let lhsSettings = lhsSettings, let rhsSettings = rhsSettings {
|
||||
return lhsSettings.isEqual(to: rhsSettings)
|
||||
} else if (lhsSettings != nil) != (rhsSettings != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case let .groupTypeSetup(isPublic):
|
||||
if case .groupTypeSetup(isPublic) = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .groupDescriptionSetup(text):
|
||||
if case .groupDescriptionSetup(text) = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .groupManagementInfoLabel(text):
|
||||
if case .groupManagementInfoLabel(text) = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .membersAdmins(lhsCount):
|
||||
if case let .membersAdmins(rhsCount) = entry, lhsCount == rhsCount {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .membersBlacklist(lhsCount):
|
||||
if case let .membersBlacklist(rhsCount) = entry, lhsCount == rhsCount {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .usersHeader:
|
||||
if case .usersHeader = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .addMember:
|
||||
if case .addMember = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .member(lhsIndex, lhsPeerId, lhsPeer, lhsPresence, lhsMemberStatus):
|
||||
if case let .member(rhsIndex, rhsPeerId, rhsPeer, rhsPresence, rhsMemberStatus) = entry {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsMemberStatus != rhsMemberStatus {
|
||||
return false
|
||||
}
|
||||
if lhsPeerId != rhsPeerId {
|
||||
return false
|
||||
}
|
||||
if let lhsPeer = lhsPeer, let rhsPeer = rhsPeer {
|
||||
if !lhsPeer.isEqual(rhsPeer) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPeer != nil) != (rhsPeer != nil) {
|
||||
return false
|
||||
}
|
||||
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
||||
if !lhsPresence.isEqual(to: rhsPresence) {
|
||||
return false
|
||||
}
|
||||
} else if (lhsPresence != nil) != (rhsPresence != nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case .leave:
|
||||
if case .leave = entry {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stableId: PeerInfoEntryStableId {
|
||||
switch self {
|
||||
case let .member(_, peerId, _, _, _):
|
||||
return GroupPeerEntryStableId(peerId: peerId)
|
||||
default:
|
||||
return IntPeerInfoEntryStableId(value: self.sortIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private var sortIndex: Int {
|
||||
switch self {
|
||||
case .info:
|
||||
return 0
|
||||
case .setGroupPhoto:
|
||||
return 1
|
||||
case .aboutHeader:
|
||||
return 2
|
||||
case .about:
|
||||
return 3
|
||||
case .notifications:
|
||||
return 4
|
||||
case .sharedMedia:
|
||||
return 5
|
||||
case .groupTypeSetup:
|
||||
return 6
|
||||
case .groupDescriptionSetup:
|
||||
return 7
|
||||
case .groupManagementInfoLabel:
|
||||
return 8
|
||||
case .membersAdmins:
|
||||
return 9
|
||||
case .membersBlacklist:
|
||||
return 10
|
||||
case .usersHeader:
|
||||
return 11
|
||||
case .addMember:
|
||||
return 12
|
||||
case let .member(index, _, _, _, _):
|
||||
return 20 + index
|
||||
case .leave:
|
||||
return 1000000
|
||||
}
|
||||
}
|
||||
|
||||
func isOrderedBefore(_ entry: PeerInfoEntry) -> Bool {
|
||||
guard let other = entry as? GroupInfoEntry else {
|
||||
return false
|
||||
}
|
||||
|
||||
return self.sortIndex < other.sortIndex
|
||||
}
|
||||
|
||||
func item(account: Account, interaction: PeerInfoControllerInteraction) -> ListViewItem {
|
||||
switch self {
|
||||
case let .info(peer, cachedData, state):
|
||||
return ItemListAvatarAndNameInfoItem(account: account, peer: peer, presence: nil, cachedData: cachedData, state: state, sectionId: self.section, style: .blocks, editingNameUpdated: { editingName in
|
||||
|
||||
})
|
||||
case .setGroupPhoto:
|
||||
return ItemListActionItem(title: "Set Group Photo", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
})
|
||||
case let .notifications(settings):
|
||||
let label: String
|
||||
if let settings = settings as? TelegramPeerNotificationSettings, case .muted = settings.muteState {
|
||||
label = "Disabled"
|
||||
} else {
|
||||
label = "Enabled"
|
||||
}
|
||||
return ItemListDisclosureItem(title: "Notifications", label: label, sectionId: self.section, style: .blocks, action: {
|
||||
interaction.changeNotificationMuteSettings()
|
||||
})
|
||||
case .sharedMedia:
|
||||
return ItemListDisclosureItem(title: "Shared Media", label: "", sectionId: self.section, style: .blocks, action: {
|
||||
interaction.openSharedMedia()
|
||||
})
|
||||
case .addMember:
|
||||
return ItemListPeerActionItem(icon: addMemberPlusIcon, title: "Add Member", sectionId: self.section, action: {
|
||||
|
||||
})
|
||||
case let .groupTypeSetup(isPublic):
|
||||
return ItemListDisclosureItem(title: "Group Type", label: isPublic ? "Public" : "Private", sectionId: self.section, style: .blocks, action: {
|
||||
|
||||
})
|
||||
case let .groupDescriptionSetup(text):
|
||||
return ItemListMultilineInputItem(text: text, sectionId: self.section, textUpdated: { updatedText in
|
||||
interaction.updateState { state in
|
||||
if let state = state as? GroupInfoState, let editingState = state.editingState {
|
||||
return state.withUpdatedEditingState(editingState.withUpdatedEditingDescriptionText(updatedText))
|
||||
}
|
||||
return state
|
||||
}
|
||||
}, action: {
|
||||
|
||||
})
|
||||
case let .membersAdmins(count):
|
||||
return ItemListDisclosureItem(title: "Admins", label: "\(count)", sectionId: self.section, style: .blocks, action: {
|
||||
|
||||
})
|
||||
case let .membersBlacklist(count):
|
||||
return ItemListDisclosureItem(title: "Blacklist", label: "\(count)", sectionId: self.section, style: .blocks, action: {
|
||||
|
||||
})
|
||||
case let .member(_, _, peer, presence, memberStatus):
|
||||
let label: String?
|
||||
switch memberStatus {
|
||||
case .admin:
|
||||
label = "admin"
|
||||
case .member:
|
||||
label = nil
|
||||
}
|
||||
return ItemListPeerItem(account: account, peer: peer, presence: presence, label: label, sectionId: self.section, action: {
|
||||
if let peer = peer {
|
||||
interaction.openPeerInfo(peer.id)
|
||||
}
|
||||
})
|
||||
case .leave:
|
||||
return ItemListActionItem(title: "Delete and Exit", kind: .destructive, alignment: .center, sectionId: self.section, style: .blocks, action: {
|
||||
})
|
||||
default:
|
||||
preconditionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GroupInfoState: PeerInfoState {
|
||||
let editingState: GroupInfoEditingState?
|
||||
let updatingName: ItemListAvatarAndNameInfoItemName?
|
||||
|
||||
func isEqual(to: PeerInfoState) -> Bool {
|
||||
if let to = to as? GroupInfoState {
|
||||
if self.editingState != to.editingState {
|
||||
return false
|
||||
}
|
||||
if self.updatingName != to.updatingName {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func withUpdatedEditingState(_ editingState: GroupInfoEditingState?) -> GroupInfoState {
|
||||
return GroupInfoState(editingState: editingState, updatingName: self.updatingName)
|
||||
}
|
||||
|
||||
func withUpdatedUpdatingName(_ updatingName: ItemListAvatarAndNameInfoItemName?) -> GroupInfoState {
|
||||
return GroupInfoState(editingState: self.editingState, updatingName: updatingName)
|
||||
}
|
||||
}
|
||||
|
||||
private struct GroupInfoEditingState: Equatable {
|
||||
let editingName: ItemListAvatarAndNameInfoItemName?
|
||||
let editingDescriptionText: String
|
||||
|
||||
func withUpdatedEditingDescriptionText(_ editingDescriptionText: String) -> GroupInfoEditingState {
|
||||
return GroupInfoEditingState(editingName: self.editingName, editingDescriptionText: editingDescriptionText)
|
||||
}
|
||||
|
||||
static func ==(lhs: GroupInfoEditingState, rhs: GroupInfoEditingState) -> Bool {
|
||||
if lhs.editingName != rhs.editingName {
|
||||
return false
|
||||
}
|
||||
if lhs.editingDescriptionText != rhs.editingDescriptionText {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func groupInfoEntries(view: PeerView, state: PeerInfoState?) -> PeerInfoEntries {
|
||||
var entries: [PeerInfoEntry] = []
|
||||
if let peer = peerViewMainPeer(view) {
|
||||
let infoState = ItemListAvatarAndNameInfoItemState(editingName: (state as? GroupInfoState)?.editingState?.editingName, updatingName: (state as? GroupInfoState)?.updatingName)
|
||||
entries.append(GroupInfoEntry.info(peer: peer, cachedData: view.cachedData, state: infoState))
|
||||
}
|
||||
|
||||
var highlightAdmins = false
|
||||
var canManageGroup = false
|
||||
if let group = view.peers[view.peerId] as? TelegramGroup {
|
||||
if group.flags.contains(.adminsEnabled) {
|
||||
highlightAdmins = true
|
||||
switch group.role {
|
||||
case .admin, .creator:
|
||||
canManageGroup = true
|
||||
case .member:
|
||||
break
|
||||
}
|
||||
} else {
|
||||
canManageGroup = true
|
||||
}
|
||||
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
||||
highlightAdmins = true
|
||||
switch channel.role {
|
||||
case .creator:
|
||||
canManageGroup = true
|
||||
case .editor, .moderator, .member:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if canManageGroup {
|
||||
entries.append(GroupInfoEntry.setGroupPhoto)
|
||||
}
|
||||
|
||||
if let editingState = (state as? GroupInfoState)?.editingState {
|
||||
if let cachedChannelData = view.cachedData as? CachedChannelData {
|
||||
entries.append(GroupInfoEntry.groupTypeSetup(isPublic: cachedChannelData.exportedInvitation != nil))
|
||||
entries.append(GroupInfoEntry.groupDescriptionSetup(text: editingState.editingDescriptionText))
|
||||
|
||||
if let adminCount = cachedChannelData.participantsSummary.adminCount {
|
||||
entries.append(GroupInfoEntry.membersAdmins(count: adminCount))
|
||||
}
|
||||
if let bannedCount = cachedChannelData.participantsSummary.bannedCount {
|
||||
entries.append(GroupInfoEntry.membersBlacklist(count: bannedCount))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
entries.append(GroupInfoEntry.notifications(settings: view.notificationSettings))
|
||||
entries.append(GroupInfoEntry.sharedMedia)
|
||||
}
|
||||
|
||||
if canManageGroup {
|
||||
entries.append(GroupInfoEntry.addMember)
|
||||
}
|
||||
|
||||
if let cachedGroupData = view.cachedData as? CachedGroupData, let participants = cachedGroupData.participants {
|
||||
let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in
|
||||
let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence
|
||||
let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence
|
||||
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
||||
if lhsPresence.status < rhsPresence.status {
|
||||
return false
|
||||
} else if lhsPresence.status > rhsPresence.status {
|
||||
return true
|
||||
}
|
||||
} else if let _ = lhsPresence {
|
||||
return true
|
||||
} else if let _ = rhsPresence {
|
||||
return false
|
||||
}
|
||||
|
||||
switch lhs {
|
||||
case .creator:
|
||||
return false
|
||||
case let .admin(lhsId, _, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .admin(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
case let .member(lhsId, _, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .admin(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
for i in 0 ..< sortedParticipants.count {
|
||||
if let peer = view.peers[sortedParticipants[i].peerId] {
|
||||
let memberStatus: GroupInfoMemberStatus
|
||||
if highlightAdmins {
|
||||
switch sortedParticipants[i] {
|
||||
case .admin, .creator:
|
||||
memberStatus = .admin
|
||||
case .member:
|
||||
memberStatus = .member
|
||||
}
|
||||
} else {
|
||||
memberStatus = .member
|
||||
}
|
||||
entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus))
|
||||
}
|
||||
}
|
||||
} else if let cachedChannelData = view.cachedData as? CachedChannelData, let participants = cachedChannelData.topParticipants {
|
||||
let sortedParticipants = participants.participants.sorted(by: { lhs, rhs in
|
||||
let lhsPresence = view.peerPresences[lhs.peerId] as? TelegramUserPresence
|
||||
let rhsPresence = view.peerPresences[rhs.peerId] as? TelegramUserPresence
|
||||
if let lhsPresence = lhsPresence, let rhsPresence = rhsPresence {
|
||||
if lhsPresence.status < rhsPresence.status {
|
||||
return false
|
||||
} else if lhsPresence.status > rhsPresence.status {
|
||||
return true
|
||||
}
|
||||
} else if let _ = lhsPresence {
|
||||
return true
|
||||
} else if let _ = rhsPresence {
|
||||
return false
|
||||
}
|
||||
|
||||
switch lhs {
|
||||
case .creator:
|
||||
return false
|
||||
case let .moderator(lhsId, _, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .moderator(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .editor(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
case let .editor(lhsId, _, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .moderator(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .editor(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
case let .member(lhsId, lhsInvitedAt):
|
||||
switch rhs {
|
||||
case .creator:
|
||||
return true
|
||||
case let .moderator(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .editor(rhsId, _, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
case let .member(rhsId, rhsInvitedAt):
|
||||
if lhsInvitedAt == rhsInvitedAt {
|
||||
return lhsId.id < rhsId.id
|
||||
}
|
||||
return lhsInvitedAt > rhsInvitedAt
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
for i in 0 ..< sortedParticipants.count {
|
||||
if let peer = view.peers[sortedParticipants[i].peerId] {
|
||||
let memberStatus: GroupInfoMemberStatus
|
||||
if highlightAdmins {
|
||||
switch sortedParticipants[i] {
|
||||
case .moderator, .editor, .creator:
|
||||
memberStatus = .admin
|
||||
case .member:
|
||||
memberStatus = .member
|
||||
}
|
||||
} else {
|
||||
memberStatus = .member
|
||||
}
|
||||
entries.append(GroupInfoEntry.member(index: i, peerId: peer.id, peer: peer, presence: view.peerPresences[peer.id], memberStatus: memberStatus))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let group = view.peers[view.peerId] as? TelegramGroup {
|
||||
if case .Member = group.membership {
|
||||
entries.append(GroupInfoEntry.leave)
|
||||
}
|
||||
} else if let channel = view.peers[view.peerId] as? TelegramChannel {
|
||||
if case .member = channel.participationStatus {
|
||||
entries.append(GroupInfoEntry.leave)
|
||||
}
|
||||
}
|
||||
|
||||
var leftNavigationButton: PeerInfoNavigationButton?
|
||||
var rightNavigationButton: PeerInfoNavigationButton?
|
||||
if canManageGroup {
|
||||
if let state = state as? GroupInfoState, let _ = state.editingState {
|
||||
leftNavigationButton = PeerInfoNavigationButton(title: "Cancel", action: { state in
|
||||
if state == nil {
|
||||
return GroupInfoState(editingState: nil, updatingName: nil)
|
||||
} else if let state = state as? GroupInfoState {
|
||||
return state.withUpdatedEditingState(nil)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
})
|
||||
rightNavigationButton = PeerInfoNavigationButton(title: "Done", action: { state in
|
||||
if state == nil {
|
||||
return GroupInfoState(editingState: nil, updatingName: nil)
|
||||
} else if let state = state as? GroupInfoState {
|
||||
return state.withUpdatedEditingState(nil)
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
})
|
||||
} else {
|
||||
var editingName: ItemListAvatarAndNameInfoItemName?
|
||||
if let peer = peerViewMainPeer(view) {
|
||||
editingName = ItemListAvatarAndNameInfoItemName(peer.indexName)
|
||||
}
|
||||
let editingDescriptionText: String
|
||||
if let cachedChannelData = view.cachedData as? CachedChannelData, let about = cachedChannelData.about {
|
||||
editingDescriptionText = about
|
||||
} else {
|
||||
editingDescriptionText = ""
|
||||
}
|
||||
rightNavigationButton = PeerInfoNavigationButton(title: "Edit", action: { state in
|
||||
if state == nil {
|
||||
return GroupInfoState(editingState: GroupInfoEditingState(editingName: editingName, editingDescriptionText: editingDescriptionText), updatingName: nil)
|
||||
} else if let state = state as? GroupInfoState {
|
||||
return state.withUpdatedEditingState(GroupInfoEditingState(editingName: editingName, editingDescriptionText: editingDescriptionText))
|
||||
} else {
|
||||
return state
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return PeerInfoEntries(entries: entries, leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton)
|
||||
}
|
||||
*/
|
||||
@ -21,14 +21,16 @@ class ItemListActionItem: ListViewItem, ItemListItem {
|
||||
let sectionId: ItemListSectionId
|
||||
let 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, NoError>?, () -> 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
|
||||
|
||||
14
TelegramUI/ItemListControllerEmptyStateItem.swift
Normal file
14
TelegramUI/ItemListControllerEmptyStateItem.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
protocol ItemListControllerEmptyStateItem {
|
||||
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool
|
||||
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode
|
||||
}
|
||||
|
||||
class ItemListControllerEmptyStateItemNode: ASDisplayNode {
|
||||
func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
}
|
||||
}
|
||||
@ -37,6 +37,7 @@ enum ItemListStyle {
|
||||
private struct ItemListNodeTransition {
|
||||
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<Entry: ItemListNodeEntry> {
|
||||
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<Entry: ItemListNodeEntry>: 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<Entry: ItemListNodeEntry>: 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<Entry: ItemListNodeEntry>: ASDisplayNode {
|
||||
|
||||
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
||||
|
||||
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<Entry: ItemListNodeEntry>: 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<Entry: ItemListNodeEntry>: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
})
|
||||
var updateEmptyStateItem = false
|
||||
if let emptyStateItem = self.emptyStateItem, let updatedEmptyStateItem = transition.emptyStateItem {
|
||||
updateEmptyStateItem = !emptyStateItem.isEqual(to: updatedEmptyStateItem)
|
||||
} else if (self.emptyStateItem != nil) != (transition.emptyStateItem != nil) {
|
||||
updateEmptyStateItem = true
|
||||
}
|
||||
if updateEmptyStateItem {
|
||||
self.emptyStateItem = transition.emptyStateItem
|
||||
if let emptyStateItem = transition.emptyStateItem {
|
||||
let updatedNode = emptyStateItem.node(current: self.emptyStateNode)
|
||||
if let emptyStateNode = self.emptyStateNode, updatedNode !== emptyStateNode {
|
||||
emptyStateNode.removeFromSupernode()
|
||||
}
|
||||
if self.emptyStateNode !== updatedNode {
|
||||
self.emptyStateNode = updatedNode
|
||||
if let validLayout = self.validLayout {
|
||||
updatedNode.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
||||
}
|
||||
self.addSubnode(updatedNode)
|
||||
}
|
||||
} else if let emptyStateNode = self.emptyStateNode {
|
||||
emptyStateNode.removeFromSupernode()
|
||||
self.emptyStateNode = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ class ItemListRevealOptionsItemNode: ListViewItemNode {
|
||||
|
||||
override func layout() {
|
||||
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)
|
||||
}
|
||||
|
||||
47
TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift
Normal file
47
TelegramUI/ItemListLoadingIndicatorEmptyStateItem.swift
Normal file
@ -0,0 +1,47 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
final class ItemListLoadingIndicatorEmptyStateItem: ItemListControllerEmptyStateItem {
|
||||
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
|
||||
return to is ItemListLoadingIndicatorEmptyStateItem
|
||||
}
|
||||
|
||||
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
|
||||
if let current = current as? ItemListLoadingIndicatorEmptyStateItemNode {
|
||||
return current
|
||||
} else {
|
||||
return ItemListLoadingIndicatorEmptyStateItemNode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemListLoadingIndicatorEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
||||
private var indicator: UIActivityIndicatorView?
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
|
||||
self.indicator = indicator
|
||||
self.view.addSubview(indicator)
|
||||
if let layout = self.validLayout {
|
||||
self.updateLayout(layout: layout.0, navigationBarHeight: layout.1, transition: .immediate)
|
||||
}
|
||||
indicator.startAnimating()
|
||||
}
|
||||
|
||||
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
if let indicator = self.indicator {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
insets.top += navigationBarHeight
|
||||
|
||||
let size = indicator.bounds.size
|
||||
indicator.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - size.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - size.height) / 2.0)), size: size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,7 @@ struct ItemListPeerItemEditing: Equatable {
|
||||
enum ItemListPeerItemText {
|
||||
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))
|
||||
|
||||
|
||||
65
TelegramUI/ItemListTextEmptyStateItem.swift
Normal file
65
TelegramUI/ItemListTextEmptyStateItem.swift
Normal file
@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
|
||||
final class ItemListTextEmptyStateItem: ItemListControllerEmptyStateItem {
|
||||
let text: String
|
||||
|
||||
init(text: String) {
|
||||
self.text = text
|
||||
}
|
||||
|
||||
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
|
||||
if let to = to as? ItemListTextEmptyStateItem {
|
||||
return self.text == to.text
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
|
||||
let result: ItemListTextEmptyStateItemNode
|
||||
if let current = current as? ItemListTextEmptyStateItemNode {
|
||||
result = current
|
||||
} else {
|
||||
result = ItemListTextEmptyStateItemNode()
|
||||
}
|
||||
result.updateText(text: self.text)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
final class ItemListTextEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
||||
private let textNode: ASTextNode
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
private var text: String?
|
||||
|
||||
override init() {
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.isLayerBacked = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
}
|
||||
|
||||
func updateText(text: String) {
|
||||
if self.text != text {
|
||||
self.text = text
|
||||
|
||||
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(14.0), textColor: .gray, paragraphAlignment: .center)
|
||||
if let validLayout = self.validLayout {
|
||||
self.updateLayout(layout: validLayout.0, navigationBarHeight: validLayout.1, transition: .immediate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (layout, navigationBarHeight)
|
||||
var insets = layout.insets(options: [.statusBar])
|
||||
insets.top += navigationBarHeight
|
||||
let textSize = self.textNode.measure(CGSize(width: layout.size.width - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
|
||||
self.textNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: insets.top + floor((layout.size.height - insets.top - insets.bottom - textSize.height) / 2.0)), size: textSize)
|
||||
}
|
||||
}
|
||||
@ -91,6 +91,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
|
||||
self.textField = SearchBarTextField()
|
||||
self.textField.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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
43
TelegramUI/ValidateAddressNameInteractive.swift
Normal file
43
TelegramUI/ValidateAddressNameInteractive.swift
Normal file
@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
|
||||
enum AddressNameValidationStatus: Equatable {
|
||||
case checking
|
||||
case invalidFormat(AddressNameFormatError)
|
||||
case availability(AddressNameAvailability)
|
||||
|
||||
static func ==(lhs: AddressNameValidationStatus, rhs: AddressNameValidationStatus) -> Bool {
|
||||
switch lhs {
|
||||
case .checking:
|
||||
if case .checking = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .invalidFormat(error):
|
||||
if case .invalidFormat(error) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .availability(availability):
|
||||
if case .availability(availability) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateAddressNameInteractive(account: Account, domain: AddressNameDomain, name: String) -> Signal<AddressNameValidationStatus, NoError> {
|
||||
if let error = checkAddressNameFormat(name) {
|
||||
return .single(.invalidFormat(error))
|
||||
} else {
|
||||
return .single(.checking) |> then(addressNameAvailability(account: account, domain: domain, name: name)
|
||||
|> delay(0.3, queue: Queue.concurrentDefaultQueue())
|
||||
|> map { result -> AddressNameValidationStatus in .availability(result) })
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user