2022-10-17 17:16:50 +04:00

536 lines
25 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
import ItemListUI
import PresentationDataUtils
import AccountContext
import AlertUI
import PresentationDataUtils
import NotificationSoundSelectionUI
import TelegramStringFormatting
import ItemListPeerItem
import ItemListPeerActionItem
private extension EnginePeer.NotificationSettings.MuteState {
var timeInterval: Int32? {
switch self {
case .default:
return nil
case .unmuted:
return 0
case let .muted(until):
return until
}
}
}
private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound {
if case .default = sound {
return defaultCloudPeerNotificationSound
} else {
return sound
}
}
private final class NotificationsPeerCategoryControllerArguments {
let context: AccountContext
let soundSelectionDisposable: MetaDisposable
let updateEnabled: (Bool) -> Void
let updatePreviews: (Bool) -> Void
let openSound: (PeerMessageSound) -> Void
let addException: () -> Void
let openException: (Int64) -> Void
let removeAllExceptions: () -> Void
let updateRevealedThreadId: (Int64?) -> Void
let removeThread: (Int64) -> Void
init(context: AccountContext, soundSelectionDisposable: MetaDisposable, updateEnabled: @escaping (Bool) -> Void, updatePreviews: @escaping (Bool) -> Void, openSound: @escaping (PeerMessageSound) -> Void, addException: @escaping () -> Void, openException: @escaping (Int64) -> Void, removeAllExceptions: @escaping () -> Void, updateRevealedThreadId: @escaping (Int64?) -> Void, removeThread: @escaping (Int64) -> Void) {
self.context = context
self.soundSelectionDisposable = soundSelectionDisposable
self.updateEnabled = updateEnabled
self.updatePreviews = updatePreviews
self.openSound = openSound
self.addException = addException
self.openException = openException
self.removeAllExceptions = removeAllExceptions
self.updateRevealedThreadId = updateRevealedThreadId
self.removeThread = removeThread
}
}
private enum NotificationsPeerCategorySection: Int32 {
case enable
case options
case exceptions
}
private enum NotificationsPeerCategoryEntryTag: ItemListItemTag {
case enable
case previews
case sound
public func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? NotificationsPeerCategoryEntryTag, self == other {
return true
} else {
return false
}
}
}
private enum NotificationsPeerCategoryEntry: ItemListNodeEntry {
case enable(String, Bool)
case optionsHeader(String)
case previews(String, Bool)
case sound(String, String, PeerMessageSound)
case exceptionsHeader(String)
case addException(String)
case exception(Int32, PresentationDateTimeFormat, PresentationPersonNameOrder, EngineMessageHistoryThread.Info, String, TelegramPeerNotificationSettings, Bool, Bool)
case removeAllExceptions(String)
var section: ItemListSectionId {
switch self {
case .enable:
return NotificationsPeerCategorySection.enable.rawValue
case .optionsHeader, .previews, .sound:
return NotificationsPeerCategorySection.options.rawValue
case .exceptionsHeader, .addException, .exception, .removeAllExceptions:
return NotificationsPeerCategorySection.exceptions.rawValue
}
}
var stableId: Int32 {
switch self {
case .enable:
return 0
case .optionsHeader:
return 1
case .previews:
return 2
case .sound:
return 3
case .exceptionsHeader:
return 4
case .addException:
return 5
case let .exception(index, _, _, _, _, _, _, _):
return 6 + index
case .removeAllExceptions:
return 100000
}
}
var tag: ItemListItemTag? {
switch self {
case .enable:
return NotificationsPeerCategoryEntryTag.enable
case .previews:
return NotificationsPeerCategoryEntryTag.previews
case .sound:
return NotificationsPeerCategoryEntryTag.sound
default:
return nil
}
}
static func ==(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool {
switch lhs {
case let .enable(lhsText, lhsValue):
if case let .enable(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .optionsHeader(lhsText):
if case let .optionsHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .previews(lhsText, lhsValue):
if case let .previews(rhsText, rhsValue) = rhs, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .sound(lhsText, lhsValue, lhsSound):
if case let .sound(rhsText, rhsValue, rhsSound) = rhs, lhsText == rhsText, lhsValue == rhsValue, lhsSound == rhsSound {
return true
} else {
return false
}
case let .exceptionsHeader(lhsText):
if case let .exceptionsHeader(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .addException(lhsText):
if case let .addException(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
case let .exception(lhsIndex, lhsDateTimeFormat, lhsDisplayNameOrder, lhsInfo, lhsDescription, lhsSettings, lhsEditing, lhsRevealed):
if case let .exception(rhsIndex, rhsDateTimeFormat, rhsDisplayNameOrder, rhsInfo, rhsDescription, rhsSettings, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsDateTimeFormat == rhsDateTimeFormat, lhsDisplayNameOrder == rhsDisplayNameOrder, lhsInfo == rhsInfo, lhsDescription == rhsDescription, lhsSettings == rhsSettings, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed {
return true
} else {
return false
}
case let .removeAllExceptions(lhsText):
if case let .removeAllExceptions(rhsText) = rhs, lhsText == rhsText {
return true
} else {
return false
}
}
}
static func <(lhs: NotificationsPeerCategoryEntry, rhs: NotificationsPeerCategoryEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! NotificationsPeerCategoryControllerArguments
switch self {
case let .enable(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { updatedValue in
arguments.updateEnabled(updatedValue)
}, tag: self.tag)
case let .optionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .previews(text, value):
return ItemListSwitchItem(presentationData: presentationData, title: text, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.updatePreviews(value)
})
case let .sound(text, value, sound):
return ItemListDisclosureItem(presentationData: presentationData, title: text, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
arguments.openSound(sound)
}, tag: self.tag)
case let .exceptionsHeader(text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .addException(text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.plusIconImage(presentationData.theme), title: text, sectionId: self.section, height: .peerList, color: .accent, editing: false, action: {
arguments.addException()
})
//case let .exception(_, dateTimeFormat, nameDisplayOrder, info, description, _, editing, revealed):
case .exception:
preconditionFailure()
/*return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .text(description, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: true, editing: editing, revealed: revealed), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
arguments.openException(peer)
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
arguments.updateRevealedPeerId(peerId)
}, removePeer: { peerId in
arguments.removePeer(peer)
}, hasTopStripe: false, hasTopGroupInset: false, noInsets: false)*/
case let .removeAllExceptions(text):
return ItemListPeerActionItem(presentationData: presentationData, icon: PresentationResourcesItemList.deleteIconImage(presentationData.theme), title: text, sectionId: self.section, height: .generic, color: .destructive, editing: false, action: {
arguments.removeAllExceptions()
})
}
}
}
private func notificationsPeerCategoryEntries(notificationSettings: EnginePeer.NotificationSettings, state: NotificationExceptionState, presentationData: PresentationData, notificationSoundList: NotificationSoundList?) -> [NotificationsPeerCategoryEntry] {
var entries: [NotificationsPeerCategoryEntry] = []
var notificationsEnabled = true
if case .muted = notificationSettings.muteState {
notificationsEnabled = false
}
var displayPreviews = true
switch notificationSettings.displayPreviews {
case .hide:
displayPreviews = false
default:
break
}
entries.append(.enable(presentationData.strings.Notifications_MessageNotificationsAlert, notificationsEnabled))
if notificationsEnabled || !state.notificationExceptions.isEmpty {
entries.append(.optionsHeader(presentationData.strings.Notifications_Options.uppercased()))
entries.append(.previews(presentationData.strings.Notifications_MessageNotificationsPreview, displayPreviews))
entries.append(.sound(presentationData.strings.Notifications_MessageNotificationsSound, localizedPeerNotificationSoundString(strings: presentationData.strings, notificationSoundList: notificationSoundList, sound: filteredGlobalSound(notificationSettings.messageSound._asMessageSound())), filteredGlobalSound(notificationSettings.messageSound._asMessageSound())))
}
entries.append(.exceptionsHeader(presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased()))
entries.append(.addException(presentationData.strings.Notification_Exceptions_AddException))
var existingThreadIds = Set<Int64>()
var index: Int = 0
for value in state.notificationExceptions {
var title: String
var muted = false
switch value.notificationSettings.muteState {
case let .muted(until):
if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
if until < Int32.max - 1 {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: presentationData.strings.baseLanguageCode)
if Calendar.current.isDateInToday(Date(timeIntervalSince1970: Double(until))) {
formatter.dateFormat = "HH:mm"
} else {
formatter.dateFormat = "E, d MMM HH:mm"
}
let dateString = formatter.string(from: Date(timeIntervalSince1970: Double(until)))
title = presentationData.strings.Notification_Exceptions_MutedUntil(dateString).string
} else {
muted = true
title = presentationData.strings.Notification_Exceptions_AlwaysOff
}
} else {
title = presentationData.strings.Notification_Exceptions_AlwaysOn
}
case .unmuted:
title = presentationData.strings.Notification_Exceptions_AlwaysOn
default:
title = ""
}
if !muted {
switch value.notificationSettings.messageSound {
case .default:
break
default:
if !title.isEmpty {
title.append(", ")
}
title.append(presentationData.strings.Notification_Exceptions_SoundCustom)
}
switch value.notificationSettings.displayPreviews {
case .default:
break
default:
if !title.isEmpty {
title += ", "
}
if case .show = value.notificationSettings.displayPreviews {
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOn
} else {
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff
}
}
}
existingThreadIds.insert(value.threadId)
entries.append(.exception(Int32(index), presentationData.dateTimeFormat, presentationData.nameDisplayOrder, value.info, title, value.notificationSettings._asNotificationSettings(), state.editing, state.revealedThreadId == value.threadId))
index += 1
}
if state.notificationExceptions.count > 0 {
entries.append(.removeAllExceptions(presentationData.strings.Notifications_DeleteAllExceptions))
}
return entries
}
private struct NotificationExceptionState: Equatable {
var revealedThreadId: Int64? = nil
var editing: Bool = false
var notificationExceptions: [EngineMessageHistoryThread.NotificationException] = []
}
public func threadNotificationExceptionsScreen(context: AccountContext, peerId: EnginePeer.Id, notificationExceptions: [EngineMessageHistoryThread.NotificationException], updated: @escaping ([EngineMessageHistoryThread.NotificationException]) -> Void) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
let initialState = NotificationExceptionState(notificationExceptions: notificationExceptions)
let stateValue = Atomic<NotificationExceptionState>(value: initialState)
let statePromise: ValuePromise<NotificationExceptionState> = ValuePromise(ignoreRepeated: true)
statePromise.set(initialState)
let updateState: ((NotificationExceptionState) -> NotificationExceptionState) -> Void = { f in
let result = stateValue.modify { f($0) }
statePromise.set(result)
//updatedMode(result.mode)
}
/*let updatePeerSound: (PeerId, PeerMessageSound) -> Signal<Void, NoError> = { peerId, sound in
return context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: sound)
|> deliverOnMainQueue
}
let updatePeerNotificationInterval: (PeerId, Int32?) -> Signal<Void, NoError> = { peerId, muteInterval in
return context.engine.peers.updatePeerMuteSetting(peerId: peerId, threadId: nil, muteInterval: muteInterval)
|> deliverOnMainQueue
}
let updatePeerDisplayPreviews: (PeerId, PeerNotificationDisplayPreviews) -> Signal<Void, NoError> = {
peerId, displayPreviews in
return context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: nil, displayPreviews: displayPreviews) |> deliverOnMainQueue
}*/
let _ = presentControllerImpl
let arguments = NotificationsPeerCategoryControllerArguments(context: context, soundSelectionDisposable: MetaDisposable(), updateEnabled: { _ in
let _ = context.engine.peers.togglePeerMuted(peerId: peerId, threadId: nil).start()
}, updatePreviews: { value in
let _ = context.engine.peers.updatePeerDisplayPreviewsSetting(peerId: peerId, threadId: nil, displayPreviews: value ? .show : .hide).start()
}, openSound: { sound in
let controller = notificationSoundSelectionController(context: context, isModal: true, currentSound: sound, defaultSound: nil, completion: { value in
let _ = context.engine.peers.updatePeerNotificationSoundInteractive(peerId: peerId, threadId: nil, sound: value).start()
})
pushControllerImpl?(controller)
}, addException: {
/*let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var filter: ChatListNodePeersFilter = [.excludeRecent, .doNotSearchMessages, .removeSearchHeader]
switch category {
case .privateChat:
filter.insert(.onlyPrivateChats)
filter.insert(.excludeSavedMessages)
filter.insert(.excludeSecretChats)
case .group:
filter.insert(.onlyGroups)
case .channel:
filter.insert(.onlyChannels)
}
let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(context: context, filter: filter, hasContactSelector: false, title: presentationData.strings.Notifications_AddExceptionTitle))
controller.peerSelected = { [weak controller] peer in
let peerId = peer.id
presentPeerSettings(peerId, {
controller?.dismiss()
})
}
pushControllerImpl?(controller)*/
}, openException: { peer in
//presentPeerSettings(peer.id, {})
}, removeAllExceptions: {
/*let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
actionSheet.setItemGroups([ActionSheetItemGroup(items: [
ActionSheetTextItem(title: presentationData.strings.Notification_Exceptions_DeleteAllConfirmation),
ActionSheetButtonItem(title: presentationData.strings.Notification_Exceptions_DeleteAll, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let values = stateValue.with { $0.mode.settings.values }
let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: values.map { EnginePeer($0.peer) })
|> deliverOnMainQueue).start(completed: {
updateNotificationsDisposable.set(nil)
updateState { state in
var state = state
for value in values {
state = state.withUpdatedPeerMuteInterval(value.peer, nil).withUpdatedPeerSound(value.peer, .default).withUpdatedPeerDisplayPreviews(value.peer, .default)
}
return state
}
let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: values.map(\.peer.id))
|> deliverOnMainQueue).start(completed: {
updateNotificationsView({})
})
})
})
]), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, nil)*/
}, updateRevealedThreadId: { threadId in
updateState { current in
var current = current
current.revealedThreadId = threadId
return current
}
}, removeThread: { threadId in
/*let _ = (context.engine.peers.ensurePeersAreLocallyAvailable(peers: [EnginePeer(peer)])
|> deliverOnMainQueue).start(completed: {
updateNotificationsDisposable.set(nil)
updateState { value in
return value.withUpdatedPeerMuteInterval(peer, nil).withUpdatedPeerSound(peer, .default).withUpdatedPeerDisplayPreviews(peer, .default)
}
let _ = (context.engine.peers.removeCustomNotificationSettings(peerIds: [peer.id])
|> deliverOnMainQueue).start(completed: {
updateNotificationsView({})
})
})*/
})
let signal = combineLatest(queue: .mainQueue(),
context.sharedContext.presentationData,
context.engine.peers.notificationSoundList(),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId)),
statePromise.get()
)
|> map { presentationData, notificationSoundList, peer, notificationSettings, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = notificationsPeerCategoryEntries(notificationSettings: notificationSettings, state: state, presentationData: presentationData, notificationSoundList: notificationSoundList)
var scrollToItem: ListViewScrollToItem?
scrollToItem = nil
/*var index = 0
if let focusOnItemTag = focusOnItemTag {
for entry in entries {
if entry.tag?.isEqual(to: focusOnItemTag) ?? false {
scrollToItem = ListViewScrollToItem(index: index, position: .top(0.0), animated: false, curve: .Default(duration: 0.0), directionHint: .Up)
}
index += 1
}
}*/
let leftNavigationButton: ItemListNavigationButton?
let rightNavigationButton: ItemListNavigationButton?
if !state.notificationExceptions.isEmpty {
if state.editing {
leftNavigationButton = ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {})
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: true, action: {
updateState { value in
var value = value
value.editing = false
return value
}
})
} else {
leftNavigationButton = nil
rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Edit), style: .regular, enabled: true, action: {
updateState { value in
var value = value
value.editing = true
return value
}
})
}
} else {
leftNavigationButton = nil
rightNavigationButton = nil
}
let title: String
if let peer = peer {
title = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
} else {
title = ""
}
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: entries, style: .blocks, ensureVisibleItemTag: nil, initialScrollToItem: scrollToItem)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushControllerImpl = { [weak controller] c in
(controller?.navigationController as? NavigationController)?.pushViewController(c)
}
return controller
}