Files
GLEGram-iOS/docs/GLEGram-features.md
T
Leeksov 4647310322 GLEGram 12.5 — Initial public release
Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
2026-04-06 09:48:12 +03:00

89 KiB
Raw Blame History

GLEGram: описание функций и полный исходный код

Документ описывает реализацию в репозитории. Раздел про текст «Чаты» в шапке не включён (по запросу). Во всех примерах ниже приведён полный фрагмент или файл без сокращений (... не используется).


1. Двойное дно

Смысл: скрытые аккаунты, отдельные пароли в Keychain, при разблокировке приложения разные коды ведут к разным сценариям (основной пароль Telegram, «секретный» пароль, переключение на скрытый аккаунт по совпадению пароля). Флаги isDoubleBottomOn / inDoubleBottom хранятся в UserDefaults (VarSystemNGSettings). Экран настроек в Swiftgram — doubleBottomSettingsController; проверки при вводе пароля приложения — в AppDelegate (additionalPasscodeCheck, onUnlockWithPasscode).

Nicegram/NGData/Sources/SystemNGSettings.swift (полностью)

// From Nicegram NGData/Sources/NGSettings.swift  only SystemNGSettings for Double Bottom
import Foundation

public class SystemNGSettings {
 let UD = UserDefaults.standard
 
 public init() {}
 
 public var dbReset: Bool {
 get {
 return UD.bool(forKey: "ng_db_reset")
 }
 set {
 UD.set(newValue, forKey: "ng_db_reset")
 }
 }
 
 public var isDoubleBottomOn: Bool {
 get {
 return UD.bool(forKey: "isDoubleBottomOn")
 }
 set {
 UD.set(newValue, forKey: "isDoubleBottomOn")
 }
 }
 
 public var inDoubleBottom: Bool {
 get {
 return UD.bool(forKey: "inDoubleBottom")
 }
 set {
 UD.set(newValue, forKey: "inDoubleBottom")
 }
 }
}

public var VarSystemNGSettings = SystemNGSettings()

Swiftgram/SGSettingsUI/Sources/DoubleBottomPasscodeStore.swift (полностью)

// MARK: Swiftgram  Keychain storage for hidden-account passcodes (Double Bottom)
import Foundation
import Security

private let serviceName = "SwiftgramDoubleBottom"

/// Key for the single "secret" passcode (second password). When user unlocks with this, only one account is shown.
private let secretPasscodeAccountKey = "secret"

public enum DoubleBottomPasscodeStore {
    // MARK: - Secret passcode (second password → show only 1 account)

    public static func setSecretPasscode(_ passcode: String) {
        let data = passcode.data(using: .utf8)!
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: secretPasscodeAccountKey
        ]
        var addQuery = query
        addQuery[kSecValueData as String] = data
        var status = SecItemAdd(addQuery as CFDictionary, nil)
        if status == errSecDuplicateItem {
            SecItemDelete(query as CFDictionary)
            status = SecItemAdd(addQuery as CFDictionary, nil)
        }
    }

    public static func secretPasscodeMatches(_ passcode: String) -> Bool {
        guard let stored = secretPasscode() else { return false }
        return stored == passcode
    }

    public static func hasSecretPasscode() -> Bool {
        return secretPasscode() != nil
    }

    public static func removeSecretPasscode() {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: secretPasscodeAccountKey
        ]
        SecItemDelete(query as CFDictionary)
    }

    private static func secretPasscode() -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: secretPasscodeAccountKey,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data, let string = String(data: data, encoding: .utf8) else {
            return nil
        }
        return string
    }

    // MARK: - Per-account passcodes (hidden accounts)

    public static func setPasscode(_ passcode: String, forAccountId accountId: Int64) {
        let account = "\(accountId)"
        let data = passcode.data(using: .utf8)!
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: account
        ]
        var addQuery = query
        addQuery[kSecValueData as String] = data
        var status = SecItemAdd(addQuery as CFDictionary, nil)
        if status == errSecDuplicateItem {
            SecItemDelete(query as CFDictionary)
            status = SecItemAdd(addQuery as CFDictionary, nil)
        }
    }

    public static func passcode(forAccountId accountId: Int64) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: "\(accountId)",
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data, let string = String(data: data, encoding: .utf8) else {
            return nil
        }
        return string
    }

    public static func removePasscode(forAccountId accountId: Int64) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: "\(accountId)"
        ]
        SecItemDelete(query as CFDictionary)
    }

    /// Returns the account id whose passcode matches the given value, or nil.
    public static func accountId(matchingPasscode passcode: String, candidateIds: [Int64]) -> Int64? {
        for id in candidateIds {
            if Self.passcode(forAccountId: id) == passcode {
                return id
            }
        }
        return nil
    }
}

Swiftgram/SGSettingsUI/Sources/DoubleBottomSettingsController.swift (полностью)

// MARK: Swiftgram  Double Bottom (full logic from Nicegram NGDoubleBottom/DoubleBottomListController)
// Ref: https://github.com/nicegram/Nicegram-iOS/blob/master/Nicegram/NGDoubleBottom/Sources/DoubleBottomListController.swift
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import PasscodeUI
import SGSimpleSettings
import TelegramStringFormatting

// MARK: - Section (Nicegram: DoubleBottomControllerSection)

private enum DoubleBottomControllerSection: Int32 {
    case isOn = 0
}

// MARK: - Entry (Nicegram: isOn + info)

private enum DoubleBottomEntry: ItemListNodeEntry {
    case isOn(String, Bool, Bool)  // title, value, enabled
    case info(String)

    var section: ItemListSectionId { DoubleBottomControllerSection.isOn.rawValue }

    var stableId: Int32 {
        switch self {
        case .isOn: return 1000
        case .info: return 1100
        }
    }

    static func < (lhs: DoubleBottomEntry, rhs: DoubleBottomEntry) -> Bool {
        lhs.stableId < rhs.stableId
    }

    static func == (lhs: DoubleBottomEntry, rhs: DoubleBottomEntry) -> Bool {
        switch (lhs, rhs) {
        case let (.isOn(lhsText, lhsBool, _), .isOn(rhsText, rhsBool, _)):
            return lhsText == rhsText && lhsBool == rhsBool
        case let (.info(lhsText), .info(rhsText)):
            return lhsText == rhsText
        default:
            return false
        }
    }

    func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
        let args = arguments as! DoubleBottomArguments
        switch self {
        case let .isOn(text, value, enabled):
            return ItemListSwitchItem(
                presentationData: presentationData,
                title: text,
                value: value,
                enabled: enabled,
                sectionId: section,
                style: .blocks,
                updated: { value in
                    args.updated(value)
                }
            )
        case let .info(text):
            return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: section)
        }
    }
}

// MARK: - Arguments (Nicegram: DoubleBottomControllerArguments)

private final class DoubleBottomArguments {
    let context: AccountContext
    let updated: (Bool) -> Void
    init(context: AccountContext, updated: @escaping (Bool) -> Void) {
        self.context = context
        self.updated = updated
    }
}

// MARK: - Controller (logic from Nicegram DoubleBottomListController)

public func doubleBottomSettingsController(context: AccountContext) -> ViewController {
    let lang = context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode
    let title = lang == "ru" ? "Двойное дно" : "Double Bottom"
    let toggleTitle = lang == "ru" ? "Двойное дно" : "Double Bottom"
    let noticeText = lang == "ru"
        ? "Скрытые аккаунты и вход по паролю. Разные пароли открывают разные профили."
        : "Hidden accounts and passcode access. Different passwords open different profiles."

    let arguments = DoubleBottomArguments(context: context, updated: { value in
        if value {
            SGSimpleSettings.shared.doubleBottomEnabled = true
            let setupController = PasscodeSetupController(context: context, mode: .setup(change: false, .digits6))
            setupController.complete = { passcode, _ in
                DoubleBottomPasscodeStore.setSecretPasscode(passcode)
                setupController.dismiss()
            }
            context.sharedContext.presentGlobalController(setupController, nil)
        } else {
            SGSimpleSettings.shared.doubleBottomEnabled = false
            DoubleBottomPasscodeStore.removeSecretPasscode()
            DoubleBottomViewingSecretStore.setViewingWithSecretPasscode(false)
            let accountManager = context.sharedContext.accountManager
            // Remove secret passcodes from Keychain for previously hidden accounts
            let _ = (accountManager.accountRecords()
                |> take(1)
                |> deliverOnMainQueue).start(next: { view in
                    for record in view.records where record.attributes.contains(where: { $0.isHiddenAccountAttribute }) {
                        DoubleBottomPasscodeStore.removePasscode(forAccountId: record.id.int64)
                    }
                })
            // Nicegram: single transaction  keep device passcode, remove HiddenAccount from all records
            let _ = accountManager.transaction { transaction in
                let challengeData = transaction.getAccessChallengeData()
                let challenge: PostboxAccessChallengeData
                switch challengeData {
                case .numericalPassword(let value):
                    challenge = .numericalPassword(value: value)
                case .plaintextPassword(let value):
                    challenge = .plaintextPassword(value: value)
                case .none:
                    challenge = .none
                }
                transaction.setAccessChallengeData(challenge)
                for record in transaction.getRecords() {
                    transaction.updateRecord(record.id) { current in
                        guard let current = current else { return nil }
                        var attributes = current.attributes
                        attributes.removeAll { $0.isHiddenAccountAttribute }
                        return AccountRecord(id: current.id, attributes: attributes, temporarySessionId: current.temporarySessionId)
                    }
                }
            }.start()
        }
    })

    let transactionStatus = context.sharedContext.accountManager.transaction { transaction -> (Bool, Bool) in
        let records = transaction.getRecords()
        let publicCount = records.filter { record in
            let attrs = record.attributes
            let hiddenOrLoggedOut = attrs.contains(where: { $0.isHiddenAccountAttribute || $0.isLoggedOutAccountAttribute })
            return !hiddenOrLoggedOut
        }.count
        let hasMoreThanOnePublic = publicCount > 1
        let hasMainPasscode = transaction.getAccessChallengeData() != .none
        return (hasMoreThanOnePublic, hasMainPasscode)
    }

    let signal: Signal<(ItemListControllerState, (ItemListNodeState, DoubleBottomArguments)), NoError> = combineLatest(context.sharedContext.presentationData, transactionStatus)
        |> map { presentationData, contextStatus -> (ItemListControllerState, (ItemListNodeState, DoubleBottomArguments)) in
            let isOn = SGSimpleSettings.shared.doubleBottomEnabled
            let enabled = isOn || (contextStatus.0 && contextStatus.1)
            let entries: [DoubleBottomEntry] = [
                .isOn(toggleTitle, isOn, enabled),
                .info(noticeText)
            ]
            let controllerState = ItemListControllerState(
                presentationData: ItemListPresentationData(presentationData),
                title: .text(title),
                leftNavigationButton: nil,
                rightNavigationButton: nil,
                backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
            )
            let listState = ItemListNodeState(
                presentationData: ItemListPresentationData(presentationData),
                entries: entries,
                style: .blocks,
                ensureVisibleItemTag: nil,
                footerItem: nil,
                initialScrollToItem: nil
            )
            return (controllerState, (listState, arguments))
        }

    return ItemListController(context: context, state: signal)
}

