import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import ItemListUI import AccountContext import LocalizedPeerData import TelegramStringFormatting import NotificationSoundSelectionUI private enum NotificationPeerExceptionSection: Int32 { case remove case switcher case displayPreviews case soundModern case soundClassic } private enum NotificationPeerExceptionSwitcher : Equatable { case alwaysOn case alwaysOff } private enum NotificationPeerExceptionEntryId : Hashable { case remove case switcher(NotificationPeerExceptionSwitcher) case sound(PeerMessageSound) case switcherHeader case displayPreviews(NotificationPeerExceptionSwitcher) case displayPreviewsHeader 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 selectDisplayPreviews: (NotificationPeerExceptionSwitcher) -> Void let removeFromExceptions: () -> Void let complete: () -> Void let cancel: () -> Void init(account: Account, selectSound: @escaping(PeerMessageSound) -> Void, selectMode: @escaping(NotificationPeerExceptionSwitcher) -> Void, selectDisplayPreviews: @escaping (NotificationPeerExceptionSwitcher) -> Void, removeFromExceptions: @escaping () -> Void, complete: @escaping()->Void, cancel: @escaping() -> Void) { self.account = account self.selectSound = selectSound self.selectMode = selectMode self.selectDisplayPreviews = selectDisplayPreviews self.removeFromExceptions = removeFromExceptions self.complete = complete self.cancel = cancel } } private enum NotificationPeerExceptionEntry: ItemListNodeEntry { typealias ItemGenerationArguments = NotificationPeerExceptionArguments case remove(index:Int32, theme: PresentationTheme, strings: PresentationStrings) case switcher(index:Int32, theme: PresentationTheme, strings: PresentationStrings, mode: NotificationPeerExceptionSwitcher, selected: Bool) case switcherHeader(index:Int32, theme: PresentationTheme, title: String) case displayPreviews(index:Int32, theme: PresentationTheme, strings: PresentationStrings, value: NotificationPeerExceptionSwitcher, selected: Bool) case displayPreviewsHeader(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 .remove(index, _, _): return index case let .switcherHeader(index, _, _): return index case let .switcher(index, _, _, _, _): return index case let .displayPreviewsHeader(index, _, _): return index case let .displayPreviews(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 .remove: return NotificationPeerExceptionSection.remove.rawValue case .switcher, .switcherHeader: return NotificationPeerExceptionSection.switcher.rawValue case .displayPreviews, .displayPreviewsHeader: return NotificationPeerExceptionSection.displayPreviews.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 .remove: return .remove case let .switcher(_, _, _, mode, _): return .switcher(mode) case .switcherHeader: return .switcherHeader case let .displayPreviews(_, _, _, mode, _): return .displayPreviews(mode) case .displayPreviewsHeader: return .displayPreviewsHeader 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 .remove(_, theme, strings): return ItemListActionItem(theme: theme, title: strings.Notification_Exceptions_RemoveFromExceptions, kind: .generic, alignment: .center, sectionId: self.section, style: .blocks, action: { arguments.removeFromExceptions() }) case let .switcher(_, theme, strings, mode, selected): let title: String switch mode { case .alwaysOn: title = strings.Notification_Exceptions_AlwaysOn case .alwaysOff: title = strings.Notification_Exceptions_AlwaysOff } 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 .displayPreviews(_, theme, strings, value, selected): let title: String switch value { case .alwaysOn: title = strings.Notification_Exceptions_AlwaysOn case .alwaysOff: title = strings.Notification_Exceptions_AlwaysOff } return ItemListCheckboxItem(theme: theme, title: title, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.selectDisplayPreviews(value) }) case let .displayPreviewsHeader(_, 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 if state.canRemove { entries.append(.remove(index: index, theme: presentationData.theme, strings: presentationData.strings)) index += 1 } entries.append(.switcherHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_NotificationHeader)) 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 if state.mode != .alwaysOff { entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notification_Exceptions_NewException_MessagePreviewHeader)) index += 1 entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.displayPreviews == .alwaysOn)) index += 1 entries.append(.displayPreviews(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOff, selected: state.displayPreviews == .alwaysOff)) index += 1 entries.append(.soundModernHeader(index: index, theme: presentationData.theme, title: presentationData.strings.Notifications_AlertTones)) index += 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 canRemove: Bool let selectedSound: PeerMessageSound let mode: NotificationPeerExceptionSwitcher let defaultSound: PeerMessageSound let displayPreviews: NotificationPeerExceptionSwitcher init(canRemove: Bool, notifications: TelegramPeerNotificationSettings? = nil) { self.canRemove = canRemove if let notifications = notifications { self.selectedSound = notifications.messageSound switch notifications.muteState { case let .muted(until) where until >= Int32.max - 1: self.mode = .alwaysOff default: self.mode = .alwaysOn } self.displayPreviews = notifications.displayPreviews == .hide ? .alwaysOff : .alwaysOn } else { self.selectedSound = .default self.mode = .alwaysOn self.displayPreviews = .alwaysOn } self.defaultSound = .default } init(canRemove: Bool, selectedSound: PeerMessageSound, mode: NotificationPeerExceptionSwitcher, defaultSound: PeerMessageSound, displayPreviews: NotificationPeerExceptionSwitcher) { self.canRemove = canRemove self.selectedSound = selectedSound self.mode = mode self.defaultSound = defaultSound self.displayPreviews = displayPreviews } func withUpdatedDefaultSound(_ defaultSound: PeerMessageSound) -> NotificationExceptionPeerState { return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: self.selectedSound, mode: self.mode, defaultSound: defaultSound, displayPreviews: self.displayPreviews) } func withUpdatedSound(_ selectedSound: PeerMessageSound) -> NotificationExceptionPeerState { return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: selectedSound, mode: self.mode, defaultSound: self.defaultSound, displayPreviews: self.displayPreviews) } func withUpdatedMode(_ mode: NotificationPeerExceptionSwitcher) -> NotificationExceptionPeerState { return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: self.selectedSound, mode: mode, defaultSound: self.defaultSound, displayPreviews: self.displayPreviews) } func withUpdatedDisplayPreviews(_ displayPreviews: NotificationPeerExceptionSwitcher) -> NotificationExceptionPeerState { return NotificationExceptionPeerState(canRemove: self.canRemove, selectedSound: self.selectedSound, mode: self.mode, defaultSound: self.defaultSound, displayPreviews: displayPreviews) } } func notificationPeerExceptionController(context: AccountContext, peer: Peer, mode: NotificationExceptionMode, updatePeerSound: @escaping(PeerId, PeerMessageSound) -> Void, updatePeerNotificationInterval: @escaping(PeerId, Int32?) -> Void, updatePeerDisplayPreviews: @escaping(PeerId, PeerNotificationDisplayPreviews) -> Void, removePeerFromExceptions: @escaping () -> Void, modifiedPeer: @escaping () -> Void) -> ViewController { let initialState = NotificationExceptionPeerState(canRemove: false) 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 removeFromExceptionsImpl: (() -> Void)? var cancelImpl: (() -> Void)? let playSoundDisposable = MetaDisposable() let arguments = NotificationPeerExceptionArguments(account: context.account, selectSound: { sound in updateState { state in playSoundDisposable.set(playSound(context: context, sound: sound, defaultSound: state.defaultSound).start()) return state.withUpdatedSound(sound) } }, selectMode: { mode in updateState { state in return state.withUpdatedMode(mode) } }, selectDisplayPreviews: { value in updateState { state in return state.withUpdatedDisplayPreviews(value) } }, removeFromExceptions: { removeFromExceptionsImpl?() }, complete: { completeImpl?() }, cancel: { cancelImpl?() }) statePromise.set(context.account.postbox.transaction { transaction -> NotificationExceptionPeerState in var state = NotificationExceptionPeerState(canRemove: mode.peerIds.contains(peer.id), notifications: transaction.getPeerNotificationSettings(peer.id) 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(queue: .mainQueue(), context.sharedContext.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(state.canRemove ? presentationData.strings.Common_Done : presentationData.strings.Notification_Exceptions_Add), style: .bold, enabled: true, action: { arguments.complete() }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)) let listState = ItemListNodeState(entries: notificationPeerExceptionEntries(presentationData: presentationData, state: state), style: .blocks, animateChanges: false) return (controllerState, (listState, arguments)) } let controller = ItemListController(context: context, state: signal |> afterDisposed { playSoundDisposable.dispose() }) controller.enableInteractiveDismiss = true completeImpl = { [weak controller] in controller?.dismiss() modifiedPeer() updateState { state in updatePeerSound(peer.id, state.selectedSound) updatePeerNotificationInterval(peer.id, state.mode == .alwaysOn ? 0 : Int32.max) updatePeerDisplayPreviews(peer.id, state.displayPreviews == .alwaysOn ? .show : .hide) return state } } removeFromExceptionsImpl = { [weak controller] in controller?.dismiss() removePeerFromExceptions() } cancelImpl = { [weak controller] in controller?.dismiss() } return controller }