import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import DeviceAccess import ItemListUI import PresentationDataUtils import AccountContext import AlertUI import PresentationDataUtils import TelegramNotices import NotificationSoundSelectionUI import TelegramStringFormatting import ItemListPeerItem import ItemListPeerActionItem import NotificationPeerExceptionController 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 final class NotificationsPeerCategoryControllerArguments { let context: AccountContext let soundSelectionDisposable: MetaDisposable let updateEnabled: (Bool) -> Void let updateEnabledImportant: (Bool) -> Void let updatePreviews: (Bool) -> Void let openSound: (PeerMessageSound) -> Void let addException: () -> Void let openException: (EnginePeer) -> Void let removeAllExceptions: () -> Void let updateRevealedPeerId: (EnginePeer.Id?) -> Void let removePeer: (EnginePeer) -> Void let updatedExceptionMode: (NotificationExceptionMode) -> Void init(context: AccountContext, soundSelectionDisposable: MetaDisposable, updateEnabled: @escaping (Bool) -> Void, updateEnabledImportant: @escaping (Bool) -> Void, updatePreviews: @escaping (Bool) -> Void, openSound: @escaping (PeerMessageSound) -> Void, addException: @escaping () -> Void, openException: @escaping (EnginePeer) -> Void, removeAllExceptions: @escaping () -> Void, updateRevealedPeerId: @escaping (EnginePeer.Id?) -> Void, removePeer: @escaping (EnginePeer) -> Void, updatedExceptionMode: @escaping (NotificationExceptionMode) -> Void) { self.context = context self.soundSelectionDisposable = soundSelectionDisposable self.updateEnabled = updateEnabled self.updateEnabledImportant = updateEnabledImportant self.updatePreviews = updatePreviews self.openSound = openSound self.addException = addException self.openException = openException self.removeAllExceptions = removeAllExceptions self.updateRevealedPeerId = updateRevealedPeerId self.removePeer = removePeer self.updatedExceptionMode = updatedExceptionMode } } private enum NotificationsPeerCategorySection: Int32 { case enable case options case exceptions } public 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 { enum StableId: Hashable { case enableHeader case enable case enableImportant case importantInfo case optionsHeader case previews case sound case exceptionsHeader case addException case peer(EnginePeer.Id) case removeAllExceptions } case enableHeader(String) case enable(PresentationTheme, String, Bool) case enableImportant(PresentationTheme, String, Bool) case importantInfo(PresentationTheme, String) case optionsHeader(PresentationTheme, String) case previews(PresentationTheme, String, Bool) case sound(PresentationTheme, String, String, PeerMessageSound) case exceptionsHeader(PresentationTheme, String) case addException(PresentationTheme, String) case exception(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, EnginePeer, String, TelegramPeerNotificationSettings, Bool, Bool, Bool) case removeAllExceptions(PresentationTheme, String) var section: ItemListSectionId { switch self { case .enableHeader, .enable, .enableImportant, .importantInfo: return NotificationsPeerCategorySection.enable.rawValue case .optionsHeader, .previews, .sound: return NotificationsPeerCategorySection.options.rawValue case .exceptionsHeader, .addException, .exception, .removeAllExceptions: return NotificationsPeerCategorySection.exceptions.rawValue } } var stableId: StableId { switch self { case .enableHeader: return .enableHeader case .enable: return .enable case .enableImportant: return .enableImportant case .importantInfo: return .importantInfo case .optionsHeader: return .optionsHeader case .previews: return .previews case .sound: return .sound case .exceptionsHeader: return .exceptionsHeader case .addException: return .addException case let .exception(_, _, _, _, _, peer, _, _, _, _, _): return .peer(peer.id) case .removeAllExceptions: return .removeAllExceptions } } var sortIndex: Int32 { switch self { case .enableHeader: return -1 case .enable: return 0 case .enableImportant: return 1 case .importantInfo: return 2 case .optionsHeader: return 3 case .previews: return 4 case .sound: return 5 case .exceptionsHeader: return 6 case .addException: return 7 case let .exception(index, _, _, _, _, _, _, _, _, _, _): return 100 + index case .removeAllExceptions: return 10000 } } 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 .enableHeader(text): if case .enableHeader(text) = rhs { return true } else { return false } case let .enable(lhsTheme, lhsText, lhsValue): if case let .enable(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .enableImportant(lhsTheme, lhsText, lhsValue): if case let .enableImportant(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .importantInfo(lhsTheme, lhsText): if case let .importantInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .optionsHeader(lhsTheme, lhsText): if case let .optionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .previews(lhsTheme, lhsText, lhsValue): if case let .previews(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue { return true } else { return false } case let .sound(lhsTheme, lhsText, lhsValue, lhsSound): if case let .sound(rhsTheme, rhsText, rhsValue, rhsSound) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound { return true } else { return false } case let .exceptionsHeader(lhsTheme, lhsText): if case let .exceptionsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .addException(lhsTheme, lhsText): if case let .addException(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } case let .exception(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsDisplayNameOrder, lhsPeer, lhsDescription, lhsSettings, lhsEditing, lhsRevealed, lhsCanRemove): if case let .exception(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsDisplayNameOrder, rhsPeer, rhsDescription, rhsSettings, rhsEditing, rhsRevealed, rhsCanRemove) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayNameOrder == rhsDisplayNameOrder, lhsPeer == rhsPeer, lhsDescription == rhsDescription, lhsSettings == rhsSettings, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed, lhsCanRemove == rhsCanRemove { return true } else { return false } case let .removeAllExceptions(lhsTheme, lhsText): if case let .removeAllExceptions(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText { return true } else { return false } } } static func <(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool { return lhs.sortIndex < rhs.sortIndex } func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { let arguments = arguments as! NotificationsPeerCategoryControllerArguments switch self { case let .enableHeader(text): return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section) 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 .enableImportant(_, text, value): return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in arguments.updateEnabledImportant(updatedValue) }, tag: self.tag) case let .importantInfo(_, text): return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section) 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(theme, text): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(theme), title: text, sectionId: self.section, height: .peerList, color: .accent, editing: false, action: { arguments.addException() }) case let .exception(_, _, _, dateTimeFormat, nameDisplayOrder, peer, description, _, editing, revealed, canRemove): return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .text(description, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: canRemove, editing: canRemove && editing, revealed: canRemove && 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(theme, text): return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: { arguments.removeAllExceptions() }) } } } private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound { if case .default = sound { return defaultCloudPeerNotificationSound } else { return sound } } private func notificationsPeerCategoryEntries(category: NotificationsPeerCategory, globalSettings: GlobalNotificationSettingsSet, state: NotificationExceptionState, presentationData: PresentationData, notificationSoundList: NotificationSoundList?, automaticTopPeers: [EnginePeer], automaticNotificationSettings: [EnginePeer.Id: EnginePeer.NotificationSettings]) -> [NotificationsPeerCategoryEntry] { var entries: [NotificationsPeerCategoryEntry] = [] let notificationSettings: MessageNotificationSettings let notificationExceptions = state.mode switch category { case .privateChat: notificationSettings = globalSettings.privateChats case .group: notificationSettings = globalSettings.groupChats case .channel: notificationSettings = globalSettings.channels case .stories: notificationSettings = globalSettings.privateChats } if case .stories = category { //TODO:localize entries.append(.enableHeader("NOTIFY ME ABOUT...")) var allEnabled = false var importantEnabled = false switch notificationSettings.storySettings.mute { case .muted: allEnabled = false importantEnabled = false case .unmuted: allEnabled = true importantEnabled = true case .default: allEnabled = false importantEnabled = true } entries.append(.enable(presentationData.theme, "New Stories", allEnabled)) if !allEnabled { entries.append(.enableImportant(presentationData.theme, "Important Stories", importantEnabled)) entries.append(.importantInfo(presentationData.theme, presentationData.strings.NotificationSettings_Stories_ShowImportantFooter)) } if notificationSettings.enabled || !notificationExceptions.isEmpty { entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased())) entries.append(.previews(presentationData.theme, "Show Sender's Name", notificationSettings.storySettings.hideSender != .hide)) entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.storySettings.sound)), filteredGlobalSound(notificationSettings.storySettings.sound))) } } else { entries.append(.enable(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsAlert, notificationSettings.enabled)) if notificationSettings.enabled || !notificationExceptions.isEmpty { entries.append(.optionsHeader(presentationData.theme, presentationData.strings.Notifications_Options.uppercased())) entries.append(.previews(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsPreview, notificationSettings.displayPreviews)) entries.append(.sound(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.sound)), filteredGlobalSound(notificationSettings.sound))) } } entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased())) entries.append(.addException(presentationData.theme, presentationData.strings.Notification_Exceptions_AddException)) var sortedExceptions = notificationExceptions.settings.sorted(by: { lhs, rhs in let lhsName = lhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) let rhsName = rhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) 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 case let .user(lhsPeer) = lhs.value.peer, case let .user(rhsPeer) = rhs.value.peer { if lhsPeer.botInfo != nil && rhsPeer.botInfo == nil { return false } else if lhsPeer.botInfo == nil && rhsPeer.botInfo != nil { return true } } return lhsName < rhsName }) var automaticSet = Set() if globalSettings.privateChats.storySettings.mute == .default { for peer in automaticTopPeers { if sortedExceptions.contains(where: { $0.key == peer.id }) { continue } sortedExceptions.append((peer.id, NotificationExceptionWrapper(settings: automaticNotificationSettings[peer.id]?._asNotificationSettings() ?? .defaultSettings, peer: peer, date: nil))) automaticSet.insert(peer.id) } } var existingPeerIds = Set() var index: Int = 0 for (_, value) in sortedExceptions { if !value.peer.isDeleted { var canRemove = true var title: String = "" if automaticSet.contains(value.peer.id) { title = presentationData.strings.NotificationSettings_Stories_AutomaticValue(presentationData.strings.Notification_Exceptions_AlwaysOn).string canRemove = false } else { if case .stories = category { var muted = false if value.settings.storySettings.mute == .muted { muted = true title.append(presentationData.strings.Notification_Exceptions_AlwaysOff) } else { title.append(presentationData.strings.Notification_Exceptions_AlwaysOn) } if !muted { switch value.settings.storySettings.sound { case .default: break default: if !title.isEmpty { title.append(", ") } title.append(presentationData.strings.Notification_Exceptions_SoundCustom) } switch value.settings.storySettings.hideSender { case .default: break default: if !title.isEmpty { title += ", " } if case .show = value.settings.storySettings.hideSender { title += presentationData.strings.NotificationSettings_Stories_CompactShowName } else { title += presentationData.strings.NotificationSettings_Stories_CompactHideName } } } } else { var muted = false switch value.settings.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.settings.messageSound { case .default: break default: if !title.isEmpty { title.append(", ") } title.append(presentationData.strings.Notification_Exceptions_SoundCustom) } switch value.settings.displayPreviews { case .default: break default: if !title.isEmpty { title += ", " } if case .show = value.settings.displayPreviews { title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOn } else { title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff } } } } } existingPeerIds.insert(value.peer.id) entries.append(.exception(Int32(index), presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, value.peer, title, value.settings, state.editing, state.revealedPeerId == value.peer.id, canRemove)) index += 1 } } if notificationExceptions.peerIds.count > 0 { entries.append(.removeAllExceptions(presentationData.theme, presentationData.strings.Notifications_DeleteAllExceptions)) } return entries } public enum NotificationsPeerCategory { case privateChat case group case channel case stories } private final class NotificationExceptionState : Equatable { let mode: NotificationExceptionMode let revealedPeerId: EnginePeer.Id? let editing: Bool init(mode: NotificationExceptionMode, revealedPeerId: EnginePeer.Id? = nil, editing: Bool = false) { self.mode = mode self.revealedPeerId = revealedPeerId self.editing = editing } func withUpdatedMode(_ mode: NotificationExceptionMode) -> NotificationExceptionState { return NotificationExceptionState(mode: mode, revealedPeerId: self.revealedPeerId, editing: self.editing) } func withUpdatedEditing(_ editing: Bool) -> NotificationExceptionState { return NotificationExceptionState(mode: self.mode, revealedPeerId: self.revealedPeerId, editing: editing) } func withUpdatedRevealedPeerId(_ revealedPeerId: EnginePeer.Id?) -> NotificationExceptionState { return NotificationExceptionState(mode: self.mode, revealedPeerId: revealedPeerId, editing: self.editing) } func withUpdatedPeerSound(_ peer: EnginePeer, _ sound: PeerMessageSound) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.withUpdatedPeerSound(peer, sound), revealedPeerId: self.revealedPeerId, editing: self.editing) } func withUpdatedPeerMuteInterval(_ peer: EnginePeer, _ muteInterval: Int32?) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.withUpdatedPeerMuteInterval(peer, muteInterval), revealedPeerId: self.revealedPeerId, editing: self.editing) } func withUpdatedPeerDisplayPreviews(_ peer: EnginePeer, _ displayPreviews: PeerNotificationDisplayPreviews) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.withUpdatedPeerDisplayPreviews(peer, displayPreviews), revealedPeerId: self.revealedPeerId, editing: self.editing) } func withUpdatedPeerStoriesMuted(_ peer: EnginePeer, _ mute: PeerStoryNotificationSettings.Mute) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.withUpdatedPeerStoriesMuted(peer, mute), revealedPeerId: self.revealedPeerId, editing: self.editing) } func withUpdatedPeerStoriesHideSender(_ peer: EnginePeer, _ hideSender: PeerStoryNotificationSettings.HideSender) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.withUpdatedPeerStoriesHideSender(peer, hideSender), revealedPeerId: self.revealedPeerId, editing: self.editing) } func withUpdatedPeerStorySound(_ peer: EnginePeer, _ sound: PeerMessageSound) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.withUpdatedPeerStorySound(peer, sound), revealedPeerId: self.revealedPeerId, editing: self.editing) } func removeStoryPeerIfDefault(id: EnginePeer.Id) -> NotificationExceptionState { return NotificationExceptionState(mode: mode.removeStoryPeerIfDefault(id: id), revealedPeerId: self.revealedPeerId, editing: self.editing) } static func == (lhs: NotificationExceptionState, rhs: NotificationExceptionState) -> Bool { return lhs.mode == rhs.mode && lhs.revealedPeerId == rhs.revealedPeerId && lhs.editing == rhs.editing } } public func notificationsPeerCategoryController(context: AccountContext, category: NotificationsPeerCategory, mode: NotificationExceptionMode, updatedMode: @escaping (NotificationExceptionMode) -> Void, focusOnItemTag: NotificationsPeerCategoryEntryTag? = nil) -> ViewController { var presentControllerImpl: ((ViewController, Any?) -> Void)? var pushControllerImpl: ((ViewController) -> Void)? let stateValue = Atomic(value: NotificationExceptionState(mode: mode)) let statePromise: ValuePromise = ValuePromise(ignoreRepeated: true) statePromise.set(NotificationExceptionState(mode: mode)) let notificationExceptions: Promise<(users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)> = Promise() let updateNotificationExceptions:((users: NotificationExceptionMode, groups: NotificationExceptionMode, channels: NotificationExceptionMode, stories: NotificationExceptionMode)) -> Void = { value in notificationExceptions.set(.single(value)) } let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in let result = stateValue.modify { f($0) } statePromise.set(result) updatedMode(result.mode) } let updatePeerSound: (EnginePeer.Id, PeerMessageSound) -> Signal = { peerId, sound in return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: sound) |> deliverOnMainQueue } let updatePeerNotificationInterval: (EnginePeer.Id, Int32?) -> Signal = { peerId, muteInterval in return context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: nil, muteInterval: muteInterval) |> deliverOnMainQueue } let updatePeerDisplayPreviews:(EnginePeer.Id, PeerNotificationDisplayPreviews) -> Signal = { peerId, displayPreviews in return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: nil, displayPreviews: displayPreviews) |> deliverOnMainQueue } let updatePeerStoriesMuted: (EnginePeer.Id, PeerStoryNotificationSettings.Mute) -> Signal = { peerId, mute in return context.engine.peers.updatePeerStoriesMutedSetting(peerId: peerId, mute: mute) |> deliverOnMainQueue } let updatePeerStoriesHideSender: (EnginePeer.Id, PeerStoryNotificationSettings.HideSender) -> Signal = { peerId, hideSender in return context.engine.peers.updatePeerStoriesHideSenderSetting(peerId: peerId, hideSender: hideSender) |> deliverOnMainQueue } let updatePeerStorySound: (EnginePeer.Id, PeerMessageSound) -> Signal = { peerId, sound in return context.engine.peers.updatePeerStorySoundInteractive(peerId: peerId, sound: sound) |> deliverOnMainQueue } var peerIds: Set = Set(mode.peerIds) let updateNotificationsDisposable = MetaDisposable() let updateNotificationsView: (@escaping () -> Void) -> Void = { completion in updateState { current in peerIds = peerIds.union(current.mode.peerIds) let combinedPeerNotificationSettings = context.engine.data.subscribe(EngineDataMap( peerIds.map(TelegramEngine.EngineData.Item.Peer.NotificationSettings.init) )) updateNotificationsDisposable.set((combinedPeerNotificationSettings |> deliverOnMainQueue).start(next: { combinedPeerNotificationSettings in let _ = (context.engine.data.get( EngineDataMap(combinedPeerNotificationSettings.keys.map(TelegramEngine.EngineData.Item.Peer.Peer.init)), EngineDataMap(combinedPeerNotificationSettings.keys.map(TelegramEngine.EngineData.Item.Peer.NotificationSettings.init)) ) |> deliverOnMainQueue).start(next: { peerMap, notificationSettingsMap in updateState { current in var current = current for (key, value) in combinedPeerNotificationSettings { if let local = current.mode.settings[key] { if !value._asNotificationSettings().isEqual(to: local.settings), let maybePeer = peerMap[key], let peer = maybePeer, let settings = notificationSettingsMap[key], !settings._asNotificationSettings().isEqual(to: local.settings) { current = current.withUpdatedPeerSound(peer, settings.messageSound._asMessageSound()).withUpdatedPeerMuteInterval(peer, settings.muteState.timeInterval).withUpdatedPeerDisplayPreviews(peer, settings.displayPreviews._asDisplayPreviews()) } } else if let maybePeer = peerMap[key], let peer = maybePeer { if case .default = value.messageSound, case .unmuted = value.muteState, case .default = value.displayPreviews { } else { current = current.withUpdatedPeerSound(peer, value.messageSound._asMessageSound()).withUpdatedPeerMuteInterval(peer, value.muteState.timeInterval).withUpdatedPeerDisplayPreviews(peer, value.displayPreviews._asDisplayPreviews()) } } } return current } completion() }) })) return current } } updateNotificationsView({}) let presentPeerSettings: (EnginePeer.Id, @escaping () -> Void) -> Void = { peerId, completion in let _ = (context.engine.data.get( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), TelegramEngine.EngineData.Item.NotificationSettings.Global() ) |> deliverOnMainQueue).start(next: { peer, globalSettings in completion() guard let peer = peer else { return } let mode = stateValue.with { $0.mode } let canRemove = mode.peerIds.contains(peerId) let defaultSound: PeerMessageSound var isStories = false switch mode { case .channels: defaultSound = globalSettings.channels.sound._asMessageSound() case .groups: defaultSound = globalSettings.groupChats.sound._asMessageSound() case .users: defaultSound = globalSettings.privateChats.sound._asMessageSound() case .stories: defaultSound = globalSettings.privateChats.storySettings.sound isStories = true } pushControllerImpl?(notificationPeerExceptionController(context: context, peer: peer, threadId: nil, isStories: isStories, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: defaultSound, updatePeerSound: { peerId, sound in _ = updatePeerSound(peer.id, sound).start(next: { _ in updateNotificationsDisposable.set(nil) _ = combineLatest(updatePeerSound(peer.id, sound), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in if let peer = peer { updateState { value in return value.withUpdatedPeerSound(peer, sound) } } updateNotificationsView({}) }) }) }, updatePeerNotificationInterval: { peerId, muteInterval in updateNotificationsDisposable.set(nil) _ = combineLatest(updatePeerNotificationInterval(peerId, muteInterval), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in if let peer = peer { updateState { value in return value.withUpdatedPeerMuteInterval(peer, muteInterval) } } updateNotificationsView({}) }) }, updatePeerDisplayPreviews: { peerId, displayPreviews in updateNotificationsDisposable.set(nil) _ = combineLatest(updatePeerDisplayPreviews(peerId, displayPreviews), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in if let peer = peer { updateState { value in return value.withUpdatedPeerDisplayPreviews(peer, displayPreviews) } } updateNotificationsView({}) }) }, updatePeerStoriesMuted: { peerId, mute in updateNotificationsDisposable.set(nil) let _ = combineLatest(updatePeerStoriesMuted(peerId, mute), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in if let peer = peer { updateState { value in return value.withUpdatedPeerStoriesMuted(peer, mute) } } updateNotificationsView({}) }) }, updatePeerStoriesHideSender: { peerId, hideSender in updateNotificationsDisposable.set(nil) let _ = combineLatest(updatePeerStoriesHideSender(peerId, hideSender), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in if let peer = peer { updateState { value in return value.withUpdatedPeerStoriesHideSender(peer, hideSender) } } updateNotificationsView({}) }) }, updatePeerStorySound: { peerId, sound in updateNotificationsDisposable.set(nil) let _ = combineLatest(updatePeerStorySound(peerId, sound), context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) |> deliverOnMainQueue).start(next: { _, peer in if let peer = peer { updateState { value in return value.withUpdatedPeerStorySound(peer, sound) } } updateNotificationsView({}) }) }, removePeerFromExceptions: { if case .stories = mode.mode { let _ = ( context.engine.peers.removeCustomStoryNotificationSettings(peerIds: [peerId]) |> map { _ -> EnginePeer? in } |> then(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) ).start(next: { peer in guard let peer = peer else { return } updateState { value in var value = value.withUpdatedPeerStorySound(peer, .default).withUpdatedPeerStoriesMuted(peer, .default).withUpdatedPeerStoriesHideSender(peer, .default) value = value.removeStoryPeerIfDefault(id: peer.id) return value } updateNotificationsView({}) }) } else { let _ = ( context.engine.peers.removeCustomNotificationSettings(peerIds: [peerId]) |> map { _ -> EnginePeer? in } |> then(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) ).start(next: { peer in guard let peer = peer else { return } updateState { value in return value.withUpdatedPeerDisplayPreviews(peer, .default).withUpdatedPeerSound(peer, .default).withUpdatedPeerMuteInterval(peer, nil) } updateNotificationsView({}) }) } }, modifiedPeer: { })) }) } let arguments = NotificationsPeerCategoryControllerArguments(context: context, soundSelectionDisposable: MetaDisposable(), updateEnabled: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings switch category { case .privateChat: settings.privateChats.enabled = value case .group: settings.groupChats.enabled = value case .channel: settings.channels.enabled = value case .stories: settings.privateChats.storySettings.mute = value ? .unmuted : .default } return settings }).start() }, updateEnabledImportant: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings switch category { case .stories: settings.privateChats.storySettings.mute = value ? .default : .muted default: break } return settings }).start() }, updatePreviews: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings switch category { case .privateChat: settings.privateChats.displayPreviews = value case .group: settings.groupChats.displayPreviews = value case .channel: settings.channels.displayPreviews = value case .stories: settings.privateChats.storySettings.hideSender = value ? .show : .hide } return settings }).start() }, openSound: { sound in let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: sound, defaultSound: nil, completion: { value in let _ = updateGlobalNotificationSettingsInteractively(postbox: context.account.postbox, { settings in var settings = settings switch category { case .privateChat: settings.privateChats.sound = value case .group: settings.groupChats.sound = value case .channel: settings.channels.sound = value case .stories: settings.privateChats.storySettings.sound = value } return settings }).start() }) pushControllerImpl?(controller) }, addException: { let presentationData = context.sharedContext.currentPresentationData.with { $0 } var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader] switch category { case .privateChat, .stories: 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 } if case .stories = mode.mode { let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: values.map { $0.peer }) |> deliverOnMainQueue).start(completed: { updateNotificationsDisposable.set(nil) updateState { state in var state = state for value in values { state = state.withUpdatedPeerStorySound(value.peer, .default).withUpdatedPeerStoriesMuted(value.peer, .default).withUpdatedPeerStoriesHideSender(value.peer, .default) state = state.removeStoryPeerIfDefault(id: value.peer.id) } return state } let _ = (context.engine.peers.removeCustomStoryNotificationSettings(peerIds: values.map(\.peer.id)) |> deliverOnMainQueue).start(completed: { updateNotificationsView({}) }) }) } else { let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: values.map { $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) }, updateRevealedPeerId: { peerId in updateState { current in return current.withUpdatedRevealedPeerId(peerId) } }, removePeer: { peer in if case .stories = mode.mode { let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: [peer]) |> deliverOnMainQueue).start(completed: { updateNotificationsDisposable.set(nil) updateState { value in var value = value.withUpdatedPeerStorySound(peer, .default).withUpdatedPeerStoriesMuted(peer, .default).withUpdatedPeerStoriesHideSender(peer, .default) value = value.removeStoryPeerIfDefault(id: peer.id) return value } let _ = (context.engine.peers.removeCustomStoryNotificationSettings(peerIds: [peer.id]) |> deliverOnMainQueue).start(completed: { updateNotificationsView({}) }) }) } else { let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: [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({}) }) }) } }, updatedExceptionMode: { mode in _ = (notificationExceptions.get() |> take(1) |> deliverOnMainQueue).start(next: { (users, groups, channels, stories) in switch mode { case .users: updateNotificationExceptions((mode, groups, channels, stories)) case .groups: updateNotificationExceptions((users, mode, channels, stories)) case .channels: updateNotificationExceptions((users, groups, mode, stories)) case .stories: updateNotificationExceptions((users, groups, channels, mode)) } }) }) let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) var automaticData: Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> = .single(([], [:])) if case .stories = category { automaticData = context.engine.peers.recentPeers() |> mapToSignal { recentPeers -> Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> in guard case let .peers(peersValue) = recentPeers else { return .single(([], [:])) } let peers = peersValue.prefix(5).map(EnginePeer.init) return context.engine.data.subscribe( EngineDataMap(peers.map { peer in return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id) }) ) |> map { settings -> ([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]) in var settingsMap: [EnginePeer.Id: EnginePeer.NotificationSettings] = [:] for peer in peers { if let value = settings[peer.id] { settingsMap[peer.id] = value } else { settingsMap[peer.id] = EnginePeer.NotificationSettings(TelegramPeerNotificationSettings.defaultSettings) } } return (peers, settingsMap) } } } let signal = combineLatest(context.sharedContext.presentationData, context.engine.peers.notificationSoundList(), sharedData, preferences, statePromise.get(), automaticData) |> map { presentationData, notificationSoundList, sharedData, view, state, automaticData -> (ItemListControllerState, (ItemListNodeState, Any)) in let viewSettings: GlobalNotificationSettingsSet if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { viewSettings = settings.effective } else { viewSettings = GlobalNotificationSettingsSet.defaultSettings } let entries = notificationsPeerCategoryEntries(category: category, globalSettings: viewSettings, state: state, presentationData: presentationData, notificationSoundList: notificationSoundList, automaticTopPeers: automaticData.0, automaticNotificationSettings: automaticData.1) var index = 0 var scrollToItem: ListViewScrollToItem? 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.mode.peerIds.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 return value.withUpdatedEditing(false) } }) } else { leftNavigationButton = nil rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: { updateState { value in return value.withUpdatedEditing(true) } }) } } else { leftNavigationButton = nil rightNavigationButton = nil } let title: String switch category { case .privateChat: title = presentationData.strings.Notifications_PrivateChatsTitle case .group: title = presentationData.strings.Notifications_GroupChatsTitle case .channel: title = presentationData.strings.Notifications_ChannelsTitle case .stories: title = presentationData.strings.Notifications_StoriesTitle } 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: focusOnItemTag, 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 }