Files
ghostgram/submodules/SettingsUI/Sources/DeletedMessagesController.swift
T

465 lines
21 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import ItemListUI
import AccountContext
import ComponentFlow
import SliderComponent
private let minDeletedMessageTransparencyPercent: Int32 = Int32(AntiDeleteManager.minDeletedMessageTransparency * 100.0)
private let maxDeletedMessageTransparencyPercent: Int32 = Int32(AntiDeleteManager.maxDeletedMessageTransparency * 100.0)
private func clampDeletedMessageTransparencyPercent(_ value: Int32) -> Int32 {
return max(minDeletedMessageTransparencyPercent, min(maxDeletedMessageTransparencyPercent, value))
}
// MARK: - Entry Definition
private enum DeletedMessagesSection: Int32 {
case settings
}
private enum DeletedMessagesEntry: ItemListNodeEntry {
case enableToggle(PresentationTheme, String, Bool)
case archiveMediaToggle(PresentationTheme, String, Bool)
case transparencySlider(PresentationTheme, Int32, Bool)
case settingsInfo(PresentationTheme, String)
var section: ItemListSectionId {
return DeletedMessagesSection.settings.rawValue
}
var stableId: Int32 {
switch self {
case .enableToggle:
return 0
case .archiveMediaToggle:
return 1
case .transparencySlider:
return 2
case .settingsInfo:
return 3
}
}
static func ==(lhs: DeletedMessagesEntry, rhs: DeletedMessagesEntry) -> Bool {
switch lhs {
case let .enableToggle(lhsTheme, lhsText, lhsValue):
if case let .enableToggle(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .archiveMediaToggle(lhsTheme, lhsText, lhsValue):
if case let .archiveMediaToggle(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .transparencySlider(lhsTheme, lhsValue, lhsIsEnabled):
if case let .transparencySlider(rhsTheme, rhsValue, rhsIsEnabled) = rhs,
lhsTheme === rhsTheme, lhsValue == rhsValue, lhsIsEnabled == rhsIsEnabled {
return true
}
return false
case let .settingsInfo(lhsTheme, lhsText):
if case let .settingsInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
}
}
static func <(lhs: DeletedMessagesEntry, rhs: DeletedMessagesEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeletedMessagesControllerArguments
switch self {
case let .enableToggle(_, text, value):
return ItemListSwitchItem(
presentationData: presentationData,
title: text,
value: value,
sectionId: self.section,
style: .blocks,
updated: { value in
arguments.toggleEnabled(value)
}
)
case let .archiveMediaToggle(_, text, value):
return ItemListSwitchItem(
presentationData: presentationData,
title: text,
value: value,
sectionId: self.section,
style: .blocks,
updated: { value in
arguments.toggleArchiveMedia(value)
}
)
case let .transparencySlider(theme, value, isEnabled):
return DeletedMessagesTransparencySliderItem(
theme: theme,
value: value,
isEnabled: isEnabled,
sectionId: self.section,
updated: { value in
arguments.updateTransparency(value)
}
)
case let .settingsInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
// MARK: - Arguments
private final class DeletedMessagesControllerArguments {
let toggleEnabled: (Bool) -> Void
let toggleArchiveMedia: (Bool) -> Void
let updateTransparency: (Int32) -> Void
init(
toggleEnabled: @escaping (Bool) -> Void,
toggleArchiveMedia: @escaping (Bool) -> Void,
updateTransparency: @escaping (Int32) -> Void
) {
self.toggleEnabled = toggleEnabled
self.toggleArchiveMedia = toggleArchiveMedia
self.updateTransparency = updateTransparency
}
}
// MARK: - State
private struct DeletedMessagesControllerState: Equatable {
var isEnabled: Bool
var archiveMedia: Bool
var transparencyPercent: Int32
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled &&
lhs.archiveMedia == rhs.archiveMedia &&
lhs.transparencyPercent == rhs.transparencyPercent
}
}
// MARK: - Entries builder
private func deletedMessagesControllerEntries(
presentationData: PresentationData,
state: DeletedMessagesControllerState
) -> [DeletedMessagesEntry] {
var entries: [DeletedMessagesEntry] = []
entries.append(.enableToggle(presentationData.theme, "Сохранять удалённые сообщения", state.isEnabled))
entries.append(.archiveMediaToggle(presentationData.theme, "Архивировать медиа", state.archiveMedia))
entries.append(.transparencySlider(presentationData.theme, state.transparencyPercent, state.isEnabled))
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Прозрачность влияет только на сообщения, которые уже помечены как удалённые."))
return entries
}
// MARK: - Controller
public func deletedMessagesController(context: AccountContext) -> ViewController {
let initialState = DeletedMessagesControllerState(
isEnabled: AntiDeleteManager.shared.isEnabled,
archiveMedia: AntiDeleteManager.shared.archiveMedia,
transparencyPercent: clampDeletedMessageTransparencyPercent(Int32(round(AntiDeleteManager.shared.deletedMessageTransparency * 100.0)))
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((DeletedMessagesControllerState) -> DeletedMessagesControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let arguments = DeletedMessagesControllerArguments(
toggleEnabled: { value in
AntiDeleteManager.shared.isEnabled = value
updateState { state in
var state = state
state.isEnabled = value
return state
}
},
toggleArchiveMedia: { value in
AntiDeleteManager.shared.archiveMedia = value
updateState { state in
var state = state
state.archiveMedia = value
return state
}
},
updateTransparency: { value in
let clampedValue = clampDeletedMessageTransparencyPercent(value)
AntiDeleteManager.shared.deletedMessageTransparency = Double(clampedValue) / 100.0
updateState { state in
var state = state
state.transparencyPercent = clampedValue
return state
}
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = deletedMessagesControllerEntries(presentationData: presentationData, state: state)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("Удалённые сообщения"),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: false
)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
animateChanges: false
)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
return controller
}
private final class DeletedMessagesTransparencySliderItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let value: Int32
let isEnabled: Bool
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, value: Int32, isEnabled: Bool, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.value = clampDeletedMessageTransparencyPercent(value)
self.isEnabled = isEnabled
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = DeletedMessagesTransparencySliderItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? DeletedMessagesTransparencySliderItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private final class DeletedMessagesTransparencySliderItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let leftTextNode: ImmediateTextNode
private let rightTextNode: ImmediateTextNode
private let centerTextNode: ImmediateTextNode
private let slider = ComponentView<Empty>()
private var item: DeletedMessagesTransparencySliderItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.leftTextNode = ImmediateTextNode()
self.rightTextNode = ImmediateTextNode()
self.centerTextNode = ImmediateTextNode()
super.init(layerBacked: false)
self.addSubnode(self.leftTextNode)
self.addSubnode(self.rightTextNode)
self.addSubnode(self.centerTextNode)
}
func asyncLayout() -> (_ item: DeletedMessagesTransparencySliderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
return { item, params, neighbors in
let separatorHeight = UIScreenPixel
let contentSize = CGSize(width: params.width, height: 88.0)
let insets = itemListNeighborsGroupedInsets(neighbors, params)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.item = item
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
strongSelf.bottomStripeNode.isHidden = false
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let sideTextColor = item.theme.list.itemSecondaryTextColor.withAlphaComponent(item.isEnabled ? 1.0 : 0.6)
let centerTextColor = item.isEnabled ? item.theme.list.itemPrimaryTextColor : item.theme.list.itemDisabledTextColor
strongSelf.leftTextNode.attributedText = NSAttributedString(string: "Меньше", font: Font.regular(13.0), textColor: sideTextColor)
strongSelf.rightTextNode.attributedText = NSAttributedString(string: "Больше", font: Font.regular(13.0), textColor: sideTextColor)
strongSelf.centerTextNode.attributedText = NSAttributedString(string: "Прозрачность \(item.value)%", font: Font.regular(16.0), textColor: centerTextColor)
let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 120.0, height: 100.0))
let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 120.0, height: 100.0))
let centerTextSize = strongSelf.centerTextNode.updateLayout(CGSize(width: params.width - params.leftInset - params.rightInset - 60.0, height: 100.0))
let sideInset: CGFloat = 18.0
strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize)
strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize)
strongSelf.centerTextNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - centerTextSize.width) / 2.0), y: 11.0), size: centerTextSize)
let maxRange = CGFloat(maxDeletedMessageTransparencyPercent - minDeletedMessageTransparencyPercent)
let normalizedValue: CGFloat
if maxRange.isZero {
normalizedValue = 0.0
} else {
normalizedValue = CGFloat(item.value - minDeletedMessageTransparencyPercent) / maxRange
}
let sliderSize = strongSelf.slider.update(
transition: .immediate,
component: AnyComponent(
SliderComponent(
content: .continuous(.init(
value: normalizedValue,
minValue: nil,
valueUpdated: { [weak self] value in
guard let self, let item = self.item, item.isEnabled else {
return
}
let transparencyValue = Int32((CGFloat(minDeletedMessageTransparencyPercent) + maxRange * value).rounded())
item.updated(clampDeletedMessageTransparencyPercent(transparencyValue))
}
)),
useNative: true,
trackBackgroundColor: item.theme.list.itemSwitchColors.frameColor,
trackForegroundColor: item.isEnabled ? item.theme.list.itemAccentColor : item.theme.list.itemDisabledTextColor
)
),
environment: {},
containerSize: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0)
)
if let sliderView = strongSelf.slider.view {
if sliderView.superview == nil {
strongSelf.view.addSubview(sliderView)
}
sliderView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - sliderSize.width) / 2.0), y: 36.0), size: sliderSize)
sliderView.isUserInteractionEnabled = item.isEnabled
sliderView.alpha = item.isEnabled ? 1.0 : 0.55
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}