From 73ae9017d27fe3041da9ca25374e0c31e28f3421 Mon Sep 17 00:00:00 2001 From: overtake <> Date: Tue, 20 Nov 2018 13:36:29 +0400 Subject: [PATCH] notification exceptions ui --- TelegramUI.xcodeproj/project.pbxproj | 8 + TelegramUI/BlockedPeersController.swift | 7 +- TelegramUI/ChatListNode.swift | 90 +- TelegramUI/ChatListNodeEntries.swift | 10 - TelegramUI/ChatListSearchContainerNode.swift | 49 +- TelegramUI/ItemListPeerItem.swift | 14 +- .../NotificationExceptionControllerNode.swift | 1039 +++++++++++ TelegramUI/NotificationExceptions.swift | 1528 ++++++----------- ...tificationExcetionSettingsController.swift | 336 ++++ TelegramUI/NotificationSoundSelection.swift | 2 +- TelegramUI/NotificationsAndSounds.swift | 57 +- TelegramUI/PeerSelectionController.swift | 10 +- TelegramUI/PeerSelectionControllerNode.swift | 69 +- TelegramUI/PresentationData.swift | 12 + 14 files changed, 2144 insertions(+), 1087 deletions(-) create mode 100644 TelegramUI/NotificationExceptionControllerNode.swift create mode 100644 TelegramUI/NotificationExcetionSettingsController.swift diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 9f34b723a6..90a9f5bdc3 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -63,6 +63,8 @@ 09D304152173C0E900C00567 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304142173C0E900C00567 /* WatchManager.swift */; }; 09D304182173C15700C00567 /* WatchSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304172173C15700C00567 /* WatchSettingsController.swift */; }; 09FE756D2153F5F900A3120F /* CallRouteActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */; }; + 9F06830921A404AB001D8EDB /* NotificationExceptionControllerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */; }; + 9F06830B21A404C4001D8EDB /* NotificationExcetionSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */; }; D0068FA821760FA300D1B315 /* StoreDownloadedMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0068FA721760FA300D1B315 /* StoreDownloadedMedia.swift */; }; D007019C2029E8F2006B9E34 /* LegqacyICloudFileController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007019B2029E8F2006B9E34 /* LegqacyICloudFileController.swift */; }; D007019E2029EFDD006B9E34 /* ICloudResources.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007019D2029EFDD006B9E34 /* ICloudResources.swift */; }; @@ -1124,6 +1126,8 @@ 09D304142173C0E900C00567 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = ""; }; 09D304172173C15700C00567 /* WatchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsController.swift; sourceTree = ""; }; 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = ""; }; + 9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExceptionControllerNode.swift; sourceTree = ""; }; + 9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationExcetionSettingsController.swift; sourceTree = ""; }; D00219051DDD1C9E00BE708A /* ImageContainingNode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageContainingNode.swift; sourceTree = ""; }; D002A0D01E9B99F500A81812 /* SoftwareVideoSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoftwareVideoSource.swift; sourceTree = ""; }; D002A0D21E9BBE6700A81812 /* MultiplexedSoftwareVideoSourceManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplexedSoftwareVideoSourceManager.swift; sourceTree = ""; }; @@ -2909,6 +2913,8 @@ D0579E6D2179178700495DC7 /* exceptions */ = { isa = PBXGroup; children = ( + 9F06830A21A404C4001D8EDB /* NotificationExcetionSettingsController.swift */, + 9F06830821A404AB001D8EDB /* NotificationExceptionControllerNode.swift */, D02C81702177729000CD1006 /* NotificationExceptions.swift */, ); name = exceptions; @@ -5286,6 +5292,7 @@ D0EC6D871EB9F58800EBF1C3 /* ChatTitleView.swift in Sources */, D04614372005094E00EC0EF2 /* DeviceLocationManager.swift in Sources */, D0EC6D881EB9F58800EBF1C3 /* ChatControllerTitlePanelNodeContainer.swift in Sources */, + 9F06830B21A404C4001D8EDB /* NotificationExcetionSettingsController.swift in Sources */, D0EC6D891EB9F58800EBF1C3 /* ChatSecretAutoremoveTimerActionSheet.swift in Sources */, D05D8B782195E0050064586F /* SetupTwoStepVerificationContentNode.swift in Sources */, D0EC6D8A1EB9F58800EBF1C3 /* ChatInfo.swift in Sources */, @@ -5331,6 +5338,7 @@ D0E8174E2011FC3800B82BBB /* ChatMessageEventLogPreviousDescriptionContentNode.swift in Sources */, D0EC6D981EB9F58900EBF1C3 /* ChatMessageItemView.swift in Sources */, 09D304152173C0E900C00567 /* WatchManager.swift in Sources */, + 9F06830921A404AB001D8EDB /* NotificationExceptionControllerNode.swift in Sources */, D039FB1921711B5D00BD1BAD /* PlatformVideoContent.swift in Sources */, D0CAD8FD20AE467D00ACD96E /* PeerChannelMemberCategoriesContextsManager.swift in Sources */, D073D2DB1FB61DA9009E1DA2 /* CallListSettings.swift in Sources */, diff --git a/TelegramUI/BlockedPeersController.swift b/TelegramUI/BlockedPeersController.swift index e3391a89aa..bdc95c7226 100644 --- a/TelegramUI/BlockedPeersController.swift +++ b/TelegramUI/BlockedPeersController.swift @@ -112,7 +112,7 @@ private enum BlockedPeersEntry: ItemListNodeEntry { func item(_ arguments: BlockedPeersControllerArguments) -> ListViewItem { switch self { case let .add(theme, text): - return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: text, sectionId: self.section, editing: false, action: { + return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.addPeer() }) case let .peerItem(_, theme, strings, dateTimeFormat, peer, editing, enabled): @@ -175,7 +175,7 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s var entries: [BlockedPeersEntry] = [] if let peers = peers { - entries.append(.add(presentationData.theme, presentationData.strings.BlockedUsers_BlockUser)) + entries.append(.add(presentationData.theme, presentationData.strings.Conversation_BlockUser)) var index: Int32 = 0 for peer in peers { @@ -213,8 +213,7 @@ public func blockedPeersController(account: Account) -> ViewController { } } }, addPeer: { - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - let controller = PeerSelectionController(account: account, filter: [.onlyUsers], title: presentationData.strings.BlockedUsers_SelectUserTitle) + let controller = PeerSelectionController(account: account, filter: [.onlyPrivateChats]) controller.peerSelected = { [weak controller] peerId in if let strongController = controller { strongController.inProgress = true diff --git a/TelegramUI/ChatListNode.swift b/TelegramUI/ChatListNode.swift index 9a18a58ce1..1235d8644c 100644 --- a/TelegramUI/ChatListNode.swift +++ b/TelegramUI/ChatListNode.swift @@ -13,11 +13,15 @@ public struct ChatListNodePeersFilter: OptionSet { } public static let onlyWriteable = ChatListNodePeersFilter(rawValue: 1 << 0) - public static let onlyUsers = ChatListNodePeersFilter(rawValue: 1 << 1) + public static let onlyPrivateChats = ChatListNodePeersFilter(rawValue: 1 << 1) public static let onlyGroups = ChatListNodePeersFilter(rawValue: 1 << 2) - public static let onlyManageable = ChatListNodePeersFilter(rawValue: 1 << 3) - public static let withoutSecretChats = ChatListNodePeersFilter(rawValue: 1 << 4) - + public static let onlyChannels = ChatListNodePeersFilter(rawValue: 1 << 3) + public static let onlyManageable = ChatListNodePeersFilter(rawValue: 1 << 4) + + public static let excludeSecretChats = ChatListNodePeersFilter(rawValue: 1 << 5) + public static let excludeRecent = ChatListNodePeersFilter(rawValue: 1 << 6) + public static let excludeSavedMessages = ChatListNodePeersFilter(rawValue: 1 << 7) + } @@ -154,7 +158,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } - if filter.contains(.onlyUsers) { + if filter.contains(.onlyPrivateChats) { if let peer = peer.peers[peer.peerId] { if !(peer is TelegramUser || peer is TelegramSecretChat) { enabled = false @@ -185,6 +189,7 @@ private func mappedInsertEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } + return ListViewInsertItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, account: account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) @@ -226,26 +231,6 @@ private func mappedUpdateEntries(account: Account, nodeInteraction: ChatListNode enabled = false } } - if filter.contains(.onlyUsers) { - if let peer = peer.peers[peer.peerId] { - if !(peer is TelegramUser || peer is TelegramSecretChat) { - enabled = false - } - } else { - enabled = false - } - } - if filter.contains(.onlyGroups) { - if let peer = peer.peers[peer.peerId] { - if let _ = peer as? TelegramGroup { - } else if let peer = peer as? TelegramChannel, case .group = peer.info { - } else { - enabled = false - } - } else { - enabled = false - } - } return ListViewUpdateItem(index: entry.index, previousIndex: entry.previousIndex, item: ContactsPeerItem(theme: presentationData.theme, strings: presentationData.strings, sortOrder: presentationData.nameSortOrder, displayOrder: presentationData.nameDisplayOrder, account: account, peerMode: .generalSearch, peer: .peer(peer: itemPeer, chatPeer: chatPeer), status: .none, enabled: enabled, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: nil, action: { _ in if let chatPeer = chatPeer { nodeInteraction.peerSelected(chatPeer) @@ -450,8 +435,51 @@ final class ChatListNode: ListView { savedMessagesPeer = .single(nil) } + let currentPeerId: PeerId = account.peerId + let chatListNodeViewTransition = combineLatest(savedMessagesPeer, chatListViewUpdate, self.statePromise.get()) |> mapToQueue { (savedMessagesPeer, update, state) -> Signal in - let processedView = ChatListNodeView(originalView: update.view, filteredEntries: chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, mode: mode)) + + let entries = chatListNodeEntriesForView(update.view, state: state, savedMessagesPeer: savedMessagesPeer, mode: mode).filter { entry in + switch entry { + case let .PeerEntry(index, _, _, _, _, _, peer, _, _, _, _, _): + //ChatListNodePeersFilter + switch mode { + case .chatList: + return true + case let .peers(filter): + + guard !filter.contains(.excludeSavedMessages) || peer.peerId != currentPeerId else { return false } + guard !filter.contains(.excludeSecretChats) || peer.peerId.namespace != Namespaces.Peer.SecretChat else { return false } + guard !filter.contains(.onlyPrivateChats) || peer.peerId.namespace == Namespaces.Peer.CloudUser else { return false } + + if filter.contains(.onlyGroups) { + var isGroup: Bool = false + if let peer = peer.chatMainPeer as? TelegramChannel, case .group = peer.info { + isGroup = true + } else if peer.peerId.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } + if !isGroup { + return false + } + } + + if filter.contains(.onlyChannels) { + if let peer = peer.chatMainPeer as? TelegramChannel, case .broadcast = peer.info { + return true + } else { + return false + } + } + + return true + } + default: + return true + } + } + + let processedView = ChatListNodeView(originalView: update.view, filteredEntries: entries) let previousView = previousView.swap(processedView) let previousState = previousState.swap(state) @@ -587,7 +615,15 @@ final class ChatListNode: ListView { self.chatListDisposable.set(appliedTransition.start()) - let initialLocation: ChatListNodeLocation = .initial(count: 50) + let initialLocation: ChatListNodeLocation + + switch mode { + case .chatList: + initialLocation = .initial(count: 50) + case .peers: + initialLocation = .initial(count: 200) + } + self.currentLocation = initialLocation self.chatListLocation.set(initialLocation) diff --git a/TelegramUI/ChatListNodeEntries.swift b/TelegramUI/ChatListNodeEntries.swift index f2affd7a09..d1e87fa5b4 100644 --- a/TelegramUI/ChatListNodeEntries.swift +++ b/TelegramUI/ChatListNodeEntries.swift @@ -229,16 +229,6 @@ func chatListNodeEntriesForView(_ view: ChatListView, state: ChatListNodeState, if let savedMessagesPeer = savedMessagesPeer, savedMessagesPeer.id == index.messageIndex.id.peerId { continue loop } - switch mode { - case let .peers(filter): - if filter.contains(.withoutSecretChats) { - if index.messageIndex.id.peerId.namespace == Namespaces.Peer.SecretChat { - continue - } - } - default: - break - } result.append(.PeerEntry(index: offsetPinnedIndex(index, offset: pinnedIndexOffset), presentationData: state.presentationData, message: message, readState: combinedReadState, notificationSettings: notificationSettings, embeddedInterfaceState: embeddedState, peer: peer, summaryInfo: summaryInfo, editing: state.editing, hasActiveRevealControls: index.messageIndex.id.peerId == state.peerIdWithRevealedOptions, inputActivities: state.peerInputActivities?.activities[index.messageIndex.id.peerId], isAd: false)) case let .HoleEntry(hole): result.append(.HoleEntry(hole, theme: state.presentationData.theme)) diff --git a/TelegramUI/ChatListSearchContainerNode.swift b/TelegramUI/ChatListSearchContainerNode.swift index d4e9252176..0a4ffa0f69 100644 --- a/TelegramUI/ChatListSearchContainerNode.swift +++ b/TelegramUI/ChatListSearchContainerNode.swift @@ -121,7 +121,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable { enabled = canSendMessagesToPeer(primaryPeer) } } - if filter.contains(.onlyUsers) { + if filter.contains(.onlyPrivateChats) { if let peer = chatPeer { if !(peer is TelegramUser || peer is TelegramSecretChat) { enabled = false @@ -362,7 +362,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { enabled = false } } - if filter.contains(.onlyUsers) { + if filter.contains(.onlyPrivateChats) { if let peer = chatPeer { if !(peer is TelegramUser || peer is TelegramSecretChat) { enabled = false @@ -396,7 +396,7 @@ enum ChatListSearchEntry: Comparable, Identifiable { if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) } - if filter.contains(.onlyUsers) { + if filter.contains(.onlyPrivateChats) { if !(peer.peer is TelegramUser || peer.peer is TelegramSecretChat) { enabled = false } @@ -495,7 +495,7 @@ private func doesPeerMatchFilter(peer: Peer, filter: ChatListNodePeersFilter) -> if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) { enabled = false } - if filter.contains(.onlyUsers), !(peer is TelegramUser || peer is TelegramSecretChat) { + if filter.contains(.onlyPrivateChats), !(peer is TelegramUser || peer is TelegramSecretChat) { enabled = false } if filter.contains(.onlyGroups) { @@ -614,6 +614,35 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { let isSearching = foundRemotePeers.2 || foundRemoteMessages.1 var index = 0 + + let filteredPeer:(Peer) -> Bool = { peer in + guard !filter.contains(.excludeSavedMessages) || peer.id != accountPeer.id else { return false } + guard !filter.contains(.excludeSecretChats) || peer.id.namespace != Namespaces.Peer.SecretChat else { return false } + guard !filter.contains(.onlyPrivateChats) || peer.id.namespace == Namespaces.Peer.CloudUser else { return false } + + if filter.contains(.onlyGroups) { + var isGroup: Bool = false + if let peer = peer as? TelegramChannel, case .group = peer.info { + isGroup = true + } else if peer.id.namespace == Namespaces.Peer.CloudGroup { + isGroup = true + } + if !isGroup { + return false + } + } + + if filter.contains(.onlyChannels) { + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + return true + } else { + return false + } + } + + return true + } + var existingPeerIds = Set() if presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(query.lowercased()) { @@ -625,7 +654,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } for renderedPeer in foundLocalPeers.peers { - if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != account.peerId { + if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != account.peerId, filteredPeer(peer) { if !existingPeerIds.contains(peer.id) { existingPeerIds.insert(peer.id) var associatedPeer: Peer? @@ -639,7 +668,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } for peer in foundRemotePeers.0 { - if !existingPeerIds.contains(peer.peer.id) { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer) { existingPeerIds.insert(peer.peer.id) entries.append(.localPeer(peer.peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) index += 1 @@ -648,7 +677,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { index = 0 for peer in foundRemotePeers.1 { - if !existingPeerIds.contains(peer.peer.id) { + if !existingPeerIds.contains(peer.peer.id), filteredPeer(peer.peer) { existingPeerIds.insert(peer.peer.id) entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) index += 1 @@ -718,7 +747,7 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { } } |> distinctUntilChanged - let recentItemsTransition = combineLatest(hasRecentPeers, recentlySearchedPeers(postbox: account.postbox), presentationDataPromise.get(), self.statePromise.get()) + var recentItemsTransition = combineLatest(hasRecentPeers, recentlySearchedPeers(postbox: account.postbox), presentationDataPromise.get(), self.statePromise.get()) |> mapToSignal { [weak self] hasRecentPeers, peers, presentationData, state -> Signal<(ChatListSearchContainerRecentTransition, Bool), NoError> in var entries: [ChatListRecentEntry] = [] if !filter.contains(.onlyGroups) { @@ -762,6 +791,10 @@ final class ChatListSearchContainerNode: SearchDisplayControllerContentNode { return .single((transition, previousEntries == nil)) } + if filter.contains(.excludeRecent) { + recentItemsTransition = .single((ChatListSearchContainerRecentTransition(deletions: [], insertions: [], updates: []), true)) + } + self.updatedRecentPeersDisposable.set(managedUpdatedRecentPeers(accountPeerId: account.peerId, postbox: account.postbox, network: account.network).start()) self.recentDisposable.set((recentItemsTransition |> deliverOnMainQueue).start(next: { [weak self] (transition, firstTime) in diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 2f46285a85..fa5437fa11 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -80,7 +80,8 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { let removePeer: (PeerId) -> Void let toggleUpdated: ((Bool) -> Void)? let hasTopStripe: Bool - init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, hasTopStripe: Bool = true) { + let hasTopGroupInset: Bool + init(theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, account: Account, peer: Peer, aliasHandling: ItemListPeerItemAliasHandling = .standard, nameColor: ItemListPeerItemNameColor = .primary, presence: PeerPresence?, text: ItemListPeerItemText, label: ItemListPeerItemLabel, editing: ItemListPeerItemEditing, revealOptions: ItemListPeerItemRevealOptions? = nil, switchValue: ItemListPeerItemSwitch?, enabled: Bool, sectionId: ItemListSectionId, action: (() -> Void)?, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, removePeer: @escaping (PeerId) -> Void, toggleUpdated: ((Bool) -> Void)? = nil, hasTopStripe: Bool = true, hasTopGroupInset: Bool = true) { self.theme = theme self.strings = strings self.dateTimeFormat = dateTimeFormat @@ -101,6 +102,7 @@ final class ItemListPeerItem: ListViewItem, ItemListItem { self.removePeer = removePeer self.toggleUpdated = toggleUpdated self.hasTopStripe = hasTopStripe + self.hasTopGroupInset = hasTopGroupInset } func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, () -> Void)) -> Void) { @@ -404,7 +406,15 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 12.0 - labelLayout.size.width - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (statusLayout, statusApply) = makeStatusLayout(TextNodeLayoutArguments(attributedString: statusAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - 8.0 - editingOffset - rightInset - labelInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) - let insets = itemListNeighborsGroupedInsets(neighbors) + var insets = itemListNeighborsGroupedInsets(neighbors) + if !item.hasTopGroupInset { + switch neighbors.top { + case .none: + insets.top = 0 + default: + break + } + } let contentSize = CGSize(width: params.width, height: 48.0) let separatorHeight = UIScreenPixel diff --git a/TelegramUI/NotificationExceptionControllerNode.swift b/TelegramUI/NotificationExceptionControllerNode.swift new file mode 100644 index 0000000000..2df932fe59 --- /dev/null +++ b/TelegramUI/NotificationExceptionControllerNode.swift @@ -0,0 +1,1039 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + + + + +private final class NotificationExceptionState : Equatable { + + let mode:NotificationExceptionMode + let isSearchMode: Bool + let revealedPeerId: PeerId? + let editing: Bool + init(mode: NotificationExceptionMode, isSearchMode: Bool = false, revealedPeerId: PeerId? = nil, editing: Bool = false) { + self.mode = mode + self.isSearchMode = isSearchMode + self.revealedPeerId = revealedPeerId + self.editing = editing + } + + func withUpdatedMode(_ mode: NotificationExceptionMode) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode, isSearchMode: self.isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + func withUpdatedSearchMode(_ isSearchMode: Bool) -> NotificationExceptionState { + return NotificationExceptionState(mode: self.mode, isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + func withUpdatedEditing(_ editing: Bool) -> NotificationExceptionState { + return NotificationExceptionState(mode: self.mode, isSearchMode: self.isSearchMode, revealedPeerId: self.revealedPeerId, editing: editing) + } + func withUpdatedRevealedPeerId(_ revealedPeerId: PeerId?) -> NotificationExceptionState { + return NotificationExceptionState(mode: self.mode, isSearchMode: self.isSearchMode, revealedPeerId: revealedPeerId, editing: self.editing) + } + + func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerIdSound(peerId, sound), isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionState { + return NotificationExceptionState(mode: mode.withUpdatedPeerIdMuteInterval(peerId, muteInterval), isSearchMode: isSearchMode, revealedPeerId: self.revealedPeerId, editing: self.editing) + } + + + static func == (lhs: NotificationExceptionState, rhs: NotificationExceptionState) -> Bool { + return lhs.mode == rhs.mode && lhs.isSearchMode == rhs.isSearchMode && lhs.revealedPeerId == rhs.revealedPeerId && lhs.editing == rhs.editing + } +} + + +public struct NotificationExceptionWrapper : Equatable { + let settings: TelegramPeerNotificationSettings + let date: TimeInterval? + init(settings: TelegramPeerNotificationSettings, date: TimeInterval? = nil) { + self.settings = settings + self.date = date + } + + func withUpdatedSettings(_ settings: TelegramPeerNotificationSettings) -> NotificationExceptionWrapper { + return NotificationExceptionWrapper(settings: settings, date: self.date) + } + + func updateSettings(_ f: (TelegramPeerNotificationSettings) -> TelegramPeerNotificationSettings) -> NotificationExceptionWrapper { + return NotificationExceptionWrapper(settings: f(self.settings), date: self.date) + } + + + func withUpdatedDate(_ date: TimeInterval) -> NotificationExceptionWrapper { + return NotificationExceptionWrapper(settings: self.settings, date: date) + } +} + +public enum NotificationExceptionMode : Equatable { + public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool { + switch lhs { + case let .users(lhsValue): + if case let .users(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + case let .groups(lhsValue): + if case let .groups(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + case let .channels(lhsValue): + if case let .channels(rhsValue) = rhs { + return lhsValue == rhsValue + } else { + return false + } + } + } + + var isEmpty: Bool { + switch self { + case let .users(value), let .groups(value), let .channels(value): + return value.isEmpty + } + } + + case users([PeerId : NotificationExceptionWrapper]) + case groups([PeerId : NotificationExceptionWrapper]) + case channels([PeerId : NotificationExceptionWrapper]) + + func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionMode { + let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMessageSound) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, sound in + var values = values + if let value = values[peerId] { + switch sound { + case .default: + switch value.settings.muteState { + case .default: + values.removeValue(forKey: peerId) + default: + values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}).withUpdatedDate(Date().timeIntervalSince1970) + } + default: + values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}).withUpdatedDate(Date().timeIntervalSince1970) + } + } else { + switch sound { + case .default: + break + default: + values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .default, messageSound: sound), date: Date().timeIntervalSince1970) + } + } + return values + } + + switch self { + case let .groups(values): + if peerId.namespace != Namespaces.Peer.CloudUser { + return .groups(apply(values, peerId, sound)) + } + case let .users(values): + if peerId.namespace == Namespaces.Peer.CloudUser { + return .users(apply(values, peerId, sound)) + } + case let .channels(values): + if peerId.namespace == Namespaces.Peer.CloudUser { + return .channels(apply(values, peerId, sound)) + } + } + + return self + } + + func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionMode { + + let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMuteState) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, muteState in + var values = values + if let value = values[peerId] { + switch muteState { + case .default: + switch value.settings.messageSound { + case .default: + values.removeValue(forKey: peerId) + default: + values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}).withUpdatedDate(Date().timeIntervalSince1970) + } + default: + values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}).withUpdatedDate(Date().timeIntervalSince1970) + } + } else { + switch muteState { + case .default: + break + default: + values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: .default), date: Date().timeIntervalSince1970) + } + } + return values + } + + let muteState: PeerMuteState + if let muteInterval = muteInterval { + if muteInterval == 0 { + muteState = .unmuted + } else { + let absoluteUntil: Int32 + if muteInterval == Int32.max { + absoluteUntil = Int32.max + } else { + absoluteUntil = Int32(Date().timeIntervalSince1970) + muteInterval + } + muteState = .muted(until: absoluteUntil) + } + } else { + muteState = .default + } + switch self { + case let .groups(values): + if peerId.namespace != Namespaces.Peer.CloudUser { + return .groups(apply(values, peerId, muteState)) + } + case let .users(values): + if peerId.namespace == Namespaces.Peer.CloudUser { + return .users(apply(values, peerId, muteState)) + } + case let .channels(values): + if peerId.namespace != Namespaces.Peer.CloudUser { + return .channels(apply(values, peerId, muteState)) + } + } + + return self + } + + var peerIds: [PeerId] { + switch self { + case let .users(settings), let .groups(settings), let .channels(settings): + return settings.map {$0.key} + } + } + + var settings: [PeerId : NotificationExceptionWrapper] { + switch self { + case let .users(settings), let .groups(settings), let .channels(settings): + return settings + } + } +} + +private func notificationsExceptionEntries(presentationData: PresentationData, peers: [PeerId : Peer], state: NotificationExceptionState) -> [NotificationExceptionEntry] { + var entries: [NotificationExceptionEntry] = [] + + if !state.isSearchMode { + if !state.mode.settings.isEmpty { + entries.append(.search(presentationData.theme, presentationData.strings)) + } + entries.append(.addException(presentationData.theme, presentationData.strings, state.editing)) + } + + + var index: Int = 0 + for (key, value) in state.mode.settings.filter({ peers[$0.key] != nil }).sorted(by: { lhs, rhs in + let lhsName = peers[lhs.key]?.displayTitle ?? "" + let rhsName = peers[rhs.key]?.displayTitle ?? "" + + if let lhsDate = lhs.value.date, let rhsDate = rhs.value.date { + return lhsDate < rhsDate + } else if lhs.value.date != nil && rhs.value.date == nil { + return true + } else if lhs.value.date == nil && rhs.value.date != nil { + return false + } + + if let lhsPeer = peers[lhs.key] as? TelegramUser, let rhsPeer = peers[rhs.key] as? TelegramUser { + if lhsPeer.botInfo != nil && rhsPeer.botInfo == nil { + return false + } else if lhsPeer.botInfo == nil && rhsPeer.botInfo != nil { + return true + } + } + + return lhsName < rhsName + }) { + if let peer = peers[key], !peer.displayTitle.isEmpty { + var title: String + switch value.settings.muteState { + case .muted: + title = "Always Off" + case .unmuted: + title = "Always On" + default: + title = "" + } + switch value.settings.messageSound { + case .default: + break + default: + title += (title.isEmpty ? "Sound" : ", Sound: ") + localizedPeerNotificationSoundString(strings: presentationData.strings, sound: value.settings.messageSound) + } + entries.append(.peer(index: index, peer: peer, theme: presentationData.theme, strings: presentationData.strings, dateFormat: presentationData.dateTimeFormat, description: title, notificationSettings: value.settings, revealed: state.revealedPeerId == peer.id, editing: state.editing)) + index += 1 + } + } + + return entries +} + +private final class NotificationExceptionArguments { + let account: Account + let activateSearch:()->Void + let openPeer: (Peer) -> Void + let selectPeer: ()->Void + let updateRevealedPeerId:(PeerId?)->Void + let deletePeer:(PeerId) -> Void + init(account: Account, activateSearch:@escaping() -> Void, openPeer: @escaping(Peer) -> Void, selectPeer: @escaping()->Void, updateRevealedPeerId:@escaping(PeerId?)->Void, deletePeer: @escaping(PeerId) -> Void) { + self.account = account + self.activateSearch = activateSearch + self.openPeer = openPeer + self.selectPeer = selectPeer + self.updateRevealedPeerId = updateRevealedPeerId + self.deletePeer = deletePeer + } +} + + +private enum NotificationExceptionEntryId: Hashable { + case search + case peerId(Int64) + case addException + var hashValue: Int { + switch self { + case .search: + return 0 + case .addException: + return 1 + case let .peerId(peerId): + return peerId.hashValue + } + } + + static func <(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool { + return lhs.hashValue < rhs.hashValue + } + + static func ==(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool { + switch lhs { + case .search: + switch rhs { + case .search: + return true + default: + return false + } + case .addException: + switch rhs { + case .addException: + return true + default: + return false + } + case let .peerId(lhsId): + switch rhs { + case let .peerId(rhsId): + return lhsId == rhsId + default: + return false + } + } + } +} + +private enum NotificationExceptionSectionId : ItemListSectionId { + case general = 0 +} + +private enum NotificationExceptionEntry : ItemListNodeEntry { + + + var section: ItemListSectionId { + return NotificationExceptionSectionId.general.rawValue + } + + typealias ItemGenerationArguments = NotificationExceptionArguments + + case search(PresentationTheme, PresentationStrings) + case peer(index: Int, peer: Peer, theme: PresentationTheme, strings: PresentationStrings, dateFormat: PresentationDateTimeFormat, description: String, notificationSettings: TelegramPeerNotificationSettings, revealed: Bool, editing: Bool) + case addException(PresentationTheme, PresentationStrings, Bool) + + func item(_ arguments: NotificationExceptionArguments) -> ListViewItem { + switch self { + case let .search(theme, strings): + return NotificationSearchItem(theme: theme, placeholder: strings.Common_Search, activate: { + arguments.activateSearch() + }) + case let .addException(theme, strings, editing): + return ItemListPeerActionItem(theme: theme, icon: PresentationResourcesItemList.addPersonIcon(theme), title: "Add Exception", sectionId: self.section, editing: editing, action: { + arguments.selectPeer() + }) + case let .peer(_, peer, theme, strings, dateTimeFormat, value, _, revealed, editing): + return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, sectionId: self.section, action: { + arguments.openPeer(peer) + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + arguments.updateRevealedPeerId(peerId) + }, removePeer: { peerId in + arguments.deletePeer(peerId) + }, hasTopStripe: false, hasTopGroupInset: false) + } + } + + var stableId: NotificationExceptionEntryId { + switch self { + case .search: + return .search + case .addException: + return .addException + case let .peer(_, peer, _, _, _, _, _, _, _): + return .peerId(peer.id.toInt64()) + } + } + + static func == (lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool { + switch lhs { + case let .search(lhsTheme, lhsStrings): + switch rhs { + case let .search(rhsTheme, rhsStrings): + return lhsTheme === rhsTheme && lhsStrings === rhsStrings + default: + return false + } + case let .addException(lhsTheme, lhsStrings, lhsEditing): + switch rhs { + case let .addException(rhsTheme, rhsStrings, rhsEditing): + return lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsEditing == rhsEditing + default: + return false + } + case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsValue, lhsSettings, lhsRevealed, lhsEditing): + switch rhs { + case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsValue, rhsSettings, rhsRevealed, rhsEditing): + return lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsDateTimeFormat == rhsDateTimeFormat && lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) && lhsValue == rhsValue && lhsSettings == rhsSettings && lhsRevealed == rhsRevealed && lhsEditing == rhsEditing + default: + return false + } + } + } + + static func <(lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool { + switch lhs { + case .search: + return true + case .addException: + switch rhs { + case .search, .addException: + return false + default: + return true + } + case let .peer(lhsIndex, _, _, _, _, _, _, _ , _): + switch rhs { + case .search, .addException: + return false + case let .peer(rhsIndex, _, _, _, _, _, _, _, _): + return lhsIndex < rhsIndex + } + } + } +} + + + + +private struct NotificationExceptionNodeTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let firstTime: Bool + let animated: Bool +} + +private func preparedExceptionsListNodeTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [NotificationExceptionEntry], to toEntries: [NotificationExceptionEntry], arguments: NotificationExceptionArguments, firstTime: Bool, forceUpdate: Bool, animated: Bool) -> NotificationExceptionNodeTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + + return NotificationExceptionNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated) +} + +final class NotificationExceptionsControllerNode: ViewControllerTracingNode { + private let account: Account + private var presentationData: PresentationData + private let navigationBar: NavigationBar + private let requestActivateSearch: () -> Void + private let requestDeactivateSearch: () -> Void + private let present: (ViewController, Any?) -> Void + + private var didSetReady = false + let _ready = ValuePromise() + + private var containerLayout: (ContainerViewLayout, CGFloat)? + private let listNode: ListView + private var queuedTransitions: [NotificationExceptionNodeTransition] = [] + + private var searchDisplayController: SearchDisplayController? + + private let presentationDataValue = Promise<(PresentationTheme, PresentationStrings)>() + private var listDisposable: Disposable? + + private var arguments: NotificationExceptionArguments? + private let stateValue: Atomic + private let statePromise:ValuePromise = ValuePromise(ignoreRepeated: true) + private let navigationActionDisposable = MetaDisposable() + + func addPressed() { + self.arguments?.selectPeer() + } + + init(account: Account, presentationData: PresentationData, navigationBar: NavigationBar, mode: NotificationExceptionMode, updatedMode:@escaping(NotificationExceptionMode)->Void, requestActivateSearch: @escaping () -> Void, requestDeactivateSearch: @escaping () -> Void, updateCanStartEditing: @escaping (Bool?) -> Void, present: @escaping (ViewController, Any?) -> Void) { + self.account = account + self.presentationData = presentationData + self.presentationDataValue.set(.single((presentationData.theme, presentationData.strings))) + self.navigationBar = navigationBar + self.requestActivateSearch = requestActivateSearch + self.requestDeactivateSearch = requestDeactivateSearch + self.present = present + self.stateValue = Atomic(value: NotificationExceptionState(mode: mode)) + self.listNode = ListView() + self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true) + + super.init() + + statePromise.set(NotificationExceptionState(mode: mode)) + + let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { [weak self] f in + guard let `self` = self else {return} + let result = self.stateValue.modify { f($0) } + self.statePromise.set(result) + updatedMode(result.mode) + } + + + var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? + + + let presentationData = account.telegramApplicationContext.currentPresentationData.modify {$0} + + let updatePeerSound: (PeerId, PeerMessageSound) -> Void = { peerId, sound in + _ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start(completed: { + updateState { value in + return value.withUpdatedPeerIdSound(peerId, sound) + } + }) + } + + let updatePeerNotificationInterval:(PeerId, Int32?) -> Void = { peerId, muteInterval in + _ = updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start(completed: { + updateState { value in + return value.withUpdatedPeerIdMuteInterval(peerId, muteInterval) + } + }) + } + + + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.addSubnode(self.listNode) + + let openSearch: () -> Void = { + requestActivateSearch() + } + + let arguments = NotificationExceptionArguments(account: account, activateSearch: { + openSearch() + }, openPeer: { [weak self] peer in + if let strongSelf = self { + if let infoController = peerInfoController(account: strongSelf.account, peer: peer) { + (strongSelf.closestViewController?.navigationController as? NavigationController)?.pushViewController(infoController) + } + } + + }, selectPeer: { + var filter: ChatListNodePeersFilter = [.excludeRecent] + switch mode { + case .groups: + filter.insert(.onlyGroups) + case .users: + filter.insert(.onlyPrivateChats) + filter.insert(.excludeSavedMessages) + filter.insert(.excludeSecretChats) + case .channels: + filter.insert(.onlyChannels) + } + let controller = PeerSelectionController(account: account, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle) + controller.peerSelected = { [weak controller] peerId in + controller?.dismiss() + + presentControllerImpl?(notificationPeerExceptionController(account: account, peerId: peerId, mode: mode, updatePeerSound: updatePeerSound, updatePeerNotificationInterval: updatePeerNotificationInterval), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + } + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + }, updateRevealedPeerId: { peerId in + updateState { current in + return current.withUpdatedRevealedPeerId(peerId) + } + }, deletePeer: { peerId in + updatePeerSound(peerId, .default) + updatePeerNotificationInterval(peerId, nil) + }) + + self.arguments = arguments + + presentControllerImpl = { [weak self] c, a in + self?.present(c, a) + } + + let peersSignal:Signal<[PeerId : Peer], NoError> = statePromise.get() |> mapToSignal { state in + return account.postbox.transaction { transaction -> [PeerId : Peer] in + var peers:[PeerId : Peer] = [:] + for peerId in state.mode.peerIds { + if let peer = transaction.getPeer(peerId) { + peers[peerId] = peer + } + } + return peers + } + } + + let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + + + let previousEntriesHolder = Atomic<([NotificationExceptionEntry], PresentationTheme, PresentationStrings)?>(value: nil) + + listDisposable = (combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersSignal, preferences) |> deliverOnMainQueue).start(next: { [weak self] (presentationData, state, peers, prefs) in + let entries = notificationsExceptionEntries(presentationData: presentationData, peers: peers, state: state) + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + + updateCanStartEditing(state.editing) + + let transition = preparedExceptionsListNodeTransition(theme: presentationData.theme, strings: presentationData.strings, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, arguments: arguments, firstTime: previousEntriesAndPresentationData == nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings, animated: previousEntriesAndPresentationData != nil) + + self?.enqueueTransition(transition) + }) + + + + //listdi + + } + + deinit { + self.listDisposable?.dispose() + navigationActionDisposable.dispose() + } + + func updatePresentationData(_ presentationData: PresentationData) { + self.presentationData = presentationData + self.presentationDataValue.set(.single((presentationData.theme, presentationData.strings))) + self.backgroundColor = presentationData.theme.list.blocksBackgroundColor + self.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true) + self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: presentationData.strings) + } + + func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + let hadValidLayout = self.containerLayout != nil + self.containerLayout = (layout, navigationBarHeight) + + var listInsets = layout.insets(options: [.input]) + listInsets.top += navigationBarHeight + listInsets.left += layout.safeInsets.left + listInsets.right += layout.safeInsets.right + if let searchDisplayController = self.searchDisplayController { + searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + if !searchDisplayController.isDeactivating { + listInsets.top += layout.statusBarHeight ?? 0.0 + } + } + + self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) + self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default(duration: duration) + } + + let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: layout.size, insets: listInsets, duration: duration, curve: listViewCurve) + + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hadValidLayout { + self.dequeueTransitions() + } + } + + private func enqueueTransition(_ transition: NotificationExceptionNodeTransition) { + self.queuedTransitions.append(transition) + + if self.containerLayout != nil { + self.dequeueTransitions() + } + } + + private func dequeueTransitions() { + if self.containerLayout != nil { + while !self.queuedTransitions.isEmpty { + let transition = self.queuedTransitions.removeFirst() + + var options = ListViewDeleteAndInsertOptions() + if transition.firstTime { + options.insert(.Synchronous) + options.insert(.LowLatency) + } else if transition.animated { + options.insert(.AnimateInsertion) + } + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in + if let strongSelf = self { + if !strongSelf.didSetReady { + strongSelf.didSetReady = true + strongSelf._ready.set(true) + } + } + }) + } + } + } + + + func toggleEditing() { + statePromise.set(stateValue.modify({$0.withUpdatedEditing(!$0.editing)})) + } + + func activateSearch() { + guard let (containerLayout, navigationBarHeight) = self.containerLayout else { + return + } + + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.listNode.forEachItemNode { node in + if let node = node as? NotificationSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + if let _ = self.searchDisplayController { + return + } + + if let placeholderNode = maybePlaceholderNode { + self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: NotificationExceptionsSearchContainerNode(account: self.account, mode: self.stateValue.modify {$0}.mode, arguments: self.arguments!), cancel: { [weak self] in + self?.requestDeactivateSearch() + }) + + self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) + self.searchDisplayController?.activate(insertSubnode: { subnode in + self.insertSubnode(subnode, belowSubnode: self.navigationBar) + }, placeholder: placeholderNode) + } + } + + func deactivateSearch() { + if let searchDisplayController = self.searchDisplayController { + var maybePlaceholderNode: SearchBarPlaceholderNode? + self.listNode.forEachItemNode { node in + if let node = node as? NotificationSearchItemNode { + maybePlaceholderNode = node.searchBarNode + } + } + + searchDisplayController.deactivate(placeholder: maybePlaceholderNode) + self.searchDisplayController = nil + } + } + + func scrollToTop() { + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + } +} + + +private struct NotificationExceptionsSearchContainerTransition { + let deletions: [ListViewDeleteItem] + let insertions: [ListViewInsertItem] + let updates: [ListViewUpdateItem] + let isSearching: Bool +} + +private func preparedNotificationExceptionsSearchContainerTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [NotificationExceptionEntry], to toEntries: [NotificationExceptionEntry], arguments: NotificationExceptionArguments, isSearching: Bool, forceUpdate: Bool) -> NotificationExceptionsSearchContainerTransition { + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) + + let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } + let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(arguments), directionHint: nil) } + + return NotificationExceptionsSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, isSearching: isSearching) +} + + +private final class NotificationExceptionsSearchContainerNode: SearchDisplayControllerContentNode { + private let dimNode: ASDisplayNode + private let listNode: ListView + + private var enqueuedTransitions: [NotificationExceptionsSearchContainerTransition] = [] + private var hasValidLayout = false + + private let searchQuery = Promise() + private let searchDisposable = MetaDisposable() + + private var presentationData: PresentationData + private var presentationDataDisposable: Disposable? + + private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> + + init(account: Account, mode: NotificationExceptionMode, arguments: NotificationExceptionArguments) { + self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + + self.themeAndStringsPromise = Promise((self.presentationData.theme, self.presentationData.strings)) + + self.dimNode = ASDisplayNode() + self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) + + self.listNode = ListView() + + super.init() + + self.listNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor + self.listNode.isHidden = true + + self.addSubnode(self.dimNode) + self.addSubnode(self.listNode) + + + let searchQuery = self.searchQuery.get() + + let stateAndPeers:Signal<(NotificationExceptionState, [PeerId : Peer], Bool), NoError> = .single(NotificationExceptionState(mode: mode, isSearchMode: true)) |> mapToSignal { state in + return account.postbox.transaction { transaction -> (NotificationExceptionState, [PeerId : Peer]) in + var peers:[PeerId : Peer] = [:] + for peerId in mode.peerIds { + if let peer = transaction.getPeer(peerId) { + peers[peerId] = peer + } + } + return (state, peers) + } + } |> mapToSignal { stateAndPeers -> Signal<(NotificationExceptionState, [PeerId : Peer], Bool), NoError> in + return searchQuery |> map { query -> (NotificationExceptionState, [PeerId : Peer], Bool) in + let filtered = stateAndPeers.1.filter { _, peer in + if let query = query?.lowercased(), !query.isEmpty { + return !peer.displayTitle.components(separatedBy: " ").filter({ $0.lowercased().hasPrefix(query)}).isEmpty + } else { + return false + } + } + return (stateAndPeers.0, filtered, query != nil && !query!.isEmpty) + } + + } + + let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) + + + let previousEntriesHolder = Atomic<([NotificationExceptionEntry], PresentationTheme, PresentationStrings)?>(value: nil) + + searchDisposable.set((combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, stateAndPeers, preferences) |> deliverOnMainQueue).start(next: { [weak self] (presentationData, stateAndPeers, prefs) in + let entries = notificationsExceptionEntries(presentationData: presentationData, peers: stateAndPeers.1, state: stateAndPeers.0) + let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.theme, presentationData.strings)) + + let transition = preparedNotificationExceptionsSearchContainerTransition(theme: presentationData.theme, strings: presentationData.strings, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, arguments: arguments, isSearching: stateAndPeers.2, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.theme || previousEntriesAndPresentationData?.2 !== presentationData.strings) + + self?.enqueueTransition(transition) + })) + + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings(theme: presentationData.theme, strings: presentationData.strings) + strongSelf.themeAndStringsPromise.set(.single((presentationData.theme, presentationData.strings))) + } + } + }) + + self.listNode.beganInteractiveDragging = { [weak self] in + self?.dismissInput?() + } + } + + deinit { + self.searchDisposable.dispose() + self.presentationDataDisposable?.dispose() + } + + override func didLoad() { + super.didLoad() + + self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) + } + + private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { + self.listNode.backgroundColor = theme.chatList.backgroundColor + } + + override func searchTextUpdated(text: String) { + if text.isEmpty { + self.searchQuery.set(.single(nil)) + } else { + self.searchQuery.set(.single(text)) + } + } + + private func enqueueTransition(_ transition: NotificationExceptionsSearchContainerTransition) { + enqueuedTransitions.append(transition) + + if self.hasValidLayout { + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + private func dequeueTransition() { + if let transition = self.enqueuedTransitions.first { + self.enqueuedTransitions.remove(at: 0) + + var options = ListViewDeleteAndInsertOptions() + options.insert(.PreferSynchronousDrawing) + + let isSearching = transition.isSearching + self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in + self?.listNode.isHidden = !isSearching + self?.dimNode.isHidden = isSearching + }) + } + } + + override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { + super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) + + let topInset = navigationBarHeight + transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) + + var duration: Double = 0.0 + var curve: UInt = 0 + switch transition { + case .immediate: + break + case let .animated(animationDuration, animationCurve): + duration = animationDuration + switch animationCurve { + case .easeInOut: + break + case .spring: + curve = 7 + } + } + + let listViewCurve: ListViewAnimationCurve + if curve == 7 { + listViewCurve = .Spring(duration: duration) + } else { + listViewCurve = .Default(duration: nil) + } + + self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) + self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: 0.0, bottom: layout.insets(options: [.input]).bottom, right: 0.0), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) + + if !hasValidLayout { + hasValidLayout = true + while !self.enqueuedTransitions.isEmpty { + self.dequeueTransition() + } + } + } + + @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { + if case .ended = recognizer.state { + self.cancel?() + } + } +} + + +/* + + let globalSettings = globalValue.modify {$0} + + let isPrivateChat = peerId.namespace == Namespaces.Peer.CloudUser + + + let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) + + var items: [ActionSheetItem] = [] + + + // ActionSheetButtonItem(title: isPrivateChat && globalSettings.privateChats.enabled || !isPrivateChat && globalSettings.groupChats.enabled ? presentationData.strings.UserInfo_NotificationsDefaultEnabled : presentationData.strings.UserInfo_NotificationsDefaultDisabled, color: .accent, action: { [weak actionSheet] in + // updatePeerNotificationInterval(peerId, nil) + // actionSheet?.dismissAnimated() + // }), + + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 0) + })) + + items.append(ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 60 * 60) + })) + + items.append(ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2) + })) + items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in + updatePeerNotificationInterval(peerId, Int32.max) + actionSheet?.dismissAnimated() + })) + + items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in + let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: isPrivateChat ? globalSettings.privateChats.sound : globalSettings.groupChats.sound, completion: { value in + updatePeerSound(peerId, value) + }) + presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) + actionSheet?.dismissAnimated() + })) + items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsResetToDefaults, color: .destructive, action: { [weak actionSheet] in + updatePeerNotificationInterval(peerId, nil) + updatePeerSound(peerId, .default) + actionSheet?.dismissAnimated() + })) + + actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil) + */ diff --git a/TelegramUI/NotificationExceptions.swift b/TelegramUI/NotificationExceptions.swift index d27e76cf93..431a55cbde 100644 --- a/TelegramUI/NotificationExceptions.swift +++ b/TelegramUI/NotificationExceptions.swift @@ -5,650 +5,62 @@ import Postbox import TelegramCore -private final class NotificationExceptionArguments { - let account: Account - let activateSearch:()->Void - let changeNotifications: (PeerId, TelegramPeerNotificationSettings) -> Void - let selectPeer: ()->Void - init(account: Account, activateSearch:@escaping() -> Void, changeNotifications: @escaping(PeerId, TelegramPeerNotificationSettings) -> Void, selectPeer: @escaping()->Void) { - self.account = account - self.activateSearch = activateSearch - self.changeNotifications = changeNotifications - self.selectPeer = selectPeer - } -} - -private enum NotificationExceptionEntryId: Hashable { - case search - case peerId(Int64) - - var hashValue: Int { - switch self { - case .search: - return 0 - case let .peerId(peerId): - return peerId.hashValue - } - } - - static func <(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool { - return lhs.hashValue < rhs.hashValue - } - - static func ==(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool { - switch lhs { - case .search: - switch rhs { - case .search: - return true - default: - return false - } - case let .peerId(lhsId): - switch rhs { - case let .peerId(rhsId): - return lhsId == rhsId - default: - return false - } - } - } -} - -private enum NotificationExceptionSectionId : ItemListSectionId { - case general = 0 -} - -private enum NotificationExceptionEntry : ItemListNodeEntry { - - - var section: ItemListSectionId { - return NotificationExceptionSectionId.general.rawValue - } - - typealias ItemGenerationArguments = NotificationExceptionArguments - - case search(PresentationTheme, PresentationStrings) - case peer(Int, Peer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, TelegramPeerNotificationSettings) - - - func item(_ arguments: NotificationExceptionArguments) -> ListViewItem { - switch self { - case let .search(theme, strings): - return NotificationSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: { - arguments.activateSearch() - }) - case let .peer(_, peer, theme, strings, dateTimeFormat, value, settings): - return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: { - arguments.changeNotifications(peer.id, settings) - }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in - - }, removePeer: { peerId in - - }, hasTopStripe: false) - } - } - - var stableId: NotificationExceptionEntryId { - switch self { - case .search: - return .search - case let .peer(_, peer, _, _, _, _, _): - return .peerId(peer.id.toInt64()) - } - } - - static func == (lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool { - switch lhs { - case let .search(lhsTheme, lhsStrings): - switch rhs { - case let .search(rhsTheme, rhsStrings): - return lhsTheme === rhsTheme && lhsStrings === rhsStrings - default: - return false - } - case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsValue, lhsSettings): - switch rhs { - case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsValue, rhsSettings): - return lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsDateTimeFormat == rhsDateTimeFormat && lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) && lhsValue == rhsValue && lhsSettings == rhsSettings - default: - return false - } - } - } - - static func <(lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool { - switch lhs { - case .search: - return true - case let .peer(lhsIndex, _, _, _, _, _, _): - switch rhs { - case .search: - return false - case let .peer(rhsIndex, _, _, _, _, _, _): - return lhsIndex < rhsIndex - } - } - } -} - -private final class NotificationExceptionState : Equatable { - - let mode:NotificationExceptionMode - let isSearchMode: Bool - init(mode: NotificationExceptionMode, isSearchMode: Bool = false) { - self.mode = mode - self.isSearchMode = isSearchMode - } - - func withUpdatedSearchMode(_ isSearchMode: Bool) -> NotificationExceptionState { - return NotificationExceptionState.init(mode: mode, isSearchMode: isSearchMode) - } - - func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionState { - return NotificationExceptionState(mode: mode.withUpdatedPeerIdSound(peerId, sound), isSearchMode: isSearchMode) - } - func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionState { - return NotificationExceptionState(mode: mode.withUpdatedPeerIdMuteInterval(peerId, muteInterval), isSearchMode: isSearchMode) - } - - static func == (lhs: NotificationExceptionState, rhs: NotificationExceptionState) -> Bool { - return lhs.mode == rhs.mode && lhs.isSearchMode == rhs.isSearchMode - } -} - - -public struct NotificationExceptionWrapper : Equatable { - let settings: TelegramPeerNotificationSettings - let date: TimeInterval? - init(settings: TelegramPeerNotificationSettings, date: TimeInterval? = nil) { - self.settings = settings - self.date = date - } - - func withUpdatedSettings(_ settings: TelegramPeerNotificationSettings) -> NotificationExceptionWrapper { - return NotificationExceptionWrapper(settings: settings, date: self.date) - } - - func updateSettings(_ f: (TelegramPeerNotificationSettings) -> TelegramPeerNotificationSettings) -> NotificationExceptionWrapper { - return NotificationExceptionWrapper(settings: f(self.settings), date: self.date) - } - - - func withUpdatedDate(_ date: TimeInterval) -> NotificationExceptionWrapper { - return NotificationExceptionWrapper(settings: self.settings, date: date) - } -} - -public enum NotificationExceptionMode : Equatable { - public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool { - switch lhs { - case let .users(lhsValue): - if case let .users(rhsValue) = rhs { - return lhsValue == rhsValue - } else { - return false - } - case let .groups(lhsValue): - if case let .groups(rhsValue) = rhs { - return lhsValue == rhsValue - } else { - return false - } - } - } - - var isEmpty: Bool { - switch self { - case let .users(value), let .groups(value): - return value.isEmpty - } - } - - case users([PeerId : NotificationExceptionWrapper]) - case groups([PeerId : NotificationExceptionWrapper]) - - func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionMode { - let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMessageSound) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, sound in - var values = values - if let value = values[peerId] { - switch sound { - case .default: - switch value.settings.muteState { - case .default: - values.removeValue(forKey: peerId) - default: - values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}) - } - default: - values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)}) - } - } else { - switch sound { - case .default: - break - default: - values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .default, messageSound: sound), date: Date().timeIntervalSince1970) - } - } - return values - } - - switch self { - case let .groups(values): - if peerId.namespace != Namespaces.Peer.CloudUser { - return .groups(apply(values, peerId, sound)) - } - case let .users(values): - if peerId.namespace == Namespaces.Peer.CloudUser { - return .users(apply(values, peerId, sound)) - } - } - - return self - } - - func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionMode { - - let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMuteState) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, muteState in - var values = values - if let value = values[peerId] { - switch muteState { - case .default: - switch value.settings.messageSound { - case .default: - values.removeValue(forKey: peerId) - default: - values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}) - } - default: - values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)}) - } - } else { - switch muteState { - case .default: - break - default: - values[peerId] = NotificationExceptionWrapper.init(settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: .default), date: Date().timeIntervalSince1970) - } - } - return values - } - - let muteState: PeerMuteState - if let muteInterval = muteInterval { - if muteInterval == 0 { - muteState = .unmuted - } else { - let absoluteUntil: Int32 - if muteInterval == Int32.max { - absoluteUntil = Int32.max - } else { - absoluteUntil = Int32(Date().timeIntervalSince1970) + muteInterval - } - muteState = .muted(until: absoluteUntil) - } - } else { - muteState = .default - } - switch self { - case let .groups(values): - if peerId.namespace != Namespaces.Peer.CloudUser { - return .groups(apply(values, peerId, muteState)) - } - case let .users(values): - if peerId.namespace == Namespaces.Peer.CloudUser { - return .users(apply(values, peerId, muteState)) - } - } - - return self - } - - var peerIds: [PeerId] { - switch self { - case let .users(settings), let .groups(settings): - return settings.map {$0.key} - } - } - - var settings: [PeerId : NotificationExceptionWrapper] { - switch self { - case let .users(settings), let .groups(settings): - return settings - } - } -} - -private func notificationsExceptionEntries(presentationData: PresentationData, peers: [PeerId : Peer], state: NotificationExceptionState) -> [NotificationExceptionEntry] { - var entries: [NotificationExceptionEntry] = [] - - entries.append(.search(presentationData.theme, presentationData.strings)) - - - var index: Int = 0 - for (key, value) in state.mode.settings.sorted(by: { lhs, rhs in - let lhsName = peers[lhs.key]?.displayTitle ?? "" - let rhsName = peers[rhs.key]?.displayTitle ?? "" - - if let lhsDate = lhs.value.date, let rhsDate = rhs.value.date { - return lhsDate < rhsDate - } else if lhs.value.date != nil && rhs.value.date == nil { - return true - } else if lhs.value.date == nil && rhs.value.date != nil { - return false - } - - if let lhsPeer = peers[lhs.key] as? TelegramUser, let rhsPeer = peers[rhs.key] as? TelegramUser { - if lhsPeer.botInfo != nil && rhsPeer.botInfo == nil { - return false - } else if lhsPeer.botInfo == nil && rhsPeer.botInfo != nil { - return true - } - } - - return lhsName < rhsName - }) { - if let peer = peers[key], !peer.displayTitle.isEmpty { - var title: String - switch value.settings.muteState { - case .muted: - title = presentationData.strings.Notifications_ExceptionsMuted - case .unmuted: - title = presentationData.strings.Notifications_ExceptionsUnmuted - default: - title = "" - } - switch value.settings.messageSound { - case .default: - break - default: - title += (title.isEmpty ? "" : ", ") + localizedPeerNotificationSoundString(strings: presentationData.strings, sound: value.settings.messageSound) - } - entries.append(.peer(index, peer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, title, value.settings)) - index += 1 - } - } - - return entries -} - -public func notificationExceptionsController(account: Account, mode: NotificationExceptionMode, updatedMode:@escaping(NotificationExceptionMode) -> Void) -> ViewController { - let statePromise = ValuePromise(NotificationExceptionState(mode: mode), ignoreRepeated: true) - let stateValue = Atomic(value: NotificationExceptionState(mode: mode)) - let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in - let result = stateValue.modify { f($0) } - statePromise.set(result) - updatedMode(result.mode) - } - - let globalValue: Atomic = Atomic(value: GlobalNotificationSettingsSet.defaultSettings) - - var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)? - - - let presentationData = account.telegramApplicationContext.currentPresentationData.modify {$0} - - let updatePeerSound: (PeerId, PeerMessageSound) -> Void = { peerId, sound in - _ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start(completed: { - updateState { value in - return value.withUpdatedPeerIdSound(peerId, sound) - } - }) - } - - let updatePeerNotificationInterval:(PeerId, Int32?) -> Void = { peerId, muteInterval in - _ = updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start(completed: { - updateState { value in - return value.withUpdatedPeerIdMuteInterval(peerId, muteInterval) - } - }) - } - - var activateSearch:(()->Void)? - - - let arguments = NotificationExceptionArguments(account: account, activateSearch: { - activateSearch?() - }, changeNotifications: { peerId, settings in - - /*let globalSettings = globalValue.modify {$0} - - let isPrivateChat = peerId.namespace == Namespaces.Peer.CloudUser - - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) - actionSheet.setItemGroups([ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: isPrivateChat && globalSettings.privateChats.enabled || !isPrivateChat && globalSettings.groupChats.enabled ? presentationData.strings.UserInfo_NotificationsDefaultEnabled : presentationData.strings.UserInfo_NotificationsDefaultDisabled, color: .accent, action: { [weak actionSheet] in - updatePeerNotificationInterval(peerId, nil) - actionSheet?.dismissAnimated() - }), - ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - updatePeerNotificationInterval(peerId, 0) - }), - ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - updatePeerNotificationInterval(peerId, 60 * 60) - }), - ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2) - }), - ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in - updatePeerNotificationInterval(peerId, Int32.max) - actionSheet?.dismissAnimated() - }), - ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in - let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: isPrivateChat ? globalSettings.privateChats.sound : globalSettings.groupChats.sound, completion: { value in - updatePeerSound(peerId, value) - }) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - actionSheet?.dismissAnimated() - }), - ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsResetToDefaults, color: .destructive, action: { [weak actionSheet] in - - updatePeerNotificationInterval(peerId, nil) - updatePeerSound(peerId, .default) - actionSheet?.dismissAnimated() - }) - ]), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil)*/ - }, selectPeer: { - /*let filter: ChatListNodePeersFilter - switch mode { - case .groups: - filter = [.withoutSecretChats] - case .users: - filter = [.withoutSecretChats] - } - let controller = PeerSelectionController(account: account, filter: filter, title: presentationData.strings.Notifications_AddExceptionTitle) - controller.peerSelected = { [weak controller] peerId in - controller?.dismiss() - - let settingsSignal = account.postbox.transaction { transaction in - return transaction.getPeerNotificationSettings(peerId) - } |> deliverOnMainQueue - - _ = settingsSignal.start(next: { settings in - if let settings = settings as? TelegramPeerNotificationSettings { - let actionSheet = ActionSheetController(presentationTheme: presentationData.theme) - - var items: [ActionSheetButtonItem] = [] - - switch settings.muteState { - case .default, .muted: - items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - updatePeerNotificationInterval(peerId, 0) - })) - default: - break - } - - items.append(ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - updatePeerNotificationInterval(peerId, 60 * 60) - })) - items.append(ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2) - })) - - switch settings.muteState { - case .default, .unmuted: - items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in - updatePeerNotificationInterval(peerId, Int32.max) - actionSheet?.dismissAnimated() - })) - default: - break - } - - items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in - let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: nil, completion: { value in - updatePeerSound(peerId, value) - }) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - actionSheet?.dismissAnimated() - })) - - if settings.muteState != .default || settings.messageSound != .default { - items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsResetToDefaults, color: .destructive, action: { [weak actionSheet] in - - updatePeerNotificationInterval(peerId, nil) - updatePeerSound(peerId, .default) - actionSheet?.dismissAnimated() - })) - } - - actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ])]) - presentControllerImpl?(actionSheet, nil) - } - }) - - - - - - } - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))*/ - }) - - let peersSignal:Signal<[PeerId : Peer], NoError> = statePromise.get() |> mapToSignal { state in - return account.postbox.transaction { transaction -> [PeerId : Peer] in - var peers:[PeerId : Peer] = [:] - for peerId in state.mode.peerIds { - if let peer = transaction.getPeer(peerId) { - peers[peerId] = peer - } - } - return peers - } - } - - let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) - - - let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersSignal, preferences) - |> map { presentationData, state, peers, prefs -> (ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)) in - - _ = globalValue.swap((prefs.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings)?.effective ?? GlobalNotificationSettingsSet.defaultSettings) - - - let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: nil) - let listState = ItemListNodeState(entries: notificationsExceptionEntries(presentationData: presentationData, peers: peers, state: state), style: .blocks, searchItem: nil) - - return (controllerState, (listState, arguments)) - } - - let controller = NotificationExceptionsController(account: account, state: signal, addAction: { - arguments.selectPeer() - }) - -// let controller = ItemListController(account: account, state: signal |> afterDisposed { -// actionsDisposable.dispose() -// }) - - - activateSearch = { [weak controller] in -// updateState { state in -// return state.withUpdatedSearchMode(true) -// } - controller?.activateSearch() - } - - - presentControllerImpl = { [weak controller] c, a in - controller?.present(c, in: .window(.root), with: a) - } - return controller -} - - - private final class NotificationExceptionsController: ViewController { +public class NotificationExceptionsController: ViewController { private let account: Account + private var controllerNode: NotificationExceptionsControllerNode { + return self.displayNode as! NotificationExceptionsControllerNode + } + + private var _ready = Promise() + override public var ready: Promise { + return self._ready + } + private var presentationData: PresentationData private var presentationDataDisposable: Disposable? - var peerSelected: ((PeerId) -> Void)? + private var editItem: UIBarButtonItem! + private var doneItem: UIBarButtonItem! - var inProgress: Bool = false { - didSet { - if self.inProgress != oldValue { - if self.isNodeLoaded { - self.controllerNode.inProgress = self.inProgress - } - - if self.inProgress { - self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: self.presentationData.theme)) - } else { - self.navigationItem.rightBarButtonItem = nil - } - } - } - } - - private var controllerNode: NotificationExceptionsControllerNode { - return super.displayNode as! NotificationExceptionsControllerNode - } - - - private let _ready = Promise() - override public var ready: Promise { - return self._ready - } - private let addAction:()->Void - - private let state: Signal<(ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> - - public init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>, addAction: @escaping()->Void) { + private let mode: NotificationExceptionMode + private let updatedMode: (NotificationExceptionMode) -> Void + public init(account: Account, mode: NotificationExceptionMode, updatedMode: @escaping(NotificationExceptionMode)->Void) { self.account = account - self.state = state - self.addAction = addAction + self.mode = mode + self.updatedMode = updatedMode self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) + + self.editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style self.title = self.presentationData.strings.Notifications_ExceptionsTitle + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.scrollToTop = { [weak self] in - if let strongSelf = self { - strongSelf.controllerNode.scrollToTop() - } + self?.controllerNode.scrollToTop() } + + self.presentationDataDisposable = (account.telegramApplicationContext.presentationData + |> deliverOnMainQueue).start(next: { [weak self] presentationData in + if let strongSelf = self { + let previousTheme = strongSelf.presentationData.theme + let previousStrings = strongSelf.presentationData.strings + + strongSelf.presentationData = presentationData + + if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { + strongSelf.updateThemeAndStrings() + } + } + }) } required public init(coder aDecoder: NSCoder) { @@ -656,49 +68,52 @@ public func notificationExceptionsController(account: Account, mode: Notificatio } deinit { + self.presentationDataDisposable?.dispose() } - @objc private func addExceptionAction() { - self.addAction() + private func updateThemeAndStrings() { + self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style + self.navigationBar?.updatePresentationData(NavigationBarPresentationData(presentationData: self.presentationData)) + self.title = self.presentationData.strings.Settings_AppLanguage + self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) + self.controllerNode.updatePresentationData(self.presentationData) + + + let editItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.editPressed)) + let doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed)) + if self.navigationItem.rightBarButtonItem === self.editItem { + self.navigationItem.rightBarButtonItem = editItem + } else if self.navigationItem.rightBarButtonItem === self.doneItem { + self.navigationItem.rightBarButtonItem = doneItem + } + self.editItem = editItem + self.doneItem = doneItem } override public func loadDisplayNode() { - let image = PresentationResourcesRootController.navigationAddIcon(presentationData.theme) - - self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItem.Style.plain, target: self, action: #selector(addExceptionAction)) - - let nodeState = self.state |> deliverOnMainQueue |> map { ($0.theme, $1) } - - self.displayNode = NotificationExceptionsControllerNode(account: self.account, navigationBar: self.navigationBar!, state: nodeState) - self.displayNode.backgroundColor = .white - - self.controllerNode.navigationBar = self.navigationBar - - self.controllerNode.requestDeactivateSearch = { [weak self] in - self?.deactivateSearch() - } - - self.controllerNode.requestActivateSearch = { [weak self] in + self.displayNode = NotificationExceptionsControllerNode(account: self.account, presentationData: self.presentationData, navigationBar: self.navigationBar!, mode: self.mode, updatedMode: self.updatedMode, requestActivateSearch: { [weak self] in self?.activateSearch() - } + }, requestDeactivateSearch: { [weak self] in + self?.deactivateSearch() + }, updateCanStartEditing: { [weak self] value in + guard let strongSelf = self else { + return + } + let item: UIBarButtonItem? + if let value = value { + item = value ? strongSelf.editItem : strongSelf.doneItem + } else { + item = nil + } + if strongSelf.navigationItem.rightBarButtonItem !== item { + strongSelf.navigationItem.setRightBarButton(item, animated: true) + } + }, present: { [weak self] c, a in + self?.present(c, in: .window(.root), with: a) + }) + self._ready.set(self.controllerNode._ready.get()) self.displayNodeDidLoad() - - self._ready.set(self.controllerNode.ready) - } - - override public func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - } - - override public func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // self.controllerNode.animateIn() - } - - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { @@ -707,11 +122,11 @@ public func notificationExceptionsController(account: Account, mode: Notificatio self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) } - @objc func cancelPressed() { - self.dismiss() + @objc private func editPressed() { + self.controllerNode.toggleEditing() } - func activateSearch() { + private func activateSearch() { if self.displayNavigationBar { if let scrollToTop = self.scrollToTop { scrollToTop() @@ -731,322 +146,260 @@ public func notificationExceptionsController(account: Account, mode: Notificatio -private final class NotificationExceptionsControllerNode: ASDisplayNode { - private let account: Account - - var inProgress: Bool = false { - didSet { - - } - } - - var navigationBar: NavigationBar? - - - private let contentNode: ItemListControllerNode - - private var contactListActive = false - - private var searchDisplayController: SearchDisplayController? - - private var containerLayout: (ContainerViewLayout, CGFloat)? - - var requestActivateSearch: (() -> Void)? - var requestDeactivateSearch: (() -> Void)? - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private var readyValue = Promise() - var ready: Signal { - return self.readyValue.get() - } - - private let state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> - - init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) { - self.account = account - self.state = state - self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - - - self.contentNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in - - }, state: state) - - super.init() - - self.setViewBlock({ - return UITracingLayerView() - }) - - self.addSubnode(self.contentNode) - self.presentationDataDisposable = (account.telegramApplicationContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - let previousStrings = strongSelf.presentationData.strings - strongSelf.presentationData = presentationData - if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { - strongSelf.updateThemeAndStrings() - } - } - }) - - self.readyValue.set(contentNode.ready) - } - - deinit { - self.presentationDataDisposable?.dispose() - } - - private func updateThemeAndStrings() { - self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) - } - - func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - self.containerLayout = (layout, navigationBarHeight) - - let cleanInsets = layout.insets(options: []) - - - var controlSize = CGSize(width: 0, height:0) - controlSize.width = min(layout.size.width, max(200.0, controlSize.width)) - - var insets = layout.insets(options: [.input]) - insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) - insets.bottom = max(insets.bottom, cleanInsets.bottom) - insets.left += layout.safeInsets.left - insets.right += layout.safeInsets.right - - self.contentNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) - self.contentNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) - - self.contentNode.containerLayoutUpdated(layout, navigationBarHeight: insets.top, transition: transition) - - if let searchDisplayController = self.searchDisplayController { - searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - } - } - - func activateSearch() { - guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else { - return - } - - if self.contentNode.supernode != nil { - var maybePlaceholderNode: SearchBarPlaceholderNode? - self.contentNode.listNode.forEachItemNode { node in - if let node = node as? NotificationSearchItemNode { - maybePlaceholderNode = node.searchBarNode - } - } - - if let _ = self.searchDisplayController { - return - } - - if let placeholderNode = maybePlaceholderNode { - self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: NotificationExceptionsSearchControllerContentNode(account: account, navigationBar: navigationBar, state: self.state), cancel: { [weak self] in - self?.requestDeactivateSearch?() - }) - - self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) - self.searchDisplayController?.activate(insertSubnode: { subnode in - self.insertSubnode(subnode, belowSubnode: navigationBar) - }, placeholder: placeholderNode) - } - } - } - - func deactivateSearch() { - if let searchDisplayController = self.searchDisplayController { - if self.contentNode.supernode != nil { - var maybePlaceholderNode: SearchBarPlaceholderNode? - self.contentNode.listNode.forEachItemNode { node in - if let node = node as? NotificationSearchItemNode { - maybePlaceholderNode = node.searchBarNode - } - } - - searchDisplayController.deactivate(placeholder: maybePlaceholderNode) - self.searchDisplayController = nil - } - } - } - - func scrollToTop() { - if self.contentNode.supernode != nil { - self.contentNode.scrollToTop() - } - } - - -} - -private final class NotificationExceptionsSearchControllerContentNode: SearchDisplayControllerContentNode { - private let account: Account - - private let listNode: ItemListControllerNode - private let dimNode: ASDisplayNode - private var validLayout: ContainerViewLayout? - - - private let searchQuery = Promise() - private let searchDisposable = MetaDisposable() - - private var presentationData: PresentationData - private var presentationDataDisposable: Disposable? - - private let presentationDataPromise: Promise - - private let _isSearching = ValuePromise(false, ignoreRepeated: true) - override var isSearching: Signal { - return self._isSearching.get() - } - - private let state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> - - - init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) { - self.account = account - self.state = state - - self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) - - self.listNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in - - }, state: searchQuery.get() |> mapToSignal { query in - return state |> map { values in - var values = values - let entries = values.1.0.entries.filter { entry in - switch entry { - case .search: - return false - case let .peer(_, peer, _, _, _, _, _): - if let query = query { - return !peer.displayTitle.components(separatedBy: " ").filter({$0.lowercased().hasPrefix(query.lowercased())}).isEmpty && !query.isEmpty - } else { - return false - } - } - } - values.1.0 = ItemListNodeState(entries: entries, style: values.1.0.style, focusItemTag: nil, emptyStateItem: nil, searchItem: nil, crossfadeState: false, animateChanges: false) - return values - } - }) - - - - self.dimNode = ASDisplayNode() - self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) - - super.init() - - - self.addSubnode(self.dimNode) - self.addSubnode(self.listNode) - self.listNode.isHidden = true - - self.presentationDataDisposable = (account.telegramApplicationContext.presentationData - |> deliverOnMainQueue).start(next: { [weak self] presentationData in - if let strongSelf = self { - let previousTheme = strongSelf.presentationData.theme - - strongSelf.presentationData = presentationData - - if previousTheme !== presentationData.theme { - strongSelf.updateTheme(theme: presentationData.theme) - } - } - }) - - } - - deinit { - self.searchDisposable.dispose() - self.presentationDataDisposable?.dispose() - } - - private func updateTheme(theme: PresentationTheme) { - self.backgroundColor = theme.chatList.backgroundColor - } - - override func didLoad() { - super.didLoad() - - self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) - } - - @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { - if case .ended = recognizer.state { - self.cancel?() - } - } - - override func searchTextUpdated(text: String) { - if text.isEmpty { - self.searchQuery.set(.single(nil)) - self.listNode.isHidden = true - } else { - self.searchQuery.set(.single(text)) - self.listNode.isHidden = false - } - - } - - override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) - - let hadValidLayout = self.validLayout != nil - self.validLayout = layout - - var duration: Double = 0.0 - var curve: UInt = 0 - switch transition { - case .immediate: - break - case let .animated(animationDuration, animationCurve): - duration = animationDuration - switch animationCurve { - case .easeInOut: - break - case .spring: - curve = 7 - } - } - - - let listViewCurve: ListViewAnimationCurve - if curve == 7 { - listViewCurve = .Spring(duration: duration) - } else { - listViewCurve = .Default(duration: duration) - } - - self.listNode.containerLayoutUpdated(layout, navigationBarHeight: 0, transition: transition) - - let insets = UIEdgeInsets(top: navigationBarHeight - 30, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right) - - transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top))) - - self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) - self.listNode.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight - 30, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) - } - -// override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { -// super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) // -// self.validLayout = layout // +//public func notificationExceptionsController(account: Account, mode: NotificationExceptionMode, updatedMode:@escaping(NotificationExceptionMode) -> Void) -> ViewController { +// +// +// var activateSearch:(()->Void)? +// +// +// +// +// let controller = NotificationExceptionsController(account: account, state: signal, addAction: { +// arguments.selectPeer() +// }) +// +//// let controller = ItemListController(account: account, state: signal |> afterDisposed { +//// actionsDisposable.dispose() +//// }) +// +// +// activateSearch = { [weak controller] in +//// updateState { state in +//// return state.withUpdatedSearchMode(true) +//// } +// controller?.activateSearch() +// } +// +// +// presentControllerImpl = { [weak controller] c, a in +// controller?.present(c, in: .window(.root), with: a) +// } +// return controller +//} + +// +// private final class NotificationExceptionsController: ViewController { +// private let account: Account +// +// private var presentationData: PresentationData +// private var presentationDataDisposable: Disposable? +// +// var peerSelected: ((PeerId) -> Void)? +// +// var inProgress: Bool = false { +// didSet { +// if self.inProgress != oldValue { +// if self.isNodeLoaded { +// self.controllerNode.inProgress = self.inProgress +// } +// +// if self.inProgress { +// self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: self.presentationData.theme)) +// } else { +// self.navigationItem.rightBarButtonItem = nil +// } +// } +// } +// } +// +// private var controllerNode: NotificationExceptionsControllerNode { +// return super.displayNode as! NotificationExceptionsControllerNode +// } +// +// +// private let _ready = Promise() +// override public var ready: Promise { +// return self._ready +// } +// private let addAction:()->Void +// +// private let state: Signal<(ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> +// +// public init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>, addAction: @escaping()->Void) { +// self.account = account +// self.state = state +// self.addAction = addAction +// self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } +// +// super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) +// self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style +// +// self.title = self.presentationData.strings.Notifications_ExceptionsTitle +// +// +// self.scrollToTop = { [weak self] in +// if let strongSelf = self { +// strongSelf.controllerNode.scrollToTop() +// } +// } +// } +// +// required public init(coder aDecoder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// deinit { +// } +// +// @objc private func addExceptionAction() { +// self.addAction() +// } +// +// override public func loadDisplayNode() { +// let image = PresentationResourcesRootController.navigationAddIcon(presentationData.theme) +// +// self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItem.Style.plain, target: self, action: #selector(addExceptionAction)) +// +// let nodeState = self.state |> deliverOnMainQueue |> map { ($0.theme, $1) } +// +// self.displayNode = NotificationExceptionsControllerNode(account: self.account, navigationBar: self.navigationBar!, state: nodeState) +// self.displayNode.backgroundColor = .white +// +// self.controllerNode.navigationBar = self.navigationBar +// +// self.controllerNode.requestDeactivateSearch = { [weak self] in +// self?.deactivateSearch() +// } +// +// self.controllerNode.requestActivateSearch = { [weak self] in +// self?.activateSearch() +// } +// +// self.displayNodeDidLoad() +// +// self._ready.set(self.controllerNode.ready) +// } +// +// override public func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// } +// +// override public func viewDidAppear(_ animated: Bool) { +// super.viewDidAppear(animated) +// +// // self.controllerNode.animateIn() +// } +// +// override public func viewDidDisappear(_ animated: Bool) { +// super.viewDidDisappear(animated) +// } +// +// override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { +// super.containerLayoutUpdated(layout, transition: transition) +// +// self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition) +// } +// +// @objc func cancelPressed() { +// self.dismiss() +// } +// +// func activateSearch() { +// if self.displayNavigationBar { +// if let scrollToTop = self.scrollToTop { +// scrollToTop() +// } +// self.controllerNode.activateSearch() +// self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring)) +// } +// } +// +// private func deactivateSearch() { +// if !self.displayNavigationBar { +// self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring)) +// self.controllerNode.deactivateSearch() +// } +// } +//} +// +// +// +//private final class NotificationExceptionsControllerNode: ASDisplayNode { +// private let account: Account +// +// var inProgress: Bool = false { +// didSet { +// +// } +// } +// +// var navigationBar: NavigationBar? +// +// +// private let contentNode: ItemListControllerNode +// +// private var contactListActive = false +// +// private var searchDisplayController: SearchDisplayController? +// +// private var containerLayout: (ContainerViewLayout, CGFloat)? +// +// var requestActivateSearch: (() -> Void)? +// var requestDeactivateSearch: (() -> Void)? +// +// private var presentationData: PresentationData +// private var presentationDataDisposable: Disposable? +// +// private var readyValue = Promise() +// var ready: Signal { +// return self.readyValue.get() +// } +// +// private let state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> +// +// init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) { +// self.account = account +// self.state = state +// self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } +// +// +// self.contentNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in +// +// }, state: state) +// +// contentNode.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true) +// contentNode.listNode.keepBottomItemOverscrollBackground = presentationData.theme.chatList.backgroundColor +// +// super.init() +// +// self.setViewBlock({ +// return UITracingLayerView() +// }) +// +// self.addSubnode(self.contentNode) +// self.presentationDataDisposable = (account.telegramApplicationContext.presentationData +// |> deliverOnMainQueue).start(next: { [weak self] presentationData in +// if let strongSelf = self { +// let previousTheme = strongSelf.presentationData.theme +// let previousStrings = strongSelf.presentationData.strings +// strongSelf.presentationData = presentationData +// if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings { +// strongSelf.updateThemeAndStrings() +// } +// } +// }) +// +// self.readyValue.set(contentNode.ready) +// } +// +// deinit { +// self.presentationDataDisposable?.dispose() +// } +// +// private func updateThemeAndStrings() { +// self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings) +// } +// +// func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { +// self.containerLayout = (layout, navigationBarHeight) // // let cleanInsets = layout.insets(options: []) // -//// var insets = layout.insets(options: [.input]) -//// insets.top += layout.insets(options: [.statusBar]).top -// let toolbarHeight: CGFloat = 44.0 + cleanInsets.bottom // +// var controlSize = CGSize(width: 0, height:0) +// controlSize.width = min(layout.size.width, max(200.0, controlSize.width)) // // var insets = layout.insets(options: [.input]) // insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) @@ -1054,14 +407,227 @@ private final class NotificationExceptionsSearchControllerContentNode: SearchDis // insets.left += layout.safeInsets.left // insets.right += layout.safeInsets.right // -// self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) -// self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) +// self.contentNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) +// self.contentNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) // -// let topInset = layout.insets(options: [.statusBar]).bottom -// transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) +// self.contentNode.containerLayoutUpdated(layout, navigationBarHeight: insets.top, transition: transition) // -// transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) -// self.listNode.containerLayoutUpdated(layout, navigationBarHeight: topInset, transition: transition) +// if let searchDisplayController = self.searchDisplayController { +// searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) +// } // } - -} +// +// func activateSearch() { +// guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else { +// return +// } +// +// if self.contentNode.supernode != nil { +// var maybePlaceholderNode: SearchBarPlaceholderNode? +// self.contentNode.listNode.forEachItemNode { node in +// if let node = node as? NotificationSearchItemNode { +// maybePlaceholderNode = node.searchBarNode +// } +// } +// +// if let _ = self.searchDisplayController { +// return +// } +// +// if let placeholderNode = maybePlaceholderNode { +// self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: NotificationExceptionsSearchControllerContentNode(account: account, navigationBar: navigationBar, state: self.state), cancel: { [weak self] in +// self?.requestDeactivateSearch?() +// }) +// +// self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate) +// self.searchDisplayController?.activate(insertSubnode: { subnode in +// self.insertSubnode(subnode, belowSubnode: navigationBar) +// }, placeholder: placeholderNode) +// } +// } +// } +// +// func deactivateSearch() { +// if let searchDisplayController = self.searchDisplayController { +// if self.contentNode.supernode != nil { +// var maybePlaceholderNode: SearchBarPlaceholderNode? +// self.contentNode.listNode.forEachItemNode { node in +// if let node = node as? NotificationSearchItemNode { +// maybePlaceholderNode = node.searchBarNode +// } +// } +// +// searchDisplayController.deactivate(placeholder: maybePlaceholderNode) +// self.searchDisplayController = nil +// } +// } +// } +// +// func scrollToTop() { +// if self.contentNode.supernode != nil { +// self.contentNode.scrollToTop() +// } +// } +// +// +//} +// +// +// +// +// +// +//private final class NotificationExceptionsSearchControllerContentNode: SearchDisplayControllerContentNode { +// private let account: Account +// +// private let listNode: ItemListControllerNode +// private let dimNode: ASDisplayNode +// private var validLayout: ContainerViewLayout? +// +// +// private let searchQuery = Promise() +// private let searchDisposable = MetaDisposable() +// +// private var presentationData: PresentationData +// private var presentationDataDisposable: Disposable? +// +// private let presentationDataPromise: Promise +// +// private let _isSearching = ValuePromise(false, ignoreRepeated: true) +// override var isSearching: Signal { +// return self._isSearching.get() +// } +// +// private let state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError> +// +// +// init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) { +// self.account = account +// self.state = state +// +// self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } +// self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations)) +// +// self.listNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in +// +// }, state: searchQuery.get() |> mapToSignal { query in +// return state |> map { values in +// var values = values +// let entries = values.1.0.entries.filter { entry in +// switch entry { +// case .search: +// return false +// case let .peer(_, peer, _, _, _, _, _): +// if let query = query { +// return !peer.displayTitle.components(separatedBy: " ").filter({$0.lowercased().hasPrefix(query.lowercased())}).isEmpty && !query.isEmpty +// } else { +// return false +// } +// } +// } +// values.1.0 = ItemListNodeState(entries: entries, style: values.1.0.style, focusItemTag: nil, emptyStateItem: nil, searchItem: nil, crossfadeState: false, animateChanges: false) +// return values +// } +// }) +// +// listNode.listNode.keepTopItemOverscrollBackground = ListViewKeepTopItemOverscrollBackground(color: presentationData.theme.chatList.backgroundColor, direction: true) +// listNode.listNode.keepBottomItemOverscrollBackground = presentationData.theme.chatList.backgroundColor +// +// +// self.dimNode = ASDisplayNode() +// self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5) +// +// super.init() +// +// +// self.addSubnode(self.dimNode) +// self.addSubnode(self.listNode) +// self.listNode.isHidden = true +// +// self.presentationDataDisposable = (account.telegramApplicationContext.presentationData +// |> deliverOnMainQueue).start(next: { [weak self] presentationData in +// if let strongSelf = self { +// let previousTheme = strongSelf.presentationData.theme +// +// strongSelf.presentationData = presentationData +// +// if previousTheme !== presentationData.theme { +// strongSelf.updateTheme(theme: presentationData.theme) +// } +// } +// }) +// +// } +// +// deinit { +// self.searchDisposable.dispose() +// self.presentationDataDisposable?.dispose() +// } +// +// private func updateTheme(theme: PresentationTheme) { +// self.backgroundColor = theme.chatList.backgroundColor +// } +// +// override func didLoad() { +// super.didLoad() +// +// self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) +// } +// +// @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { +// if case .ended = recognizer.state { +// self.cancel?() +// } +// } +// +// override func searchTextUpdated(text: String) { +// if text.isEmpty { +// self.searchQuery.set(.single(nil)) +// self.listNode.isHidden = true +// } else { +// self.searchQuery.set(.single(text)) +// self.listNode.isHidden = false +// } +// +// } +// +// override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { +// super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) +// +// let hadValidLayout = self.validLayout != nil +// self.validLayout = layout +// +// var duration: Double = 0.0 +// var curve: UInt = 0 +// switch transition { +// case .immediate: +// break +// case let .animated(animationDuration, animationCurve): +// duration = animationDuration +// switch animationCurve { +// case .easeInOut: +// break +// case .spring: +// curve = 7 +// } +// } +// +// +// let listViewCurve: ListViewAnimationCurve +// if curve == 7 { +// listViewCurve = .Spring(duration: duration) +// } else { +// listViewCurve = .Default(duration: duration) +// } +// +// self.listNode.containerLayoutUpdated(layout, navigationBarHeight: 0, transition: transition) +// +// let insets = UIEdgeInsets(top: navigationBarHeight - 30, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right) +// +// transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top))) +// +// self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size) +// self.listNode.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight - 30, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) +// } +// +//} diff --git a/TelegramUI/NotificationExcetionSettingsController.swift b/TelegramUI/NotificationExcetionSettingsController.swift new file mode 100644 index 0000000000..bc0f830817 --- /dev/null +++ b/TelegramUI/NotificationExcetionSettingsController.swift @@ -0,0 +1,336 @@ +import Foundation +import Display +import AsyncDisplayKit +import Postbox +import TelegramCore +import SwiftSignalKit + + +private enum NotificationPeerExceptionSection: Int32 { + case switcher + case soundModern + case soundClassic +} + +private enum NotificationPeerExceptionSwitcher : Equatable { + case alwaysOn + case alwaysOff +} + +private enum NotificationPeerExceptionEntryId : Hashable { + case switcher(NotificationPeerExceptionSwitcher) + case sound(PeerMessageSound) + case switcherHeader + case soundModernHeader + case soundClassicHeader + case none + case `default` + + var hashValue: Int { + return 0 + } +} + +private final class NotificationPeerExceptionArguments { + let account: Account + + let selectSound: (PeerMessageSound) -> Void + let selectMode: (NotificationPeerExceptionSwitcher) -> Void + let complete: () -> Void + let cancel: () -> Void + + init(account: Account, selectSound: @escaping(PeerMessageSound) -> Void, selectMode: @escaping(NotificationPeerExceptionSwitcher) -> Void, complete: @escaping()->Void, cancel: @escaping() -> Void) { + self.account = account + self.selectSound = selectSound + self.selectMode = selectMode + self.complete = complete + self.cancel = cancel + } +} + + +private enum NotificationPeerExceptionEntry: ItemListNodeEntry { + + typealias ItemGenerationArguments = NotificationPeerExceptionArguments + + case switcher(index:Int32, theme: PresentationTheme, strings: PresentationStrings, mode: NotificationPeerExceptionSwitcher, selected: Bool) + case switcherHeader(index:Int32, theme: PresentationTheme, title: String) + case soundModernHeader(index:Int32, theme: PresentationTheme, title: String) + case soundClassicHeader(index:Int32, theme: PresentationTheme, title: String) + case none(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool) + case `default`(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, selected: Bool) + case sound(index:Int32, section: NotificationPeerExceptionSection, theme: PresentationTheme, text: String, sound: PeerMessageSound, selected: Bool) + + + var index: Int32 { + switch self { + case let .switcherHeader(index, _, _): + return index + case let .switcher(index, _, _, _, _): + return index + case let .soundModernHeader(index, _, _): + return index + case let .soundClassicHeader(index, _, _): + return index + case let .none(index, _, _, _, _): + return index + case let .default(index, _, _, _, _): + return index + case let .sound(index, _, _, _, _, _): + return index + } + } + + var section: ItemListSectionId { + switch self { + case .switcher, .switcherHeader: + return NotificationPeerExceptionSection.switcher.rawValue + case .soundModernHeader: + return NotificationPeerExceptionSection.soundModern.rawValue + case .soundClassicHeader: + return NotificationPeerExceptionSection.soundClassic.rawValue + case let .none(_, section, _, _, _): + return section.rawValue + case let .default(_, section, _, _, _): + return section.rawValue + case let .sound(_, section, _, _, _, _): + return section.rawValue + } + } + + var stableId: NotificationPeerExceptionEntryId { + switch self { + case let .switcher(_, _, _, mode, _): + return .switcher(mode) + case .switcherHeader: + return .switcherHeader + case .soundModernHeader: + return .soundModernHeader + case .soundClassicHeader: + return .soundClassicHeader + case .none: + return .none + case .default: + return .default + case let .sound(_, _, _, _, sound, _): + return .sound(sound) + } + } + + static func <(lhs: NotificationPeerExceptionEntry, rhs: NotificationPeerExceptionEntry) -> Bool { + return lhs.index < rhs.index + } + + func item(_ arguments: NotificationPeerExceptionArguments) -> ListViewItem { + switch self { + case let .switcher(_, theme, strings, mode, selected): + let title: String + switch mode { + case .alwaysOn: + title = "Always On" + case .alwaysOff: + title = "Always Off" + } + return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.selectMode(mode) + }) + case let .switcherHeader(_, theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .soundModernHeader(_, theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .soundClassicHeader(_, theme, text): + return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) + case let .none(_, _, theme, text, selected): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: true, sectionId: self.section, action: { + arguments.selectSound(.none) + }) + case let .default(_, _, theme, text, selected): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.selectSound(.default) + }) + case let .sound(_, _, theme, text, sound, selected): + return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { + arguments.selectSound(sound) + }) + } + } +} + + +private func notificationPeerExceptionEntries(presentationData: PresentationData, state: NotificationExceptionPeerState) -> [NotificationPeerExceptionEntry] { + var entries:[NotificationPeerExceptionEntry] = [] + + var index: Int32 = 0 + + entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: "NOTIFICATIONS")) + index += 1 + + + entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOn, selected: state.mode == .alwaysOn)) + index += 1 + entries.append(.switcher(index: index, theme: presentationData.theme, strings: presentationData.strings, mode: .alwaysOff, selected: state.mode == .alwaysOff)) + index += 1 + + + entries.append(.soundModernHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_AlertTones)) + index += 1 + + if state.selectedSound == .default { + var bp:Int = 0 + bp += 1 + } + + entries.append(.default(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: .default, default: state.defaultSound), selected: state.selectedSound == .default)) + index += 1 + + entries.append(.none(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: .none), selected: state.selectedSound == .none)) + index += 1 + + for i in 0 ..< 12 { + let sound: PeerMessageSound = .bundledModern(id: Int32(i)) + entries.append(.sound(index: index, section: .soundModern, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: sound), sound: sound, selected: sound == state.selectedSound)) + index += 1 + } + + entries.append(.soundClassicHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_ClassicTones)) + for i in 0 ..< 8 { + let sound: PeerMessageSound = .bundledClassic(id: Int32(i)) + entries.append(.sound(index: index, section: .soundClassic, theme: presentationData.theme, text: localizedPeerNotificationSoundString(strings: presentationData.strings, sound: sound), sound: sound, selected: sound == state.selectedSound)) + index += 1 + } + + return entries +} + +private struct NotificationExceptionPeerState : Equatable { + let selectedSound: PeerMessageSound + let mode: NotificationPeerExceptionSwitcher + let defaultSound: PeerMessageSound + init(notifications: TelegramPeerNotificationSettings? = nil) { + + if let notifications = notifications { + self.selectedSound = notifications.messageSound + switch notifications.muteState { + case .muted: + self.mode = .alwaysOff + case .unmuted: + self.mode = .alwaysOn + case .default: + self.mode = .alwaysOn + } + } else { + self.selectedSound = .default + self.mode = .alwaysOn + } + + + self.defaultSound = .default + } + + init(selectedSound: PeerMessageSound, mode: NotificationPeerExceptionSwitcher, defaultSound: PeerMessageSound) { + self.selectedSound = selectedSound + self.mode = mode + self.defaultSound = defaultSound + } + + func withUpdatedDefaultSound(_ defaultSound: PeerMessageSound) -> NotificationExceptionPeerState { + return NotificationExceptionPeerState(selectedSound: self.selectedSound, mode: self.mode, defaultSound: defaultSound) + } + func withUpdatedSound(_ selectedSound: PeerMessageSound) -> NotificationExceptionPeerState { + return NotificationExceptionPeerState(selectedSound: selectedSound, mode: self.mode, defaultSound: self.defaultSound) + } + func withUpdatedMode(_ mode: NotificationPeerExceptionSwitcher) -> NotificationExceptionPeerState { + return NotificationExceptionPeerState(selectedSound: self.selectedSound, mode: mode, defaultSound: self.defaultSound) + } +} + + +func notificationPeerExceptionController(account: Account, peerId: PeerId, mode: NotificationExceptionMode, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void) -> ViewController { + + + let initialState = NotificationExceptionPeerState() + let statePromise = Promise(initialState) + let stateValue = Atomic(value: initialState) + let updateState: ((NotificationExceptionPeerState) -> NotificationExceptionPeerState) -> Void = { f in + statePromise.set(.single(stateValue.modify { f($0) })) + } + + var completeImpl: (() -> Void)? + var cancelImpl: (() -> Void)? + let playSoundDisposable = MetaDisposable() + + + let arguments = NotificationPeerExceptionArguments(account: account, selectSound: { sound in + + updateState { state in + playSoundDisposable.set(playSound(account: account, sound: sound, defaultSound: state.defaultSound).start()) + return state.withUpdatedSound(sound) + } + + }, selectMode: { mode in + updateState { state in + return state.withUpdatedMode(mode) + } + }, complete: { + completeImpl?() + }, cancel: { + cancelImpl?() + }) + + + + + statePromise.set(account.postbox.transaction { transaction -> NotificationExceptionPeerState in + var state = NotificationExceptionPeerState(notifications: transaction.getPeerNotificationSettings(peerId) as? TelegramPeerNotificationSettings) + let globalSettings: GlobalNotificationSettings = (transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications) as? GlobalNotificationSettings) ?? GlobalNotificationSettings.defaultSettings + switch mode { + case .channels: + state = state.withUpdatedDefaultSound(globalSettings.effective.channels.sound) + case .groups: + state = state.withUpdatedDefaultSound(globalSettings.effective.groupChats.sound) + case .users: + state = state.withUpdatedDefaultSound(globalSettings.effective.privateChats.sound) + } + _ = stateValue.swap(state) + return state + }) + + + let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get() |> distinctUntilChanged) + |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, NotificationPeerExceptionEntry.ItemGenerationArguments)) in + + let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { + arguments.cancel() + }) + + let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + arguments.complete() + }) + + let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("New Exception"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) + let listState = ItemListNodeState(entries: notificationPeerExceptionEntries(presentationData: presentationData, state: state), style: .blocks) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(account: account, state: signal |> afterDisposed { + playSoundDisposable.dispose() + }) + + controller.enableInteractiveDismiss = true + + completeImpl = { [weak controller] in + controller?.dismiss() + updateState { state in + updatePeerSound(peerId, state.selectedSound) + updatePeerNotificationInterval(peerId, state.mode == .alwaysOn ? 0 : Int32.max) + return state + } + } + + cancelImpl = { [weak controller] in + controller?.dismiss() + } + + return controller +} diff --git a/TelegramUI/NotificationSoundSelection.swift b/TelegramUI/NotificationSoundSelection.swift index 336c75816a..636617d28f 100644 --- a/TelegramUI/NotificationSoundSelection.swift +++ b/TelegramUI/NotificationSoundSelection.swift @@ -209,7 +209,7 @@ public func fileNameForNotificationSound(_ sound: PeerMessageSound, defaultSound } } -private func playSound(account: Account, sound: PeerMessageSound, defaultSound: PeerMessageSound?) -> Signal { +func playSound(account: Account, sound: PeerMessageSound, defaultSound: PeerMessageSound?) -> Signal { if case .none = sound { return .complete() } else { diff --git a/TelegramUI/NotificationsAndSounds.swift b/TelegramUI/NotificationsAndSounds.swift index 336ff6a5e0..6c2c2dbc45 100644 --- a/TelegramUI/NotificationsAndSounds.swift +++ b/TelegramUI/NotificationsAndSounds.swift @@ -422,7 +422,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .userExceptions(theme, strings, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: strings.Notifications_Exceptions(Int32(value.settings.count)), sectionId: self.section, style: .blocks, action: { - let controller = notificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) + let controller = NotificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) arguments.pushController(controller) }) case let .messageNotice(theme, text): @@ -446,7 +446,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .groupExceptions(theme, strings, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: strings.Notifications_Exceptions(Int32(value.settings.count)), sectionId: self.section, style: .blocks, action: { - let controller = notificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) + let controller = NotificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) arguments.pushController(controller) }) case let .groupNotice(theme, text): @@ -470,7 +470,7 @@ private enum NotificationsAndSoundsEntry: ItemListNodeEntry { }) case let .channelExceptions(theme, strings, text, value): return ItemListDisclosureItem(theme: theme, title: text, label: strings.Notifications_Exceptions(Int32(value.settings.count)), sectionId: self.section, style: .blocks, action: { - let controller = notificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) + let controller = NotificationExceptionsController(account: arguments.account, mode: value, updatedMode: arguments.updatedExceptionMode) arguments.pushController(controller) }) case let .channelNotice(theme, text): @@ -533,33 +533,33 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound } } -private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (NotificationExceptionMode, NotificationExceptionMode), presentationData: PresentationData) -> [NotificationsAndSoundsEntry] { +private func notificationsAndSoundsEntries(globalSettings: GlobalNotificationSettingsSet, inAppSettings: InAppNotificationSettings, exceptions: (users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode), presentationData: PresentationData) -> [NotificationsAndSoundsEntry] { var entries: [NotificationsAndSoundsEntry] = [] entries.append(.messageHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotifications)) entries.append(.messageAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.privateChats.enabled)) entries.append(.messagePreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.privateChats.displayPreviews)) entries.append(.messageSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.privateChats.sound)), filteredGlobalSound(globalSettings.privateChats.sound))) - if !exceptions.0.isEmpty { - // entries.append(.userExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.0)) - } + //if !exceptions.users.isEmpty { + entries.append(.userExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.users)) + // } entries.append(.messageNotice(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsHelp)) entries.append(.groupHeader(presentationData.theme, presentationData.strings.Notifications_GroupNotifications)) entries.append(.groupAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.groupChats.enabled)) entries.append(.groupPreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.groupChats.displayPreviews)) entries.append(.groupSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.groupChats.sound)), filteredGlobalSound(globalSettings.groupChats.sound))) - if !exceptions.1.isEmpty { - // entries.append(.groupExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.1)) - } + // if !exceptions.groups.isEmpty { + entries.append(.groupExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.groups)) + // } entries.append(.channelHeader(presentationData.theme, presentationData.strings.Notifications_ChannelNotifications)) entries.append(.channelAlerts(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, globalSettings.channels.enabled)) entries.append(.channelPreviews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, globalSettings.channels.displayPreviews)) entries.append(.channelSound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, sound: filteredGlobalSound(globalSettings.channels.sound)), filteredGlobalSound(globalSettings.channels.sound))) - if !exceptions.1.isEmpty { - // entries.append(.groupExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.1)) - } + // if !exceptions.channels.isEmpty { + entries.append(.channelExceptions(presentationData.theme, presentationData.strings, presentationData.strings.Notifications_MessageNotificationsExceptions, exceptions.channels)) + // } entries.append(.channelNotice(presentationData.theme, presentationData.strings.Notifications_ChannelNotificationsHelp)) @@ -590,9 +590,9 @@ public func notificationsAndSoundsController(account: Account) -> ViewController - let notificationExceptions: Promise<(NotificationExceptionMode, NotificationExceptionMode)> = Promise() + let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)> = Promise() - let updateNotificationExceptions:((NotificationExceptionMode, NotificationExceptionMode)) -> Void = { value in + let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode)) -> Void = { value in notificationExceptions.set(.single(value)) } @@ -724,12 +724,14 @@ public func notificationsAndSoundsController(account: Account) -> ViewController ])]) presentControllerImpl?(actionSheet, nil) }, updatedExceptionMode: { mode in - _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups) in + _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels) in switch mode { case .users: - updateNotificationExceptions((mode, groups)) + updateNotificationExceptions((mode, groups, channels)) case .groups: - updateNotificationExceptions((users, mode)) + updateNotificationExceptions((users, mode, channels)) + case .channels: + updateNotificationExceptions((users, groups, mode)) } }) }) @@ -738,11 +740,11 @@ public func notificationsAndSoundsController(account: Account) -> ViewController - notificationExceptions.set(account.postbox.transaction{ transaction -> (NotificationExceptionMode, NotificationExceptionMode) in + notificationExceptions.set(account.postbox.transaction { transaction -> (NotificationExceptionMode, NotificationExceptionMode, NotificationExceptionMode) in let allSettings = transaction.getAllPeerNotificationSettings() ?? [:] var users:[PeerId : NotificationExceptionWrapper] = [:] var groups: [PeerId : NotificationExceptionWrapper] = [:] - + var channels:[PeerId : NotificationExceptionWrapper] = [:] for (key, value) in allSettings { let peer = transaction.getPeer(key) if let value = value as? TelegramPeerNotificationSettings, let peer = peer, !peer.displayTitle.isEmpty, peer.id != account.peerId { @@ -756,7 +758,11 @@ public func notificationsAndSoundsController(account: Account) -> ViewController case Namespaces.Peer.CloudUser: users[key] = NotificationExceptionWrapper(settings: value) default: - groups[key] = NotificationExceptionWrapper(settings: value) + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + channels[key] = NotificationExceptionWrapper(settings: value) + } else { + groups[key] = NotificationExceptionWrapper(settings: value) + } } } default: @@ -764,13 +770,16 @@ public func notificationsAndSoundsController(account: Account) -> ViewController case Namespaces.Peer.CloudUser: users[key] = NotificationExceptionWrapper(settings: value) default: - groups[key] = NotificationExceptionWrapper(settings: value) - } + if let peer = peer as? TelegramChannel, case .broadcast = peer.info { + channels[key] = NotificationExceptionWrapper(settings: value) + } else { + groups[key] = NotificationExceptionWrapper(settings: value) + } } } } } - return (.users(users), .groups(groups)) + return (.users(users), .groups(groups), .channels(channels)) }) let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, preferences, notificationExceptions.get()) diff --git a/TelegramUI/PeerSelectionController.swift b/TelegramUI/PeerSelectionController.swift index 02912b4450..0b7e79df4c 100644 --- a/TelegramUI/PeerSelectionController.swift +++ b/TelegramUI/PeerSelectionController.swift @@ -40,10 +40,12 @@ public final class PeerSelectionController: ViewController { return self._ready } - public init(account: Account, filter: ChatListNodePeersFilter = [.onlyWriteable], title: String? = nil) { + private let hasContactSelector: Bool + + public init(account: Account, filter: ChatListNodePeersFilter = [.onlyWriteable], hasContactSelector: Bool = true, title: String? = nil) { self.account = account self.filter = filter - + self.hasContactSelector = hasContactSelector self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) @@ -69,7 +71,7 @@ public final class PeerSelectionController: ViewController { } override public func loadDisplayNode() { - self.displayNode = PeerSelectionControllerNode(account: self.account, filter: self.filter, dismiss: { [weak self] in + self.displayNode = PeerSelectionControllerNode(account: self.account, filter: self.filter, hasContactSelector: hasContactSelector, dismiss: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) }) @@ -152,7 +154,7 @@ public final class PeerSelectionController: ViewController { } } - override open func dismiss(completion: (() -> Void)? = nil) { + override public func dismiss(completion: (() -> Void)? = nil) { self.peerSelectionNode.view.endEditing(true) self.peerSelectionNode.animateOut(completion: completion) } diff --git a/TelegramUI/PeerSelectionControllerNode.swift b/TelegramUI/PeerSelectionControllerNode.swift index 414ef0fbb6..eaa805da41 100644 --- a/TelegramUI/PeerSelectionControllerNode.swift +++ b/TelegramUI/PeerSelectionControllerNode.swift @@ -18,9 +18,9 @@ final class PeerSelectionControllerNode: ASDisplayNode { var navigationBar: NavigationBar? - private let toolbarBackgroundNode: ASDisplayNode - private let toolbarSeparatorNode: ASDisplayNode - private let segmentedControl: UISegmentedControl + private let toolbarBackgroundNode: ASDisplayNode? + private let toolbarSeparatorNode: ASDisplayNode? + private let segmentedControl: UISegmentedControl? private var contactListNode: ContactListNode? private let chatListNode: ChatListNode @@ -45,22 +45,30 @@ final class PeerSelectionControllerNode: ASDisplayNode { return self.readyValue.get() } - init(account: Account, filter: ChatListNodePeersFilter, dismiss: @escaping () -> Void) { + + init(account: Account, filter: ChatListNodePeersFilter, hasContactSelector: Bool, dismiss: @escaping () -> Void) { self.account = account self.dismiss = dismiss self.filter = filter self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - self.toolbarBackgroundNode = ASDisplayNode() - self.toolbarBackgroundNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor - - self.toolbarSeparatorNode = ASDisplayNode() - self.toolbarSeparatorNode.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor - - self.segmentedControl = UISegmentedControl(items: [self.presentationData.strings.DialogList_TabTitle, self.presentationData.strings.Contacts_TabTitle]) - self.segmentedControl.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor - self.segmentedControl.selectedSegmentIndex = 0 + if hasContactSelector { + self.toolbarBackgroundNode = ASDisplayNode() + self.toolbarBackgroundNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.backgroundColor + + self.toolbarSeparatorNode = ASDisplayNode() + self.toolbarSeparatorNode?.backgroundColor = self.presentationData.theme.rootController.navigationBar.separatorColor + + self.segmentedControl = UISegmentedControl(items: [self.presentationData.strings.DialogList_TabTitle, self.presentationData.strings.Contacts_TabTitle]) + self.segmentedControl?.tintColor = self.presentationData.theme.rootController.navigationBar.accentTextColor + self.segmentedControl?.selectedSegmentIndex = 0 + } else { + self.toolbarBackgroundNode = nil + self.toolbarSeparatorNode = nil + self.segmentedControl = nil + } + self.chatListNode = ChatListNode(account: account, groupId: nil, controlsHistoryPreload: false, mode: .peers(filter: filter), theme: presentationData.theme, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations) @@ -93,12 +101,13 @@ final class PeerSelectionControllerNode: ASDisplayNode { } }) - self.addSubnode(self.toolbarBackgroundNode) - self.addSubnode(self.toolbarSeparatorNode) + if hasContactSelector { + self.addSubnode(self.toolbarBackgroundNode!) + self.addSubnode(self.toolbarSeparatorNode!) + self.view.addSubview(self.segmentedControl!) + self.segmentedControl!.addTarget(self, action: #selector(indexChanged), for: .valueChanged) + } - self.view.addSubview(self.segmentedControl) - - self.segmentedControl.addTarget(self, action: #selector(indexChanged), for: .valueChanged) self.readyValue.set(self.chatListNode.ready) } @@ -118,14 +127,22 @@ final class PeerSelectionControllerNode: ASDisplayNode { let cleanInsets = layout.insets(options: []) - let toolbarHeight: CGFloat = 44.0 + cleanInsets.bottom + var toolbarHeight: CGFloat = cleanInsets.bottom + - transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) - transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + if let segmentedControl = segmentedControl, let toolbarBackgroundNode = toolbarBackgroundNode, let toolbarSeparatorNode = toolbarSeparatorNode { + toolbarHeight += 44 + transition.updateFrame(node: toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: toolbarHeight))) + transition.updateFrame(node: toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - toolbarHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel))) + + var controlSize = segmentedControl.sizeThatFits(layout.size) + controlSize.width = min(layout.size.width, max(200.0, controlSize.width)) + transition.updateFrame(view: segmentedControl, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: layout.size.height - toolbarHeight + floor((44.0 - controlSize.height) / 2.0)), size: controlSize)) + } - var controlSize = self.segmentedControl.sizeThatFits(layout.size) - controlSize.width = min(layout.size.width, max(200.0, controlSize.width)) - transition.updateFrame(view: self.segmentedControl, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - controlSize.width) / 2.0), y: layout.size.height - toolbarHeight + floor((44.0 - controlSize.height) / 2.0)), size: controlSize)) + + + var insets = layout.insets(options: [.input]) insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top) @@ -304,11 +321,11 @@ final class PeerSelectionControllerNode: ASDisplayNode { } @objc func indexChanged() { - guard let (layout, navigationHeight) = self.containerLayout else { + guard let (layout, navigationHeight) = self.containerLayout, let segmentedControl = self.segmentedControl else { return } - let contactListActive = self.segmentedControl.selectedSegmentIndex == 1 + let contactListActive = segmentedControl.selectedSegmentIndex == 1 if contactListActive != self.contactListActive { self.contactListActive = contactListActive if contactListActive { diff --git a/TelegramUI/PresentationData.swift b/TelegramUI/PresentationData.swift index 87dab1dbcf..7b8c86a1e2 100644 --- a/TelegramUI/PresentationData.swift +++ b/TelegramUI/PresentationData.swift @@ -26,6 +26,18 @@ public enum PresentationPersonNameOrder { case lastFirst } +extension PresentationStrings : Equatable { + public static func ==(lhs: PresentationStrings, rhs: PresentationStrings) -> Bool { + return lhs === rhs + } +} +// +//extension PresentationTheme : Equatable { +// public static func ==(lhs: PresentationTheme, rhs: PresentationTheme) -> Bool { +// return lhs === rhs +// } +//} + public final class PresentationData: Equatable { public let strings: PresentationStrings public let theme: PresentationTheme