// MARK: - Passcode check (Nicegram: check(passcode:challengeData:) for device passcode validation)

public func doubleBottomCheckPasscode(_ passcode: String, challengeData: PostboxAccessChallengeData) -> Bool {
    let passcodeType: PasscodeEntryFieldType
    switch challengeData {
    case let .numericalPassword(value):
        passcodeType = value.count == 6 ? .digits6 : .digits4
    default:
        passcodeType = .alphanumeric
    }
    switch challengeData {
    case .none:
        return true
    case let .numericalPassword(code):
        if passcodeType == .alphanumeric {
            return false
        }
        return passcode == normalizeArabicNumeralString(code, type: .western)
    case let .plaintextPassword(code):
        if passcodeType != .alphanumeric {
            return false
        }
        return passcode == code
    }
}

submodules/TelegramUI/Sources/AppDelegate.swift — класс кэша и хуки разблокировки (полные строки файла)

#if canImport(SGSettingsUI)
private final class DoubleBottomHiddenIdsCache {
    var hiddenIds: [Int64] = []
    var disposable: Disposable?

    init(accountManager: AccountManager<TelegramAccountManagerTypes>) {
        self.disposable = (accountManager.accountRecords()
            |> deliverOnMainQueue).start(next: { [weak self] view in
                self?.hiddenIds = view.records
                    .filter { $0.attributes.contains(where: { $0.isHiddenAccountAttribute }) }
                    .map { $0.id.int64 }
            })
    }
}
#endif
            #if canImport(SGSettingsUI)
            let doubleBottomHiddenIdsCache = DoubleBottomHiddenIdsCache(accountManager: accountManager)
            appLockContext.additionalPasscodeCheck = { passcode in
                guard VarSystemNGSettings.isDoubleBottomOn else { return false }
                if DoubleBottomPasscodeStore.secretPasscodeMatches(passcode) { return true }
                let ids = doubleBottomHiddenIdsCache.hiddenIds
                if !ids.isEmpty, DoubleBottomPasscodeStore.accountId(matchingPasscode: passcode, candidateIds: ids) != nil { return true }
                return false
            }
            appLockContext.onUnlockWithPasscode = { [weak sharedContext] passcode in
                guard let sharedContext = sharedContext, VarSystemNGSettings.isDoubleBottomOn else { return }
                let _ = (accountManager.accessChallengeData()
                    |> take(1)
                    |> deliverOnMainQueue).start(next: { [weak sharedContext] challengeView in
                        guard let sharedContext = sharedContext else { return }
                        let challengeData = challengeView.data
                        if doubleBottomCheckPasscode(passcode, challengeData: challengeData) {
                            DoubleBottomViewingSecretStore.setViewingWithSecretPasscode(false)
                        } else if DoubleBottomPasscodeStore.secretPasscodeMatches(passcode) {
                            DoubleBottomViewingSecretStore.setViewingWithSecretPasscode(true)
                        } else {
                            let _ = (accountManager.accountRecords()
                                |> take(1)
                                |> deliverOnMainQueue).start(next: { view in
                                    let hiddenIds = view.records
                                        .filter { $0.attributes.contains(where: { $0.isHiddenAccountAttribute }) }
                                        .map { $0.id.int64 }
                                    guard !hiddenIds.isEmpty,
                                          let matchedId = DoubleBottomPasscodeStore.accountId(matchingPasscode: passcode, candidateIds: hiddenIds) else { return }
                                    sharedContext.switchToAccount(id: AccountRecordId(rawValue: matchedId))
                                })
                        }
                    })
            }
            #endif

Точка входа в настройках GLEGram (пункты меню, не вся функция gleGramAppearanceEntries):

    entries.append(.header(id: id.count, section: .doubleBottom, text: (lang == "ru" ? "ДВОЙНОЕ ДНО" : "DOUBLE BOTTOM"), badge: nil))
    entries.append(.disclosure(id: id.count, section: .doubleBottom, link: .doubleBottomSettings, text: (lang == "ru" ? "Двойное дно" : "Double Bottom")))
    entries.append(.notice(id: id.count, section: .doubleBottom, text: (lang == "ru" ? "Скрытые аккаунты и вход по паролю. Разные пароли открывают разные профили." : "Hidden accounts and passcode access. Different passwords open different profiles.")))

2. Пароль при заходе в чат

Смысл: список защищённых peer id в UserDefaults; при открытии чата показывается UIAlertController с полем пароля; проверка либо через doubleBottomCheckPasscode (код Telegram на устройстве), либо через отдельный пароль в Keychain (ProtectedChatsStore).

Swiftgram/SGSettingsUI/Sources/ProtectedChatsStore.swift (полностью)

См. файл в репозитории — ниже идентичное содержимое.

// MARK: Swiftgram  Password for selected chats/folders
import Foundation
import Security

private let enabledKey = "sg_protected_chats_enabled"
private let peerIdsKey = "sg_protected_chat_peer_ids"
private let folderIdsKey = "sg_protected_folder_ids"
private let useDevicePasscodeKey = "sg_protected_chats_use_device_passcode"
private let serviceName = "SwiftgramProtectedChats"
private let customPasscodeAccount = "chats"

public enum ProtectedChatsStore {
    public static var isEnabled: Bool {
        get { UserDefaults.standard.bool(forKey: enabledKey) }
        set { UserDefaults.standard.set(newValue, forKey: enabledKey) }
    }

    public static var useDevicePasscode: Bool {
        get { UserDefaults.standard.object(forKey: useDevicePasscodeKey) as? Bool ?? true }
        set { UserDefaults.standard.set(newValue, forKey: useDevicePasscodeKey) }
    }

    public static var protectedPeerIds: Set<Int64> {
        get {
            let list = UserDefaults.standard.array(forKey: peerIdsKey) as? [Int64] ?? []
            return Set(list)
        }
        set {
            UserDefaults.standard.set(Array(newValue), forKey: peerIdsKey)
        }
    }

    public static var protectedFolderIds: Set<Int32> {
        get {
            let list = UserDefaults.standard.array(forKey: folderIdsKey) as? [Int32] ?? []
            return Set(list)
        }
        set {
            UserDefaults.standard.set(Array(newValue), forKey: folderIdsKey)
        }
    }

    public static func addProtectedPeer(_ peerId: Int64) {
        var set = protectedPeerIds
        set.insert(peerId)
        protectedPeerIds = set
    }

    public static func removeProtectedPeer(_ peerId: Int64) {
        var set = protectedPeerIds
        set.remove(peerId)
        protectedPeerIds = set
    }

    public static func addProtectedFolder(_ folderId: Int32) {
        var set = protectedFolderIds
        set.insert(folderId)
        protectedFolderIds = set
    }

    public static func removeProtectedFolder(_ folderId: Int32) {
        var set = protectedFolderIds
        set.remove(folderId)
        protectedFolderIds = set
    }

    public static func isProtected(peerId: Int64) -> Bool {
        isEnabled && protectedPeerIds.contains(peerId)
    }

    public static func isProtected(folderId: Int32) -> Bool {
        isEnabled && protectedFolderIds.contains(folderId)
    }

    // MARK: - Custom passcode (when not using device passcode)

    public static func setCustomPasscode(_ passcode: String) {
        let data = passcode.data(using: .utf8)!
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: customPasscodeAccount
        ]
        var addQuery = query
        addQuery[kSecValueData as String] = data
        var status = SecItemAdd(addQuery as CFDictionary, nil)
        if status == errSecDuplicateItem {
            SecItemDelete(query as CFDictionary)
            status = SecItemAdd(addQuery as CFDictionary, nil)
        }
    }

    public static func customPasscodeMatches(_ passcode: String) -> Bool {
        guard let stored = getCustomPasscode() else { return false }
        return stored == passcode
    }

    public static func hasCustomPasscode() -> Bool {
        getCustomPasscode() != nil
    }

    public static func removeCustomPasscode() {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: customPasscodeAccount
        ]
        SecItemDelete(query as CFDictionary)
    }

    private static func getCustomPasscode() -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: serviceName,
            kSecAttrAccount as String: customPasscodeAccount,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        guard status == errSecSuccess, let data = result as? Data, let string = String(data: data, encoding: .utf8) else {
            return nil
        }
        return string
    }
}

