feat: add Ghostgram features

- Anti-Delete: save deleted messages locally
- Ghost Mode: hide online status, read receipts
- Voice Morpher: audio processing effects
- Device Spoof: spoof device info
- Custom GhostIcon app icon
- User Notes: personal notes for contacts
- Misc settings and controllers
- GPLv2 License
This commit is contained in:
ichmagmaus 812
2026-01-19 12:01:00 +01:00
parent b61c8870fd
commit 03f35f40b5
42 changed files with 3659 additions and 0 deletions
@@ -0,0 +1,197 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import Postbox
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Entry Definition
private enum DeletedMessagesSection: Int32 {
case settings
}
private enum DeletedMessagesEntry: ItemListNodeEntry {
case enableToggle(PresentationTheme, String, Bool)
case archiveMediaToggle(PresentationTheme, String, 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 .settingsInfo:
return 2
}
}
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 .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 .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
init(
toggleEnabled: @escaping (Bool) -> Void,
toggleArchiveMedia: @escaping (Bool) -> Void
) {
self.toggleEnabled = toggleEnabled
self.toggleArchiveMedia = toggleArchiveMedia
}
}
// MARK: - State
private struct DeletedMessagesControllerState: Equatable {
var isEnabled: Bool
var archiveMedia: Bool
static func ==(lhs: DeletedMessagesControllerState, rhs: DeletedMessagesControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled &&
lhs.archiveMedia == rhs.archiveMedia
}
}
// 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(.settingsInfo(presentationData.theme, "Когда включено, сообщения, удалённые другими пользователями, будут сохраняться локально. Рядом со временем сообщения появится иконка корзины."))
return entries
}
// MARK: - Controller
public func deletedMessagesController(context: AccountContext) -> ViewController {
let initialState = DeletedMessagesControllerState(
isEnabled: AntiDeleteManager.shared.isEnabled,
archiveMedia: AntiDeleteManager.shared.archiveMedia
)
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
}
}
)
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
}
@@ -0,0 +1,292 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Entry Definition
private enum DeviceSpoofSection: Int32 {
case enable
case profiles
case custom
}
private enum DeviceSpoofEntry: ItemListNodeEntry {
case enableHeader(PresentationTheme, String)
case enableToggle(PresentationTheme, String, Bool)
case enableInfo(PresentationTheme, String)
case profilesHeader(PresentationTheme, String)
case profile(PresentationTheme, Int, String, Bool)
case customHeader(PresentationTheme, String)
case customDeviceModel(PresentationTheme, String, String)
case customSystemVersion(PresentationTheme, String, String)
case customInfo(PresentationTheme, String)
var section: ItemListSectionId {
switch self {
case .enableHeader, .enableToggle, .enableInfo:
return DeviceSpoofSection.enable.rawValue
case .profilesHeader, .profile:
return DeviceSpoofSection.profiles.rawValue
case .customHeader, .customDeviceModel, .customSystemVersion, .customInfo:
return DeviceSpoofSection.custom.rawValue
}
}
var stableId: Int32 {
switch self {
case .enableHeader: return 0
case .enableToggle: return 1
case .enableInfo: return 2
case .profilesHeader: return 3
case let .profile(_, id, _, _): return 10 + Int32(id)
case .customHeader: return 500
case .customDeviceModel: return 501
case .customSystemVersion: return 502
case .customInfo: return 503
}
}
static func ==(lhs: DeviceSpoofEntry, rhs: DeviceSpoofEntry) -> Bool {
switch lhs {
case let .enableHeader(lhsTheme, lhsText):
if case let .enableHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
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 .enableInfo(lhsTheme, lhsText):
if case let .enableInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .profilesHeader(lhsTheme, lhsText):
if case let .profilesHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .profile(lhsTheme, lhsId, lhsName, lhsSelected):
if case let .profile(rhsTheme, rhsId, rhsName, rhsSelected) = rhs,
lhsTheme === rhsTheme, lhsId == rhsId, lhsName == rhsName, lhsSelected == rhsSelected {
return true
}
return false
case let .customHeader(lhsTheme, lhsText):
if case let .customHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .customDeviceModel(lhsTheme, lhsTitle, lhsValue):
if case let .customDeviceModel(rhsTheme, rhsTitle, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
}
return false
case let .customSystemVersion(lhsTheme, lhsTitle, lhsValue):
if case let .customSystemVersion(rhsTheme, rhsTitle, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsTitle == rhsTitle, lhsValue == rhsValue {
return true
}
return false
case let .customInfo(lhsTheme, lhsText):
if case let .customInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
}
}
static func <(lhs: DeviceSpoofEntry, rhs: DeviceSpoofEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! DeviceSpoofControllerArguments
switch self {
case let .enableHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
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 .enableInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .profilesHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .profile(_, id, name, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: name, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectProfile(id)
})
case let .customHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .customDeviceModel(_, title, value):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title), text: value, placeholder: "iPhone 14 Pro", sectionId: self.section, textUpdated: { text in
arguments.updateCustomDeviceModel(text)
}, action: {})
case let .customSystemVersion(_, title, value):
return ItemListSingleLineInputItem(presentationData: presentationData, title: NSAttributedString(string: title), text: value, placeholder: "iOS 17.2", sectionId: self.section, textUpdated: { text in
arguments.updateCustomSystemVersion(text)
}, action: {})
case let .customInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
// MARK: - Arguments
private final class DeviceSpoofControllerArguments {
let toggleEnabled: (Bool) -> Void
let selectProfile: (Int) -> Void
let updateCustomDeviceModel: (String) -> Void
let updateCustomSystemVersion: (String) -> Void
init(
toggleEnabled: @escaping (Bool) -> Void,
selectProfile: @escaping (Int) -> Void,
updateCustomDeviceModel: @escaping (String) -> Void,
updateCustomSystemVersion: @escaping (String) -> Void
) {
self.toggleEnabled = toggleEnabled
self.selectProfile = selectProfile
self.updateCustomDeviceModel = updateCustomDeviceModel
self.updateCustomSystemVersion = updateCustomSystemVersion
}
}
// MARK: - State
private struct DeviceSpoofControllerState: Equatable {
var isEnabled: Bool
var selectedProfileId: Int
var customDeviceModel: String
var customSystemVersion: String
}
// MARK: - Entries Builder
private func deviceSpoofControllerEntries(presentationData: PresentationData, state: DeviceSpoofControllerState) -> [DeviceSpoofEntry] {
var entries: [DeviceSpoofEntry] = []
let theme = presentationData.theme
entries.append(.enableHeader(theme, "ПОДМЕНА УСТРОЙСТВА"))
entries.append(.enableToggle(theme, "Включить подмену", state.isEnabled))
entries.append(.enableInfo(theme, "Изменяет информацию об устройстве для серверов Telegram. Требуется перезапуск приложения."))
entries.append(.profilesHeader(theme, "ВЫБЕРИТЕ УСТРОЙСТВО"))
for profile in DeviceSpoofManager.profiles {
let isSelected = profile.id == state.selectedProfileId
entries.append(.profile(theme, profile.id, profile.name, isSelected))
}
// Show custom input fields only when custom profile is selected
if state.selectedProfileId == 100 {
entries.append(.customHeader(theme, "СВОЁ УСТРОЙСТВО"))
entries.append(.customDeviceModel(theme, "Модель: ", state.customDeviceModel))
entries.append(.customSystemVersion(theme, "Система: ", state.customSystemVersion))
// Warning if fields are empty
if state.customDeviceModel.isEmpty || state.customSystemVersion.isEmpty {
entries.append(.customInfo(theme, "⚠️ Заполните оба поля. Пока поля пустые — используется реальное устройство."))
} else {
entries.append(.customInfo(theme, "Перезапустите приложение для применения."))
}
}
return entries
}
// MARK: - Controller
public func deviceSpoofController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(
DeviceSpoofControllerState(
isEnabled: DeviceSpoofManager.shared.isEnabled,
selectedProfileId: DeviceSpoofManager.shared.selectedProfileId,
customDeviceModel: DeviceSpoofManager.shared.customDeviceModel,
customSystemVersion: DeviceSpoofManager.shared.customSystemVersion
),
ignoreRepeated: true
)
let stateValue = Atomic(value: DeviceSpoofControllerState(
isEnabled: DeviceSpoofManager.shared.isEnabled,
selectedProfileId: DeviceSpoofManager.shared.selectedProfileId,
customDeviceModel: DeviceSpoofManager.shared.customDeviceModel,
customSystemVersion: DeviceSpoofManager.shared.customSystemVersion
))
let updateState: ((inout DeviceSpoofControllerState) -> Void) -> Void = { f in
let result = stateValue.modify { state in
var state = state
f(&state)
return state
}
statePromise.set(result)
}
let arguments = DeviceSpoofControllerArguments(
toggleEnabled: { value in
DeviceSpoofManager.shared.isEnabled = value
updateState { state in
state.isEnabled = value
}
},
selectProfile: { id in
DeviceSpoofManager.shared.selectedProfileId = id
updateState { state in
state.selectedProfileId = id
}
},
updateCustomDeviceModel: { text in
DeviceSpoofManager.shared.customDeviceModel = text
updateState { state in
state.customDeviceModel = text
}
},
updateCustomSystemVersion: { text in
DeviceSpoofManager.shared.customSystemVersion = text
updateState { state in
state.customSystemVersion = text
}
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = deviceSpoofControllerEntries(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
}
@@ -0,0 +1,331 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Entry Definition
private enum GhostModeSection: Int32 {
case master
case features
}
private enum GhostModeEntry: ItemListNodeEntry {
case masterHeader(PresentationTheme, String)
case masterToggle(PresentationTheme, String, Bool, Int, Int) // title, isOn, activeCount, totalCount
case masterInfo(PresentationTheme, String)
case featuresHeader(PresentationTheme, String)
case hideReadReceipts(PresentationTheme, String, Bool)
case hideStoryViews(PresentationTheme, String, Bool)
case hideOnlineStatus(PresentationTheme, String, Bool)
case hideTypingIndicator(PresentationTheme, String, Bool)
case forceOffline(PresentationTheme, String, Bool)
var section: ItemListSectionId {
switch self {
case .masterHeader, .masterToggle, .masterInfo:
return GhostModeSection.master.rawValue
case .featuresHeader, .hideReadReceipts, .hideStoryViews, .hideOnlineStatus, .hideTypingIndicator, .forceOffline:
return GhostModeSection.features.rawValue
}
}
var stableId: Int32 {
switch self {
case .masterHeader: return 0
case .masterToggle: return 1
case .masterInfo: return 2
case .featuresHeader: return 3
case .hideReadReceipts: return 4
case .hideStoryViews: return 5
case .hideOnlineStatus: return 6
case .hideTypingIndicator: return 7
case .forceOffline: return 8
}
}
static func ==(lhs: GhostModeEntry, rhs: GhostModeEntry) -> Bool {
switch lhs {
case let .masterHeader(lhsTheme, lhsText):
if case let .masterHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .masterToggle(lhsTheme, lhsText, lhsValue, lhsActive, lhsTotal):
if case let .masterToggle(rhsTheme, rhsText, rhsValue, rhsActive, rhsTotal) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsActive == rhsActive, lhsTotal == rhsTotal {
return true
}
return false
case let .masterInfo(lhsTheme, lhsText):
if case let .masterInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .featuresHeader(lhsTheme, lhsText):
if case let .featuresHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .hideReadReceipts(lhsTheme, lhsText, lhsValue):
if case let .hideReadReceipts(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .hideStoryViews(lhsTheme, lhsText, lhsValue):
if case let .hideStoryViews(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .hideOnlineStatus(lhsTheme, lhsText, lhsValue):
if case let .hideOnlineStatus(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .hideTypingIndicator(lhsTheme, lhsText, lhsValue):
if case let .hideTypingIndicator(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .forceOffline(lhsTheme, lhsText, lhsValue):
if case let .forceOffline(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
}
}
static func <(lhs: GhostModeEntry, rhs: GhostModeEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GhostModeControllerArguments
switch self {
case let .masterHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .masterToggle(_, text, value, activeCount, totalCount):
let title = "\(text) \(activeCount)/\(totalCount)"
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleMaster(value)
})
case let .masterInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .featuresHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .hideReadReceipts(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleHideReadReceipts()
})
case let .hideStoryViews(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleHideStoryViews()
})
case let .hideOnlineStatus(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleHideOnlineStatus()
})
case let .hideTypingIndicator(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleHideTypingIndicator()
})
case let .forceOffline(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleForceOffline()
})
}
}
}
// MARK: - Arguments
private final class GhostModeControllerArguments {
let toggleMaster: (Bool) -> Void
let toggleHideReadReceipts: () -> Void
let toggleHideStoryViews: () -> Void
let toggleHideOnlineStatus: () -> Void
let toggleHideTypingIndicator: () -> Void
let toggleForceOffline: () -> Void
init(
toggleMaster: @escaping (Bool) -> Void,
toggleHideReadReceipts: @escaping () -> Void,
toggleHideStoryViews: @escaping () -> Void,
toggleHideOnlineStatus: @escaping () -> Void,
toggleHideTypingIndicator: @escaping () -> Void,
toggleForceOffline: @escaping () -> Void
) {
self.toggleMaster = toggleMaster
self.toggleHideReadReceipts = toggleHideReadReceipts
self.toggleHideStoryViews = toggleHideStoryViews
self.toggleHideOnlineStatus = toggleHideOnlineStatus
self.toggleHideTypingIndicator = toggleHideTypingIndicator
self.toggleForceOffline = toggleForceOffline
}
}
// MARK: - State
private struct GhostModeControllerState: Equatable {
var isEnabled: Bool
var hideReadReceipts: Bool
var hideStoryViews: Bool
var hideOnlineStatus: Bool
var hideTypingIndicator: Bool
var forceOffline: Bool
static func ==(lhs: GhostModeControllerState, rhs: GhostModeControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled &&
lhs.hideReadReceipts == rhs.hideReadReceipts &&
lhs.hideStoryViews == rhs.hideStoryViews &&
lhs.hideOnlineStatus == rhs.hideOnlineStatus &&
lhs.hideTypingIndicator == rhs.hideTypingIndicator &&
lhs.forceOffline == rhs.forceOffline
}
}
// MARK: - Entries Builder
private func ghostModeControllerEntries(presentationData: PresentationData, state: GhostModeControllerState) -> [GhostModeEntry] {
var entries: [GhostModeEntry] = []
let theme = presentationData.theme
// Count active features
var activeCount = 0
if state.hideReadReceipts { activeCount += 1 }
if state.hideStoryViews { activeCount += 1 }
if state.hideOnlineStatus { activeCount += 1 }
if state.hideTypingIndicator { activeCount += 1 }
if state.forceOffline { activeCount += 1 }
// Master section
entries.append(.masterHeader(theme, "РЕЖИМ ПРИЗРАКА"))
entries.append(.masterToggle(theme, "Режим призрака", state.isEnabled, activeCount, 5))
entries.append(.masterInfo(theme, "Когда включен, выбранные функции приватности будут активны."))
// Features section
entries.append(.featuresHeader(theme, "ФУНКЦИИ"))
entries.append(.hideReadReceipts(theme, "Не читать сообщения", state.hideReadReceipts))
entries.append(.hideStoryViews(theme, "Не читать истории", state.hideStoryViews))
entries.append(.hideOnlineStatus(theme, "Не отправлять «онлайн»", state.hideOnlineStatus))
entries.append(.hideTypingIndicator(theme, "Не отправлять «печатает»", state.hideTypingIndicator))
entries.append(.forceOffline(theme, "Автоматический «офлайн»", state.forceOffline))
return entries
}
// MARK: - Controller
public func ghostModeController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(
GhostModeControllerState(
isEnabled: GhostModeManager.shared.isEnabled,
hideReadReceipts: GhostModeManager.shared.hideReadReceipts,
hideStoryViews: GhostModeManager.shared.hideStoryViews,
hideOnlineStatus: GhostModeManager.shared.hideOnlineStatus,
hideTypingIndicator: GhostModeManager.shared.hideTypingIndicator,
forceOffline: GhostModeManager.shared.forceOffline
),
ignoreRepeated: true
)
let stateValue = Atomic(value: GhostModeControllerState(
isEnabled: GhostModeManager.shared.isEnabled,
hideReadReceipts: GhostModeManager.shared.hideReadReceipts,
hideStoryViews: GhostModeManager.shared.hideStoryViews,
hideOnlineStatus: GhostModeManager.shared.hideOnlineStatus,
hideTypingIndicator: GhostModeManager.shared.hideTypingIndicator,
forceOffline: GhostModeManager.shared.forceOffline
))
let updateState: ((inout GhostModeControllerState) -> Void) -> Void = { f in
let result = stateValue.modify { state in
var state = state
f(&state)
return state
}
statePromise.set(result)
}
let arguments = GhostModeControllerArguments(
toggleMaster: { value in
GhostModeManager.shared.isEnabled = value
updateState { state in
state.isEnabled = value
}
},
toggleHideReadReceipts: {
let newValue = !GhostModeManager.shared.hideReadReceipts
GhostModeManager.shared.hideReadReceipts = newValue
updateState { state in
state.hideReadReceipts = newValue
}
},
toggleHideStoryViews: {
let newValue = !GhostModeManager.shared.hideStoryViews
GhostModeManager.shared.hideStoryViews = newValue
updateState { state in
state.hideStoryViews = newValue
}
},
toggleHideOnlineStatus: {
let newValue = !GhostModeManager.shared.hideOnlineStatus
GhostModeManager.shared.hideOnlineStatus = newValue
updateState { state in
state.hideOnlineStatus = newValue
}
},
toggleHideTypingIndicator: {
let newValue = !GhostModeManager.shared.hideTypingIndicator
GhostModeManager.shared.hideTypingIndicator = newValue
updateState { state in
state.hideTypingIndicator = newValue
}
},
toggleForceOffline: {
let newValue = !GhostModeManager.shared.forceOffline
GhostModeManager.shared.forceOffline = newValue
updateState { state in
state.forceOffline = newValue
}
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = ghostModeControllerEntries(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))
}
let controller = ItemListController(context: context, state: signal)
return controller
}
@@ -0,0 +1,301 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Entry Definition
private enum GhostgramSettingsSection: Int32 {
case features
}
private enum GhostgramSettingsEntry: ItemListNodeEntry {
case deletedMessages(PresentationTheme, String, String)
case ghostMode(PresentationTheme, String, String)
case misc(PresentationTheme, String, String)
case deviceSpoof(PresentationTheme, String, String)
case voiceMorpher(PresentationTheme, String, String)
case info(PresentationTheme, String)
var section: ItemListSectionId {
return GhostgramSettingsSection.features.rawValue
}
var stableId: Int32 {
switch self {
case .deletedMessages:
return 0
case .ghostMode:
return 1
case .misc:
return 2
case .deviceSpoof:
return 3
case .voiceMorpher:
return 4
case .info:
return 5
}
}
static func ==(lhs: GhostgramSettingsEntry, rhs: GhostgramSettingsEntry) -> Bool {
switch lhs {
case let .deletedMessages(lhsTheme, lhsText, lhsValue):
if case let .deletedMessages(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .ghostMode(lhsTheme, lhsText, lhsValue):
if case let .ghostMode(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .misc(lhsTheme, lhsText, lhsValue):
if case let .misc(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .deviceSpoof(lhsTheme, lhsText, lhsValue):
if case let .deviceSpoof(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .voiceMorpher(lhsTheme, lhsText, lhsValue):
if case let .voiceMorpher(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: GhostgramSettingsEntry, rhs: GhostgramSettingsEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! GhostgramSettingsControllerArguments
switch self {
case let .deletedMessages(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openDeletedMessages()
}
)
case let .ghostMode(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openGhostMode()
}
)
case let .misc(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openMisc()
}
)
case let .deviceSpoof(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openDeviceSpoof()
}
)
case let .voiceMorpher(_, text, value):
return ItemListDisclosureItem(
presentationData: presentationData,
title: text,
label: value,
sectionId: self.section,
style: .blocks,
action: {
arguments.openVoiceMorpher()
}
)
case let .info(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
}
}
}
// MARK: - Arguments
private final class GhostgramSettingsControllerArguments {
let openDeletedMessages: () -> Void
let openGhostMode: () -> Void
let openMisc: () -> Void
let openDeviceSpoof: () -> Void
let openVoiceMorpher: () -> Void
init(
openDeletedMessages: @escaping () -> Void,
openGhostMode: @escaping () -> Void,
openMisc: @escaping () -> Void,
openDeviceSpoof: @escaping () -> Void,
openVoiceMorpher: @escaping () -> Void
) {
self.openDeletedMessages = openDeletedMessages
self.openGhostMode = openGhostMode
self.openMisc = openMisc
self.openDeviceSpoof = openDeviceSpoof
self.openVoiceMorpher = openVoiceMorpher
}
}
// MARK: - State
private struct GhostgramSettingsState: Equatable {
var deletedMessagesEnabled: Bool
var ghostModeEnabled: Bool
var ghostModeActiveCount: Int
var miscEnabled: Bool
var miscActiveCount: Int
var deviceSpoofEnabled: Bool
var voiceMorpherEnabled: Bool
static func current() -> GhostgramSettingsState {
return GhostgramSettingsState(
deletedMessagesEnabled: AntiDeleteManager.shared.isEnabled,
ghostModeEnabled: GhostModeManager.shared.isEnabled,
ghostModeActiveCount: GhostModeManager.shared.activeFeatureCount,
miscEnabled: MiscSettingsManager.shared.isEnabled,
miscActiveCount: MiscSettingsManager.shared.activeFeatureCount,
deviceSpoofEnabled: DeviceSpoofManager.shared.isEnabled,
voiceMorpherEnabled: VoiceMorpherManager.shared.isEnabled
)
}
}
// MARK: - Entries builder
private func ghostgramSettingsControllerEntries(
presentationData: PresentationData,
state: GhostgramSettingsState
) -> [GhostgramSettingsEntry] {
var entries: [GhostgramSettingsEntry] = []
// Deleted Messages
let deletedStatus = state.deletedMessagesEnabled ? "Вкл" : "Выкл"
entries.append(.deletedMessages(presentationData.theme, "Удалённые сообщения", deletedStatus))
// Ghost Mode
let ghostModeStatus = state.ghostModeEnabled ? "\(state.ghostModeActiveCount)/5" : "Выкл"
entries.append(.ghostMode(presentationData.theme, "Режим призрака", ghostModeStatus))
// Misc
let miscStatus = state.miscEnabled ? "\(state.miscActiveCount)/5" : "Выкл"
entries.append(.misc(presentationData.theme, "Прочее", miscStatus))
// Device Spoofing
let deviceSpoofStatus = state.deviceSpoofEnabled ? "Вкл" : "Выкл"
entries.append(.deviceSpoof(presentationData.theme, "Подмена устройства", deviceSpoofStatus))
// Voice Morpher
let voiceMorpherStatus = state.voiceMorpherEnabled ? VoiceMorpherManager.shared.selectedPreset.name : "Выкл"
entries.append(.voiceMorpher(presentationData.theme, "Голосовой двойник", voiceMorpherStatus))
// Info
entries.append(.info(presentationData.theme, "Функции конфиденциальности Ghostgram. Скрытые отметки о прочтении, обход исчезающих сообщений, обход защиты от пересылки и другое."))
return entries
}
// MARK: - Controller
public func ghostgramSettingsController(context: AccountContext) -> ViewController {
var pushControllerImpl: ((ViewController, Bool) -> Void)?
let stateValue = Atomic(value: GhostgramSettingsState.current())
let statePromise = ValuePromise(GhostgramSettingsState.current(), ignoreRepeated: true)
let arguments = GhostgramSettingsControllerArguments(
openDeletedMessages: {
pushControllerImpl?(deletedMessagesController(context: context), true)
},
openGhostMode: {
pushControllerImpl?(ghostModeController(context: context), true)
},
openMisc: {
pushControllerImpl?(miscController(context: context), true)
},
openDeviceSpoof: {
pushControllerImpl?(deviceSpoofController(context: context), true)
},
openVoiceMorpher: {
pushControllerImpl?(voiceMorpherController(context: context), true)
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = ghostgramSettingsControllerEntries(presentationData: presentationData, state: state)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("Ghostgram"),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back),
animateChanges: true
)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
animateChanges: true
)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
// Refresh state when view appears
controller.visibleBottomContentOffsetChanged = { _ in }
controller.didAppear = { _ in
let newState = GhostgramSettingsState.current()
let _ = stateValue.modify { _ in newState }
statePromise.set(newState)
}
pushControllerImpl = { [weak controller] c, animated in
controller?.push(c)
}
return controller
}
@@ -0,0 +1,328 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Entry Definition
private enum MiscSection: Int32 {
case master
case features
}
private enum MiscEntry: ItemListNodeEntry {
case masterHeader(PresentationTheme, String)
case masterToggle(PresentationTheme, String, Bool, Int, Int)
case masterInfo(PresentationTheme, String)
case featuresHeader(PresentationTheme, String)
case bypassCopyProtection(PresentationTheme, String, Bool)
case disableViewOnceAutoDelete(PresentationTheme, String, Bool)
case bypassScreenshotProtection(PresentationTheme, String, Bool)
case blockAds(PresentationTheme, String, Bool)
case alwaysOnline(PresentationTheme, String, Bool)
var section: ItemListSectionId {
switch self {
case .masterHeader, .masterToggle, .masterInfo:
return MiscSection.master.rawValue
case .featuresHeader, .bypassCopyProtection, .disableViewOnceAutoDelete, .bypassScreenshotProtection, .blockAds, .alwaysOnline:
return MiscSection.features.rawValue
}
}
var stableId: Int32 {
switch self {
case .masterHeader: return 0
case .masterToggle: return 1
case .masterInfo: return 2
case .featuresHeader: return 3
case .bypassCopyProtection: return 4
case .disableViewOnceAutoDelete: return 5
case .bypassScreenshotProtection: return 6
case .blockAds: return 7
case .alwaysOnline: return 8
}
}
static func ==(lhs: MiscEntry, rhs: MiscEntry) -> Bool {
switch lhs {
case let .masterHeader(lhsTheme, lhsText):
if case let .masterHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .masterToggle(lhsTheme, lhsText, lhsValue, lhsActive, lhsTotal):
if case let .masterToggle(rhsTheme, rhsText, rhsValue, rhsActive, rhsTotal) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsActive == rhsActive, lhsTotal == rhsTotal {
return true
}
return false
case let .masterInfo(lhsTheme, lhsText):
if case let .masterInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .featuresHeader(lhsTheme, lhsText):
if case let .featuresHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .bypassCopyProtection(lhsTheme, lhsText, lhsValue):
if case let .bypassCopyProtection(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .disableViewOnceAutoDelete(lhsTheme, lhsText, lhsValue):
if case let .disableViewOnceAutoDelete(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .bypassScreenshotProtection(lhsTheme, lhsText, lhsValue):
if case let .bypassScreenshotProtection(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .blockAds(lhsTheme, lhsText, lhsValue):
if case let .blockAds(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
case let .alwaysOnline(lhsTheme, lhsText, lhsValue):
if case let .alwaysOnline(rhsTheme, rhsText, rhsValue) = rhs,
lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
}
return false
}
}
static func <(lhs: MiscEntry, rhs: MiscEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! MiscControllerArguments
switch self {
case let .masterHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .masterToggle(_, text, value, activeCount, totalCount):
let title = "\(text) \(activeCount)/\(totalCount)"
return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: self.section, style: .blocks, updated: { value in
arguments.toggleMaster(value)
})
case let .masterInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .featuresHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .bypassCopyProtection(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleBypassCopyProtection()
})
case let .disableViewOnceAutoDelete(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleDisableViewOnceAutoDelete()
})
case let .bypassScreenshotProtection(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleBypassScreenshotProtection()
})
case let .blockAds(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleBlockAds()
})
case let .alwaysOnline(_, text, value):
return ItemListCheckboxItem(presentationData: presentationData, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.toggleAlwaysOnline()
})
}
}
}
// MARK: - Arguments
private final class MiscControllerArguments {
let toggleMaster: (Bool) -> Void
let toggleBypassCopyProtection: () -> Void
let toggleDisableViewOnceAutoDelete: () -> Void
let toggleBypassScreenshotProtection: () -> Void
let toggleBlockAds: () -> Void
let toggleAlwaysOnline: () -> Void
init(
toggleMaster: @escaping (Bool) -> Void,
toggleBypassCopyProtection: @escaping () -> Void,
toggleDisableViewOnceAutoDelete: @escaping () -> Void,
toggleBypassScreenshotProtection: @escaping () -> Void,
toggleBlockAds: @escaping () -> Void,
toggleAlwaysOnline: @escaping () -> Void
) {
self.toggleMaster = toggleMaster
self.toggleBypassCopyProtection = toggleBypassCopyProtection
self.toggleDisableViewOnceAutoDelete = toggleDisableViewOnceAutoDelete
self.toggleBypassScreenshotProtection = toggleBypassScreenshotProtection
self.toggleBlockAds = toggleBlockAds
self.toggleAlwaysOnline = toggleAlwaysOnline
}
}
// MARK: - State
private struct MiscControllerState: Equatable {
var isEnabled: Bool
var bypassCopyProtection: Bool
var disableViewOnceAutoDelete: Bool
var bypassScreenshotProtection: Bool
var blockAds: Bool
var alwaysOnline: Bool
static func ==(lhs: MiscControllerState, rhs: MiscControllerState) -> Bool {
return lhs.isEnabled == rhs.isEnabled &&
lhs.bypassCopyProtection == rhs.bypassCopyProtection &&
lhs.disableViewOnceAutoDelete == rhs.disableViewOnceAutoDelete &&
lhs.bypassScreenshotProtection == rhs.bypassScreenshotProtection &&
lhs.blockAds == rhs.blockAds &&
lhs.alwaysOnline == rhs.alwaysOnline
}
}
// MARK: - Entries Builder
private func miscControllerEntries(presentationData: PresentationData, state: MiscControllerState) -> [MiscEntry] {
var entries: [MiscEntry] = []
let theme = presentationData.theme
var activeCount = 0
if state.bypassCopyProtection { activeCount += 1 }
if state.disableViewOnceAutoDelete { activeCount += 1 }
if state.bypassScreenshotProtection { activeCount += 1 }
if state.blockAds { activeCount += 1 }
if state.alwaysOnline { activeCount += 1 }
entries.append(.masterHeader(theme, "РАСШИРЕННЫЕ ВОЗМОЖНОСТИ"))
entries.append(.masterToggle(theme, "Misc", state.isEnabled, activeCount, 5))
entries.append(.masterInfo(theme, "Когда включено, выбранные функции обхода ограничений будут активны."))
entries.append(.featuresHeader(theme, "ФУНКЦИИ"))
entries.append(.bypassCopyProtection(theme, "Разрешить пересылку", state.bypassCopyProtection))
entries.append(.disableViewOnceAutoDelete(theme, "Сохранять View Once", state.disableViewOnceAutoDelete))
entries.append(.bypassScreenshotProtection(theme, "Разрешить скриншоты", state.bypassScreenshotProtection))
entries.append(.blockAds(theme, "Блокировать рекламу", state.blockAds))
entries.append(.alwaysOnline(theme, "Вечный онлайн", state.alwaysOnline))
return entries
}
// MARK: - Controller
public func miscController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(
MiscControllerState(
isEnabled: MiscSettingsManager.shared.isEnabled,
bypassCopyProtection: MiscSettingsManager.shared.bypassCopyProtection,
disableViewOnceAutoDelete: MiscSettingsManager.shared.disableViewOnceAutoDelete,
bypassScreenshotProtection: MiscSettingsManager.shared.bypassScreenshotProtection,
blockAds: MiscSettingsManager.shared.blockAds,
alwaysOnline: MiscSettingsManager.shared.alwaysOnline
),
ignoreRepeated: true
)
let stateValue = Atomic(value: MiscControllerState(
isEnabled: MiscSettingsManager.shared.isEnabled,
bypassCopyProtection: MiscSettingsManager.shared.bypassCopyProtection,
disableViewOnceAutoDelete: MiscSettingsManager.shared.disableViewOnceAutoDelete,
bypassScreenshotProtection: MiscSettingsManager.shared.bypassScreenshotProtection,
blockAds: MiscSettingsManager.shared.blockAds,
alwaysOnline: MiscSettingsManager.shared.alwaysOnline
))
let updateState: ((inout MiscControllerState) -> Void) -> Void = { f in
let result = stateValue.modify { state in
var state = state
f(&state)
return state
}
statePromise.set(result)
}
let arguments = MiscControllerArguments(
toggleMaster: { value in
MiscSettingsManager.shared.isEnabled = value
updateState { state in
state.isEnabled = value
}
},
toggleBypassCopyProtection: {
let newValue = !MiscSettingsManager.shared.bypassCopyProtection
MiscSettingsManager.shared.bypassCopyProtection = newValue
updateState { state in
state.bypassCopyProtection = newValue
}
},
toggleDisableViewOnceAutoDelete: {
let newValue = !MiscSettingsManager.shared.disableViewOnceAutoDelete
MiscSettingsManager.shared.disableViewOnceAutoDelete = newValue
updateState { state in
state.disableViewOnceAutoDelete = newValue
}
},
toggleBypassScreenshotProtection: {
let newValue = !MiscSettingsManager.shared.bypassScreenshotProtection
MiscSettingsManager.shared.bypassScreenshotProtection = newValue
updateState { state in
state.bypassScreenshotProtection = newValue
}
},
toggleBlockAds: {
let newValue = !MiscSettingsManager.shared.blockAds
MiscSettingsManager.shared.blockAds = newValue
updateState { state in
state.blockAds = newValue
}
},
toggleAlwaysOnline: {
let newValue = !MiscSettingsManager.shared.alwaysOnline
MiscSettingsManager.shared.alwaysOnline = newValue
updateState { state in
state.alwaysOnline = newValue
}
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = miscControllerEntries(presentationData: presentationData, state: state)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("Misc"),
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))
}
let controller = ItemListController(context: context, state: signal)
return controller
}
@@ -0,0 +1,211 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import AccountContext
// MARK: - Entry Definition
private enum VoiceMorpherSection: Int32 {
case enable
case presets
}
private enum VoiceMorpherEntry: ItemListNodeEntry {
case enableHeader(PresentationTheme, String)
case enableToggle(PresentationTheme, String, Bool)
case enableInfo(PresentationTheme, String)
case presetsHeader(PresentationTheme, String)
case preset(PresentationTheme, Int, String, String, Bool) // id, name, description, selected
var section: ItemListSectionId {
switch self {
case .enableHeader, .enableToggle, .enableInfo:
return VoiceMorpherSection.enable.rawValue
case .presetsHeader, .preset:
return VoiceMorpherSection.presets.rawValue
}
}
var stableId: Int32 {
switch self {
case .enableHeader: return 0
case .enableToggle: return 1
case .enableInfo: return 2
case .presetsHeader: return 3
case let .preset(_, id, _, _, _): return 10 + Int32(id)
}
}
static func ==(lhs: VoiceMorpherEntry, rhs: VoiceMorpherEntry) -> Bool {
switch lhs {
case let .enableHeader(lhsTheme, lhsText):
if case let .enableHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
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 .enableInfo(lhsTheme, lhsText):
if case let .enableInfo(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .presetsHeader(lhsTheme, lhsText):
if case let .presetsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
}
return false
case let .preset(lhsTheme, lhsId, lhsName, lhsDesc, lhsSelected):
if case let .preset(rhsTheme, rhsId, rhsName, rhsDesc, rhsSelected) = rhs,
lhsTheme === rhsTheme, lhsId == rhsId, lhsName == rhsName, lhsDesc == rhsDesc, lhsSelected == rhsSelected {
return true
}
return false
}
}
static func <(lhs: VoiceMorpherEntry, rhs: VoiceMorpherEntry) -> Bool {
return lhs.stableId < rhs.stableId
}
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
let arguments = arguments as! VoiceMorpherControllerArguments
switch self {
case let .enableHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
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 .enableInfo(_, text):
return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: self.section)
case let .presetsHeader(_, text):
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .preset(_, id, name, _, selected):
return ItemListCheckboxItem(presentationData: presentationData, title: name, style: .left, checked: selected, zeroSeparatorInsets: false, sectionId: self.section, action: {
arguments.selectPreset(id)
})
}
}
}
// MARK: - Arguments
private final class VoiceMorpherControllerArguments {
let toggleEnabled: (Bool) -> Void
let selectPreset: (Int) -> Void
init(
toggleEnabled: @escaping (Bool) -> Void,
selectPreset: @escaping (Int) -> Void
) {
self.toggleEnabled = toggleEnabled
self.selectPreset = selectPreset
}
}
// MARK: - State
private struct VoiceMorpherControllerState: Equatable {
var isEnabled: Bool
var selectedPresetId: Int
}
// MARK: - Entries Builder
private func voiceMorpherControllerEntries(presentationData: PresentationData, state: VoiceMorpherControllerState) -> [VoiceMorpherEntry] {
var entries: [VoiceMorpherEntry] = []
let theme = presentationData.theme
entries.append(.enableHeader(theme, "ИЗМЕНЕНИЕ ГОЛОСА"))
entries.append(.enableToggle(theme, "Включить Voice Morpher", state.isEnabled))
entries.append(.enableInfo(theme, "Изменяет твой голос при записи голосовых сообщений. Использует встроенные аудио-эффекты iOS."))
entries.append(.presetsHeader(theme, "ВЫБЕРИТЕ ЭФФЕКТ"))
// Add all presets except disabled (it's controlled by toggle)
for preset in VoiceMorpherManager.VoicePreset.allCases where preset != .disabled {
let isSelected = preset.rawValue == state.selectedPresetId
entries.append(.preset(theme, preset.rawValue, preset.name, preset.description, isSelected))
}
return entries
}
// MARK: - Controller
public func voiceMorpherController(context: AccountContext) -> ViewController {
let statePromise = ValuePromise(
VoiceMorpherControllerState(
isEnabled: VoiceMorpherManager.shared.isEnabled,
selectedPresetId: VoiceMorpherManager.shared.selectedPresetId == 0 ? 1 : VoiceMorpherManager.shared.selectedPresetId
),
ignoreRepeated: true
)
let stateValue = Atomic(value: VoiceMorpherControllerState(
isEnabled: VoiceMorpherManager.shared.isEnabled,
selectedPresetId: VoiceMorpherManager.shared.selectedPresetId == 0 ? 1 : VoiceMorpherManager.shared.selectedPresetId
))
let updateState: ((inout VoiceMorpherControllerState) -> Void) -> Void = { f in
let result = stateValue.modify { state in
var state = state
f(&state)
return state
}
statePromise.set(result)
}
let arguments = VoiceMorpherControllerArguments(
toggleEnabled: { value in
VoiceMorpherManager.shared.isEnabled = value
updateState { state in
state.isEnabled = value
}
},
selectPreset: { id in
VoiceMorpherManager.shared.selectedPresetId = id
updateState { state in
state.selectedPresetId = id
}
}
)
let signal = combineLatest(
context.sharedContext.presentationData,
statePromise.get()
)
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let entries = voiceMorpherControllerEntries(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
}