Files
ghostgram/submodules/SettingsUI/Sources/DeletedMessagesController.swift
2026-03-07 18:15:32 +01:00

750 lines
32 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import ItemListUI
import AccountContext
import PresentationDataUtils
import ComponentFlow
import SliderComponent
import AlertUI
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 history(PresentationTheme, String, String)
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 .history:
return 2
case .transparencySlider:
return 3
case .settingsInfo:
return 4
}
}
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 .history(lhsTheme, lhsText, lhsValue):
if case let .history(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 .history(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openHistory()
}
)
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 openHistory: () -> Void
let updateTransparency: (Int32) -> Void
init(
toggleEnabled: @escaping (Bool) -> Void,
toggleArchiveMedia: @escaping (Bool) -> Void,
openHistory: @escaping () -> Void,
updateTransparency: @escaping (Int32) -> Void
) {
self.toggleEnabled = toggleEnabled
self.toggleArchiveMedia = toggleArchiveMedia
self.openHistory = openHistory
self.updateTransparency = updateTransparency
}
}
// MARK: - State
private struct DeletedMessagesControllerState: Equatable {
var isEnabled: Bool
var archiveMedia: Bool
var archivedCount: Int
var transparencyPercent: Int32
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled &&
lhs.archiveMedia == rhs.archiveMedia &&
lhs.archivedCount == rhs.archivedCount &&
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(.history(presentationData.theme, "История удалений", state.archivedCount == 0 ? "Пусто" : "\(state.archivedCount)"))
entries.append(.transparencySlider(presentationData.theme, state.transparencyPercent, state.isEnabled))
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Прозрачность влияет только на сообщения, которые уже помечены как удалённые."))
return entries
}
// MARK: - Controller
public func deletedMessagesController(context: AccountContext) -> ViewController {
var pushControllerImpl: ((ViewController, Bool) -> Void)?
let initialState = DeletedMessagesControllerState(
isEnabled: AntiDeleteManager.shared.isEnabled,
archiveMedia: AntiDeleteManager.shared.archiveMedia,
archivedCount: AntiDeleteManager.shared.archivedCount,
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
}
},
openHistory: {
pushControllerImpl?(deletedMessagesHistoryController(context: context), true)
},
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)
controller.didAppear = { _ in
updateState { state in
var state = state
state.isEnabled = AntiDeleteManager.shared.isEnabled
state.archiveMedia = AntiDeleteManager.shared.archiveMedia
state.archivedCount = AntiDeleteManager.shared.archivedCount
state.transparencyPercent = clampDeletedMessageTransparencyPercent(Int32(round(AntiDeleteManager.shared.deletedMessageTransparency * 100.0)))
return state
}
}
pushControllerImpl = { [weak controller] c, _ in
controller?.push(c)
}
return controller
}
// MARK: - Deleted History
private enum DeletedMessagesHistorySection: Int32 {
case actions
case messages
}
private enum DeletedMessagesHistoryEntry: ItemListNodeEntry {
case clear(PresentationTheme, Bool)
case message(PresentationTheme, Int32, String, String)
case empty(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .clear:
return DeletedMessagesHistorySection.actions.rawValue
case .message, .empty:
return DeletedMessagesHistorySection.messages.rawValue
}
}
var stableId: Int32 {
switch self {
case .clear:
return 0
case let .message(_, index, _, _):
return 1000 + index
case .empty:
return 1
}
}
static func ==(lhs: DeletedMessagesHistoryEntry, rhs: DeletedMessagesHistoryEntry) -> Bool {
switch lhs {
case let .clear(lhsTheme, lhsEnabled):
if case let .clear(rhsTheme, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsEnabled == rhsEnabled {
return true
}
return false
case let .message(lhsTheme, lhsIndex, lhsTitle, lhsLabel):
if case let .message(rhsTheme, rhsIndex, rhsTitle, rhsLabel) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsTitle == rhsTitle, lhsLabel == rhsLabel {
return true
}
return false
case let .empty(lhsTheme, lhsText):
if case let .empty(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
}
}
static func <(lhs: DeletedMessagesHistoryEntry, rhs: DeletedMessagesHistoryEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeletedMessagesHistoryControllerArguments
switch self {
case let .clear(_, enabled):
return ItemListActionItem(
presentationData: presentationData,
systemStyle: .glass,
title: "Очистить историю",
kind: enabled ? .destructive : .disabled,
alignment: .natural,
sectionId: self.section,
style: .blocks,
action: {
if enabled {
arguments.clearHistory()
}
}
)
case let .message(_, _, title, label):
return ItemListTextItem(
presentationData: presentationData,
text: .plain("\(label)\n\(title)"),
sectionId: self.section
)
case let .empty(_, text):
return ItemListTextItem(
presentationData: presentationData,
text: .plain(text),
sectionId: self.section
)
}
}
}
private final class DeletedMessagesHistoryControllerArguments {
let clearHistory: () -> Void
init(clearHistory: @escaping () -> Void) {
self.clearHistory = clearHistory
}
}
private struct DeletedMessagesHistoryControllerState: Equatable {
var messages: [AntiDeleteManager.ArchivedMessage]
static func ==(lhs: DeletedMessagesHistoryControllerState, rhs: DeletedMessagesHistoryControllerState) -> Bool {
if lhs.messages.count != rhs.messages.count {
return false
}
for (lhsMessage, rhsMessage) in zip(lhs.messages, rhs.messages) {
if lhsMessage.globalId != rhsMessage.globalId ||
lhsMessage.peerId != rhsMessage.peerId ||
lhsMessage.messageId != rhsMessage.messageId ||
lhsMessage.timestamp != rhsMessage.timestamp ||
lhsMessage.deletedAt != rhsMessage.deletedAt ||
lhsMessage.authorId != rhsMessage.authorId ||
lhsMessage.text != rhsMessage.text ||
lhsMessage.forwardAuthorId != rhsMessage.forwardAuthorId ||
lhsMessage.mediaDescription != rhsMessage.mediaDescription {
return false
}
}
return true
}
}
private func deletedMessagesHistoryDateString(timestamp: Int32) -> String {
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter.string(from: Date(timeIntervalSince1970: TimeInterval(timestamp)))
}
private func deletedMessagesHistoryEntries(
presentationData: PresentationData,
state: DeletedMessagesHistoryControllerState
) -> [DeletedMessagesHistoryEntry] {
var entries: [DeletedMessagesHistoryEntry] = []
entries.append(.clear(presentationData.theme, !state.messages.isEmpty))
if state.messages.isEmpty {
entries.append(.empty(presentationData.theme, "История удалений пуста."))
return entries
}
for (index, message) in state.messages.enumerated() {
var preview = message.text.trimmingCharacters(in: .whitespacesAndNewlines)
if preview.isEmpty {
preview = message.mediaDescription ?? "Сообщение без текста"
}
preview = preview.replacingOccurrences(of: "\n", with: " ")
if preview.count > 80 {
preview = String(preview.prefix(80)) + "..."
}
let title = "Чат \(message.peerId): \(preview)"
let label = deletedMessagesHistoryDateString(timestamp: message.deletedAt)
entries.append(.message(presentationData.theme, Int32(index), title, label))
}
return entries
}
private func deletedMessagesHistoryController(context: AccountContext) -> ViewController {
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
let initialState = DeletedMessagesHistoryControllerState(
messages: AntiDeleteManager.shared.getAllArchivedMessages()
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((DeletedMessagesHistoryControllerState) -> DeletedMessagesHistoryControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let arguments = DeletedMessagesHistoryControllerArguments(
clearHistory: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alert = textAlertController(
context: context,
title: "Очистить историю?",
text: "Это удалит локально сохранённые удалённые сообщения.",
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: "Очистить", action: {
AntiDeleteManager.shared.clearArchive()
updateState { state in
var state = state
state.messages = AntiDeleteManager.shared.getAllArchivedMessages()
return state
}
})
]
)
presentControllerImpl?(alert, nil)
}
)
let signal: Signal<(ItemListControllerState, (ItemListNodeState, DeletedMessagesHistoryControllerArguments)), NoError> = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, DeletedMessagesHistoryControllerArguments)) in
let entries = deletedMessagesHistoryEntries(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)
controller.didAppear = { _ in
updateState { state in
var state = state
state.messages = AntiDeleteManager.shared.getAllArchivedMessages()
return state
}
}
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
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)
}
}