From df35dcf3c4c2dcf547bdceccc995a4babdddcff4 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Mon, 17 Oct 2022 17:16:50 +0400 Subject: [PATCH] [WIP] API update --- .../TelegramCore/Sources/ForumChannels.swift | 71 ++- .../Peers/TelegramEnginePeers.swift | 2 +- submodules/TelegramUI/BUILD | 2 +- .../Sources/ForumTopicListScreen.swift | 494 ---------------- .../BUILD | 8 +- .../NotificationExceptionsScreen.swift | 535 ++++++++++++++++++ .../Sources/NavigateToChatController.swift | 1 - .../Sources/PeerInfo/PeerInfoScreen.swift | 14 +- 8 files changed, 617 insertions(+), 510 deletions(-) delete mode 100644 submodules/TelegramUI/Components/ForumTopicListScreen/Sources/ForumTopicListScreen.swift rename submodules/TelegramUI/Components/{ForumTopicListScreen => NotificationExceptionsScreen}/BUILD (83%) create mode 100644 submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift diff --git a/submodules/TelegramCore/Sources/ForumChannels.swift b/submodules/TelegramCore/Sources/ForumChannels.swift index 420f75b017..8faa7edc77 100644 --- a/submodules/TelegramCore/Sources/ForumChannels.swift +++ b/submodules/TelegramCore/Sources/ForumChannels.swift @@ -536,17 +536,44 @@ func _internal_loadMessageHistoryThreads(account: Account, peerId: PeerId) -> Si return signal } -func _internal_forumChannelTopicNotificationExceptions(account: Account, id: EnginePeer.Id) -> Signal<[(threadId: Int64, notificationSettiongs: EnginePeer.NotificationSettings)], NoError> { - return account.postbox.transaction { transaction -> Api.InputPeer? in - return transaction.getPeer(id).flatMap(apiInputPeer) +public extension EngineMessageHistoryThread { + struct NotificationException: Equatable { + public var threadId: Int64 + public var info: EngineMessageHistoryThread.Info + public var notificationSettings: EnginePeer.NotificationSettings + + public init( + threadId: Int64, + info: EngineMessageHistoryThread.Info, + notificationSettings: EnginePeer.NotificationSettings + ) { + self.threadId = threadId + self.info = info + self.notificationSettings = notificationSettings + } } - |> mapToSignal { inputPeer -> Signal<[(threadId: Int64, notificationSettiongs: EnginePeer.NotificationSettings)], NoError> in - guard let inputPeer = inputPeer else { +} + +func _internal_forumChannelTopicNotificationExceptions(account: Account, id: EnginePeer.Id) -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> { + return account.postbox.transaction { transaction -> Peer? in + return transaction.getPeer(id) + } + |> mapToSignal { peer -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> in + guard let inputPeer = peer.flatMap(apiInputPeer), let inputChannel = peer.flatMap(apiInputChannel) else { return .single([]) } + return account.network.request(Api.functions.account.getNotifyExceptions(flags: 1 << 0, peer: Api.InputNotifyPeer.inputNotifyPeer(peer: inputPeer))) - |> map { result -> [(threadId: Int64, notificationSettiongs: EnginePeer.NotificationSettings)] in - var list: [(threadId: Int64, notificationSettiongs: EnginePeer.NotificationSettings)] = [] + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> map { result -> [(threadId: Int64, notificationSettings: EnginePeer.NotificationSettings)] in + guard let result = result else { + return [] + } + + var list: [(threadId: Int64, notificationSettings: EnginePeer.NotificationSettings)] = [] for update in result.allUpdates { switch update { case let .updateNotifySettings(peer, notifySettings): @@ -562,8 +589,34 @@ func _internal_forumChannelTopicNotificationExceptions(account: Account, id: Eng } return list } - |> `catch` { _ -> Signal<[(threadId: Int64, notificationSettiongs: EnginePeer.NotificationSettings)], NoError> in - return .single([]) + |> mapToSignal { list -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> in + return account.network.request(Api.functions.channels.getForumTopicsByID(channel: inputChannel, topics: list.map { Int32(clamping: $0.threadId) })) + |> map { result -> [EngineMessageHistoryThread.NotificationException] in + var infoMapping: [Int64: EngineMessageHistoryThread.Info] = [:] + + switch result { + case let .forumTopics(_, _, topics, _, _, _, _): + for topic in topics { + switch topic { + case let .forumTopic(_, id, _, title, iconColor, iconEmojiId, _, _, _, _, _, _, _, _, _): + infoMapping[Int64(id)] = EngineMessageHistoryThread.Info(title: title, icon: iconEmojiId, iconColor: iconColor) + case .forumTopicDeleted: + break + } + } + } + + return list.compactMap { item -> EngineMessageHistoryThread.NotificationException? in + if let info = infoMapping[item.threadId] { + return EngineMessageHistoryThread.NotificationException(threadId: item.threadId, info: info, notificationSettings: item.notificationSettings) + } else { + return nil + } + } + } + |> `catch` { _ -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> in + return .single([]) + } } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index f648e7956b..b53cb4a59e 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -840,7 +840,7 @@ public extension TelegramEngine { return _internal_setForumChannelTopicPinned(account: self.account, id: id, threadId: threadId, isPinned: isPinned) } - public func forumChannelTopicNotificationExceptions(id: EnginePeer.Id) -> Signal<[(threadId: Int64, notificationSettiongs: EnginePeer.NotificationSettings)], NoError> { + public func forumChannelTopicNotificationExceptions(id: EnginePeer.Id) -> Signal<[EngineMessageHistoryThread.NotificationException], NoError> { return _internal_forumChannelTopicNotificationExceptions(account: self.account, id: id) } } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index 951535423a..af9e887c9e 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -297,7 +297,7 @@ swift_library( "//submodules/Media/LocalAudioTranscription:LocalAudioTranscription", "//submodules/Components/PagerComponent:PagerComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", - "//submodules/TelegramUI/Components/ForumTopicListScreen:ForumTopicListScreen", + "//submodules/TelegramUI/Components/NotificationExceptionsScreen:NotificationExceptionsScreen", "//submodules/TelegramUI/Components/ForumCreateTopicScreen:ForumCreateTopicScreen", "//submodules/TelegramUI/Components/ChatTitleView", "//submodules/InviteLinksUI:InviteLinksUI", diff --git a/submodules/TelegramUI/Components/ForumTopicListScreen/Sources/ForumTopicListScreen.swift b/submodules/TelegramUI/Components/ForumTopicListScreen/Sources/ForumTopicListScreen.swift deleted file mode 100644 index 05f622b17a..0000000000 --- a/submodules/TelegramUI/Components/ForumTopicListScreen/Sources/ForumTopicListScreen.swift +++ /dev/null @@ -1,494 +0,0 @@ -import Foundation -import UIKit -import Display -import AsyncDisplayKit -import ComponentFlow -import SwiftSignalKit -import AnimationCache -import MultiAnimationRenderer -import ComponentDisplayAdapters -import TelegramPresentationData -import AccountContext -import Postbox -import TelegramCore - -private final class ForumTopicListItemComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let item: ForumChannelTopics.Item - let action: () -> Void - - init( - context: AccountContext, - theme: PresentationTheme, - strings: PresentationStrings, - item: ForumChannelTopics.Item, - action: @escaping () -> Void - ) { - self.context = context - self.theme = theme - self.strings = strings - self.item = item - self.action = action - } - - static func ==(lhs: ForumTopicListItemComponent, rhs: ForumTopicListItemComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.item != rhs.item { - return false - } - return true - } - - final class View: HighlightTrackingButton { - private var highlightedBackgroundLayer: SimpleLayer? - private let title: ComponentView - - private var component: ForumTopicListItemComponent? - - override init(frame: CGRect) { - self.title = ComponentView() - - super.init(frame: frame) - - self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside) - - self.highligthedChanged = { [weak self] highlighted in - if let self, let component = self.component { - if highlighted { - if let superview = self.superview { - superview.bringSubviewToFront(self) - } - let highlightedBackgroundLayer: SimpleLayer - if let current = self.highlightedBackgroundLayer { - highlightedBackgroundLayer = current - } else { - highlightedBackgroundLayer = SimpleLayer() - self.highlightedBackgroundLayer = highlightedBackgroundLayer - highlightedBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor - highlightedBackgroundLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: self.bounds.width, height: self.bounds.height + UIScreenPixel)) - self.layer.insertSublayer(highlightedBackgroundLayer, at: 0) - } - highlightedBackgroundLayer.removeAllAnimations() - highlightedBackgroundLayer.opacity = 1.0 - } else { - if let highlightedBackgroundLayer = self.highlightedBackgroundLayer { - self.highlightedBackgroundLayer = nil - highlightedBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak highlightedBackgroundLayer] _ in - highlightedBackgroundLayer?.removeFromSuperlayer() - }) - } - } - } - } - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - @objc private func pressed() { - self.component?.action() - } - - func update(component: ForumTopicListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.component = component - - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(Text( - text: component.item.info.title, - font: Font.regular(17.0), - color: component.theme.list.itemPrimaryTextColor - )), - environment: {}, - containerSize: availableSize - ) - if let titleView = self.title.view { - if titleView.superview == nil { - self.addSubview(titleView) - titleView.isUserInteractionEnabled = false - } - transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 11.0, y: floor((availableSize.height - titleSize.height) / 2.0)), size: titleSize)) - } - - return availableSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -private final class ForumTopicListComponent: Component { - let context: AccountContext - let theme: PresentationTheme - let strings: PresentationStrings - let items: [ForumChannelTopics.Item] - let navigationHeight: CGFloat - let action: (ForumChannelTopics.Item) -> Void - - init( - context: AccountContext, - theme: PresentationTheme, - strings: PresentationStrings, - items: [ForumChannelTopics.Item], - navigationHeight: CGFloat, - action: @escaping (ForumChannelTopics.Item) -> Void - ) { - self.context = context - self.theme = theme - self.strings = strings - self.items = items - self.navigationHeight = navigationHeight - self.action = action - } - - static func ==(lhs: ForumTopicListComponent, rhs: ForumTopicListComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.theme !== rhs.theme { - return false - } - if lhs.strings !== rhs.strings { - return false - } - if lhs.items != rhs.items { - return false - } - if lhs.navigationHeight != rhs.navigationHeight { - return false - } - return true - } - - final class View: UIView, UIScrollViewDelegate { - private struct ItemLayout { - let containerSize: CGSize - let itemHeight: CGFloat - let contentSize: CGSize - let itemsInsets: UIEdgeInsets - - init(containerSize: CGSize, navigationHeight: CGFloat, itemCount: Int) { - self.itemHeight = 44.0 - self.containerSize = containerSize - self.itemsInsets = UIEdgeInsets(top: navigationHeight, left: 0.0, bottom: 0.0, right: 0.0) - self.contentSize = CGSize(width: containerSize.width, height: self.itemsInsets.top + self.itemsInsets.bottom + CGFloat(itemCount) * self.itemHeight) - } - - func frame(at index: Int) -> CGRect { - return CGRect(origin: CGPoint(x: 0.0, y: self.itemsInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight)) - } - } - - private final class ItemView { - let host: ComponentView - let separatorLayer: SimpleLayer - - init() { - self.host = ComponentView() - self.separatorLayer = SimpleLayer() - } - } - - private let scrollView: UIScrollView - - private var component: ForumTopicListComponent? - private var itemLayout: ItemLayout? - - private var ignoreScrolling: Bool = false - private var visibleItemViews: [Int64: ItemView] = [:] - - override init(frame: CGRect) { - self.scrollView = UIScrollView() - - super.init(frame: frame) - - self.scrollView.layer.anchorPoint = CGPoint() - self.scrollView.delaysContentTouches = true - self.scrollView.clipsToBounds = false - if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { - self.scrollView.contentInsetAdjustmentBehavior = .never - } - if #available(iOS 13.0, *) { - self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false - } - self.scrollView.showsVerticalScrollIndicator = true - self.scrollView.showsHorizontalScrollIndicator = false - self.scrollView.alwaysBounceHorizontal = false - self.scrollView.alwaysBounceVertical = true - self.scrollView.scrollsToTop = false - self.scrollView.delegate = self - self.scrollView.clipsToBounds = true - self.scrollView.canCancelContentTouches = true - - self.addSubview(self.scrollView) - } - - required init?(coder: NSCoder) { - preconditionFailure() - } - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - if !self.ignoreScrolling { - self.updateVisibleItems(transition: .immediate, synchronous: false) - } - } - - private func updateVisibleItems(transition: Transition, synchronous: Bool) { - guard let component = self.component, let itemLayout = self.itemLayout else { - return - } - - var validIds = Set() - let visibleBounds = self.scrollView.bounds - for index in 0 ..< component.items.count { - let itemFrame = itemLayout.frame(at: index) - if !visibleBounds.intersects(itemFrame) { - continue - } - - let item = component.items[index] - validIds.insert(item.id) - - let itemView: ItemView - var itemTransition = transition - if let current = self.visibleItemViews[item.id] { - itemView = current - } else { - itemTransition = .immediate - itemView = ItemView() - self.visibleItemViews[item.id] = itemView - } - - let id = item.id - let _ = itemView.host.update( - transition: itemTransition, - component: AnyComponent(ForumTopicListItemComponent( - context: component.context, - theme: component.theme, - strings: component.strings, - item: item, - action: { [weak self] in - guard let strongSelf = self, let component = strongSelf.component else { - return - } - for item in component.items { - if item.id == id { - component.action(item) - break - } - } - } - )), - environment: {}, - containerSize: itemFrame.size - ) - if let itemComponentView = itemView.host.view { - if itemComponentView.superview == nil { - self.scrollView.addSubview(itemComponentView) - self.scrollView.layer.addSublayer(itemView.separatorLayer) - } - itemTransition.setFrame(view: itemComponentView, frame: itemFrame) - - let separatorInset: CGFloat - if index == component.items.count - 1 { - separatorInset = 0.0 - } else { - separatorInset = 16.0 - } - itemView.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor - itemTransition.setFrame(layer: itemView.separatorLayer, frame: CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: itemLayout.contentSize.width - separatorInset, height: UIScreenPixel))) - } - } - - var removedIds: [Int64] = [] - for (id, itemView) in self.visibleItemViews { - if !validIds.contains(id) { - itemView.host.view?.removeFromSuperview() - itemView.separatorLayer.removeFromSuperlayer() - removedIds.append(id) - } - } - for id in removedIds { - self.visibleItemViews.removeValue(forKey: id) - } - } - - func update(component: ForumTopicListComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - self.component = component - - let itemLayout = ItemLayout(containerSize: availableSize, navigationHeight: component.navigationHeight, itemCount: component.items.count) - self.itemLayout = itemLayout - - self.ignoreScrolling = true - self.scrollView.frame = CGRect(origin: CGPoint(), size: availableSize) - if self.scrollView.contentSize != itemLayout.contentSize { - self.scrollView.contentSize = itemLayout.contentSize - } - if self.scrollView.scrollIndicatorInsets != itemLayout.itemsInsets { - self.scrollView.scrollIndicatorInsets = itemLayout.itemsInsets - } - self.ignoreScrolling = false - - self.updateVisibleItems(transition: transition, synchronous: false) - - return availableSize - } - } - - func makeView() -> View { - return View(frame: CGRect()) - } - - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) - } -} - -public final class ForumTopicListScreen: ViewController { - private final class Node: ViewControllerTracingNode { - private weak var controller: ForumTopicListScreen? - - private let context: AccountContext - private let id: EnginePeer.Id - private var presentationData: PresentationData - - private let topicList: ComponentView - - private let forumChannelContext: ForumChannelTopics - private var stateDisposable: Disposable? - private var currentState: ForumChannelTopics.State? - - private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? - - init(controller: ForumTopicListScreen, context: AccountContext, id: EnginePeer.Id) { - self.controller = controller - - self.context = context - self.id = id - self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - - self.topicList = ComponentView() - - self.forumChannelContext = ForumChannelTopics(account: self.context.account, peerId: self.id) - - super.init() - - self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor - - self.stateDisposable = (self.forumChannelContext.state - |> deliverOnMainQueue).start(next: { [weak self] state in - guard let strongSelf = self else { - return - } - strongSelf.currentState = state - strongSelf.update(transition: .immediate) - }) - } - - deinit { - self.stateDisposable?.dispose() - } - - func createPressed() { - } - - private func update(transition: Transition) { - if let currentLayout = self.currentLayout { - self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: transition) - } - } - - func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { - self.currentLayout = (layout, navigationHeight) - - let _ = self.topicList.update( - transition: transition, - component: AnyComponent(ForumTopicListComponent( - context: self.context, - theme: self.presentationData.theme, - strings: self.presentationData.strings, - items: self.currentState?.items ?? [], - navigationHeight: navigationHeight, - action: { [weak self] item in - guard let strongSelf = self else { - return - } - strongSelf.controller?.openTopic(item) - } - )), - environment: {}, - containerSize: layout.size - ) - if let topicListView = self.topicList.view { - if topicListView.superview == nil { - if let navigationBar = self.controller?.navigationBar { - self.view.insertSubview(topicListView, belowSubview: navigationBar.view) - } else { - self.view.addSubview(topicListView) - } - } - transition.setFrame(view: topicListView, frame: CGRect(origin: CGPoint(), size: layout.size)) - } - } - } - - private var node: Node { - return self.displayNode as! Node - } - - private let context: AccountContext - private let id: EnginePeer.Id - private var presentationData: PresentationData - private let openTopic: (ForumChannelTopics.Item) -> Void - - public init(context: AccountContext, id: EnginePeer.Id, openTopic: @escaping (ForumChannelTopics.Item) -> Void) { - self.context = context - self.id = id - self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.openTopic = openTopic - - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData)) - - //TODO:localize - self.title = "Forum" - - self.navigationItem.setRightBarButton(UIBarButtonItem(title: "Create", style: .plain, target: self, action: #selector(self.createPressed)), animated: false) - } - - public required init(coder aDecoder: NSCoder) { - preconditionFailure() - } - - @objc private func createPressed() { - self.node.createPressed() - } - - override public func loadDisplayNode() { - self.displayNode = Node(controller: self, context: self.context, id: self.id) - - self.displayNodeDidLoad() - } - - override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { - super.containerLayoutUpdated(layout, transition: transition) - - self.node.containerLayoutUpdated(layout: layout, navigationHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: Transition(transition)) - } -} diff --git a/submodules/TelegramUI/Components/ForumTopicListScreen/BUILD b/submodules/TelegramUI/Components/NotificationExceptionsScreen/BUILD similarity index 83% rename from submodules/TelegramUI/Components/ForumTopicListScreen/BUILD rename to submodules/TelegramUI/Components/NotificationExceptionsScreen/BUILD index 120b7b7d71..abbe3ec4fa 100644 --- a/submodules/TelegramUI/Components/ForumTopicListScreen/BUILD +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/BUILD @@ -1,8 +1,8 @@ load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") swift_library( - name = "ForumTopicListScreen", - module_name = "ForumTopicListScreen", + name = "NotificationExceptionsScreen", + module_name = "NotificationExceptionsScreen", srcs = glob([ "Sources/**/*.swift", ]), @@ -26,6 +26,10 @@ swift_library( "//submodules/AppBundle:AppBundle", "//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/PresentationDataUtils:PresentationDataUtils", + "//submodules/ItemListUI", + "//submodules/ItemListPeerItem", + "//submodules/ItemListPeerActionItem", + "//submodules/NotificationSoundSelectionUI", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift new file mode 100644 index 0000000000..822363fc5a --- /dev/null +++ b/submodules/TelegramUI/Components/NotificationExceptionsScreen/Sources/NotificationExceptionsScreen.swift @@ -0,0 +1,535 @@ +import Foundation +import UIKit +import Display +import AsyncDisplayKit +import SwiftSignalKit +import Postbox +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext +import AlertUI +import PresentationDataUtils +import NotificationSoundSelectionUI +import TelegramStringFormatting +import ItemListPeerItem +import ItemListPeerActionItem + +private extension EnginePeer.NotificationSettings.MuteState { + var timeInterval: Int32? { + switch self { + case .default: + return nil + case .unmuted: + return 0 + case let .muted(until): + return until + } + } +} + +private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound { + if case .default = sound { + return defaultCloudPeerNotificationSound + } else { + return sound + } +} + +private final class NotificationsPeerCategoryControllerArguments { + let context: AccountContext + let soundSelectionDisposable: MetaDisposable + + let updateEnabled: (Bool) -> Void + let updatePreviews: (Bool) -> Void + + let openSound: (PeerMessageSound) -> Void + + let addException: () -> Void + let openException: (Int64) -> Void + let removeAllExceptions: () -> Void + let updateRevealedThreadId: (Int64?) -> Void + let removeThread: (Int64) -> Void + + init(context: AccountContext, soundSelectionDisposable: MetaDisposable, updateEnabled: @escaping (Bool) -> Void, updatePreviews: @escaping (Bool) -> Void, openSound: @escaping (PeerMessageSound) -> Void, addException: @escaping () -> Void, openException: @escaping (Int64) -> Void, removeAllExceptions: @escaping () -> Void, updateRevealedThreadId: @escaping (Int64?) -> Void, removeThread: @escaping (Int64) -> Void) { + self.context = context + self.soundSelectionDisposable = soundSelectionDisposable + + self.updateEnabled = updateEnabled + self.updatePreviews = updatePreviews + self.openSound = openSound + + self.addException = addException + self.openException = openException + self.removeAllExceptions = removeAllExceptions + + self.updateRevealedThreadId = updateRevealedThreadId + self.removeThread = removeThread + } +} + +private enum NotificationsPeerCategorySection: Int32 { + case enable + case options + case exceptions +} + +private enum NotificationsPeerCategoryEntryTag: ItemListItemTag { + case enable + case previews + case sound + + public func isEqual(to other: ItemListItemTag) -> Bool { + if let other = other as? NotificationsPeerCategoryEntryTag, self == other { + return true + } else { + return false + } + } +} + +private enum NotificationsPeerCategoryEntry: ItemListNodeEntry { + case enable(String, Bool) + case optionsHeader(String) + case previews(String, Bool) + case sound(String, String, PeerMessageSound) + + case exceptionsHeader(String) + case addException(String) + case exception(Int32, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessageHistoryThread.Info, String, TelegramPeerNotificationSettings, Bool, Bool) + case removeAllExceptions(String) + + var section: ItemListSectionId { + switch self { + case .enable: + return NotificationsPeerCategorySection.enable.rawValue + case .optionsHeader, .previews, .sound: + return NotificationsPeerCategorySection.options.rawValue + case .exceptionsHeader, .addException, .exception, .removeAllExceptions: + return NotificationsPeerCategorySection.exceptions.rawValue + } + } + + var stableId: Int32 { + switch self { + case .enable: + return 0 + case .optionsHeader: + return 1 + case .previews: + return 2 + case .sound: + return 3 + case .exceptionsHeader: + return 4 + case .addException: + return 5 + case let .exception(index, _, _, _, _, _, _, _): + return 6 + index + case .removeAllExceptions: + return 100000 + } + } + + var tag: ItemListItemTag? { + switch self { + case .enable: + return NotificationsPeerCategoryEntryTag.enable + case .previews: + return NotificationsPeerCategoryEntryTag.previews + case .sound: + return NotificationsPeerCategoryEntryTag.sound + default: + return nil + } + } + + static func ==(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool { + switch lhs { + case let .enable(lhsText, lhsValue): + if case let .enable(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .optionsHeader(lhsText): + if case let .optionsHeader(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + case let .previews(lhsText, lhsValue): + if case let .previews(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue { + return true + } else { + return false + } + case let .sound(lhsText, lhsValue, lhsSound): + if case let .sound(rhsText, rhsValue, rhsSound) = rhs, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { + return true + } else { + return false + } + case let .exceptionsHeader(lhsText): + if case let .exceptionsHeader(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + case let .addException(lhsText): + if case let .addException(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + case let .exception(lhsIndex, lhsDateTimeFormat, lhsDisplayNameOrder, lhsInfo, lhsDescription, lhsSettings, lhsEditing, lhsRevealed): + if case let .exception(rhsIndex, rhsDateTimeFormat, rhsDisplayNameOrder, rhsInfo, rhsDescription, rhsSettings, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayNameOrder == rhsDisplayNameOrder, lhsInfo == rhsInfo, lhsDescription == rhsDescription, lhsSettings == rhsSettings, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed { + return true + } else { + return false + } + case let .removeAllExceptions(lhsText): + if case let .removeAllExceptions(rhsText) = rhs, lhsText == rhsText { + return true + } else { + return false + } + } + } + + static func <(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! NotificationsPeerCategoryControllerArguments + switch self { + case let .enable(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in + arguments.updateEnabled(updatedValue) + }, tag: self.tag) + case let .optionsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .previews(text, value): + return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.updatePreviews(value) + }) + case let .sound(text, value, sound): + return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: { + arguments.openSound(sound) + }, tag: self.tag) + case let .exceptionsHeader(text): + return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) + case let .addException(text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .peerList, color: .accent, editing: false, action: { + arguments.addException() + }) + //case let .exception(_, dateTimeFormat, nameDisplayOrder, info, description, _, editing, revealed): + case .exception: + preconditionFailure() + /*return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .text(description, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: { + arguments.openException(peer) + }, setPeerIdWithRevealedOptions: { peerId, fromPeerId in + arguments.updateRevealedPeerId(peerId) + }, removePeer: { peerId in + arguments.removePeer(peer) + }, hasTopStripe: false, hasTopGroupInset: false, noInsets: false)*/ + case let .removeAllExceptions(text): + return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { + arguments.removeAllExceptions() + }) + } + } +} + +private func notificationsPeerCategoryEntries(notificationSettings: EnginePeer.NotificationSettings, state: NotificationExceptionState, presentationData: PresentationData, notificationSoundList: NotificationSoundList?) -> [NotificationsPeerCategoryEntry] { + var entries: [NotificationsPeerCategoryEntry] = [] + + var notificationsEnabled = true + if case .muted = notificationSettings.muteState { + notificationsEnabled = false + } + var displayPreviews = true + switch notificationSettings.displayPreviews { + case .hide: + displayPreviews = false + default: + break + } + entries.append(.enable(presentationData.strings.Notifications_MessageNotificationsAlert, notificationsEnabled)) + + if notificationsEnabled || !state.notificationExceptions.isEmpty { + entries.append(.optionsHeader(presentationData.strings.Notifications_Options.uppercased())) + entries.append(.previews(presentationData.strings.Notifications_MessageNotificationsPreview, displayPreviews)) + entries.append(.sound(presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.messageSound._asMessageSound())), filteredGlobalSound(notificationSettings.messageSound._asMessageSound()))) + } + + entries.append(.exceptionsHeader(presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased())) + entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException)) + + var existingThreadIds = Set() + var index: Int = 0 + + for value in state.notificationExceptions { + var title: String + var muted = false + switch value.notificationSettings.muteState { + case let .muted(until): + if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { + if until < Int32.max - 1 { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: presentationData.strings.baseLanguageCode) + + if Calendar.current.isDateInToday(Date(timeIntervalSince1970: Double(until))) { + formatter.dateFormat = "HH:mm" + } else { + formatter.dateFormat = "E, d MMM HH:mm" + } + + let dateString = formatter.string(from: Date(timeIntervalSince1970: Double(until))) + + title = presentationData.strings.Notification_Exceptions_MutedUntil(dateString).string + } else { + muted = true + title = presentationData.strings.Notification_Exceptions_AlwaysOff + } + } else { + title = presentationData.strings.Notification_Exceptions_AlwaysOn + } + case .unmuted: + title = presentationData.strings.Notification_Exceptions_AlwaysOn + default: + title = "" + } + if !muted { + switch value.notificationSettings.messageSound { + case .default: + break + default: + if !title.isEmpty { + title.append(", ") + } + title.append(presentationData.strings.Notification_Exceptions_SoundCustom) + } + switch value.notificationSettings.displayPreviews { + case .default: + break + default: + if !title.isEmpty { + title += ", " + } + if case .show = value.notificationSettings.displayPreviews { + title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOn + } else { + title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff + } + } + } + existingThreadIds.insert(value.threadId) + entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId)) + index += 1 + } + + if state.notificationExceptions.count > 0 { + entries.append(.removeAllExceptions(presentationData.strings.Notifications_DeleteAllExceptions)) + } + + return entries +} + +private struct NotificationExceptionState: Equatable { + var revealedThreadId: Int64? = nil + var editing: Bool = false + var notificationExceptions: [EngineMessageHistoryThread.NotificationException] = [] +} + +public func threadNotificationExceptionsScreen(context: AccountContext, peerId: EnginePeer.Id, notificationExceptions: [EngineMessageHistoryThread.NotificationException], updated: @escaping ([EngineMessageHistoryThread.NotificationException]) -> Void) -> ViewController { + var presentControllerImpl: ((ViewController, Any?) -> Void)? + var pushControllerImpl: ((ViewController) -> Void)? + + let initialState = NotificationExceptionState(notificationExceptions: notificationExceptions) + let stateValue = Atomic(value: initialState) + let statePromise: ValuePromise = ValuePromise(ignoreRepeated: true) + + statePromise.set(initialState) + + let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in + let result = stateValue.modify { f($0) } + statePromise.set(result) + //updatedMode(result.mode) + } + + /*let updatePeerSound: (PeerId, PeerMessageSound) -> Signal = { peerId, sound in + return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: sound) + |> deliverOnMainQueue + } + + let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal = { peerId, muteInterval in + return context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: nil, muteInterval: muteInterval) + |> deliverOnMainQueue + } + + let updatePeerDisplayPreviews: (PeerId, PeerNotificationDisplayPreviews) -> Signal = { + peerId, displayPreviews in + return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: nil, displayPreviews: displayPreviews) |> deliverOnMainQueue + }*/ + + let _ = presentControllerImpl + + let arguments = NotificationsPeerCategoryControllerArguments(context: context, soundSelectionDisposable: MetaDisposable(), updateEnabled: { _ in + let _ = context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil).start() + }, updatePreviews: { value in + let _ = context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: nil, displayPreviews: value ? .show : .hide).start() + }, openSound: { sound in + let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: sound, defaultSound: nil, completion: { value in + let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: value).start() + }) + pushControllerImpl?(controller) + }, addException: { + /*let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader] + switch category { + case .privateChat: + filter.insert(.onlyPrivateChats) + filter.insert(.excludeSavedMessages) + filter.insert(.excludeSecretChats) + case .group: + filter.insert(.onlyGroups) + case .channel: + filter.insert(.onlyChannels) + } + let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle)) + controller.peerSelected = { [weak controller] peer in + let peerId = peer.id + + presentPeerSettings(peerId, { + controller?.dismiss() + }) + } + pushControllerImpl?(controller)*/ + }, openException: { peer in + //presentPeerSettings(peer.id, {}) + }, removeAllExceptions: { + /*let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let actionSheet = ActionSheetController(presentationData: presentationData) + actionSheet.setItemGroups([ActionSheetItemGroup(items: [ + ActionSheetTextItem(title: presentationData.strings.Notification_Exceptions_DeleteAllConfirmation), + ActionSheetButtonItem(title: presentationData.strings.Notification_Exceptions_DeleteAll, color: .destructive, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + + let values = stateValue.with { $0.mode.settings.values } + + let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: values.map { EnginePeer($0.peer) }) + |> deliverOnMainQueue).start(completed: { + updateNotificationsDisposable.set(nil) + updateState { state in + var state = state + for value in values { + state = state.withUpdatedPeerMuteInterval(value.peer, nil).withUpdatedPeerSound(value.peer, .default).withUpdatedPeerDisplayPreviews(value.peer, .default) + } + return state + } + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: values.map(\.peer.id)) + |> deliverOnMainQueue).start(completed: { + updateNotificationsView({}) + }) + }) + }) + ]), ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ])]) + presentControllerImpl?(actionSheet, nil)*/ + }, updateRevealedThreadId: { threadId in + updateState { current in + var current = current + current.revealedThreadId = threadId + return current + } + }, removeThread: { threadId in + /*let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: [EnginePeer(peer)]) + |> deliverOnMainQueue).start(completed: { + updateNotificationsDisposable.set(nil) + updateState { value in + return value.withUpdatedPeerMuteInterval(peer, nil).withUpdatedPeerSound(peer, .default).withUpdatedPeerDisplayPreviews(peer, .default) + } + let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: [peer.id]) + |> deliverOnMainQueue).start(completed: { + updateNotificationsView({}) + }) + })*/ + }) + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + context.engine.peers.notificationSoundList(), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId)), + statePromise.get() + ) + |> map { presentationData, notificationSoundList, peer, notificationSettings, state -> (ItemListControllerState, (ItemListNodeState, Any)) in + let entries = notificationsPeerCategoryEntries(notificationSettings: notificationSettings, state: state, presentationData: presentationData, notificationSoundList: notificationSoundList) + + var scrollToItem: ListViewScrollToItem? + scrollToItem = nil + /*var index = 0 + if let focusOnItemTag = focusOnItemTag { + for entry in entries { + if entry.tag?.isEqual(to: focusOnItemTag) ?? false { + scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up) + } + index += 1 + } + }*/ + + let leftNavigationButton: ItemListNavigationButton? + let rightNavigationButton: ItemListNavigationButton? + if !state.notificationExceptions.isEmpty { + if state.editing { + leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}) + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: { + updateState { value in + var value = value + value.editing = false + return value + } + }) + } else { + leftNavigationButton = nil + rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { + updateState { value in + var value = value + value.editing = true + return value + } + }) + } + } else { + leftNavigationButton = nil + rightNavigationButton = nil + } + + let title: String + if let peer = peer { + title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) + } else { + title = "" + } + let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true) + let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: nil, initialScrollToItem: scrollToItem) + + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + presentControllerImpl = { [weak controller] c, a in + controller?.present(c, in: .window(.root), with: a) + } + pushControllerImpl = { [weak controller] c in + (controller?.navigationController as? NavigationController)?.pushViewController(c) + } + return controller +} diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 70093d9666..2fefbe504b 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -12,7 +12,6 @@ import PeerAvatarGalleryUI import SettingsUI import ChatPresentationInterfaceState import AttachmentUI -import ForumTopicListScreen import ForumCreateTopicScreen public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 4b6728954a..a3f71c4922 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -78,6 +78,7 @@ import ComponentFlow import EmojiStatusComponent import ChatTitleView import ForumCreateTopicScreen +import NotificationExceptionsScreen protocol PeerInfoScreenItem: AnyObject { var id: AnyHashable { get } @@ -1874,7 +1875,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate private weak var copyProtectionTooltipController: TooltipController? weak var emojiStatusSelectionController: ViewController? - private var forumTopicNotificationExceptions: [(threadId: Int64, EnginePeer.NotificationSettings)] = [] + private var forumTopicNotificationExceptions: [EngineMessageHistoryThread.NotificationException] = [] private var forumTopicNotificationExceptionsDisposable: Disposable? private let _ready = Promise() @@ -4148,7 +4149,16 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate } else { text = "There are \(self.forumTopicNotificationExceptions.count) topics that are listed as exceptions." } - tip = .notificationTopicExceptions(text: text, action: { + tip = .notificationTopicExceptions(text: text, action: { [weak self] in + guard let self else { + return + } + self.controller?.present(threadNotificationExceptionsScreen(context: self.context, peerId: self.peerId, notificationExceptions: self.forumTopicNotificationExceptions, updated: { [weak self] value in + guard let self else { + return + } + self.forumTopicNotificationExceptions = value + }), in: .window(.root)) }) }