feat: новые функции, исправлены критические ошибки сборки и баги интерфейса, больше подписей в файлах

This commit is contained in:
ichmagmaus 812
2026-03-04 22:06:16 +01:00
parent a614259289
commit f033954db2
81 changed files with 1256 additions and 298 deletions
@@ -1,12 +1,22 @@
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
@@ -17,6 +27,7 @@ private enum DeletedMessagesSection: Int32 {
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 {
@@ -29,8 +40,10 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
return 0
case .archiveMediaToggle:
return 1
case .settingsInfo:
case .transparencySlider:
return 2
case .settingsInfo:
return 3
}
}
@@ -48,6 +61,12 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
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
@@ -85,6 +104,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
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)
}
@@ -96,13 +125,16 @@ private enum DeletedMessagesEntry: ItemListNodeEntry {
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
toggleArchiveMedia: @escaping (Bool) -> Void,
updateTransparency: @escaping (Int32) -> Void
) {
self.toggleEnabled = toggleEnabled
self.toggleArchiveMedia = toggleArchiveMedia
self.updateTransparency = updateTransparency
}
}
@@ -111,10 +143,12 @@ private final class DeletedMessagesControllerArguments {
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.archiveMedia == rhs.archiveMedia &&
lhs.transparencyPercent == rhs.transparencyPercent
}
}
@@ -128,7 +162,8 @@ private func deletedMessagesControllerEntries(
entries.append(.enableToggle(presentationData.theme, "Сохранять удалённые сообщения", state.isEnabled))
entries.append(.archiveMediaToggle(presentationData.theme, "Архивировать медиа", state.archiveMedia))
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Рядом со временем сообщения появится иконка корзины."))
entries.append(.transparencySlider(presentationData.theme, state.transparencyPercent, state.isEnabled))
entries.append(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Прозрачность влияет только на сообщения, которые уже помечены как удалённые."))
return entries
}
@@ -138,7 +173,8 @@ private func deletedMessagesControllerEntries(
public func deletedMessagesController(context: AccountContext) -> ViewController {
let initialState = DeletedMessagesControllerState(
isEnabled: AntiDeleteManager.shared.isEnabled,
archiveMedia: AntiDeleteManager.shared.archiveMedia
archiveMedia: AntiDeleteManager.shared.archiveMedia,
transparencyPercent: clampDeletedMessageTransparencyPercent(Int32(round(AntiDeleteManager.shared.deletedMessageTransparency * 100.0)))
)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
@@ -163,6 +199,15 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
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
}
}
)
@@ -195,3 +240,225 @@ public func deletedMessagesController(context: AccountContext) -> ViewController
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)
}
}
@@ -19,6 +19,7 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
case misc(PresentationTheme, String, String)
case deviceSpoof(PresentationTheme, String, String)
case voiceMorpher(PresentationTheme, String, String)
case sendDelay(PresentationTheme, String, String)
case info(PresentationTheme, String)
var section: ItemListSectionId {
@@ -37,8 +38,10 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
return 3
case .voiceMorpher:
return 4
case .info:
case .sendDelay:
return 5
case .info:
return 6
}
}
@@ -74,6 +77,12 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
return true
}
return false
case let .sendDelay(lhsTheme, lhsText, lhsValue):
if case let .sendDelay(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
@@ -144,6 +153,17 @@ private enum GhostgramSettingsEntry: ItemListNodeEntry {
arguments.openVoiceMorpher()
}
)
case let .sendDelay(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openSendDelay()
}
)
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
@@ -158,19 +178,22 @@ private final class GhostgramSettingsControllerArguments {
let openMisc: () -> Void
let openDeviceSpoof: () -> Void
let openVoiceMorpher: () -> Void
let openSendDelay: () -> Void
init(
openDeletedMessages: @escaping () -> Void,
openGhostMode: @escaping () -> Void,
openMisc: @escaping () -> Void,
openDeviceSpoof: @escaping () -> Void,
openVoiceMorpher: @escaping () -> Void
openVoiceMorpher: @escaping () -> Void,
openSendDelay: @escaping () -> Void
) {
self.openDeletedMessages = openDeletedMessages
self.openGhostMode = openGhostMode
self.openMisc = openMisc
self.openDeviceSpoof = openDeviceSpoof
self.openVoiceMorpher = openVoiceMorpher
self.openSendDelay = openSendDelay
}
}
@@ -184,6 +207,8 @@ private struct GhostgramSettingsState: Equatable {
var miscActiveCount: Int
var deviceSpoofEnabled: Bool
var voiceMorpherEnabled: Bool
var voiceMorpherPresetName: String
var sendDelayEnabled: Bool
static func current() -> GhostgramSettingsState {
return GhostgramSettingsState(
@@ -193,7 +218,9 @@ private struct GhostgramSettingsState: Equatable {
miscEnabled: MiscSettingsManager.shared.isEnabled,
miscActiveCount: MiscSettingsManager.shared.activeFeatureCount,
deviceSpoofEnabled: DeviceSpoofManager.shared.isEnabled,
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled,
voiceMorpherPresetName: VoiceMorpherManager.shared.selectedPreset.name,
sendDelayEnabled: SendDelayManager.shared.isEnabled
)
}
}
@@ -223,9 +250,13 @@ private func ghostgramSettingsControllerEntries(
entries.append(.deviceSpoof(presentationData.theme, "Подмена устройства", deviceSpoofStatus))
// Voice Morpher
let voiceMorpherStatus = state.voiceMorpherEnabled ? VoiceMorpherManager.shared.selectedPreset.name : "Выкл"
let voiceMorpherStatus = state.voiceMorpherEnabled ? state.voiceMorpherPresetName : "Выкл"
entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus))
// Send Delay
let sendDelayStatus = state.sendDelayEnabled ? "Вкл" : "Выкл"
entries.append(.sendDelay(presentationData.theme, "Отложка сообщений", sendDelayStatus))
// Info
entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое."))
@@ -255,6 +286,9 @@ public func ghostgramSettingsController(context: AccountContext) -> ViewControll
},
openVoiceMorpher: {
pushControllerImpl?(voiceMorpherController(context: context), true)
},
openSendDelay: {
pushControllerImpl?(sendDelayController(context: context), true)
}
)
@@ -328,7 +328,7 @@ public func miscController(context: AccountContext) -> ViewController {
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("Misc"),
title: .text("Прочее"),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
@@ -90,7 +90,7 @@ class NotificationSearchItemNode: ListViewItemNode {
required init() {
self.searchBarNode = SearchBarPlaceholderNode()
super.init(layerBacked: false, dynamicBounce: false)
super.init(layerBacked: false)
self.addSubnode(self.searchBarNode)
}
@@ -104,16 +104,16 @@ class NotificationSearchItemNode: ListViewItemNode {
}
func asyncLayout() -> (_ item: NotificationSearchItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let searchBarNodeLayout = self.searchBarNode.asyncLayout()
let placeholder = self.placeholder
return { item, params in
let baseWidth = params.width - params.leftInset - params.rightInset
let backgroundColor = item.theme.chatList.itemBackgroundColor
let iconColor = UIColor(rgb: 0x8e8e93)
let controlColor = item.theme.chat.inputPanel.panelControlColor
let placeholderString = NSAttributedString(string: placeholder ?? "", font: searchBarFont, textColor: UIColor(rgb: 0x8e8e93))
let (_, searchBarApply) = searchBarNodeLayout(placeholderString, placeholderString, CGSize(width: baseWidth - 16.0, height: 28.0), 1.0, UIColor(rgb: 0x8e8e93), item.theme.chatList.regularSearchBarColor, backgroundColor, .immediate)
let layout = ListViewItemNodeLayout(contentSize: CGSize(width: params.width, height: 44.0), insets: UIEdgeInsets())
@@ -126,10 +126,9 @@ class NotificationSearchItemNode: ListViewItemNode {
transition = .immediate
}
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: CGSize(width: baseWidth - 16.0, height: 28.0))
searchBarApply()
strongSelf.searchBarNode.bounds = CGRect(origin: CGPoint(), size: CGSize(width: baseWidth - 16.0, height: 28.0))
let searchBarSize = CGSize(width: baseWidth - 16.0, height: 28.0)
strongSelf.searchBarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 8.0, y: 8.0), size: searchBarSize)
_ = strongSelf.searchBarNode.updateLayout(placeholderString: placeholderString, compactPlaceholderString: placeholderString, constrainedSize: searchBarSize, expansionProgress: 1.0, iconColor: iconColor, foregroundColor: item.theme.chatList.regularSearchBarColor, backgroundColor: backgroundColor, controlColor: controlColor, transition: transition)
transition.updateBackgroundColor(node: strongSelf, color: backgroundColor)
}
@@ -0,0 +1,148 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Section / Entry definitions
private enum SendDelaySection: Int32 {
case main
}
private enum SendDelayEntry: ItemListNodeEntry {
case toggle(PresentationTheme, String, Bool)
case info(PresentationTheme, String)
var section: ItemListSectionId {
return SendDelaySection.main.rawValue
}
var stableId: Int32 {
switch self {
case .toggle: return 0
case .info: return 1
}
}
static func ==(lhs: SendDelayEntry, rhs: SendDelayEntry) -> Bool {
switch lhs {
case let .toggle(lhsTheme, lhsText, lhsValue):
if case let .toggle(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .info(lhsTheme, lhsText):
if case let .info(rhsTheme, rhsText) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
}
}
static func <(lhs: SendDelayEntry, rhs: SendDelayEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! SendDelayControllerArguments
switch self {
case let .toggle(_, text, value):
return ItemListSwitchItem(
presentationData: presentationData,
title: text,
value: value,
sectionId: self.section,
style: .blocks,
updated: { newValue in
arguments.toggleEnabled(newValue)
}
)
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
// MARK: - Arguments
private final class SendDelayControllerArguments {
let toggleEnabled: (Bool) -> Void
init(toggleEnabled: @escaping (Bool) -> Void) {
self.toggleEnabled = toggleEnabled
}
}
// MARK: - State
private struct SendDelayControllerState: Equatable {
var isEnabled: Bool
}
// MARK: - Entries builder
private func sendDelayControllerEntries(
presentationData: PresentationData,
state: SendDelayControllerState
) -> [SendDelayEntry] {
let theme = presentationData.theme
return [
.toggle(theme, "Использовать отложку", state.isEnabled),
.info(theme, "Автоматически ставит задержку в ~12 секунд (дольше для сообщений с вложениями) при отправке сообщений. При использовании этой функции вы не будете появляться в сети.")
]
}
// MARK: - Controller
public func sendDelayController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(
SendDelayControllerState(isEnabled: SendDelayManager.shared.isEnabled),
ignoreRepeated: true
)
let stateValue = Atomic(value: SendDelayControllerState(isEnabled: SendDelayManager.shared.isEnabled))
let updateState: ((inout SendDelayControllerState) -> Void) -> Void = { f in
let result = stateValue.modify { state in
var s = state; f(&s); return s
}
statePromise.set(result)
}
let arguments = SendDelayControllerArguments(
toggleEnabled: { value in
SendDelayManager.shared.isEnabled = value
updateState { $0.isEnabled = value }
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = sendDelayControllerEntries(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: true
)
return (controllerState, (listState, arguments))
}
return ItemListController(context: context, state: signal)
}