Swiftgram/SGSettingsUI/Sources/ProtectedChatsSettingsController.swift (полностью)

// MARK: Swiftgram  Password for selected chats/folders settings
import Foundation
import UIKit
import Display
import SwiftSignalKit
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
import PasscodeUI

private enum ProtectedChatsEntry: ItemListNodeEntry {
    case enabled(String, Bool)
    case useDevicePasscode(String, Bool)
    case setCustomPasscode(String)
    case addChat(String)
    case protectedPeer(id: Int64, title: String)
    case notice(String)

    var section: ItemListSectionId {
        switch self {
        case .enabled, .useDevicePasscode, .setCustomPasscode, .notice: return 0
        case .addChat, .protectedPeer: return 1
        }
    }

    var stableId: Int {
        switch self {
        case .enabled: return 0
        case .useDevicePasscode: return 1
        case .setCustomPasscode: return 2
        case .addChat: return 3
        case .protectedPeer(let id, _): return 100 + Int(id % 100000)
        case .notice: return 200
        }
    }

    static func < (lhs: ProtectedChatsEntry, rhs: ProtectedChatsEntry) -> Bool {
        lhs.stableId < rhs.stableId
    }

    func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
        let args = arguments as! ProtectedChatsArguments
        let lang = presentationData.strings.baseLanguageCode
        switch self {
        case let .enabled(title, value):
            return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: section, style: .blocks, updated: { args.toggleEnabled($0) })
        case let .useDevicePasscode(title, value):
            return ItemListSwitchItem(presentationData: presentationData, title: title, value: value, sectionId: section, style: .blocks, updated: { args.toggleUseDevicePasscode($0) })
        case let .setCustomPasscode(title):
            return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", sectionId: section, style: .blocks, action: { args.setCustomPasscode() })
        case let .addChat(title):
            return ItemListDisclosureItem(presentationData: presentationData, title: title, label: "", sectionId: section, style: .blocks, action: { args.addChat() })
        case let .protectedPeer(_, title):
            return ItemListDisclosureItem(presentationData: presentationData, title: title, label: lang == "ru" ? "Удалить" : "Remove", sectionId: section, style: .blocks, action: { [self] in
                if case let .protectedPeer(peerId, _) = self { args.removePeer(peerId) }
            })
        case let .notice(text):
            return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: section)
        }
    }
}

private final class ProtectedChatsArguments {
    let context: AccountContext
    let toggleEnabled: (Bool) -> Void
    let toggleUseDevicePasscode: (Bool) -> Void
    let setCustomPasscode: () -> Void
    let addChat: () -> Void
    let removePeer: (Int64) -> Void

    init(context: AccountContext, toggleEnabled: @escaping (Bool) -> Void, toggleUseDevicePasscode: @escaping (Bool) -> Void, setCustomPasscode: @escaping () -> Void, addChat: @escaping () -> Void, removePeer: @escaping (Int64) -> Void) {
        self.context = context
        self.toggleEnabled = toggleEnabled
        self.toggleUseDevicePasscode = toggleUseDevicePasscode
        self.setCustomPasscode = setCustomPasscode
        self.addChat = addChat
        self.removePeer = removePeer
    }
}

public func protectedChatsSettingsController(context: AccountContext) -> ViewController {
    let lang = context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode
    let title = lang == "ru" ? "Пароль для чатов" : "Password for chats"

    let statePromise = Promise<[(Int64, String)]>()
    let peerTitles: [(Int64, String)] = ProtectedChatsStore.protectedPeerIds.map { ($0, "Chat \($0)") }
    statePromise.set(.single(peerTitles))

    var pushControllerImpl: ((ViewController) -> Void)?

    let arguments = ProtectedChatsArguments(
        context: context,
        toggleEnabled: { value in
            ProtectedChatsStore.isEnabled = value
        },
        toggleUseDevicePasscode: { value in
            ProtectedChatsStore.useDevicePasscode = value
        },
        setCustomPasscode: {
            let setup = PasscodeSetupController(context: context, mode: .setup(change: false, .digits6))
            setup.complete = { passcode, _ in
                ProtectedChatsStore.setCustomPasscode(passcode)
                ProtectedChatsStore.useDevicePasscode = false
                _ = (setup.navigationController as? NavigationController)?.popViewController(animated: true)
            }
            pushControllerImpl?(setup)
        },
        addChat: {
            let filter: ChatListNodePeersFilter = [.onlyWriteable, .excludeDisabled, .doNotSearchMessages]
            let controller = context.sharedContext.makePeerSelectionController(PeerSelectionControllerParams(
                context: context,
                filter: filter,
                hasContactSelector: false,
                hasGlobalSearch: true,
                title: lang == "ru" ? "Выберите чат" : "Select chat"
            ))
            controller.peerSelected = { [weak controller] peer, _ in
                let peerId = peer.id.toInt64()
                ProtectedChatsStore.addProtectedPeer(peerId)
                statePromise.set(.single(ProtectedChatsStore.protectedPeerIds.map { ($0, "Chat \($0)") }))
                _ = (controller?.navigationController as? NavigationController)?.popViewController(animated: true)
            }
            pushControllerImpl?(controller)
        },
        removePeer: { peerId in
            ProtectedChatsStore.removeProtectedPeer(peerId)
            statePromise.set(.single(ProtectedChatsStore.protectedPeerIds.map { ($0, "Chat \($0)") }))
        }
    )

    let signal = combineLatest(
        context.sharedContext.presentationData,
        statePromise.get()
    )
    |> map { presentationData, peerTitles -> (ItemListControllerState, (ItemListNodeState, ProtectedChatsArguments)) in
        let enabled = ProtectedChatsStore.isEnabled
        let useDevice = ProtectedChatsStore.useDevicePasscode
        let lang = presentationData.strings.baseLanguageCode

        var entries: [ProtectedChatsEntry] = []
        entries.append(.enabled(lang == "ru" ? "Пароль для чатов" : "Password for chats", enabled))
        if enabled {
            entries.append(.useDevicePasscode(lang == "ru" ? "Использовать пароль Telegram" : "Use Telegram passcode", useDevice))
            if !useDevice {
                entries.append(.setCustomPasscode(lang == "ru" ? "Установить отдельный пароль" : "Set separate passcode"))
            }
            entries.append(.notice(lang == "ru" ? "При открытии выбранных чатов будет запрашиваться пароль." : "Opening selected chats will require passcode."))
        }

        entries.append(.addChat(lang == "ru" ? "Добавить чат" : "Add chat"))
        for (id, t) in peerTitles.sorted(by: { $0.0 < $1.0 }) {
            entries.append(.protectedPeer(id: id, title: t))
        }

        let controllerState = ItemListControllerState(
            presentationData: ItemListPresentationData(presentationData),
            title: .text(title),
            leftNavigationButton: nil,
            rightNavigationButton: nil,
            backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
        )
        let listState = ItemListNodeState(
            presentationData: ItemListPresentationData(presentationData),
            entries: entries,
            style: .blocks,
            ensureVisibleItemTag: nil,
            footerItem: nil,
            initialScrollToItem: nil
        )
        return (controllerState, (listState, arguments))
    }

    let signalTyped: Signal<(ItemListControllerState, (ItemListNodeState, ProtectedChatsArguments)), NoError> = signal
    let controller = ItemListController(context: context, state: signalTyped)
    pushControllerImpl = { [weak controller] (vc: ViewController) in
        (controller?.navigationController as? NavigationController)?.pushViewController(vc)
    }
    return controller
}

submodules/TelegramUI/Sources/SharedAccountContext.swift — проверка при открытии чата (полный фрагмент)

    public func isChatProtected(peerId: PeerId) -> Bool {
        #if canImport(SGSettingsUI)
        return ProtectedChatsStore.isEnabled && ProtectedChatsStore.isProtected(peerId: peerId.toInt64())
        #else
        return false
        #endif
    }
    public func navigateToChatController(_ params: NavigateToChatControllerParams) {
        if case let .peer(peer) = params.chatLocation {
            let accountId = params.context.account.peerId.toInt64()
            let peerId = peer.id.toInt64()
            SGPluginHooks.willOpenChatRunner?(accountId, peerId)
            if let eventResult = SGPluginHooks.emitEvent("chat.willOpen", ["accountId": accountId, "peerId": peerId, "subject": params.subject.map { String(describing: $0) } ?? ""]), eventResult["cancel"] as? Bool == true {
                return
            }
        }
        #if canImport(SGSettingsUI)
        /// Saved Messages «Chats» tab opens dialogs as `.replyThread` with `peerId == account` and real peer id in `threadId`.
        let peerIdValue: Int64 = {
            switch params.chatLocation {
            case let .peer(peer):
                return peer.id.toInt64()
            case let .replyThread(message):
                if message.peerId == params.context.account.peerId,
                   !message.isForumPost, !message.isChannelPost, !message.isMonoforumPost {
                    return message.threadId
                }
                return message.peerId.toInt64()
            }
        }()
        if ProtectedChatsStore.isEnabled && ProtectedChatsStore.isProtected(peerId: peerIdValue) {
            let presentationData = params.context.sharedContext.currentPresentationData.with { $0 }
            let strings = presentationData.strings
            let useDevice = ProtectedChatsStore.useDevicePasscode
            let title = presentationData.strings.baseLanguageCode == "ru" ? "Введите пароль" : "Enter passcode"
            let message = presentationData.strings.baseLanguageCode == "ru" ? "Этот чат защищён паролем." : "This chat is protected with a passcode."
            var textField: UITextField?
            let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
            alert.addTextField { field in
                field.isSecureTextEntry = true
                field.placeholder = presentationData.strings.baseLanguageCode == "ru" ? "Пароль" : "Passcode"
                textField = field
            }
            let cancelTitle = strings.Common_Cancel
            let okTitle = strings.Common_OK
            alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel) { _ in })
            alert.addAction(UIAlertAction(title: okTitle, style: .default) { [weak self] _ in
                guard let self = self, let entered = textField?.text, !entered.isEmpty else { return }
                let accountManager = self.accountManager
                let proceed: () -> Void = {
                    DispatchQueue.main.async {
                        navigateToChatControllerImpl(params)
                    }
                }
                if useDevice {
                    let _ = (accountManager.accessChallengeData()
                        |> take(1)
                        |> deliverOnMainQueue).start(next: { view in
                            if doubleBottomCheckPasscode(entered, challengeData: view.data) {
                                alert.dismiss(animated: true, completion: proceed)
                            } else {
                                let err = UIAlertController(title: nil, message: presentationData.strings.baseLanguageCode == "ru" ? "Неверный пароль" : "Wrong passcode", preferredStyle: .alert)
                                err.addAction(UIAlertAction(title: strings.Common_OK, style: .default) { _ in })
                                alert.present(err, animated: true)
                            }
                        })
                } else {
                    if ProtectedChatsStore.customPasscodeMatches(entered) {
                        alert.dismiss(animated: true, completion: proceed)
                    } else {
                        let err = UIAlertController(title: nil, message: presentationData.strings.baseLanguageCode == "ru" ? "Неверный пароль" : "Wrong passcode", preferredStyle: .alert)
                        err.addAction(UIAlertAction(title: strings.Common_OK, style: .default) { _ in })
                        alert.present(err, animated: true)
                    }
                }
            })
            if let top = params.navigationController.viewControllers.last {
                top.present(alert, animated: true)
            } else {
                navigateToChatControllerImpl(params)
            }
            return
        }
        #endif
        navigateToChatControllerImpl(params)
    }

