Swiftgram/submodules/SettingsUI/Sources/Notifications/Exceptions/NotificationExceptionSettingsController.swift
2019-10-06 04:25:10 +04:00

409 lines
19 KiB
Swift

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: Any) -> ListViewItem {
let arguments = arguments as! NotificationPeerExceptionArguments
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, Any)) 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
}