mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1063 lines
44 KiB
Swift
1063 lines
44 KiB
Swift
import Foundation
|
|
import Display
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
|
|
|
|
private final class NotificationExceptionArguments {
|
|
let account: Account
|
|
let activateSearch:()->Void
|
|
let changeNotifications: (PeerId, TelegramPeerNotificationSettings) -> Void
|
|
let selectPeer: ()->Void
|
|
init(account: Account, activateSearch:@escaping() -> Void, changeNotifications: @escaping(PeerId, TelegramPeerNotificationSettings) -> Void, selectPeer: @escaping()->Void) {
|
|
self.account = account
|
|
self.activateSearch = activateSearch
|
|
self.changeNotifications = changeNotifications
|
|
self.selectPeer = selectPeer
|
|
}
|
|
}
|
|
|
|
private enum NotificationExceptionEntryId: Hashable {
|
|
case search
|
|
case peerId(Int64)
|
|
|
|
var hashValue: Int {
|
|
switch self {
|
|
case .search:
|
|
return 0
|
|
case let .peerId(peerId):
|
|
return peerId.hashValue
|
|
}
|
|
}
|
|
|
|
static func <(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool {
|
|
return lhs.hashValue < rhs.hashValue
|
|
}
|
|
|
|
static func ==(lhs: NotificationExceptionEntryId, rhs: NotificationExceptionEntryId) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
switch rhs {
|
|
case .search:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case let .peerId(lhsId):
|
|
switch rhs {
|
|
case let .peerId(rhsId):
|
|
return lhsId == rhsId
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum NotificationExceptionSectionId : ItemListSectionId {
|
|
case general = 0
|
|
}
|
|
|
|
private enum NotificationExceptionEntry : ItemListNodeEntry {
|
|
|
|
|
|
var section: ItemListSectionId {
|
|
return NotificationExceptionSectionId.general.rawValue
|
|
}
|
|
|
|
typealias ItemGenerationArguments = NotificationExceptionArguments
|
|
|
|
case search(PresentationTheme, PresentationStrings)
|
|
case peer(Int, Peer, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, String, TelegramPeerNotificationSettings)
|
|
|
|
|
|
func item(_ arguments: NotificationExceptionArguments) -> ListViewItem {
|
|
switch self {
|
|
case let .search(theme, strings):
|
|
return NotificationSearchItem(theme: theme, placeholder: strings.Contacts_SearchLabel, activate: {
|
|
arguments.activateSearch()
|
|
})
|
|
case let .peer(_, peer, theme, strings, dateTimeFormat, value, settings):
|
|
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, account: arguments.account, peer: peer, presence: nil, text: .text(value), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, sectionId: self.section, action: {
|
|
arguments.changeNotifications(peer.id, settings)
|
|
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
|
|
|
|
}, removePeer: { peerId in
|
|
|
|
}, hasTopStripe: false)
|
|
}
|
|
}
|
|
|
|
var stableId: NotificationExceptionEntryId {
|
|
switch self {
|
|
case .search:
|
|
return .search
|
|
case let .peer(_, peer, _, _, _, _, _):
|
|
return .peerId(peer.id.toInt64())
|
|
}
|
|
}
|
|
|
|
static func == (lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool {
|
|
switch lhs {
|
|
case let .search(lhsTheme, lhsStrings):
|
|
switch rhs {
|
|
case let .search(rhsTheme, rhsStrings):
|
|
return lhsTheme === rhsTheme && lhsStrings === rhsStrings
|
|
default:
|
|
return false
|
|
}
|
|
case let .peer(lhsIndex, lhsPeer, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsValue, lhsSettings):
|
|
switch rhs {
|
|
case let .peer(rhsIndex, rhsPeer, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsValue, rhsSettings):
|
|
return lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsDateTimeFormat == rhsDateTimeFormat && lhsIndex == rhsIndex && lhsPeer.isEqual(rhsPeer) && lhsValue == rhsValue && lhsSettings == rhsSettings
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
static func <(lhs: NotificationExceptionEntry, rhs: NotificationExceptionEntry) -> Bool {
|
|
switch lhs {
|
|
case .search:
|
|
return true
|
|
case let .peer(lhsIndex, _, _, _, _, _, _):
|
|
switch rhs {
|
|
case .search:
|
|
return false
|
|
case let .peer(rhsIndex, _, _, _, _, _, _):
|
|
return lhsIndex < rhsIndex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private final class NotificationExceptionState : Equatable {
|
|
|
|
let mode:NotificationExceptionMode
|
|
let isSearchMode: Bool
|
|
init(mode: NotificationExceptionMode, isSearchMode: Bool = false) {
|
|
self.mode = mode
|
|
self.isSearchMode = isSearchMode
|
|
}
|
|
|
|
func withUpdatedSearchMode(_ isSearchMode: Bool) -> NotificationExceptionState {
|
|
return NotificationExceptionState.init(mode: mode, isSearchMode: isSearchMode)
|
|
}
|
|
|
|
func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionState {
|
|
return NotificationExceptionState(mode: mode.withUpdatedPeerIdSound(peerId, sound), isSearchMode: isSearchMode)
|
|
}
|
|
func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionState {
|
|
return NotificationExceptionState(mode: mode.withUpdatedPeerIdMuteInterval(peerId, muteInterval), isSearchMode: isSearchMode)
|
|
}
|
|
|
|
static func == (lhs: NotificationExceptionState, rhs: NotificationExceptionState) -> Bool {
|
|
return lhs.mode == rhs.mode && lhs.isSearchMode == rhs.isSearchMode
|
|
}
|
|
}
|
|
|
|
|
|
public struct NotificationExceptionWrapper : Equatable {
|
|
let settings: TelegramPeerNotificationSettings
|
|
let date: TimeInterval?
|
|
init(settings: TelegramPeerNotificationSettings, date: TimeInterval? = nil) {
|
|
self.settings = settings
|
|
self.date = date
|
|
}
|
|
|
|
func withUpdatedSettings(_ settings: TelegramPeerNotificationSettings) -> NotificationExceptionWrapper {
|
|
return NotificationExceptionWrapper(settings: settings, date: self.date)
|
|
}
|
|
|
|
func updateSettings(_ f: (TelegramPeerNotificationSettings) -> TelegramPeerNotificationSettings) -> NotificationExceptionWrapper {
|
|
return NotificationExceptionWrapper(settings: f(self.settings), date: self.date)
|
|
}
|
|
|
|
|
|
func withUpdatedDate(_ date: TimeInterval) -> NotificationExceptionWrapper {
|
|
return NotificationExceptionWrapper(settings: self.settings, date: date)
|
|
}
|
|
}
|
|
|
|
public enum NotificationExceptionMode : Equatable {
|
|
public static func == (lhs: NotificationExceptionMode, rhs: NotificationExceptionMode) -> Bool {
|
|
switch lhs {
|
|
case let .users(lhsValue):
|
|
if case let .users(rhsValue) = rhs {
|
|
return lhsValue == rhsValue
|
|
} else {
|
|
return false
|
|
}
|
|
case let .groups(lhsValue):
|
|
if case let .groups(rhsValue) = rhs {
|
|
return lhsValue == rhsValue
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
case users([PeerId : NotificationExceptionWrapper])
|
|
case groups([PeerId : NotificationExceptionWrapper])
|
|
|
|
func withUpdatedPeerIdSound(_ peerId: PeerId, _ sound: PeerMessageSound) -> NotificationExceptionMode {
|
|
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMessageSound) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, sound in
|
|
var values = values
|
|
if let value = values[peerId] {
|
|
switch sound {
|
|
case .default:
|
|
switch value.settings.muteState {
|
|
case .default:
|
|
values.removeValue(forKey: peerId)
|
|
default:
|
|
values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)})
|
|
}
|
|
default:
|
|
values[peerId] = value.updateSettings({$0.withUpdatedMessageSound(sound)})
|
|
}
|
|
} else {
|
|
switch sound {
|
|
case .default:
|
|
break
|
|
default:
|
|
values[peerId] = NotificationExceptionWrapper(settings: TelegramPeerNotificationSettings(muteState: .default, messageSound: sound), date: Date().timeIntervalSince1970)
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
switch self {
|
|
case let .groups(values):
|
|
if peerId.namespace != Namespaces.Peer.CloudUser {
|
|
return .groups(apply(values, peerId, sound))
|
|
}
|
|
case let .users(values):
|
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
|
return .users(apply(values, peerId, sound))
|
|
}
|
|
}
|
|
|
|
return self
|
|
}
|
|
|
|
func withUpdatedPeerIdMuteInterval(_ peerId: PeerId, _ muteInterval: Int32?) -> NotificationExceptionMode {
|
|
|
|
let apply:([PeerId : NotificationExceptionWrapper], PeerId, PeerMuteState) -> [PeerId : NotificationExceptionWrapper] = { values, peerId, muteState in
|
|
var values = values
|
|
if let value = values[peerId] {
|
|
switch muteState {
|
|
case .default:
|
|
switch value.settings.messageSound {
|
|
case .default:
|
|
values.removeValue(forKey: peerId)
|
|
default:
|
|
values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)})
|
|
}
|
|
default:
|
|
values[peerId] = value.updateSettings({$0.withUpdatedMuteState(muteState)})
|
|
}
|
|
} else {
|
|
switch muteState {
|
|
case .default:
|
|
break
|
|
default:
|
|
values[peerId] = NotificationExceptionWrapper.init(settings: TelegramPeerNotificationSettings(muteState: muteState, messageSound: .default), date: Date().timeIntervalSince1970)
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
let muteState: PeerMuteState
|
|
if let muteInterval = muteInterval {
|
|
if muteInterval == 0 {
|
|
muteState = .unmuted
|
|
} else {
|
|
let absoluteUntil: Int32
|
|
if muteInterval == Int32.max {
|
|
absoluteUntil = Int32.max
|
|
} else {
|
|
absoluteUntil = Int32(Date().timeIntervalSince1970) + muteInterval
|
|
}
|
|
muteState = .muted(until: absoluteUntil)
|
|
}
|
|
} else {
|
|
muteState = .default
|
|
}
|
|
switch self {
|
|
case let .groups(values):
|
|
if peerId.namespace != Namespaces.Peer.CloudUser {
|
|
return .groups(apply(values, peerId, muteState))
|
|
}
|
|
case let .users(values):
|
|
if peerId.namespace == Namespaces.Peer.CloudUser {
|
|
return .users(apply(values, peerId, muteState))
|
|
}
|
|
}
|
|
|
|
return self
|
|
}
|
|
|
|
var peerIds: [PeerId] {
|
|
switch self {
|
|
case let .users(settings), let .groups(settings):
|
|
return settings.map {$0.key}
|
|
}
|
|
}
|
|
|
|
var settings: [PeerId : NotificationExceptionWrapper] {
|
|
switch self {
|
|
case let .users(settings), let .groups(settings):
|
|
return settings
|
|
}
|
|
}
|
|
}
|
|
|
|
private func notificationsExceptionEntries(presentationData: PresentationData, peers: [PeerId : Peer], state: NotificationExceptionState) -> [NotificationExceptionEntry] {
|
|
var entries: [NotificationExceptionEntry] = []
|
|
|
|
entries.append(.search(presentationData.theme, presentationData.strings))
|
|
|
|
|
|
var index: Int = 0
|
|
for (key, value) in state.mode.settings.sorted(by: { lhs, rhs in
|
|
let lhsName = peers[lhs.key]?.displayTitle ?? ""
|
|
let rhsName = peers[rhs.key]?.displayTitle ?? ""
|
|
|
|
if let lhsDate = lhs.value.date, let rhsDate = rhs.value.date {
|
|
return lhsDate < rhsDate
|
|
} else if lhs.value.date != nil && rhs.value.date == nil {
|
|
return true
|
|
} else if lhs.value.date == nil && rhs.value.date != nil {
|
|
return false
|
|
}
|
|
|
|
if let lhsPeer = peers[lhs.key] as? TelegramUser, let rhsPeer = peers[rhs.key] as? TelegramUser {
|
|
if lhsPeer.botInfo != nil && rhsPeer.botInfo == nil {
|
|
return false
|
|
} else if lhsPeer.botInfo == nil && rhsPeer.botInfo != nil {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return lhsName < rhsName
|
|
}) {
|
|
if let peer = peers[key], !peer.displayTitle.isEmpty {
|
|
var title: String
|
|
switch value.settings.muteState {
|
|
case .muted:
|
|
title = presentationData.strings.Notifications_ExceptionsMuted
|
|
case .unmuted:
|
|
title = presentationData.strings.Notifications_ExceptionsUnmuted
|
|
default:
|
|
title = ""
|
|
}
|
|
switch value.settings.messageSound {
|
|
case .default:
|
|
break
|
|
default:
|
|
title += (title.isEmpty ? "" : ", ") + localizedPeerNotificationSoundString(strings: presentationData.strings, sound: value.settings.messageSound)
|
|
}
|
|
entries.append(.peer(index, peer, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, title, value.settings))
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
return entries
|
|
}
|
|
|
|
public func notificationExceptionsController(account: Account, mode: NotificationExceptionMode, updatedMode:@escaping(NotificationExceptionMode) -> Void) -> ViewController {
|
|
let statePromise = ValuePromise(NotificationExceptionState(mode: mode), ignoreRepeated: true)
|
|
let stateValue = Atomic(value: NotificationExceptionState(mode: mode))
|
|
let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in
|
|
let result = stateValue.modify { f($0) }
|
|
statePromise.set(result)
|
|
updatedMode(result.mode)
|
|
}
|
|
|
|
let globalValue: Atomic<GlobalNotificationSettingsSet> = Atomic(value: GlobalNotificationSettingsSet.defaultSettings)
|
|
|
|
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
|
|
|
|
|
|
let presentationData = account.telegramApplicationContext.currentPresentationData.modify {$0}
|
|
|
|
let updatePeerSound: (PeerId, PeerMessageSound) -> Void = { peerId, sound in
|
|
_ = updatePeerNotificationSoundInteractive(account: account, peerId: peerId, sound: sound).start(completed: {
|
|
updateState { value in
|
|
return value.withUpdatedPeerIdSound(peerId, sound)
|
|
}
|
|
})
|
|
}
|
|
|
|
let updatePeerNotificationInterval:(PeerId, Int32?) -> Void = { peerId, muteInterval in
|
|
_ = updatePeerMuteSetting(account: account, peerId: peerId, muteInterval: muteInterval).start(completed: {
|
|
updateState { value in
|
|
return value.withUpdatedPeerIdMuteInterval(peerId, muteInterval)
|
|
}
|
|
})
|
|
}
|
|
|
|
var activateSearch:(()->Void)?
|
|
|
|
|
|
let arguments = NotificationExceptionArguments(account: account, activateSearch: {
|
|
activateSearch?()
|
|
}, changeNotifications: { peerId, settings in
|
|
|
|
let globalSettings = globalValue.modify {$0}
|
|
|
|
let isPrivateChat = peerId.namespace == Namespaces.Peer.CloudUser
|
|
|
|
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: isPrivateChat && globalSettings.privateChats.enabled || !isPrivateChat && globalSettings.groupChats.enabled ? presentationData.strings.UserInfo_NotificationsDefaultEnabled : presentationData.strings.UserInfo_NotificationsDefaultDisabled, color: .accent, action: { [weak actionSheet] in
|
|
updatePeerNotificationInterval(peerId, nil)
|
|
actionSheet?.dismissAnimated()
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
updatePeerNotificationInterval(peerId, 0)
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
updatePeerNotificationInterval(peerId, 60 * 60)
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2)
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in
|
|
updatePeerNotificationInterval(peerId, Int32.max)
|
|
actionSheet?.dismissAnimated()
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in
|
|
let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: isPrivateChat ? globalSettings.privateChats.sound : globalSettings.groupChats.sound, completion: { value in
|
|
updatePeerSound(peerId, value)
|
|
})
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
actionSheet?.dismissAnimated()
|
|
}),
|
|
ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsResetToDefaults, color: .destructive, action: { [weak actionSheet] in
|
|
|
|
updatePeerNotificationInterval(peerId, nil)
|
|
updatePeerSound(peerId, .default)
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
]), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
presentControllerImpl?(actionSheet, nil)
|
|
}, selectPeer: {
|
|
let filter: ChatListNodePeersFilter
|
|
switch mode {
|
|
case .groups:
|
|
filter = [.withoutSecretChats]
|
|
case .users:
|
|
filter = [.withoutSecretChats]
|
|
}
|
|
let controller = PeerSelectionController(account: account, filter: filter, title: presentationData.strings.Notifications_AddExceptionTitle)
|
|
controller.peerSelected = { [weak controller] peerId in
|
|
controller?.dismiss()
|
|
|
|
let settingsSignal = account.postbox.transaction { transaction in
|
|
return transaction.getPeerNotificationSettings(peerId)
|
|
} |> deliverOnMainQueue
|
|
|
|
_ = settingsSignal.start(next: { settings in
|
|
if let settings = settings as? TelegramPeerNotificationSettings {
|
|
let actionSheet = ActionSheetController(presentationTheme: presentationData.theme)
|
|
|
|
var items: [ActionSheetButtonItem] = []
|
|
|
|
switch settings.muteState {
|
|
case .default, .muted:
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsEnable, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
updatePeerNotificationInterval(peerId, 0)
|
|
}))
|
|
default:
|
|
break
|
|
}
|
|
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Notification_Mute1h, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
updatePeerNotificationInterval(peerId, 60 * 60)
|
|
}))
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.MuteFor_Days(2), color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
updatePeerNotificationInterval(peerId, 60 * 60 * 24 * 2)
|
|
}))
|
|
|
|
switch settings.muteState {
|
|
case .default, .unmuted:
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.UserInfo_NotificationsDisable, color: .accent, action: { [weak actionSheet] in
|
|
updatePeerNotificationInterval(peerId, Int32.max)
|
|
actionSheet?.dismissAnimated()
|
|
}))
|
|
default:
|
|
break
|
|
}
|
|
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsChangeSound(localizedPeerNotificationSoundString(strings: presentationData.strings, sound: settings.messageSound)).0, color: .accent, action: { [weak actionSheet] in
|
|
let controller = notificationSoundSelectionController(account: account, isModal: true, currentSound: settings.messageSound, defaultSound: nil, completion: { value in
|
|
updatePeerSound(peerId, value)
|
|
})
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
actionSheet?.dismissAnimated()
|
|
}))
|
|
|
|
if settings.muteState != .default || settings.messageSound != .default {
|
|
items.append(ActionSheetButtonItem(title: presentationData.strings.Notifications_ExceptionsResetToDefaults, color: .destructive, action: { [weak actionSheet] in
|
|
|
|
updatePeerNotificationInterval(peerId, nil)
|
|
updatePeerSound(peerId, .default)
|
|
actionSheet?.dismissAnimated()
|
|
}))
|
|
}
|
|
|
|
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
|
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, action: { [weak actionSheet] in
|
|
actionSheet?.dismissAnimated()
|
|
})
|
|
])])
|
|
presentControllerImpl?(actionSheet, nil)
|
|
}
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
|
})
|
|
|
|
let peersSignal:Signal<[PeerId : Peer], NoError> = statePromise.get() |> mapToSignal { state in
|
|
return account.postbox.transaction { transaction -> [PeerId : Peer] in
|
|
var peers:[PeerId : Peer] = [:]
|
|
for peerId in state.mode.peerIds {
|
|
if let peer = transaction.getPeer(peerId) {
|
|
peers[peerId] = peer
|
|
}
|
|
}
|
|
return peers
|
|
}
|
|
}
|
|
|
|
let preferences = account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
|
|
|
|
|
|
let signal = combineLatest((account.applicationContext as! TelegramApplicationContext).presentationData, statePromise.get(), peersSignal, preferences)
|
|
|> map { presentationData, state, peers, prefs -> (ItemListControllerState, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)) in
|
|
|
|
_ = globalValue.swap((prefs.values[PreferencesKeys.globalNotifications] as? GlobalNotificationSettings)?.effective ?? GlobalNotificationSettingsSet.defaultSettings)
|
|
|
|
|
|
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Notifications_ExceptionsTitle), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: nil)
|
|
let listState = ItemListNodeState(entries: notificationsExceptionEntries(presentationData: presentationData, peers: peers, state: state), style: .blocks, searchItem: nil)
|
|
|
|
return (controllerState, (listState, arguments))
|
|
}
|
|
|
|
let controller = NotificationExceptionsController(account: account, state: signal, addAction: {
|
|
arguments.selectPeer()
|
|
})
|
|
|
|
// let controller = ItemListController(account: account, state: signal |> afterDisposed {
|
|
// actionsDisposable.dispose()
|
|
// })
|
|
|
|
|
|
activateSearch = { [weak controller] in
|
|
// updateState { state in
|
|
// return state.withUpdatedSearchMode(true)
|
|
// }
|
|
controller?.activateSearch()
|
|
}
|
|
|
|
|
|
presentControllerImpl = { [weak controller] c, a in
|
|
controller?.present(c, in: .window(.root), with: a)
|
|
}
|
|
return controller
|
|
}
|
|
|
|
|
|
private final class NotificationExceptionsController: ViewController {
|
|
private let account: Account
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
var peerSelected: ((PeerId) -> Void)?
|
|
|
|
var inProgress: Bool = false {
|
|
didSet {
|
|
if self.inProgress != oldValue {
|
|
if self.isNodeLoaded {
|
|
self.controllerNode.inProgress = self.inProgress
|
|
}
|
|
|
|
if self.inProgress {
|
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(theme: self.presentationData.theme))
|
|
} else {
|
|
self.navigationItem.rightBarButtonItem = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var controllerNode: NotificationExceptionsControllerNode {
|
|
return super.displayNode as! NotificationExceptionsControllerNode
|
|
}
|
|
|
|
|
|
private let _ready = Promise<Bool>()
|
|
override public var ready: Promise<Bool> {
|
|
return self._ready
|
|
}
|
|
private let addAction:()->Void
|
|
|
|
private let state: Signal<(ItemListControllerState, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>
|
|
|
|
public init(account: Account, state: Signal<(ItemListControllerState, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>, addAction: @escaping()->Void) {
|
|
self.account = account
|
|
self.state = state
|
|
self.addAction = addAction
|
|
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
|
|
|
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
|
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBar.style.style
|
|
|
|
self.title = self.presentationData.strings.Notifications_ExceptionsTitle
|
|
|
|
|
|
self.scrollToTop = { [weak self] in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerNode.scrollToTop()
|
|
}
|
|
}
|
|
}
|
|
|
|
required public init(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
@objc private func addExceptionAction() {
|
|
self.addAction()
|
|
}
|
|
|
|
override public func loadDisplayNode() {
|
|
let image = PresentationResourcesRootController.navigationAddIcon(presentationData.theme)
|
|
|
|
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItem.Style.plain, target: self, action: #selector(addExceptionAction))
|
|
|
|
let nodeState = self.state |> deliverOnMainQueue |> map { ($0.theme, $1) }
|
|
|
|
self.displayNode = NotificationExceptionsControllerNode(account: self.account, navigationBar: self.navigationBar!, state: nodeState)
|
|
self.displayNode.backgroundColor = .white
|
|
|
|
self.controllerNode.navigationBar = self.navigationBar
|
|
|
|
self.controllerNode.requestDeactivateSearch = { [weak self] in
|
|
self?.deactivateSearch()
|
|
}
|
|
|
|
self.controllerNode.requestActivateSearch = { [weak self] in
|
|
self?.activateSearch()
|
|
}
|
|
|
|
self.displayNodeDidLoad()
|
|
|
|
self._ready.set(self.controllerNode.ready)
|
|
}
|
|
|
|
override public func viewWillAppear(_ animated: Bool) {
|
|
super.viewWillAppear(animated)
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
// self.controllerNode.animateIn()
|
|
}
|
|
|
|
override public func viewDidDisappear(_ animated: Bool) {
|
|
super.viewDidDisappear(animated)
|
|
}
|
|
|
|
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, transition: transition)
|
|
|
|
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationHeight, transition: transition)
|
|
}
|
|
|
|
@objc func cancelPressed() {
|
|
self.dismiss()
|
|
}
|
|
|
|
func activateSearch() {
|
|
if self.displayNavigationBar {
|
|
if let scrollToTop = self.scrollToTop {
|
|
scrollToTop()
|
|
}
|
|
self.controllerNode.activateSearch()
|
|
self.setDisplayNavigationBar(false, transition: .animated(duration: 0.5, curve: .spring))
|
|
}
|
|
}
|
|
|
|
private func deactivateSearch() {
|
|
if !self.displayNavigationBar {
|
|
self.setDisplayNavigationBar(true, transition: .animated(duration: 0.5, curve: .spring))
|
|
self.controllerNode.deactivateSearch()
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private final class NotificationExceptionsControllerNode: ASDisplayNode {
|
|
private let account: Account
|
|
|
|
var inProgress: Bool = false {
|
|
didSet {
|
|
|
|
}
|
|
}
|
|
|
|
var navigationBar: NavigationBar?
|
|
|
|
|
|
private let contentNode: ItemListControllerNode<NotificationExceptionEntry>
|
|
|
|
private var contactListActive = false
|
|
|
|
private var searchDisplayController: SearchDisplayController?
|
|
|
|
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
var requestActivateSearch: (() -> Void)?
|
|
var requestDeactivateSearch: (() -> Void)?
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
private var readyValue = Promise<Bool>()
|
|
var ready: Signal<Bool, NoError> {
|
|
return self.readyValue.get()
|
|
}
|
|
|
|
private let state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>
|
|
|
|
init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) {
|
|
self.account = account
|
|
self.state = state
|
|
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
|
|
|
|
|
self.contentNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in
|
|
|
|
}, state: state)
|
|
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
return UITracingLayerView()
|
|
})
|
|
|
|
self.addSubnode(self.contentNode)
|
|
self.presentationDataDisposable = (account.telegramApplicationContext.presentationData
|
|
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
|
if let strongSelf = self {
|
|
let previousTheme = strongSelf.presentationData.theme
|
|
let previousStrings = strongSelf.presentationData.strings
|
|
strongSelf.presentationData = presentationData
|
|
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
|
|
strongSelf.updateThemeAndStrings()
|
|
}
|
|
}
|
|
})
|
|
|
|
|
|
|
|
self.readyValue.set(contentNode.ready)
|
|
}
|
|
|
|
deinit {
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
private func updateThemeAndStrings() {
|
|
self.searchDisplayController?.updateThemeAndStrings(theme: self.presentationData.theme, strings: self.presentationData.strings)
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.containerLayout = (layout, navigationBarHeight)
|
|
|
|
let cleanInsets = layout.insets(options: [])
|
|
|
|
|
|
var controlSize = CGSize(width: 0, height:0)
|
|
controlSize.width = min(layout.size.width, max(200.0, controlSize.width))
|
|
|
|
var insets = layout.insets(options: [.input])
|
|
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
|
insets.bottom = max(insets.bottom, cleanInsets.bottom)
|
|
insets.left += layout.safeInsets.left
|
|
insets.right += layout.safeInsets.right
|
|
|
|
self.contentNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
self.contentNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
|
|
|
self.contentNode.containerLayoutUpdated(layout, navigationBarHeight: insets.top, transition: transition)
|
|
|
|
if let searchDisplayController = self.searchDisplayController {
|
|
searchDisplayController.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
|
}
|
|
}
|
|
|
|
func activateSearch() {
|
|
guard let (containerLayout, navigationBarHeight) = self.containerLayout, let navigationBar = self.navigationBar else {
|
|
return
|
|
}
|
|
|
|
if self.contentNode.supernode != nil {
|
|
var maybePlaceholderNode: SearchBarPlaceholderNode?
|
|
self.contentNode.listNode.forEachItemNode { node in
|
|
if let node = node as? NotificationSearchItemNode {
|
|
maybePlaceholderNode = node.searchBarNode
|
|
}
|
|
}
|
|
|
|
if let _ = self.searchDisplayController {
|
|
return
|
|
}
|
|
|
|
if let placeholderNode = maybePlaceholderNode {
|
|
self.searchDisplayController = SearchDisplayController(theme: self.presentationData.theme, strings: self.presentationData.strings, contentNode: NotificationExceptionsSearchControllerContentNode(account: account, navigationBar: navigationBar, state: self.state), cancel: { [weak self] in
|
|
self?.requestDeactivateSearch?()
|
|
})
|
|
|
|
self.searchDisplayController?.containerLayoutUpdated(containerLayout, navigationBarHeight: navigationBarHeight, transition: .immediate)
|
|
self.searchDisplayController?.activate(insertSubnode: { subnode in
|
|
self.insertSubnode(subnode, belowSubnode: navigationBar)
|
|
}, placeholder: placeholderNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func deactivateSearch() {
|
|
if let searchDisplayController = self.searchDisplayController {
|
|
if self.contentNode.supernode != nil {
|
|
var maybePlaceholderNode: SearchBarPlaceholderNode?
|
|
self.contentNode.listNode.forEachItemNode { node in
|
|
if let node = node as? NotificationSearchItemNode {
|
|
maybePlaceholderNode = node.searchBarNode
|
|
}
|
|
}
|
|
|
|
searchDisplayController.deactivate(placeholder: maybePlaceholderNode)
|
|
self.searchDisplayController = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func scrollToTop() {
|
|
if self.contentNode.supernode != nil {
|
|
self.contentNode.scrollToTop()
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
|
|
private final class NotificationExceptionsSearchControllerContentNode: SearchDisplayControllerContentNode {
|
|
private let account: Account
|
|
|
|
private let listNode: ItemListControllerNode<NotificationExceptionEntry>
|
|
private let dimNode: ASDisplayNode
|
|
private var validLayout: ContainerViewLayout?
|
|
|
|
|
|
private let searchQuery = Promise<String?>()
|
|
private let searchDisposable = MetaDisposable()
|
|
|
|
private var presentationData: PresentationData
|
|
private var presentationDataDisposable: Disposable?
|
|
|
|
private let presentationDataPromise: Promise<ChatListPresentationData>
|
|
|
|
private let _isSearching = ValuePromise<Bool>(false, ignoreRepeated: true)
|
|
override var isSearching: Signal<Bool, NoError> {
|
|
return self._isSearching.get()
|
|
}
|
|
|
|
private let state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>
|
|
|
|
|
|
init(account: Account, navigationBar: NavigationBar, state: Signal<(PresentationTheme, (ItemListNodeState<NotificationExceptionEntry>, NotificationExceptionEntry.ItemGenerationArguments)), NoError>) {
|
|
self.account = account
|
|
self.state = state
|
|
|
|
self.presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 }
|
|
self.presentationDataPromise = Promise(ChatListPresentationData(theme: self.presentationData.theme, strings: self.presentationData.strings, dateTimeFormat: self.presentationData.dateTimeFormat, nameSortOrder: self.presentationData.nameSortOrder, nameDisplayOrder: self.presentationData.nameDisplayOrder, disableAnimations: self.presentationData.disableAnimations))
|
|
|
|
self.listNode = ItemListControllerNode(navigationBar: navigationBar, updateNavigationOffset: { _ in
|
|
|
|
}, state: searchQuery.get() |> mapToSignal { query in
|
|
return state |> map { values in
|
|
var values = values
|
|
let entries = values.1.0.entries.filter { entry in
|
|
switch entry {
|
|
case .search:
|
|
return false
|
|
case let .peer(_, peer, _, _, _, _, _):
|
|
if let query = query {
|
|
return !peer.displayTitle.components(separatedBy: " ").filter({$0.lowercased().hasPrefix(query.lowercased())}).isEmpty && !query.isEmpty
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
values.1.0 = ItemListNodeState(entries: entries, style: values.1.0.style, focusItemTag: nil, emptyStateItem: nil, searchItem: nil, crossfadeState: false, animateChanges: false)
|
|
return values
|
|
}
|
|
})
|
|
|
|
|
|
|
|
self.dimNode = ASDisplayNode()
|
|
self.dimNode.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
|
|
|
super.init()
|
|
|
|
|
|
self.addSubnode(self.dimNode)
|
|
self.addSubnode(self.listNode)
|
|
self.listNode.isHidden = true
|
|
|
|
self.presentationDataDisposable = (account.telegramApplicationContext.presentationData
|
|
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
|
if let strongSelf = self {
|
|
let previousTheme = strongSelf.presentationData.theme
|
|
|
|
strongSelf.presentationData = presentationData
|
|
|
|
if previousTheme !== presentationData.theme {
|
|
strongSelf.updateTheme(theme: presentationData.theme)
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|
|
|
|
deinit {
|
|
self.searchDisposable.dispose()
|
|
self.presentationDataDisposable?.dispose()
|
|
}
|
|
|
|
private func updateTheme(theme: PresentationTheme) {
|
|
self.backgroundColor = theme.chatList.backgroundColor
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:))))
|
|
}
|
|
|
|
@objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) {
|
|
if case .ended = recognizer.state {
|
|
self.cancel?()
|
|
}
|
|
}
|
|
|
|
override func searchTextUpdated(text: String) {
|
|
if text.isEmpty {
|
|
self.searchQuery.set(.single(nil))
|
|
self.listNode.isHidden = true
|
|
} else {
|
|
self.searchQuery.set(.single(text))
|
|
self.listNode.isHidden = false
|
|
}
|
|
|
|
}
|
|
|
|
override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
|
|
|
let hadValidLayout = self.validLayout != nil
|
|
self.validLayout = layout
|
|
|
|
var duration: Double = 0.0
|
|
var curve: UInt = 0
|
|
switch transition {
|
|
case .immediate:
|
|
break
|
|
case let .animated(animationDuration, animationCurve):
|
|
duration = animationDuration
|
|
switch animationCurve {
|
|
case .easeInOut:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default
|
|
}
|
|
|
|
self.listNode.containerLayoutUpdated(layout, navigationBarHeight: 0, transition: transition)
|
|
|
|
let insets = UIEdgeInsets(top: navigationBarHeight - 30, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right)
|
|
|
|
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top)))
|
|
|
|
self.listNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
|
self.listNode.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight - 30, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
}
|
|
|
|
// override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
// super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
|
|
//
|
|
// self.validLayout = layout
|
|
//
|
|
//
|
|
// let cleanInsets = layout.insets(options: [])
|
|
//
|
|
//// var insets = layout.insets(options: [.input])
|
|
//// insets.top += layout.insets(options: [.statusBar]).top
|
|
// let toolbarHeight: CGFloat = 44.0 + cleanInsets.bottom
|
|
//
|
|
//
|
|
// var insets = layout.insets(options: [.input])
|
|
// insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
|
// insets.bottom = max(insets.bottom, cleanInsets.bottom)
|
|
// insets.left += layout.safeInsets.left
|
|
// insets.right += layout.safeInsets.right
|
|
//
|
|
// self.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height)
|
|
// self.listNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
|
//
|
|
// let topInset = layout.insets(options: [.statusBar]).bottom
|
|
// transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
|
|
//
|
|
// transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset)))
|
|
// self.listNode.containerLayoutUpdated(layout, navigationBarHeight: topInset, transition: transition)
|
|
// }
|
|
|
|
}
|