Пункты в GLEGram (фрагмент GLEGramSettingsController.swift):

    entries.append(.header(id: id.count, section: .protectedChats, text: (lang == "ru" ? "ПАРОЛЬ ДЛЯ ЧАТОВ" : "PASSWORD FOR CHATS"), badge: nil))
    entries.append(.disclosure(id: id.count, section: .protectedChats, link: .protectedChatsSettings, text: (lang == "ru" ? "Пароль при заходе в чат" : "Password when entering chat")))
    entries.append(.notice(id: id.count, section: .protectedChats, text: (lang == "ru" ? "Выберите чаты и/или папки, при открытии которых нужно вводить пароль (пароль Telegram или отдельный)." : "Select chats and/or folders that require a passcode to open (device passcode or a separate one).")))

3. Смена голоса (Voice Morpher)

Смысл: локальная обработка OGG голосовых сообщений: декодирование Opus → AVAudioEngine (тон, скорость, искажение) → снова OGG. Настройки и пресеты — VoiceMorpherManager; вызов нативного процессора — VoiceMorpherEngineVoiceMorpherProcessor.

Пункты в GLEGram (Swiftgram/SGSettingsUI/Sources/GLEGramSettingsController.swift)

    // MARK: Voice Morpher (Privacy tab) — ghostgram-style local processing
    entries.append(.header(id: id.count, section: .voiceChanger, text: (lang == "ru" ? "СМЕНА ГОЛОСА" : "VOICE MORPHER"), badge: nil))
    let vm = VoiceMorpherManager.shared
    entries.append(.toggle(id: id.count, section: .voiceChanger, settingName: .voiceChangerEnabled, value: vm.isEnabled, text: (lang == "ru" ? "Изменять голос при записи" : "Change voice when recording"), enabled: true))
    let ru = lang == "ru"
    let displayedPreset: VoiceMorpherManager.VoicePreset = {
        if !vm.isEnabled { return .disabled }
        if vm.selectedPresetId == 0 { return .anonymous }
        return vm.selectedPreset
    }()
    let presetTitle = displayedPreset.title(langIsRu: ru)
    entries.append(.disclosure(id: id.count, section: .voiceChanger, link: .voiceChangerVoicePicker, text: (ru ? "Эффект: \(presetTitle)" : "Effect: \(presetTitle)")))
    entries.append(.notice(id: id.count, section: .voiceChanger, text: (ru
        ? "Локально: OGG → эффекты iOS (тон, искажение) → снова OGG. Без серверов. Как в ghostgram iOS."
        : "On-device: OGG → iOS audio effects (pitch, distortion) → OGG. No servers. Same approach as ghostgram iOS.")))

submodules/TelegramCore/Sources/VoiceMorpher/VoiceMorpherManager.swift (полностью)

import Foundation

/// GLEGram / ghostgram-style: local voice morphing for outgoing voice messages (UserDefaults).
public final class VoiceMorpherManager {
    public static let shared = VoiceMorpherManager()

    public enum VoicePreset: Int, CaseIterable {
        case disabled = 0
        case anonymous = 1
        case female = 2
        case male = 3
        case child = 4
        case robot = 5

        public func title(langIsRu: Bool) -> String {
            switch self {
            case .disabled:
                return langIsRu ? "Выключено" : "Off"
            case .anonymous:
                return langIsRu ? "Аноним" : "Anonymous"
            case .female:
                return langIsRu ? "Женский" : "Female"
            case .male:
                return langIsRu ? "Мужской" : "Male"
            case .child:
                return langIsRu ? "Ребёнок" : "Child"
            case .robot:
                return langIsRu ? "Робот" : "Robot"
            }
        }

        public func subtitle(langIsRu: Bool) -> String {
            switch self {
            case .disabled:
                return langIsRu ? "Без изменений" : "Unchanged"
            case .anonymous:
                return langIsRu ? "Искажённый голос" : "Distorted voice"
            case .female:
                return langIsRu ? "Выше тон" : "Higher pitch"
            case .male:
                return langIsRu ? "Ниже тон" : "Lower pitch"
            case .child:
                return langIsRu ? "Детский тон" : "Child-like"
            case .robot:
                return langIsRu ? "Металлический эффект" : "Metallic effect"
            }
        }
    }

    private enum Keys {
        static let isEnabled = "VoiceMorpher.isEnabled"
        static let selectedPreset = "VoiceMorpher.selectedPreset"
    }

    private let defaults = UserDefaults.standard

    public var isEnabled: Bool {
        get { defaults.bool(forKey: Keys.isEnabled) }
        set {
            defaults.set(newValue, forKey: Keys.isEnabled)
            notifyChanged()
        }
    }

    public var selectedPresetId: Int {
        get { defaults.integer(forKey: Keys.selectedPreset) }
        set {
            defaults.set(newValue, forKey: Keys.selectedPreset)
            notifyChanged()
        }
    }

    public var selectedPreset: VoicePreset {
        VoicePreset(rawValue: selectedPresetId) ?? .disabled
    }

    public var effectivePreset: VoicePreset {
        guard isEnabled else { return .disabled }
        return selectedPreset
    }

    public static let settingsChangedNotification = Notification.Name("VoiceMorpherSettingsChanged")

    private func notifyChanged() {
        NotificationCenter.default.post(name: Self.settingsChangedNotification, object: nil)
    }

    private init() {}
}

submodules/TelegramUI/Sources/VoiceMorpher/VoiceMorpherEngine.swift (полностью)

import Foundation
import OpusBinding
import TelegramCore

/// Local OGG voice morphing (ghostgram-style): decode → AVAudioEngine effects → encode.
public final class VoiceMorpherEngine {
    public static let shared = VoiceMorpherEngine()

    private init() {}

    public func processOggData(
        _ inputData: Data,
        completion: @escaping (Swift.Result<Data, Error>) -> Void
    ) {
        let preset = VoiceMorpherManager.shared.effectivePreset

        guard preset != .disabled else {
            completion(.success(inputData))
            return
        }

        let objcPreset: VoiceMorpherPreset
        switch preset {
        case .disabled:
            objcPreset = .disabled
        case .anonymous:
            objcPreset = .anonymous
        case .female:
            objcPreset = .female
        case .male:
            objcPreset = .male
        case .child:
            objcPreset = .child
        case .robot:
            objcPreset = .robot
        }

        VoiceMorpherProcessor.processOggData(inputData, preset: objcPreset) { outputData, error in
            if let error {
                completion(.failure(error))
            } else if let outputData {
                completion(.success(outputData))
            } else {
                completion(.failure(VoiceMorpherError.processingFailed))
            }
        }
    }

    public enum VoiceMorpherError: Error, LocalizedError {
        case processingFailed

        public var errorDescription: String? {
            switch self {
            case .processingFailed:
                return "Voice morphing processing failed"
            }
        }
    }
}

submodules/OpusBinding/PublicHeaders/OpusBinding/VoiceMorpherProcessor.h (полностью)

#import <AVFoundation/AVFoundation.h>
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/// VoiceMorpherProcessor - Processes OGG/Opus audio with voice effects
/// Decodes OGG -> applies effects -> re-encodes to OGG
@interface VoiceMorpherProcessor : NSObject

typedef NS_ENUM(NSInteger, VoiceMorpherPreset) {
    VoiceMorpherPresetDisabled = 0,
    VoiceMorpherPresetAnonymous = 1,
    VoiceMorpherPresetFemale = 2,
    VoiceMorpherPresetMale = 3,
    VoiceMorpherPresetChild = 4,
    VoiceMorpherPresetRobot = 5
};

/// Process OGG audio data with voice morphing effect
+ (void)processOggData:(NSData *)inputData
                preset:(VoiceMorpherPreset)preset
            completion:(void (^)(NSData *_Nullable outputData,
                                 NSError *_Nullable error))completion;

+ (float)pitchShiftForPreset:(VoiceMorpherPreset)preset;
+ (float)rateForPreset:(VoiceMorpherPreset)preset;

@end

NS_ASSUME_NONNULL_END

submodules/OpusBinding/Sources/VoiceMorpherProcessor.m (полностью)

#import "VoiceMorpherProcessor.h"
#import "OggOpusReader.h"
#import "TGDataItem.h"
#import "TGOggOpusWriter.h"

@implementation VoiceMorpherProcessor

+ (float)pitchShiftForPreset:(VoiceMorpherPreset)preset {
  switch (preset) {
  case VoiceMorpherPresetDisabled:
    return 0;
  case VoiceMorpherPresetAnonymous:
    return -200;
  case VoiceMorpherPresetFemale:
    return 600; // More feminine - higher pitch
  case VoiceMorpherPresetMale:
    return -300;
  case VoiceMorpherPresetChild:
    return 600;
  case VoiceMorpherPresetRobot:
    return 0;
  }
}

+ (float)rateForPreset:(VoiceMorpherPreset)preset {
  switch (preset) {
  case VoiceMorpherPresetDisabled:
    return 1.0;
  case VoiceMorpherPresetAnonymous:
    return 0.95;
  case VoiceMorpherPresetFemale:
    return 1.08; // Slightly faster for feminine effect
  case VoiceMorpherPresetMale:
    return 0.95;
  case VoiceMorpherPresetChild:
    return 1.1;
  case VoiceMorpherPresetRobot:
    return 1.0;
  }
}

+ (void)processOggData:(NSData *)inputData
                preset:(VoiceMorpherPreset)preset
            completion:
                (void (^)(NSData *_Nullable, NSError *_Nullable))completion {

  if (preset == VoiceMorpherPresetDisabled) {
    completion(inputData, nil);
    return;
  }

  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
                 ^{
                   NSError *error = nil;
                   NSData *result = [self processOggDataSync:inputData
                                                      preset:preset
                                                       error:&error];

                   // Call completion on background thread to avoid deadlock
                   // when caller uses semaphore on main thread
                   completion(result, error);
                 });
}

+ (NSData *_Nullable)processOggDataSync:(NSData *)inputData
                                 preset:(VoiceMorpherPreset)preset
                                  error:(NSError **)error {
  // Save input OGG to temp file for decoding
  NSString *tempInputPath = [NSTemporaryDirectory()
      stringByAppendingPathComponent:
          [NSString
              stringWithFormat:@"vm_in_%lld.ogg", (long long)[[NSDate date]
                                                      timeIntervalSince1970] *
                                                      1000]];

  [inputData writeToFile:tempInputPath atomically:YES];

  // Decode OGG to PCM
  OggOpusReader *reader = [[OggOpusReader alloc] initWithPath:tempInputPath];
  if (!reader) {
    if (error) {
      *error = [NSError
          errorWithDomain:@"VoiceMorpher"
                     code:1
                 userInfo:@{
                   NSLocalizedDescriptionKey : @"Failed to open OGG file"
                 }];
    }
    [[NSFileManager defaultManager] removeItemAtPath:tempInputPath error:nil];
    return nil;
  }

  // Opus outputs 16-bit stereo at 48kHz
  NSMutableData *pcmData = [[NSMutableData alloc] init];
  int16_t buffer[5760 * 2]; // Max frame size * channels
  int32_t samplesRead;

  while ((samplesRead = [reader read:buffer
                             bufSize:sizeof(buffer) / sizeof(buffer[0])]) > 0) {
    [pcmData appendBytes:buffer length:samplesRead * sizeof(int16_t)];
  }

  [[NSFileManager defaultManager] removeItemAtPath:tempInputPath error:nil];

  if (pcmData.length == 0) {
    if (error) {
      *error =
          [NSError errorWithDomain:@"VoiceMorpher"
                              code:2
                          userInfo:@{
                            NSLocalizedDescriptionKey : @"No PCM data decoded"
                          }];
    }
    return nil;
  }

  // Apply voice effects using AVAudioEngine
  NSData *processedPcm = [self applyEffectsToPcmData:pcmData
                                              preset:preset
                                               error:error];
  if (!processedPcm) {
    return nil;
  }

  // Encode processed PCM back to OGG
  TGDataItem *dataItem = [[TGDataItem alloc] init];
  TGOggOpusWriter *writer = [[TGOggOpusWriter alloc] init];

  if (![writer beginWithDataItem:dataItem]) {
    if (error) {
      *error = [NSError
          errorWithDomain:@"VoiceMorpher"
                     code:4
                 userInfo:@{
                   NSLocalizedDescriptionKey : @"Failed to begin OGG encoding"
                 }];
    }
    return nil;
  }

  // Write PCM data in frames (960 samples = 20ms at 48kHz)
  const int frameSize = 960 * sizeof(int16_t);
  const uint8_t *bytes = processedPcm.bytes;
  NSUInteger remaining = processedPcm.length;
  NSUInteger offset = 0;

  while (remaining >= frameSize) {
    [writer writeFrame:(uint8_t *)(bytes + offset) frameByteCount:frameSize];
    offset += frameSize;
    remaining -= frameSize;
  }

  if (remaining > 0) {
    uint8_t lastFrame[frameSize];
    memset(lastFrame, 0, frameSize);
    memcpy(lastFrame, bytes + offset, remaining);
    [writer writeFrame:lastFrame frameByteCount:frameSize];
  }

  return [dataItem data];
}

+ (NSData *_Nullable)applyEffectsToPcmData:(NSData *)pcmData
                                    preset:(VoiceMorpherPreset)preset
                                     error:(NSError **)error {
  NSUInteger sampleCount = pcmData.length / sizeof(int16_t);
  const int16_t *int16Samples = (const int16_t *)pcmData.bytes;

  float *floatSamples = (float *)malloc(sampleCount * sizeof(float));
  if (!floatSamples) {
    if (error) {
      *error = [NSError
          errorWithDomain:@"VoiceMorpher"
                     code:5
                 userInfo:@{
                   NSLocalizedDescriptionKey : @"Memory allocation failed"
                 }];
    }
    return nil;
  }

  // Convert int16 to float (-1.0 to 1.0 range)
  for (NSUInteger i = 0; i < sampleCount; i++) {
    floatSamples[i] = (float)int16Samples[i] / 32768.0f;
  }

  // Create audio format (mono, 48kHz, float)
  AVAudioFormat *format =
      [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                       sampleRate:48000
                                         channels:1
                                      interleaved:NO];

  AVAudioFrameCount frameCount = (AVAudioFrameCount)sampleCount;
  AVAudioPCMBuffer *inputBuffer =
      [[AVAudioPCMBuffer alloc] initWithPCMFormat:format
                                    frameCapacity:frameCount];
  inputBuffer.frameLength = frameCount;

  memcpy(inputBuffer.floatChannelData[0], floatSamples,
         sampleCount * sizeof(float));
  free(floatSamples);

  // Create engine and nodes
  AVAudioEngine *engine = [[AVAudioEngine alloc] init];
  AVAudioPlayerNode *playerNode = [[AVAudioPlayerNode alloc] init];
  AVAudioUnitTimePitch *pitchNode = [[AVAudioUnitTimePitch alloc] init];

  pitchNode.pitch = [self pitchShiftForPreset:preset];
  pitchNode.rate = [self rateForPreset:preset];

  [engine attachNode:playerNode];
  [engine attachNode:pitchNode];
  [engine connect:playerNode to:pitchNode format:format];

  AVAudioNode *lastNode = pitchNode;

  if (preset == VoiceMorpherPresetRobot) {
    AVAudioUnitDistortion *distortion = [[AVAudioUnitDistortion alloc] init];
    [distortion loadFactoryPreset:AVAudioUnitDistortionPresetSpeechRadioTower];
    distortion.wetDryMix = 40;
    [engine attachNode:distortion];
    [engine connect:pitchNode to:distortion format:format];
    lastNode = distortion;
  } else if (preset == VoiceMorpherPresetAnonymous) {
    AVAudioUnitDistortion *distortion = [[AVAudioUnitDistortion alloc] init];
    [distortion
        loadFactoryPreset:AVAudioUnitDistortionPresetSpeechCosmicInterference];
    distortion.wetDryMix = 30;
    [engine attachNode:distortion];
    [engine connect:pitchNode to:distortion format:format];
    lastNode = distortion;
  }

  [engine connect:lastNode to:engine.mainMixerNode format:format];

  __block NSMutableData *outputData = [[NSMutableData alloc] init];

  [engine.mainMixerNode
      installTapOnBus:0
           bufferSize:4096
               format:format
                block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) {
                  float *samples = buffer.floatChannelData[0];
                  AVAudioFrameCount count = buffer.frameLength;

                  int16_t *int16Buffer =
                      (int16_t *)malloc(count * sizeof(int16_t));
                  for (AVAudioFrameCount i = 0; i < count; i++) {
                    float sample = samples[i];
                    if (sample > 1.0f)
                      sample = 1.0f;
                    if (sample < -1.0f)
                      sample = -1.0f;
                    int16Buffer[i] = (int16_t)(sample * 32767.0f);
                  }

                  [outputData appendBytes:int16Buffer
                                   length:count * sizeof(int16_t)];
                  free(int16Buffer);
                }];

  NSError *startError = nil;
  [engine startAndReturnError:&startError];
  if (startError) {
    if (error) {
      *error = startError;
    }
    return nil;
  }

  [playerNode scheduleBuffer:inputBuffer
                      atTime:nil
                     options:0
           completionHandler:nil];
  [playerNode play];

  float rate = [self rateForPreset:preset];
  NSTimeInterval duration = (double)sampleCount / 48000.0 / rate + 0.5;
  [NSThread sleepForTimeInterval:duration];

  [playerNode stop];
  [engine.mainMixerNode removeTapOnBus:0];
  [engine stop];

  return outputData;
}

@end

4. Поиск во вкладке сохранённых удалённых сообщений

Смысл: экран savedDeletedMessagesListController строит записи через savedDeletedListEntries, поле поиска — первая строка; filterSavedDeletedListEntries отфильтровывает секции без совпадений по запросу (имя чата, текст сообщения, дата, подписи кнопок).

Swiftgram/SGSettingsUI/Sources/SavedDeletedMessagesListController.swift (полностью)

Файл приведён целиком в репозитории (292 строки). Ниже — полная копия без изменений.

// MARK: Swiftgram  Saved Deleted Messages List
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AccountContext
#if canImport(SGDeletedMessages)
import SGDeletedMessages
#endif

// MARK: - Entry

private enum SavedDeletedListEntry: ItemListNodeEntry {
    case search(id: Int, query: String)
    case empty(id: Int, text: String)
    case peerHeader(id: Int, sectionIndex: Int32, text: String)
    case messageRow(id: Int, sectionIndex: Int32, text: String, dateText: String, peerId: PeerId, messageId: MessageId, searchableText: String)
    case deleteAction(id: Int, sectionIndex: Int32, text: String, peerId: PeerId)

    var stableId: Int {
        switch self {
        case .search(let id, _): return id
        case .empty(let id, _): return id
        case .peerHeader(let id, _, _): return id
        case .messageRow(let id, _, _, _, _, _, _): return id
        case .deleteAction(let id, _, _, _): return id
        }
    }

    var section: ItemListSectionId {
        switch self {
        case .search(_, _): return 0
        case .empty: return 0
        case .peerHeader(_, let s, _): return s
        case .messageRow(_, let s, _, _, _, _, _): return s
        case .deleteAction(_, let s, _, _): return s
        }
    }

    static func < (lhs: SavedDeletedListEntry, rhs: SavedDeletedListEntry) -> Bool {
        lhs.stableId < rhs.stableId
    }

    static func == (lhs: SavedDeletedListEntry, rhs: SavedDeletedListEntry) -> Bool {
        switch (lhs, rhs) {
        case let (.search(a, q1), .search(b, q2)): return a == b && q1 == q2
        case let (.empty(a, t1), .empty(b, t2)): return a == b && t1 == t2
        case let (.peerHeader(a, s1, t1), .peerHeader(b, s2, t2)): return a == b && s1 == s2 && t1 == t2
        case let (.messageRow(a, s1, t1, d1, p1, m1, _), .messageRow(b, s2, t2, d2, p2, m2, _)): return a == b && s1 == s2 && t1 == t2 && d1 == d2 && p1 == p2 && m1 == m2
        case let (.deleteAction(a, s1, t1, p1), .deleteAction(b, s2, t2, p2)): return a == b && s1 == s2 && t1 == t2 && p1 == p2
        default: return false
        }
    }

    func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
        let args = arguments as! SavedDeletedListArguments
        switch self {
        case .search(_, let query):
            return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "🔍"), text: query, placeholder: presentationData.strings.Common_Search, type: .regular(capitalization: false, autocorrection: false), spacing: 0.0, clearType: .always, tag: nil, sectionId: section, textUpdated: { args.searchUpdated($0) }, action: {})
        case .empty(_, let text):
            return ItemListTextItem(presentationData: presentationData, text: .plain(text), sectionId: section)
        case .peerHeader(_, _, let text):
            return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: section)
        case .messageRow(_, _, let text, let dateText, let peerId, let messageId, _):
            return ItemListDisclosureItem(presentationData: presentationData, title: text, label: dateText, sectionId: section, style: .blocks, action: {
                args.openMessage(peerId, messageId)
            })
        case .deleteAction(_, _, let text, let peerId):
            return ItemListActionItem(presentationData: presentationData, title: text, kind: .destructive, alignment: .natural, sectionId: section, style: .blocks, action: {
                args.deleteMessagesForPeer(peerId)
            })
        }
    }
}

// MARK: - Arguments

private final class SearchQueryRef {
    var value: String = ""
}

private final class SavedDeletedListArguments {
    let searchQueryRef: SearchQueryRef
    var searchQuery: String { searchQueryRef.value }
    let searchUpdated: (String) -> Void
    let deleteMessagesForPeer: (PeerId) -> Void
    let openMessage: (PeerId, MessageId) -> Void
    init(searchQueryRef: SearchQueryRef, searchUpdated: @escaping (String) -> Void, deleteMessagesForPeer: @escaping (PeerId) -> Void, openMessage: @escaping (PeerId, MessageId) -> Void) {
        self.searchQueryRef = searchQueryRef
        self.searchUpdated = searchUpdated
        self.deleteMessagesForPeer = deleteMessagesForPeer
        self.openMessage = openMessage
    }
}

// MARK: - Date formatting

private let dateFormatter: DateFormatter = {
    let f = DateFormatter()
    f.dateStyle = .medium
    f.timeStyle = .short
    return f
}()

// MARK: - Entries builder (full list, no filter — like GLEGram settings)

#if canImport(SGDeletedMessages)
private func savedDeletedListEntries(
    data: [(peer: Peer?, peerId: PeerId, messages: [Message])],
    lang: String
) -> [SavedDeletedListEntry] {
    var entries: [SavedDeletedListEntry] = []
    var id = 0

    entries.append(.search(id: id, query: ""))
    id += 1

    if data.isEmpty {
        let text = (lang == "ru" ? "Нет сохранённых удалённых сообщений." : "No saved deleted messages.")
        entries.append(.empty(id: id, text: text))
        return entries
    }

    var sectionIndex: Int32 = 0
    for group in data {
        let peerName: String
        if let peer = group.peer {
            peerName = peer.debugDisplayTitle
        } else {
            peerName = "Peer \(group.peerId.id._internalGetInt64Value())"
        }
        sectionIndex += 1
        let countStr = lang == "ru" ? "\(group.messages.count) сообщ." : "\(group.messages.count) msg"
        entries.append(.peerHeader(id: id, sectionIndex: sectionIndex, text: "\(peerName.uppercased()) (\(countStr))"))
        id += 1

        for message in group.messages {
            let text = message.text.isEmpty
                ? (lang == "ru" ? "[медиа]" : "[media]")
                : String(message.text.prefix(120)).replacingOccurrences(of: "\n", with: " ")
            let searchableText = (message.text + " " + (message.sgDeletedAttribute.originalText ?? "")).trimmingCharacters(in: .whitespacesAndNewlines)
            let date = dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(message.timestamp)))
            entries.append(.messageRow(id: id, sectionIndex: sectionIndex, text: text, dateText: date, peerId: group.peerId, messageId: message.id, searchableText: searchableText))
            id += 1
        }

        let deleteText = lang == "ru" ? "Удалить все для этого чата" : "Delete all for this chat"
        entries.append(.deleteAction(id: id, sectionIndex: sectionIndex, text: deleteText, peerId: group.peerId))
        id += 1
    }

    return entries
}

/// Filter by search query — same logic as filterSGItemListUIEntrires in GLEGram settings: two-pass, keep search, keep sections that have matches.
private func filterSavedDeletedListEntries(_ entries: [SavedDeletedListEntry], by searchQuery: String?, lang: String) -> [SavedDeletedListEntry] {
    guard let query = searchQuery?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), !query.isEmpty else {
        return entries
    }
    var sectionIdsWithMatches: Set<Int32> = []
    for entry in entries {
        switch entry {
        case .search(_, _), .empty:
            break
        case .peerHeader(_, let s, let text):
            if text.lowercased().contains(query) { sectionIdsWithMatches.insert(s) }
        case .messageRow(_, let s, _, let dateText, _, _, let searchableText):
            if searchableText.lowercased().contains(query) || dateText.lowercased().contains(query) { sectionIdsWithMatches.insert(s) }
        case .deleteAction(_, let s, let text, _):
            if text.lowercased().contains(query) { sectionIdsWithMatches.insert(s) }
        }
    }
    var filtered: [SavedDeletedListEntry] = []
    for entry in entries {
        switch entry {
        case .search(_, _):
            filtered.append(entry)
        case .empty:
            continue
        case .peerHeader(_, let s, _), .messageRow(_, let s, _, _, _, _, _), .deleteAction(_, let s, _, _):
            if sectionIdsWithMatches.contains(s) {
                filtered.append(entry)
            }
        }
    }
    if filtered.count == 1, case .search(_, _) = filtered[0] {
        filtered.append(.empty(id: Int.max, text: lang == "ru" ? "Ничего не найдено." : "No results."))
    }
    return filtered
}
#endif

// MARK: - Controller

public func savedDeletedMessagesListController(context: AccountContext) -> ViewController {
    #if canImport(SGDeletedMessages)
    var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
    var pushControllerImpl: ((ViewController) -> Void)?
    let reloadPromise = ValuePromise(true, ignoreRepeated: false)
    let searchQueryPromise = ValuePromise("", ignoreRepeated: false)
    let searchQueryRef = SearchQueryRef()

    let arguments = SavedDeletedListArguments(
        searchQueryRef: searchQueryRef,
        searchUpdated: { value in
            searchQueryRef.value = value
            searchQueryPromise.set(value)
        },
        deleteMessagesForPeer: { peerId in
            let presentationData = context.sharedContext.currentPresentationData.with { $0 }
            let lang = presentationData.strings.baseLanguageCode
            let title = lang == "ru" ? "Удалить" : "Delete"
            let text = lang == "ru" ? "Удалить все сохранённые удалённые сообщения для этого чата?" : "Delete all saved deleted messages for this chat?"
            let alert = textAlertController(
                context: context,
                title: title,
                text: text,
                actions: [
                    TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
                        let _ = (SGDeletedMessages.getAllSavedDeletedMessages(postbox: context.account.postbox)
                        |> mapToSignal { groups -> Signal<Void, NoError> in
                            var idsToDelete: [MessageId] = []
                            for group in groups where group.peerId == peerId {
                                idsToDelete.append(contentsOf: group.messages.map { $0.id })
                            }
                            return SGDeletedMessages.deleteSavedDeletedMessages(ids: idsToDelete, postbox: context.account.postbox)
                        }
                        |> deliverOnMainQueue).start(completed: {
                            reloadPromise.set(true)
                        })
                    }),
                    TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
                ]
            )
            presentControllerImpl?(alert, nil)
        },
        openMessage: { peerId, messageId in
            let chatController = context.sharedContext.makeChatController(context: context, chatLocation: .peer(id: peerId), subject: .message(id: .id(messageId), highlight: nil, timecode: nil, setupReply: false), botStart: nil, mode: .standard(.default), params: nil)
            pushControllerImpl?(chatController)
        }
    )

    let dataSignal = reloadPromise.get()
    |> mapToSignal { _ -> Signal<[(peer: Peer?, peerId: PeerId, messages: [Message])], NoError> in
        return SGDeletedMessages.getAllSavedDeletedMessages(postbox: context.account.postbox)
    }

    let signal = combineLatest(dataSignal, searchQueryPromise.get(), context.sharedContext.presentationData)
    |> map { data, searchQuery, presentationData -> (ItemListControllerState, (ItemListNodeState, SavedDeletedListArguments)) in
        let lang = presentationData.strings.baseLanguageCode
        let title = lang == "ru" ? "Сохранённые удалённые" : "Saved Deleted"
        let controllerState = ItemListControllerState(
            presentationData: ItemListPresentationData(presentationData),
            title: .text(title),
            leftNavigationButton: nil,
            rightNavigationButton: nil,
            backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
        )
        let allEntries = savedDeletedListEntries(data: data, lang: lang)
        let entriesWithQuery = allEntries.map { entry -> SavedDeletedListEntry in
            if case .search(let id, _) = entry { return .search(id: id, query: searchQuery) }
            return entry
        }
        let entries = filterSavedDeletedListEntries(entriesWithQuery, by: searchQuery, lang: lang)
        let listState = ItemListNodeState(
            presentationData: ItemListPresentationData(presentationData),
            entries: entries,
            style: .blocks,
            ensureVisibleItemTag: nil,
            initialScrollToItem: nil
        )
        return (controllerState, (listState, arguments))
    }

    let controller = ItemListController(context: context, state: signal)
    presentControllerImpl = { [weak controller] c, a in
        controller?.present(c, in: PresentationContextType.window(PresentationSurfaceLevel.root), with: a)
    }
    pushControllerImpl = { [weak controller] c in
        controller?.navigationController?.pushViewController(c, animated: true)
    }
    return controller
    #else
    return ViewController(navigationBarPresentationData: nil)
    #endif
}

Swiftgram/SGDeletedMessages/Sources/SGDeletedMessages.swift (полностью)

import Foundation
import Postbox
import SwiftSignalKit
import SGSimpleSettings
#if canImport(SGLogging)
import SGLogging
#endif

// Local constants to avoid circular dependency with TelegramCore (SyncCore_Namespaces).
// Namespaces.Message.Cloud = 0
private let messageNamespaceCloud: Int32 = 0
// Namespaces.Message.SavedDeleted = 1338
private let messageNamespaceSavedDeleted: Int32 = 1338

public struct SGDeletedMessages {
    public static var showDeletedMessages: Bool {
        get {
            return SGSimpleSettings.shared.showDeletedMessages
        }
        set {
            SGSimpleSettings.shared.showDeletedMessages = newValue
        }
    }
    
    private static func savedDeletedId(for originalId: MessageId) -> MessageId {
        return MessageId(peerId: originalId.peerId, namespace: messageNamespaceSavedDeleted, id: originalId.id)
    }
    
    /// AyuGram-style: create a local SavedDeleted snapshot (separate namespace) and return `true` if saved.
    private static func saveSnapshotIfPossible(
        originalId: MessageId,
        transaction: Transaction,
        shouldSave: ((MessageId, Message) -> Bool)?,
        transformAttributes: ((Message, inout [MessageAttribute]) -> Void)?,
        transformMedia: ((Message, [Media]) -> [Media])?
    ) -> Bool {
        // If we're deleting an already-saved snapshot, don't re-save it.
        if originalId.namespace == messageNamespaceSavedDeleted {
            return false
        }
        
        guard let message = transaction.getMessage(originalId) else {
            // No local copy -> can't save (AyuGram behavior).
            return false
        }
        
        if let shouldSave, !shouldSave(originalId, message) {
            return false
        }
        
        let snapshotId = savedDeletedId(for: originalId)
        if transaction.messageExists(id: snapshotId) {
            return true
        }
        
        let storeForwardInfo = message.forwardInfo.flatMap(StoreMessageForwardInfo.init)
        var attributes = message.attributes
        var hasDeletedAttribute = false
        for attribute in attributes {
            if let deletedAttribute = attribute as? SGDeletedMessageAttribute {
                deletedAttribute.isDeleted = true
                if deletedAttribute.originalText == nil {
                    deletedAttribute.originalText = message.text
                }
                deletedAttribute.originalNamespace = originalId.namespace
                deletedAttribute.originalId = originalId.id
                hasDeletedAttribute = true
                break
            }
        }
        if !hasDeletedAttribute {
            attributes.append(SGDeletedMessageAttribute(isDeleted: true, originalText: message.text, originalNamespace: originalId.namespace, originalId: originalId.id))
        }
        
        transformAttributes?(message, &attributes)
        
        let media: [Media]
        if let transformMedia {
            media = transformMedia(message, message.media)
        } else {
            media = message.media
        }
        
        // Important: this is a local-only snapshot, so we don't keep a globallyUniqueId
        // (to avoid collisions with the original message).
        let storeMessage = StoreMessage(
            id: snapshotId,
            customStableId: nil,
            globallyUniqueId: nil,
            groupingKey: message.groupingKey,
            threadId: message.threadId,
            timestamp: message.timestamp,
            flags: StoreMessageFlags(message.flags),
            tags: message.tags,
            globalTags: message.globalTags,
            localTags: message.localTags,
            forwardInfo: storeForwardInfo,
            authorId: message.author?.id,
            text: message.text,
            attributes: attributes,
            media: media
        )
        let _ = transaction.addMessages([storeMessage], location: .UpperHistoryBlock)
        #if canImport(SGLogging)
        SGLogger.shared.log("SGDeletedMessages", "saveSnapshotIfPossible: saved snapshot \(snapshotId) for original \(originalId)")
        #endif
        return true
    }
    
    /// AyuGram-style: save snapshots (when possible).
    /// Returns the set of message ids for which a snapshot exists (created or already present).
    public static func saveSnapshots(
        ids: [MessageId],
        transaction: Transaction,
        shouldSave: ((MessageId, Message) -> Bool)? = nil,
        transformAttributes: ((Message, inout [MessageAttribute]) -> Void)? = nil,
        transformMedia: ((Message, [Media]) -> [Media])? = nil
    ) -> Set<MessageId> {
        guard showDeletedMessages, !ids.isEmpty else { return Set() }
        
        var result = Set<MessageId>()
        result.reserveCapacity(ids.count)
        
        for id in ids {
            if saveSnapshotIfPossible(originalId: id, transaction: transaction, shouldSave: shouldSave, transformAttributes: transformAttributes, transformMedia: transformMedia) {
                result.insert(id)
            }
        }
        return result
    }
    
    /// AyuGram-style: for delete-by-global-id pipelines, save snapshots for locally-present messages.
    public static func saveSnapshotsForGlobalIds(
        _ globalIds: [Int32],
        transaction: Transaction,
        shouldSave: ((MessageId, Message) -> Bool)? = nil,
        transformAttributes: ((Message, inout [MessageAttribute]) -> Void)? = nil,
        transformMedia: ((Message, [Media]) -> [Media])? = nil
    ) {
        guard showDeletedMessages else { return }
        for globalId in globalIds {
            if let id = transaction.messageIdsForGlobalIds([globalId]).first {
                _ = saveSnapshotIfPossible(originalId: id, transaction: transaction, shouldSave: shouldSave, transformAttributes: transformAttributes, transformMedia: transformMedia)
            }
        }
    }
    
    /// AyuGram-style: save snapshots (when possible) and return ids to physically delete.
    /// If the id itself is already a SavedDeleted snapshot, it will be deleted (no resave).
    public static func saveSnapshotsAndReturnIdsToDelete(ids: [MessageId], transaction: Transaction) -> [MessageId] {
        _ = saveSnapshots(ids: ids, transaction: transaction, shouldSave: nil, transformAttributes: nil, transformMedia: nil)
        return ids
    }
    
    /// Check if message is marked as deleted (using extension like Nicegram)
    public static func isMessageDeleted(_ message: Message) -> Bool {
        return message.sgDeletedAttribute.isDeleted
    }
    
    /// Get original text from message attribute (for edit history, using extension like Nicegram)
    public static func getOriginalText(_ message: Message) -> String? {
        return message.sgDeletedAttribute.originalText
    }
    
    /// Returns the combined on-disk size (in bytes) of the saved-deleted-attachments folder.
    public static func storageSizeBytes(mediaBoxBasePath: String) -> Int64 {
        let attachmentsPath = mediaBoxBasePath + "/saved-deleted-attachments"
        guard let enumerator = FileManager.default.enumerator(
            at: URL(fileURLWithPath: attachmentsPath),
            includingPropertiesForKeys: [.fileSizeKey],
            options: [.skipsHiddenFiles]
        ) else { return 0 }
        var total: Int64 = 0
        for case let url as URL in enumerator {
            total += Int64((try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize) ?? 0)
        }
        return total
    }

    /// Fetch all saved deleted messages grouped by peer.
    public static func getAllSavedDeletedMessages(
        postbox: Postbox
    ) -> Signal<[(peer: Peer?, peerId: PeerId, messages: [Message])], NoError> {
        return postbox.transaction { transaction -> [(peer: Peer?, peerId: PeerId, messages: [Message])] in
            var result: [(peer: Peer?, peerId: PeerId, messages: [Message])] = []
            let allPeerIds = transaction.chatListGetAllPeerIds()
            for peerId in allPeerIds {
                var messages: [Message] = []
                transaction.scanMessageAttributes(peerId: peerId, namespace: messageNamespaceSavedDeleted, limit: Int.max) { messageId, _ in
                    if let message = transaction.getMessage(messageId) {
                        messages.append(message)
                    }
                    return true
                }
                if !messages.isEmpty {
                    messages.sort { $0.timestamp > $1.timestamp }
                    let peer = transaction.getPeer(peerId)
                    result.append((peer: peer, peerId: peerId, messages: messages))
                }
            }
            result.sort { ($0.messages.first?.timestamp ?? 0) > ($1.messages.first?.timestamp ?? 0) }
            return result
        }
    }

    /// Delete specific saved deleted messages by their IDs.
    public static func deleteSavedDeletedMessages(
        ids: [MessageId],
        postbox: Postbox
    ) -> Signal<Void, NoError> {
        return postbox.transaction { transaction -> Void in
            if !ids.isEmpty {
                transaction.deleteMessages(ids, forEachMedia: { _ in })
            }
        }
    }

    /// Clear all saved deleted messages (actually delete them). Returns the number of deleted messages.
    public static func clearAllDeletedMessages(
        postbox: Postbox
    ) -> Signal<Int, NoError> {
        return postbox.transaction { transaction -> Int in
            // Remove saved attachment copies (AyuGram-style "Saved Attachments").
            let attachmentsPath = postbox.mediaBox.basePath + "/saved-deleted-attachments"
            let _ = try? FileManager.default.removeItem(atPath: attachmentsPath)
            let _ = try? FileManager.default.createDirectory(atPath: attachmentsPath, withIntermediateDirectories: true, attributes: nil)

            // All messages in the SavedDeleted namespace (1338) are snapshots — no attribute check needed.
            var messageIdsToDelete: [MessageId] = []
            let allPeerIds = transaction.chatListGetAllPeerIds()
            for peerId in allPeerIds {
                transaction.scanMessageAttributes(peerId: peerId, namespace: messageNamespaceSavedDeleted, limit: Int.max) { messageId, _ in
                    messageIdsToDelete.append(messageId)
                    return true
                }
            }

            let count = messageIdsToDelete.count
            if !messageIdsToDelete.isEmpty {
                transaction.deleteMessages(messageIdsToDelete, forEachMedia: { _ in })
            }

            return count
        }
    }
}

5. Ответ на удалённое сообщение: цитата оформляется сущностью .Pre

Смысл: если ответ идёт на сообщение с признаком «удалённое» (sgDeletedAttribute.isDeleted), вместо обычного ReplyMessageAttribute текст исходного сообщения вставляется перед вашим текстом с переводом строки, а диапазон цитаты помечается MessageTextEntity с типом .Pre(language: nil) (в клиентах Telegram это отображается как моноширинный / «блок кода» блок — визуальное выделение цитаты).

submodules/TelegramCore/Sources/PendingMessages/EnqueueMessage.swift — полный фрагмент if let replyToMessageId = replyToMessageId { ... }

                    if let replyToMessageId = replyToMessageId {
                        #if canImport(SGDeletedMessages)
                        let useDeletedCitation: Bool = {
                            if let replyMessage = transaction.getMessage(replyToMessageId.messageId) {
                                return replyMessage.sgDeletedAttribute.isDeleted
                            }
                            return false
                        }()
                        #else
                        let useDeletedCitation = false
                        #endif
                        if useDeletedCitation {
                            #if canImport(SGDeletedMessages)
                            if let replyMessage = transaction.getMessage(replyToMessageId.messageId) {
                                let quoteText = replyMessage.sgDeletedAttribute.originalText ?? replyMessage.text
                                let citationPrefix = quoteText + "\n"
                                effectiveText = citationPrefix + text
                                let offset = citationPrefix.count
                                let citationEntities = [MessageTextEntity(range: 0..<offset, type: .Pre(language: nil))]
                                var foundEntities = false
                                for i in attributes.indices {
                                    if let entityAttr = attributes[i] as? TextEntitiesMessageAttribute {
                                        let shifted = entityAttr.entities.map { MessageTextEntity(range: $0.range.lowerBound + offset ..< $0.range.upperBound + offset, type: $0.type) }
                                        attributes[i] = TextEntitiesMessageAttribute(entities: citationEntities + shifted)
                                        foundEntities = true
                                        break
                                    }
                                }
                                if !foundEntities {
                                    attributes.append(TextEntitiesMessageAttribute(entities: citationEntities))
                                }
                            }
                            #endif
                        } else {
                            var threadMessageId: MessageId?
                            var quote = replyToMessageId.quote
                            let isQuote = quote != nil
                            if let replyMessage = transaction.getMessage(replyToMessageId.messageId) {
                                if replyMessage.id.namespace == Namespaces.Message.Cloud, let threadId = replyMessage.threadId {
                                    threadMessageId = MessageId(peerId: replyMessage.id.peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))
                                }
                                if quote == nil, replyToMessageId.messageId.peerId != peerId {
                                    let nsText = replyMessage.text as NSString
                                    var replyMedia: Media?
                                    for m in replyMessage.media {
                                        switch m {
                                        case _ as TelegramMediaImage, _ as TelegramMediaFile:
                                            replyMedia = m
                                        default:
                                            break
                                        }
                                    }
                                    quote = EngineMessageReplyQuote(text: replyMessage.text, offset: nil, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true), media: replyMedia)
                                }
                            }
                            attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: quote, isQuote: isQuote, todoItemId: replyToMessageId.todoItemId))
                        }
                    }

6. Добавление «чужих» подарков в свой профиль (локально, только у вас)

Смысл: в профиле другого пользователя по долгому нажатию на уникальный подарок появляется пункт «Добавить в свой профиль (только вы увидите)». Slug подарка сохраняется в SGSimpleSettings.customProfileGiftSlugs и customProfileGiftShownSlugs; в своём профиле можно удалить запись из списка.

Фрагмент submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoGiftsPaneNode.swift

        #if canImport(SGSimpleSettings)
        let isMyProfile = self.peerId == self.context.account.peerId
        if !isMyProfile, case let .unique(uniqueGift) = gift.gift {
            let slug = uniqueGift.slug
            let alreadyAdded = SGSimpleSettings.shared.customProfileGiftSlugs.contains(slug)
            if !alreadyAdded {
                let addTitle = presentationData.strings.baseLanguageCode == "ru" ? "Добавить в свой профиль (только вы увидите)" : "Add to my profile (only you will see)"
                items.append(.action(ContextMenuActionItem(text: addTitle, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
                    var slugs = SGSimpleSettings.shared.customProfileGiftSlugs
                    if !slugs.contains(slug) {
                        slugs.append(slug)
                        SGSimpleSettings.shared.customProfileGiftSlugs = slugs
                    }
                    var shown = SGSimpleSettings.shared.customProfileGiftShownSlugs
                    if !shown.contains(slug) {
                        shown.append(slug)
                        SGSimpleSettings.shared.customProfileGiftShownSlugs = shown
                    }
                    self?.giftsListView.triggerCustomShownRefresh()
                    c?.dismiss(completion: nil)
                })))
                items.append(.separator)
            }
        } else if isMyProfile, case let .slug(slug) = gift.reference {
            let removeTitle = presentationData.strings.baseLanguageCode == "ru" ? "Удалить из профиля" : "Remove from profile"
            items.append(.action(ContextMenuActionItem(text: removeTitle, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, _ in
                var slugs = SGSimpleSettings.shared.customProfileGiftSlugs
                slugs.removeAll { $0 == slug }
                SGSimpleSettings.shared.customProfileGiftSlugs = slugs
                var shown = SGSimpleSettings.shared.customProfileGiftShownSlugs
                shown.removeAll { $0 == slug }
                SGSimpleSettings.shared.customProfileGiftShownSlugs = shown
                self?.giftsListView.triggerCustomShownRefresh()
            })))
            items.append(.separator)
        }
        #endif

Ключи и свойства в Swiftgram/SGSimpleSettings/Sources/SimpleSettings.swift

        case customProfileGiftSlugs
        case customProfileGiftShownSlugs
        case pinnedCustomProfileGiftSlugs
        case localProfileGiftStatusFileId
    @UserDefault(key: Keys.customProfileGiftSlugs.rawValue)
    public var customProfileGiftSlugs: [String]

    /// Slugs of custom gifts that are shown on profile (worn). Persisted locally so "Show/Hide" state doesn't reset.
    @UserDefault(key: Keys.customProfileGiftShownSlugs.rawValue)
    public var customProfileGiftShownSlugs: [String]

    @UserDefault(key: Keys.pinnedCustomProfileGiftSlugs.rawValue)
    public var pinnedCustomProfileGiftSlugs: [String]

    /// When set, show this fileId as emoji status on my profile (so gift status doesn't disappear).
    @UserDefault(key: Keys.localProfileGiftStatusFileId.rawValue)
    public var localProfileGiftStatusFileId: String

Файл: GLEGram-features.md в корне репозитория.