Files
GLEGram-iOS/GLEGram/GLESettingsUI/Sources/GLEGramSettingsController.swift
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

1240 lines
84 KiB
Swift
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// MARK: Swiftgram
import SGSimpleSettings
import SGStrings
import SGItemListUI
import SGSupporters
#if canImport(SGDeletedMessages)
import SGDeletedMessages
#endif
import Foundation
import UIKit
import AppBundle
import CoreText
import CoreGraphics
#if canImport(UniformTypeIdentifiers)
import UniformTypeIdentifiers
#endif
import Display
import PromptUI
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
import UndoUI
import AccountContext
import LocalizedPeerData
import LegacyUI
import LegacyMediaPickerUI
import SettingsUI
#if canImport(SGFakeLocation)
import SGFakeLocation
#endif
#if canImport(FaceScanScreen)
import FaceScanScreen
#endif
// MARK: - Back button helper
private class BackButtonTarget: NSObject {
private weak var controller: UIViewController?
init(controller: UIViewController) {
self.controller = controller
}
@objc func backAction() {
if let nav = controller?.navigationController, nav.viewControllers.count > 1 {
nav.popViewController(animated: true)
} else {
controller?.dismiss(animated: true)
}
}
}
private var backButtonTargetKey: UInt8 = 0
private func makeBackBarButtonItem(presentationData: PresentationData, controller: ViewController) -> UIBarButtonItem {
let target = BackButtonTarget(controller: controller)
objc_setAssociatedObject(controller, &backButtonTargetKey, target, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return UIBarButtonItem(backButtonAppearanceWithTitle: presentationData.strings.Common_Back, target: target, action: #selector(BackButtonTarget.backAction))
}
/// Масштабирует изображение до maxSize по большей стороне с чётким рендером (как иконки «Канал, Чат, Форум»).
private func scaleImageForListIcon(_ image: UIImage, maxSize: CGFloat) -> UIImage? {
let size = image.size
guard size.width > 0, size.height > 0 else { return image }
guard size.width > maxSize || size.height > maxSize else { return image }
let scale = min(maxSize / size.width, maxSize / size.height)
let newSizePt = CGSize(width: size.width * scale, height: size.height * scale)
let screenScale = UIScreen.main.scale
let format = UIGraphicsImageRendererFormat()
format.scale = screenScale
format.opaque = false
let renderer = UIGraphicsImageRenderer(size: newSizePt, format: format)
return renderer.image { ctx in
ctx.cgContext.interpolationQuality = .high
image.draw(in: CGRect(origin: .zero, size: newSizePt))
}
}
private enum GLEGramTab: Int, CaseIterable {
case appearance = 0
case security
case other
}
private enum GLEGramSection: Int32, SGItemListSection {
case search
case functions
case links
case notifications
case messages
case chatList
case onlineStatus
case readReceipts
case content
case localPremium
case interface
case appearance
case fontReplacement
case fakeLocation
case onlineStatusRecording
case other
}
private func tab(for section: GLEGramSection) -> GLEGramTab {
switch section {
case .search: return .appearance
case .functions, .links: return .appearance
case .notifications: return .other
case .localPremium, .interface, .appearance, .fontReplacement: return .appearance
case .messages, .chatList, .onlineStatus, .readReceipts, .content, .fakeLocation, .onlineStatusRecording: return .security
case .other: return .other
}
}
private func sectionForEntry(_ entry: GLEGramEntry) -> GLEGramSection {
switch entry {
case .header(_, let s, _, _): return s
case .toggle(_, let s, _, _, _, _): return s
case .toggleWithIcon(_, let s, _, _, _, _, _): return s
case .notice(_, let s, _): return s
case .percentageSlider(_, let s, _, _): return s
case .delaySecondsSlider(_, let s, _, _, _, _, _): return s
case .fontSizeMultiplierSlider(_, let s, _, _): return s
case .oneFromManySelector(_, let s, _, _, _, _): return s
case .disclosure(_, let s, _, _): return s
case .disclosureWithIcon(_, let s, _, _, _): return s
case .peerColorDisclosurePreview(_, let s, _, _): return s
case .action(_, let s, _, _, _): return s
case .searchInput(_, let s, _, _, _): return s
case .reorderableRow(_, let s, _, _, _): return s
}
}
private func gleGramEntriesFiltered(by selectedTab: GLEGramTab, entries: [GLEGramEntry]) -> [GLEGramEntry] {
entries.filter { entry in
let sec = sectionForEntry(entry)
return sec == .search || tab(for: sec) == selectedTab
}
}
/// Account info tuple for per-account notification toggles.
typealias AccountInfo = (recordId: Int64, peerId: Int64, name: String)
/// Root GLEGram screen: exteraGram-style header (icon + title + tagline), Функции (4 tabs), Ссылки (Канал, Чат, Форум).
private func gleGramRootEntries(presentationData: PresentationData, accounts: [AccountInfo] = []) -> [GLEGramEntry] {
let lang = presentationData.strings.baseLanguageCode
var entries: [GLEGramEntry] = []
let id = SGItemListCounter()
let functionsHeader = lang == "ru" ? "ФУНКЦИИ" : "FEATURES"
let linksHeader = lang == "ru" ? "ССЫЛКИ" : "LINKS"
let appearanceTitle = lang == "ru" ? "Оформление" : "Appearance"
let securityTitle = lang == "ru" ? "Приватность" : "Privacy"
let otherTitle = lang == "ru" ? "Другие функции" : "Other"
let channelTitle = lang == "ru" ? "Канал" : "Channel"
let chatTitle = lang == "ru" ? "Чат" : "Chat"
let forumTitle = lang == "ru" ? "Форум" : "Forum"
entries.append(.header(id: id.count, section: .functions, text: functionsHeader, badge: nil))
entries.append(GLEGramEntry.disclosureWithIcon(id: id.count, section: .functions, link: .appearanceTab, text: appearanceTitle, iconRef: "GLEGramTabAppearance"))
entries.append(GLEGramEntry.disclosureWithIcon(id: id.count, section: .functions, link: .securityTab, text: securityTitle, iconRef: "GLEGramTabSecurity"))
entries.append(GLEGramEntry.disclosureWithIcon(id: id.count, section: .functions, link: .otherTab, text: otherTitle, iconRef: "GLEGramTabOther"))
entries.append(.header(id: id.count, section: .links, text: linksHeader, badge: nil))
entries.append(GLEGramEntry.disclosureWithIcon(id: id.count, section: .links, link: .channelLink, text: channelTitle, iconRef: "Settings/Menu/Channels"))
entries.append(GLEGramEntry.disclosureWithIcon(id: id.count, section: .links, link: .chatLink, text: chatTitle, iconRef: "Settings/Menu/GroupChats"))
entries.append(GLEGramEntry.disclosureWithIcon(id: id.count, section: .links, link: .forumLink, text: forumTitle, iconRef: "Settings/Menu/Topics"))
if cachedAggregateAccess().betaBuilds, let betaConfig = cachedAggregateBetaConfig(), betaConfig.channelUrl != nil {
let betaHeader = lang == "ru" ? "БЕТА" : "BETA"
entries.append(.header(id: id.count, section: .links, text: betaHeader, badge: nil))
let betaChannelTitle = lang == "ru" ? "Перейти в канал с бета-версиями" : "Go to Beta Channel"
entries.append(GLEGramEntry.disclosure(id: id.count, section: .links, link: .betaChannel, text: betaChannelTitle))
}
return entries
}
private enum GLEGramSliderSetting: Hashable {
case fontReplacementSize
case ghostModeMessageSendDelay
}
private enum GLEGramOneFromManySetting: Hashable {
case onlineStatusRecordingInterval
}
private enum GLEGramDisclosureLink: Hashable {
case fakeLocationPicker
case tabOrganizer
case profileCover
case fontReplacementPicker
case fontReplacementBoldPicker
case fontReplacementImportFile
case fontReplacementBoldImportFile
case appearanceTab
case securityTab
case otherTab
case fakeProfileSettings
case feelRichAmount
case savedDeletedMessagesList
/// Read receipts: peers to exclude from sending read receipts.
case readReceiptsExclusions
/// Links section: open t.me URLs.
case channelLink
case chatLink
case forumLink
/// Beta section: channel with beta versions.
case betaChannel
}
private typealias GLEGramEntry = SGItemListUIEntry<GLEGramSection, SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>
private struct GLEGramSettingsControllerState: Equatable {
var searchQuery: String?
var selectedTab: GLEGramTab = .appearance
}
/// Removes gated toggle entries, their dependent entries (disclosures, sliders, actions), following notices,
/// and orphaned headers (headers whose sub-section contains only notices after filtering).
private func filterGatedFeatures(entries: [GLEGramEntry]) -> [GLEGramEntry] {
let settings = SGSimpleSettings.shared
// Collect which toggle keys are gated (not visible)
var gatedKeys = Set<String>()
for entry in entries {
if case let .toggle(_, _, settingName, _, _, _) = entry {
if !settings.isFeatureVisible(settingName.rawValue) {
gatedKeys.insert(settingName.rawValue)
}
}
}
if gatedKeys.isEmpty { return entries }
// Disclosures that should be hidden when their parent toggle is gated
let dependentDisclosures: [GLEGramDisclosureLink: SGBoolSetting] = [
.fakeProfileSettings: .fakeProfileEnabled,
.fakeLocationPicker: .fakeLocationEnabled,
.feelRichAmount: .feelRichEnabled,
.readReceiptsExclusions: .disableMessageReadReceipt,
.savedDeletedMessagesList: .showDeletedMessages,
]
// Sliders that should be hidden when their parent toggle is gated
let dependentSliders: [GLEGramSliderSetting: SGBoolSetting] = [
.ghostModeMessageSendDelay: .disableOnlineStatus,
]
// Actions that should be hidden when their parent toggle is gated
let dependentActions: [String: SGBoolSetting] = [
"clearDeletedMessages": .showDeletedMessages,
]
// First pass: remove gated entries and their dependents + following notices
var filtered: [GLEGramEntry] = []
var skipNextNotice = false
for entry in entries {
if skipNextNotice {
skipNextNotice = false
if case .notice = entry { continue }
}
// Skip gated toggles
if case let .toggle(_, _, settingName, _, _, _) = entry {
if gatedKeys.contains(settingName.rawValue) {
skipNextNotice = true
continue
}
}
// Skip dependent disclosures
if case let .disclosure(_, _, link, _) = entry {
if let parent = dependentDisclosures[link], gatedKeys.contains(parent.rawValue) {
skipNextNotice = true
continue
}
}
// Skip dependent delay sliders
if case let .delaySecondsSlider(_, _, settingName, _, _, _, _) = entry {
if let parent = dependentSliders[settingName], gatedKeys.contains(parent.rawValue) {
skipNextNotice = true
continue
}
}
// Skip dependent actions
if case let .action(_, _, actionType, _, _) = entry {
if let actionStr = actionType as? String,
let parent = dependentActions[actionStr],
gatedKeys.contains(parent.rawValue) {
skipNextNotice = true
continue
}
}
filtered.append(entry)
}
// Second pass: remove orphaned headers.
// A header is orphaned if all entries until the next header (or end) are only notices.
var result: [GLEGramEntry] = []
var i = 0
while i < filtered.count {
if case .header = filtered[i] {
// Look ahead to find the extent of this sub-section
var j = i + 1
var hasContent = false
while j < filtered.count {
if case .header = filtered[j] { break }
if case .notice = filtered[j] {
// notices alone don't count as content
} else {
hasContent = true
}
j += 1
}
if hasContent {
for k in i..<j {
result.append(filtered[k])
}
}
// else: skip this header and its trailing notices
i = j
} else {
result.append(filtered[i])
i += 1
}
}
return result
}
private func gleGramEntries(presentationData: PresentationData, contentSettingsConfiguration: ContentSettingsConfiguration?, state: GLEGramSettingsControllerState, mediaBoxBasePath: String, accounts: [AccountInfo] = []) -> [GLEGramEntry] {
let lang = presentationData.strings.baseLanguageCode
let strings = presentationData.strings
var entries: [GLEGramEntry] = []
let id = SGItemListCounter()
entries.append(.searchInput(id: id.count, section: .search, title: NSAttributedString(string: "🔍"), text: state.searchQuery ?? "", placeholder: strings.Common_Search))
// MARK: Messages
entries.append(.header(id: id.count, section: .messages, text: i18n("Settings.DeletedMessages.Header", lang), badge: nil))
let showDeleted = SGSimpleSettings.shared.showDeletedMessages
entries.append(.toggle(id: id.count, section: .messages, settingName: .showDeletedMessages, value: showDeleted, text: i18n("Settings.DeletedMessages.Save", lang), enabled: true))
entries.append(.notice(id: id.count, section: .messages, text: i18n("Settings.DeletedMessages.Save.Notice", lang)))
entries.append(.toggle(id: id.count, section: .messages, settingName: .saveDeletedMessagesMedia, value: SGSimpleSettings.shared.saveDeletedMessagesMedia, text: i18n("Settings.DeletedMessages.SaveMedia", lang), enabled: showDeleted))
entries.append(.toggle(id: id.count, section: .messages, settingName: .saveDeletedMessagesReactions, value: SGSimpleSettings.shared.saveDeletedMessagesReactions, text: i18n("Settings.DeletedMessages.SaveReactions", lang), enabled: showDeleted))
entries.append(.toggle(id: id.count, section: .messages, settingName: .saveDeletedMessagesForBots, value: SGSimpleSettings.shared.saveDeletedMessagesForBots, text: i18n("Settings.DeletedMessages.SaveForBots", lang), enabled: showDeleted))
let storageSizeFormatted = ByteCountFormatter.string(fromByteCount: SGDeletedMessages.storageSizeBytes(mediaBoxBasePath: mediaBoxBasePath), countStyle: .file)
entries.append(.notice(id: id.count, section: .messages, text: i18n("Settings.DeletedMessages.StorageSize", lang) + ": " + storageSizeFormatted))
entries.append(.disclosure(id: id.count, section: .messages, link: .savedDeletedMessagesList, text: (lang == "ru" ? "Просмотреть сохранённые" : "View saved messages")))
entries.append(.action(id: id.count, section: .messages, actionType: "clearDeletedMessages" as AnyHashable, text: i18n("Settings.DeletedMessages.Clear", lang), kind: .destructive))
let saveEditHistoryTitle = (lang == "ru" ? "Сохранять историю редактирования" : "Save edit history")
let saveEditHistoryNotice = (lang == "ru"
? "Сохраняет оригинальный текст сообщений при редактировании (в т.ч. чужих)."
: "Keeps original message text when messages are edited (including from others).")
entries.append(.toggle(id: id.count, section: .messages, settingName: .saveEditHistory, value: SGSimpleSettings.shared.saveEditHistory, text: saveEditHistoryTitle, enabled: true))
entries.append(.notice(id: id.count, section: .messages, text: saveEditHistoryNotice))
let localEditTitle = (lang == "ru" ? "Редактировать сообщения собеседника (локально)" : "Edit other's messages (local only)")
let localEditNotice = (lang == "ru"
? "В контекстном меню входящих сообщений появится «Редактировать». Изменения видны только на вашем устройстве."
: "Adds «Edit» to context menu for incoming messages. Changes are visible only on your device.")
entries.append(.toggle(id: id.count, section: .messages, settingName: .enableLocalMessageEditing, value: SGSimpleSettings.shared.enableLocalMessageEditing, text: localEditTitle, enabled: true))
entries.append(.notice(id: id.count, section: .messages, text: localEditNotice))
// MARK: Chat list / Read all
entries.append(.header(id: id.count, section: .chatList, text: i18n("READ_ALL_HEADER", lang), badge: nil))
entries.append(.action(id: id.count, section: .chatList, actionType: "markAllReadLocal" as AnyHashable, text: i18n("READ_ALL_LOCAL_TITLE", lang), kind: .generic))
entries.append(.notice(id: id.count, section: .chatList, text: i18n("READ_ALL_LOCAL_SUBTITLE", lang)))
entries.append(.action(id: id.count, section: .chatList, actionType: "markAllReadServer" as AnyHashable, text: i18n("READ_ALL_SERVER_TITLE", lang), kind: .generic))
entries.append(.notice(id: id.count, section: .chatList, text: i18n("READ_ALL_SERVER_SUBTITLE", lang)))
let keepRemovedTitle = (lang == "ru" ? "Сохранять удалённые каналы/группы" : "Keep removed channels/groups")
let keepRemovedNotice = (lang == "ru"
? "Когда вас удаляют или блокируют из группы/канала, он остаётся видимым с пометкой. Новые сообщения не приходят, информация профиля не обновляется."
: "When you are removed or blocked from a group/channel, it remains visible with a marker. New messages don't arrive, profile information doesn't update.")
entries.append(.toggle(id: id.count, section: .chatList, settingName: .keepRemovedChannels, value: SGSimpleSettings.shared.keepRemovedChannels, text: keepRemovedTitle, enabled: true))
entries.append(.notice(id: id.count, section: .chatList, text: keepRemovedNotice))
// MARK: Online status / Ghost mode
entries.append(.header(id: id.count, section: .onlineStatus, text: (lang == "ru" ? "ОНЛАЙН-СТАТУС" : "ONLINE STATUS"), badge: nil))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableOnlineStatus, value: SGSimpleSettings.shared.disableOnlineStatus, text: i18n("DISABLE_ONLINE_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_ONLINE_STATUS_SUBTITLE", lang)))
let delaySeconds = SGSimpleSettings.shared.ghostModeMessageSendDelaySeconds
let delayLeftLabel = lang == "ru" ? "Выкл" : "Off"
let delayRightLabel = lang == "ru" ? "45 сек" : "45 sec"
let delayCenterLabels = lang == "ru" ? ["Выкл", "12 сек", "30 сек", "45 сек"] : ["Off", "12 sec", "30 sec", "45 sec"]
entries.append(.delaySecondsSlider(id: id.count, section: .onlineStatus, settingName: .ghostModeMessageSendDelay, value: delaySeconds, leftLabel: delayLeftLabel, rightLabel: delayRightLabel, centerLabels: delayCenterLabels))
let delayNotice = (lang == "ru" ? "При включённой задержке сообщения будут отправляться через выбранный интервал (12, 30 или 45 секунд). Онлайн-статус не будет отображаться во время отправки." : "When delay is enabled, messages will be sent after the selected interval (12, 30 or 45 seconds). Online status will not appear during sending.")
entries.append(.notice(id: id.count, section: .onlineStatus, text: delayNotice))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableTypingStatus, value: SGSimpleSettings.shared.disableTypingStatus, text: i18n("DISABLE_TYPING_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_TYPING_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableRecordingVideoStatus, value: SGSimpleSettings.shared.disableRecordingVideoStatus, text: i18n("DISABLE_RECORDING_VIDEO_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_RECORDING_VIDEO_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableUploadingVideoStatus, value: SGSimpleSettings.shared.disableUploadingVideoStatus, text: i18n("DISABLE_UPLOADING_VIDEO_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_UPLOADING_VIDEO_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableVCMessageRecordingStatus, value: SGSimpleSettings.shared.disableVCMessageRecordingStatus, text: i18n("DISABLE_VC_MESSAGE_RECORDING_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_VC_MESSAGE_RECORDING_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableVCMessageUploadingStatus, value: SGSimpleSettings.shared.disableVCMessageUploadingStatus, text: i18n("DISABLE_VC_MESSAGE_UPLOADING_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_VC_MESSAGE_UPLOADING_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableUploadingPhotoStatus, value: SGSimpleSettings.shared.disableUploadingPhotoStatus, text: i18n("DISABLE_UPLOADING_PHOTO_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_UPLOADING_PHOTO_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableUploadingFileStatus, value: SGSimpleSettings.shared.disableUploadingFileStatus, text: i18n("DISABLE_UPLOADING_FILE_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_UPLOADING_FILE_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableChoosingLocationStatus, value: SGSimpleSettings.shared.disableChoosingLocationStatus, text: i18n("DISABLE_CHOOSING_LOCATION_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_CHOOSING_LOCATION_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableChoosingContactStatus, value: SGSimpleSettings.shared.disableChoosingContactStatus, text: i18n("DISABLE_CHOOSING_CONTACT_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_CHOOSING_CONTACT_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disablePlayingGameStatus, value: SGSimpleSettings.shared.disablePlayingGameStatus, text: i18n("DISABLE_PLAYING_GAME_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_PLAYING_GAME_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableRecordingRoundVideoStatus, value: SGSimpleSettings.shared.disableRecordingRoundVideoStatus, text: i18n("DISABLE_RECORDING_ROUND_VIDEO_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_RECORDING_ROUND_VIDEO_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableUploadingRoundVideoStatus, value: SGSimpleSettings.shared.disableUploadingRoundVideoStatus, text: i18n("DISABLE_UPLOADING_ROUND_VIDEO_STATUS_TITLE", lang), enabled: true))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableSpeakingInGroupCallStatus, value: SGSimpleSettings.shared.disableSpeakingInGroupCallStatus, text: i18n("DISABLE_SPEAKING_IN_GROUP_CALL_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_SPEAKING_IN_GROUP_CALL_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableChoosingStickerStatus, value: SGSimpleSettings.shared.disableChoosingStickerStatus, text: i18n("DISABLE_CHOOSING_STICKER_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_CHOOSING_STICKER_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableEmojiInteractionStatus, value: SGSimpleSettings.shared.disableEmojiInteractionStatus, text: i18n("DISABLE_EMOJI_INTERACTION_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_EMOJI_INTERACTION_STATUS_SUBTITLE", lang)))
entries.append(.toggle(id: id.count, section: .onlineStatus, settingName: .disableEmojiAcknowledgementStatus, value: SGSimpleSettings.shared.disableEmojiAcknowledgementStatus, text: i18n("DISABLE_EMOJI_ACKNOWLEDGEMENT_STATUS_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatus, text: i18n("DISABLE_EMOJI_ACKNOWLEDGEMENT_STATUS_SUBTITLE", lang)))
// MARK: Read receipts
entries.append(.header(id: id.count, section: .readReceipts, text: (lang == "ru" ? "ОТЧЁТЫ О ПРОЧТЕНИИ" : "READ RECEIPTS"), badge: nil))
let disableMessageReadReceiptTitle = (lang == "ru" ? "Отчёты: сообщения" : i18n("DISABLE_MESSAGE_READ_RECEIPT_TITLE", lang))
entries.append(.toggle(id: id.count, section: .readReceipts, settingName: .disableMessageReadReceipt, value: SGSimpleSettings.shared.disableMessageReadReceipt, text: disableMessageReadReceiptTitle, enabled: true))
entries.append(.notice(id: id.count, section: .readReceipts, text: i18n("DISABLE_MESSAGE_READ_RECEIPT_SUBTITLE", lang)))
let sendToCount = SGSimpleSettings.shared.messageReadReceiptsSendToPeerIds.count
let sendToText = (lang == "ru"
? "Отправлять отчёты выбранным" + (sendToCount > 0 ? " (\(sendToCount))" : "")
: "Send receipts to selected" + (sendToCount > 0 ? " (\(sendToCount))" : ""))
entries.append(.disclosure(id: id.count, section: .readReceipts, link: .readReceiptsExclusions, text: sendToText))
let sendToNotice = (lang == "ru"
? "Пустой список = никому не отправлять. Иначе — только выбранным."
: "Empty list = send to no one. Otherwise — only to selected.")
entries.append(.notice(id: id.count, section: .readReceipts, text: sendToNotice))
let disableStoryReadReceiptTitle = (lang == "ru" ? "Отчёты: истории" : i18n("DISABLE_STORY_READ_RECEIPT_TITLE", lang))
entries.append(.toggle(id: id.count, section: .readReceipts, settingName: .disableStoryReadReceipt, value: SGSimpleSettings.shared.disableStoryReadReceipt, text: disableStoryReadReceiptTitle, enabled: true))
entries.append(.notice(id: id.count, section: .readReceipts, text: i18n("DISABLE_STORY_READ_RECEIPT_SUBTITLE", lang)))
// MARK: Content / security / ads
entries.append(.header(id: id.count, section: .content, text: (lang == "ru" ? "КОНТЕНТ И БЕЗОПАСНОСТЬ" : "CONTENT & SECURITY"), badge: nil))
let disableAllAdsTitle = (lang == "ru" ? "Отключить рекламу" : i18n("DISABLE_ALL_ADS_TITLE", lang))
entries.append(.toggle(id: id.count, section: .content, settingName: .disableAllAds, value: SGSimpleSettings.shared.disableAllAds, text: disableAllAdsTitle, enabled: true))
entries.append(.notice(id: id.count, section: .content, text: i18n("DISABLE_ALL_ADS_SUBTITLE", lang)))
let hideProxySponsorTitle = (lang == "ru" ? "Скрыть спонсора прокси" : i18n("HIDE_PROXY_SPONSOR_TITLE", lang))
entries.append(.toggle(id: id.count, section: .content, settingName: .hideProxySponsor, value: SGSimpleSettings.shared.hideProxySponsor, text: hideProxySponsorTitle, enabled: true))
entries.append(.notice(id: id.count, section: .content, text: i18n("HIDE_PROXY_SPONSOR_SUBTITLE", lang)))
let enableSavingProtectedTitle = (lang == "ru" ? "Сохранять защищённый контент" : i18n("ENABLE_SAVING_PROTECTED_CONTENT_TITLE", lang))
entries.append(.toggle(id: id.count, section: .content, settingName: .enableSavingProtectedContent, value: SGSimpleSettings.shared.enableSavingProtectedContent, text: enableSavingProtectedTitle, enabled: true))
entries.append(.notice(id: id.count, section: .content, text: i18n("ENABLE_SAVING_PROTECTED_CONTENT_SUBTITLE", lang)))
let enableSavingSelfDestructTitle = (lang == "ru" ? "Сохранять самоуничтож." : i18n("ENABLE_SAVING_SELF_DESTRUCTING_MESSAGES_TITLE", lang))
entries.append(.toggle(id: id.count, section: .content, settingName: .enableSavingSelfDestructingMessages, value: SGSimpleSettings.shared.enableSavingSelfDestructingMessages, text: enableSavingSelfDestructTitle, enabled: true))
entries.append(.notice(id: id.count, section: .content, text: i18n("ENABLE_SAVING_SELF_DESTRUCTING_MESSAGES_SUBTITLE", lang)))
let disableScreenshotDetectionTitle = (lang == "ru" ? "Скрыть скриншоты" : i18n("DISABLE_SCREENSHOT_DETECTION_TITLE", lang))
entries.append(.toggle(id: id.count, section: .content, settingName: .disableScreenshotDetection, value: SGSimpleSettings.shared.disableScreenshotDetection, text: disableScreenshotDetectionTitle, enabled: true))
entries.append(.notice(id: id.count, section: .content, text: i18n("DISABLE_SCREENSHOT_DETECTION_SUBTITLE", lang)))
let disableSecretBlurTitle = (lang == "ru" ? "Не размывать секретные" : i18n("DISABLE_SECRET_CHAT_BLUR_ON_SCREENSHOT_TITLE", lang))
entries.append(.toggle(id: id.count, section: .content, settingName: .disableSecretChatBlurOnScreenshot, value: SGSimpleSettings.shared.disableSecretChatBlurOnScreenshot, text: disableSecretBlurTitle, enabled: true))
entries.append(.notice(id: id.count, section: .content, text: i18n("DISABLE_SECRET_CHAT_BLUR_ON_SCREENSHOT_SUBTITLE", lang)))
// MARK: 18+ / Sensitive content (server-side)
if let contentSettingsConfiguration {
let canAdjust = contentSettingsConfiguration.canAdjustSensitiveContent
let sensitiveTitle = (lang == "ru" ? "Разрешить 18+ контент" : presentationData.strings.Settings_SensitiveContent)
let sensitiveInfo = presentationData.strings.Settings_SensitiveContentInfo
entries.append(.toggle(
id: id.count,
section: .content,
settingName: .sensitiveContentEnabled,
value: contentSettingsConfiguration.sensitiveContentEnabled,
text: sensitiveTitle,
enabled: canAdjust
))
entries.append(.notice(id: id.count, section: .content, text: canAdjust ? sensitiveInfo : (lang == "ru" ? "Сервер Telegram не разрешает менять эту настройку для данного аккаунта." : "Telegram server does not allow changing this setting for this account.")))
} else {
// Configuration not loaded yet show disabled placeholder.
let sensitiveTitle = (lang == "ru" ? "Разрешить 18+ контент" : "Sensitive content")
entries.append(.toggle(
id: id.count,
section: .content,
settingName: .sensitiveContentEnabled,
value: false,
text: sensitiveTitle,
enabled: false
))
entries.append(.notice(id: id.count, section: .content, text: (lang == "ru" ? "Загрузка настроек… (нужен доступ к серверу Telegram)" : "Loading settings… (requires Telegram server access)")))
}
// MARK: Local premium
entries.append(.header(id: id.count, section: .localPremium, text: i18n("Settings.Other.LocalPremium", lang), badge: nil))
entries.append(.toggle(id: id.count, section: .localPremium, settingName: .enableLocalPremium, value: SGSimpleSettings.shared.enableLocalPremium, text: i18n("Settings.Other.EnableLocalPremium", lang), enabled: true))
entries.append(.notice(id: id.count, section: .localPremium, text: i18n("Settings.Other.LocalPremium.Notice", lang)))
// MARK: Interface (appearance tab: only tab organizer)
entries.append(.header(id: id.count, section: .interface, text: (lang == "ru" ? "ИНТЕРФЕЙС" : "INTERFACE"), badge: nil))
entries.append(.disclosure(id: id.count, section: .interface, link: .tabOrganizer, text: (lang == "ru" ? "Органайзер таббара" : "Tab Bar Organizer")))
entries.append(.notice(id: id.count, section: .interface, text: (lang == "ru" ? "Порядок и видимость вкладок внизу экрана (Чаты, Контакты, Звонки, Настройки)." : "Order and visibility of bottom tabs (Chats, Contacts, Calls, Settings).")))
// MARK: Оформление (Appearance)
entries.append(.header(id: id.count, section: .appearance, text: (lang == "ru" ? "ОБЛОЖКА ПРОФИЛЯ" : "PROFILE COVER"), badge: nil))
entries.append(.disclosure(id: id.count, section: .appearance, link: .profileCover, text: (lang == "ru" ? "Обложка профиля" : "Profile cover")))
entries.append(.notice(id: id.count, section: .appearance, text: (lang == "ru" ? "Фото или видео вместо цвета в профиле (видно только вам)." : "Photo or video instead of color in profile (visible only to you).")))
let giftIdTitle = (lang == "ru" ? "Показывать ID подарка" : "Show gift ID")
let giftIdNotice = (lang == "ru" ? "При нажатии на информацию о подарке отображается его ID." : "When tapping gift info, its ID is shown.")
entries.append(.toggle(id: id.count, section: .appearance, settingName: .giftIdEnabled, value: SGSimpleSettings.shared.giftIdEnabled, text: giftIdTitle, enabled: true))
entries.append(.notice(id: id.count, section: .appearance, text: giftIdNotice))
entries.append(.header(id: id.count, section: .appearance, text: (lang == "ru" ? "ПОДМЕНА ПРОФИЛЯ" : "FAKE PROFILE"), badge: nil))
let fakeProfileTitle = (lang == "ru" ? "Подмена профиля" : "Fake profile")
entries.append(.toggle(id: id.count, section: .appearance, settingName: .fakeProfileEnabled, value: SGSimpleSettings.shared.fakeProfileEnabled, text: fakeProfileTitle, enabled: true))
entries.append(.disclosure(id: id.count, section: .appearance, link: .fakeProfileSettings, text: (lang == "ru" ? "Изменить" : "Change")))
entries.append(.header(id: id.count, section: .appearance, text: (lang == "ru" ? "ЗАМЕНА ШРИФТА" : "FONT REPLACEMENT"), badge: nil))
entries.append(.toggle(id: id.count, section: .appearance, settingName: .enableFontReplacement, value: SGSimpleSettings.shared.enableFontReplacement, text: (lang == "ru" ? "Замена шрифта" : "Font replacement"), enabled: true))
let fontLabelApp = SGSimpleSettings.shared.fontReplacementName.isEmpty ? (lang == "ru" ? "Системный" : "System") : SGSimpleSettings.shared.fontReplacementName
entries.append(.disclosure(id: id.count, section: .appearance, link: .fontReplacementPicker, text: (lang == "ru" ? "Шрифт" : "Font")))
entries.append(.disclosure(id: id.count, section: .appearance, link: .fontReplacementImportFile, text: (lang == "ru" ? "Загрузить из файла (.ttf)" : "Import from file (.ttf)")))
entries.append(.notice(id: id.count, section: .appearance, text: (lang == "ru" ? "Текущий: " : "Current: ") + fontLabelApp))
let boldFontLabelApp = SGSimpleSettings.shared.fontReplacementBoldName.isEmpty ? (lang == "ru" ? "Авто" : "Auto") : SGSimpleSettings.shared.fontReplacementBoldName
entries.append(.disclosure(id: id.count, section: .appearance, link: .fontReplacementBoldPicker, text: (lang == "ru" ? "Жирный шрифт" : "Bold font")))
entries.append(.disclosure(id: id.count, section: .appearance, link: .fontReplacementBoldImportFile, text: i18n("FONT_IMPORT_BOLD_FROM_FILE", lang)))
entries.append(.notice(id: id.count, section: .appearance, text: (lang == "ru" ? "Текущий: " : "Current: ") + boldFontLabelApp))
entries.append(.fontSizeMultiplierSlider(id: id.count, section: .appearance, settingName: .fontReplacementSize, value: max(50, min(150, SGSimpleSettings.shared.fontReplacementSizeMultiplier))))
entries.append(.notice(id: id.count, section: .appearance, text: (lang == "ru" ? "Размер шрифта (50–150%)." : "Font size (50150%).")))
entries.append(.header(id: id.count, section: .appearance, text: (lang == "ru" ? "ТЕКСТ И ЧИСЛА" : "TEXT & NUMBERS"), badge: nil))
let disableCompactNumbersTitle = (lang == "ru" ? "Полные числа вместо округления" : "Full numbers instead of rounding")
let disableCompactNumbersNotice = (lang == "ru" ? "Просмотры на постах будут показываться полным числом (например 1400 вместо 1.4K)." : "View counts on posts will show full number (e.g. 1400 instead of 1.4K).")
entries.append(.toggle(id: id.count, section: .appearance, settingName: .disableCompactNumbers, value: SGSimpleSettings.shared.disableCompactNumbers, text: disableCompactNumbersTitle, enabled: true))
entries.append(.notice(id: id.count, section: .appearance, text: disableCompactNumbersNotice))
let disableZalgoTitle = (lang == "ru" ? "Убирать символы Zalgo" : "Remove Zalgo characters")
let disableZalgoNotice = (lang == "ru" ? "Убирает искажающие текст символы Zalgo в именах и сообщениях." : "Removes Zalgo text distortion in names and messages.")
entries.append(.toggle(id: id.count, section: .appearance, settingName: .disableZalgoText, value: SGSimpleSettings.shared.disableZalgoText, text: disableZalgoTitle, enabled: true))
entries.append(.notice(id: id.count, section: .appearance, text: disableZalgoNotice))
// MARK: Other (Другие функции)
entries.append(.header(id: id.count, section: .other, text: (lang == "ru" ? "ДРУГИЕ ФУНКЦИИ" : "OTHER"), badge: nil))
let chatExportTitle = (lang == "ru" ? "Экспорт чата" : "Export chat")
let chatExportNotice = (lang == "ru"
? "В профиле пользователя во вкладке «Ещё» появится пункт «Экспорт чата» — экспорт истории в JSON, TXT или HTML."
: "In the user profile under «More» a «Export chat» item will appear — export history to JSON, TXT or HTML.")
entries.append(.toggle(id: id.count, section: .other, settingName: .chatExportEnabled, value: SGSimpleSettings.shared.chatExportEnabled, text: chatExportTitle, enabled: true))
entries.append(.notice(id: id.count, section: .other, text: chatExportNotice))
entries.append(.toggle(id: id.count, section: .other, settingName: .scrollToTopButtonEnabled, value: SGSimpleSettings.shared.scrollToTopButtonEnabled, text: i18n("SCROLL_TO_TOP_TITLE", lang), enabled: true))
entries.append(.notice(id: id.count, section: .other, text: i18n("SCROLL_TO_TOP_NOTICE", lang)))
let unlimitedFavTitle = (lang == "ru" ? "Неограниченные избранные стикеры" : "Unlimited favorite stickers")
let unlimitedFavNotice = (lang == "ru" ? "Убирает ограничение на число стикеров в избранном." : "Removes the limit on favorite stickers count.")
entries.append(.toggle(id: id.count, section: .other, settingName: .unlimitedFavoriteStickers, value: SGSimpleSettings.shared.unlimitedFavoriteStickers, text: unlimitedFavTitle, enabled: true))
entries.append(.notice(id: id.count, section: .other, text: unlimitedFavNotice))
let telescopeTitle = (lang == "ru" ? "Создание видео кружков и голосовых сообщений" : "Creating video circles and voice messages")
let telescopeNotice = (lang == "ru"
? "Позволяет создавать видео кружки и голосовые сообщения из видео."
: "Allows creating video circles and voice messages from video.")
entries.append(.toggle(id: id.count, section: .other, settingName: .enableTelescope, value: SGSimpleSettings.shared.enableTelescope, text: telescopeTitle, enabled: true))
entries.append(.notice(id: id.count, section: .other, text: telescopeNotice))
let emojiDownloadTitle = (lang == "ru" ? "Скачивать эмодзи и стикеры в галерею" : "Download emoji and stickers to gallery")
let emojiDownloadNotice = (lang == "ru" ? "При зажатии эмодзи или стикера в контекстном меню появится сохранение в галерею." : "When you long-press an emoji or sticker, save to gallery appears in the context menu.")
entries.append(.toggle(id: id.count, section: .other, settingName: .emojiDownloaderEnabled, value: SGSimpleSettings.shared.emojiDownloaderEnabled, text: emojiDownloadTitle, enabled: true))
entries.append(.notice(id: id.count, section: .other, text: emojiDownloadNotice))
let feelRichTitle = (lang == "ru" ? "Локальный баланс звёзд" : "Local stars balance")
entries.append(.toggle(id: id.count, section: .other, settingName: .feelRichEnabled, value: SGSimpleSettings.shared.feelRichEnabled, text: feelRichTitle, enabled: true))
entries.append(.disclosure(id: id.count, section: .other, link: .feelRichAmount, text: (lang == "ru" ? "Изменить сумму" : "Change amount") + " (\(SGSimpleSettings.shared.feelRichStarsAmount))"))
// MARK: Per-account notification mute
if accounts.count > 1 {
let notifHeader = lang == "ru" ? "УВЕДОМЛЕНИЯ" : "NOTIFICATIONS"
entries.append(.header(id: id.count, section: .notifications, text: notifHeader, badge: nil))
for account in accounts {
let isMuted = SGSimpleSettings.shared.isAccountNotificationMuted(recordId: account.recordId)
let stateText = isMuted ? (lang == "ru" ? " (выкл)" : " (off)") : ""
let title = account.name + stateText
entries.append(.action(id: id.count, section: .notifications, actionType: "toggleNotificationMute_\(account.recordId)" as AnyHashable, text: title, kind: .generic))
}
let notifNotice = lang == "ru"
? "Отключите уведомления для аккаунтов, от которых не нужны push-уведомления."
: "Disable notifications for accounts that should not receive push notifications."
entries.append(.notice(id: id.count, section: .notifications, text: notifNotice))
}
// MARK: Fake Location
entries.append(.header(id: id.count, section: .fakeLocation, text: (lang == "ru" ? "ФЕЙКОВАЯ ГЕОЛОКАЦИЯ" : "FAKE LOCATION"), badge: nil))
let fakeLocationTitle = (lang == "ru" ? "Включить фейковую геолокацию" : "Enable Fake Location")
let fakeLocationNotice = (lang == "ru"
? "Подменяет ваше реальное местоположение на выбранное. Работает во всех приложениях, использующих геолокацию."
: "Replaces your real location with the selected one. Works in all apps that use location services.")
entries.append(.toggle(id: id.count, section: .fakeLocation, settingName: .fakeLocationEnabled, value: SGSimpleSettings.shared.fakeLocationEnabled, text: fakeLocationTitle, enabled: true))
entries.append(.notice(id: id.count, section: .fakeLocation, text: fakeLocationNotice))
let pickLocationTitle = (lang == "ru" ? "Выбрать местоположение" : "Pick Location")
entries.append(.disclosure(id: id.count, section: .fakeLocation, link: .fakeLocationPicker, text: pickLocationTitle))
// Show current coordinates if set
if SGSimpleSettings.shared.fakeLatitude != 0.0 && SGSimpleSettings.shared.fakeLongitude != 0.0 {
let coordsText = String(format: (lang == "ru" ? "Текущие координаты: lat: %.6f lon: %.6f" : "Current coordinates: lat: %.6f lon: %.6f"), SGSimpleSettings.shared.fakeLatitude, SGSimpleSettings.shared.fakeLongitude)
entries.append(.notice(id: id.count, section: .fakeLocation, text: coordsText))
} else {
let noCoordsText = (lang == "ru" ? "Координаты не выбраны. Нажмите 'Выбрать местоположение' для настройки." : "No coordinates selected. Tap 'Pick Location' to configure.")
entries.append(.notice(id: id.count, section: .fakeLocation, text: noCoordsText))
}
// MARK: Подглядеть онлайн (Peek online)
entries.append(.header(id: id.count, section: .onlineStatusRecording, text: (lang == "ru" ? "ПОДГЛЯДЕТЬ ОНЛАЙН" : "PEEK ONLINE"), badge: nil))
let peekOnlineTitle = (lang == "ru" ? "Включить «Подглядеть онлайн»" : "Enable «Peek online»")
let peekOnlineNotice = (lang == "ru"
? "Эмулирует возможность Premium «Время захода»: показывает последний онлайн у тех, кто не скрывал время захода, но скрыл его от вас. Пользователи с надписью «когда?» в профиле — время можно подсмотреть. Подписчикам Premium не нужно. Принцип: 1) Если аккаунтов несколько — статус может быть взят через другой аккаунт (мост). 2) Краткосрочная инверсия: на долю секунды «Видно всем» → фиксируется и показывается статус → настройки возвращаются."
: "Emulates Premium «Last seen»: shows last online for users who did not hide it from everyone but hid it from you. Users with «when?» in profile can be peeked. Not needed for Premium subscribers. How: 1) With multiple accounts, status may be fetched via another account (bridge). 2) Short inversion: «Visible to everyone» for a fraction of a second → status captured and shown → settings restored.")
entries.append(.toggle(id: id.count, section: .onlineStatusRecording, settingName: .enableOnlineStatusRecording, value: SGSimpleSettings.shared.enableOnlineStatusRecording, text: peekOnlineTitle, enabled: true))
entries.append(.notice(id: id.count, section: .onlineStatusRecording, text: peekOnlineNotice))
// MARK: Gated features hide toggles that are gated and not unlocked
let filteredEntries = filterGatedFeatures(entries: entries)
return filterSGItemListUIEntrires(entries: filteredEntries, by: state.searchQuery)
}
public func gleGramSettingsController(context: AccountContext) -> ViewController {
let access = cachedAggregateAccess()
// Primary gate: server-provided access flag (verified through integrity layers)
let hasAccess = access.glegramTab
// Secondary gate: accumulator-derived access (independent verification path)
let accumulatorOK = SupportersIntegrity.deriveGlegramTab()
// Access granted only when BOTH paths agree (or accumulator not yet populated on cold start)
let fragmentsReady = SupportersIntegrity.fragmentCount() >= 3
let granted = hasAccess && (!fragmentsReady || accumulatorOK)
if !granted, let promoData = cachedAggregatePromo() {
return gleGramPaywallController(context: context, promo: promoData.promo, trialAvailable: promoData.trialAvailable)
}
var presentControllerImpl: ((ViewController, ViewControllerPresentationArguments?) -> Void)?
var pushControllerImpl: ((ViewController) -> Void)?
#if canImport(FaceScanScreen)
var presentAgeVerificationImpl: ((@escaping () -> Void) -> Void)?
#endif
let reloadPromise = ValuePromise(true, ignoreRepeated: false)
var fontNotifyWorkItem: DispatchWorkItem?
let initialState = GLEGramSettingsControllerState()
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((GLEGramSettingsControllerState) -> GLEGramSettingsControllerState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let updateSensitiveContentDisposable = MetaDisposable()
let updatedContentSettingsConfiguration = contentSettingsConfiguration(network: context.account.network)
|> map(Optional.init)
let contentSettingsConfigurationPromise = Promise<ContentSettingsConfiguration?>()
contentSettingsConfigurationPromise.set(.single(nil)
|> then(updatedContentSettingsConfiguration))
var argumentsRef: SGItemListArguments<SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>?
let arguments = SGItemListArguments<SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>(
context: context,
setBoolValue: { setting, value in
switch setting {
case .showDeletedMessages:
SGSimpleSettings.shared.showDeletedMessages = value
case .saveDeletedMessagesMedia:
SGSimpleSettings.shared.saveDeletedMessagesMedia = value
case .saveDeletedMessagesReactions:
SGSimpleSettings.shared.saveDeletedMessagesReactions = value
case .saveDeletedMessagesForBots:
SGSimpleSettings.shared.saveDeletedMessagesForBots = value
case .saveEditHistory:
SGSimpleSettings.shared.saveEditHistory = value
case .enableLocalMessageEditing:
SGSimpleSettings.shared.enableLocalMessageEditing = value
case .disableOnlineStatus:
SGSimpleSettings.shared.disableOnlineStatus = value
case .disableTypingStatus:
SGSimpleSettings.shared.disableTypingStatus = value
case .disableRecordingVideoStatus:
SGSimpleSettings.shared.disableRecordingVideoStatus = value
case .disableUploadingVideoStatus:
SGSimpleSettings.shared.disableUploadingVideoStatus = value
case .disableVCMessageRecordingStatus:
SGSimpleSettings.shared.disableVCMessageRecordingStatus = value
case .disableVCMessageUploadingStatus:
SGSimpleSettings.shared.disableVCMessageUploadingStatus = value
case .disableUploadingPhotoStatus:
SGSimpleSettings.shared.disableUploadingPhotoStatus = value
case .disableUploadingFileStatus:
SGSimpleSettings.shared.disableUploadingFileStatus = value
case .disableChoosingLocationStatus:
SGSimpleSettings.shared.disableChoosingLocationStatus = value
case .disableChoosingContactStatus:
SGSimpleSettings.shared.disableChoosingContactStatus = value
case .disablePlayingGameStatus:
SGSimpleSettings.shared.disablePlayingGameStatus = value
case .disableRecordingRoundVideoStatus:
SGSimpleSettings.shared.disableRecordingRoundVideoStatus = value
case .disableUploadingRoundVideoStatus:
SGSimpleSettings.shared.disableUploadingRoundVideoStatus = value
case .disableSpeakingInGroupCallStatus:
SGSimpleSettings.shared.disableSpeakingInGroupCallStatus = value
case .disableChoosingStickerStatus:
SGSimpleSettings.shared.disableChoosingStickerStatus = value
case .disableEmojiInteractionStatus:
SGSimpleSettings.shared.disableEmojiInteractionStatus = value
case .disableEmojiAcknowledgementStatus:
SGSimpleSettings.shared.disableEmojiAcknowledgementStatus = value
case .disableMessageReadReceipt:
SGSimpleSettings.shared.disableMessageReadReceipt = value
case .disableStoryReadReceipt:
SGSimpleSettings.shared.disableStoryReadReceipt = value
case .disableAllAds:
SGSimpleSettings.shared.disableAllAds = value
case .hideProxySponsor:
SGSimpleSettings.shared.hideProxySponsor = value
NotificationCenter.default.post(name: .sgHideProxySponsorDidChange, object: nil)
case .enableSavingProtectedContent:
SGSimpleSettings.shared.enableSavingProtectedContent = value
case .enableSavingSelfDestructingMessages:
SGSimpleSettings.shared.enableSavingSelfDestructingMessages = value
case .disableScreenshotDetection:
SGSimpleSettings.shared.disableScreenshotDetection = value
case .disableSecretChatBlurOnScreenshot:
SGSimpleSettings.shared.disableSecretChatBlurOnScreenshot = value
case .enableLocalPremium:
SGSimpleSettings.shared.enableLocalPremium = value
case .scrollToTopButtonEnabled:
SGSimpleSettings.shared.scrollToTopButtonEnabled = value
case .hideReactions:
SGSimpleSettings.shared.hideReactions = value
case .chatExportEnabled:
SGSimpleSettings.shared.chatExportEnabled = value
case .disableCompactNumbers:
SGSimpleSettings.shared.disableCompactNumbers = value
case .disableZalgoText:
SGSimpleSettings.shared.disableZalgoText = value
case .fakeLocationEnabled:
SGSimpleSettings.shared.fakeLocationEnabled = value
case .keepRemovedChannels:
SGSimpleSettings.shared.keepRemovedChannels = value
case .enableVideoToCircleOrVoice:
SGSimpleSettings.shared.enableVideoToCircleOrVoice = value
case .enableTelescope:
SGSimpleSettings.shared.enableTelescope = value
case .enableFontReplacement:
SGSimpleSettings.shared.enableFontReplacement = value
context.sharedContext.notifyFontSettingsChanged()
case .unlimitedFavoriteStickers:
SGSimpleSettings.shared.unlimitedFavoriteStickers = value
case .enableOnlineStatusRecording:
SGSimpleSettings.shared.enableOnlineStatusRecording = value
case .sensitiveContentEnabled:
let update = {
let _ = (contentSettingsConfigurationPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak contentSettingsConfigurationPromise] settings in
if var settings {
settings.sensitiveContentEnabled = value
contentSettingsConfigurationPromise?.set(.single(settings))
}
})
updateSensitiveContentDisposable.set(updateRemoteContentSettingsConfiguration(postbox: context.account.postbox, network: context.account.network, sensitiveContentEnabled: value).start())
}
if value {
#if canImport(FaceScanScreen)
if requireAgeVerification(context: context) {
presentAgeVerificationImpl?(update)
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
title: presentationData.strings.SensitiveContent_Enable_Title,
text: presentationData.strings.SensitiveContent_Enable_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.SensitiveContent_Enable_Confirm, action: {
update()
})
]
)
presentControllerImpl?(alertController, nil)
}
#else
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let alertController = textAlertController(
context: context,
title: presentationData.strings.SensitiveContent_Enable_Title,
text: presentationData.strings.SensitiveContent_Enable_Text,
actions: [
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.SensitiveContent_Enable_Confirm, action: {
update()
})
]
)
presentControllerImpl?(alertController, nil)
#endif
} else {
update()
}
case .emojiDownloaderEnabled:
SGSimpleSettings.shared.emojiDownloaderEnabled = value
case .feelRichEnabled:
SGSimpleSettings.shared.feelRichEnabled = value
case .giftIdEnabled:
SGSimpleSettings.shared.giftIdEnabled = value
case .fakeProfileEnabled:
SGSimpleSettings.shared.fakeProfileEnabled = value
default:
break
}
reloadPromise.set(true)
},
updateSliderValue: { setting, value in
if case .fontReplacementSize = setting {
SGSimpleSettings.shared.fontReplacementSizeMultiplier = value
// Троттлинг: не перезагружаем список (подпись обновляется в ноде), notifyFontSettingsChanged раз в 120 мс
fontNotifyWorkItem?.cancel()
let item = DispatchWorkItem { [weak context] in
context?.sharedContext.notifyFontSettingsChanged()
}
fontNotifyWorkItem = item
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12, execute: item)
// reloadPromise не вызываем SliderFontSizeMultiplierItemNode обновляет подпись локально
} else if case .ghostModeMessageSendDelay = setting {
SGSimpleSettings.shared.ghostModeMessageSendDelaySeconds = value
reloadPromise.set(true)
}
},
setOneFromManyValue: { setting in
if case .onlineStatusRecordingInterval = setting {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
let actionSheet = ActionSheetController(presentationData: presentationData)
let intervals: [Int32] = [5, 10, 15, 20, 30, 60]
var items: [ActionSheetItem] = []
for min in intervals {
let title = lang == "ru" ? "\(min) мин" : "\(min) min"
items.append(ActionSheetButtonItem(title: title, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
SGSimpleSettings.shared.onlineStatusRecordingIntervalMinutes = min
reloadPromise.set(true)
}))
}
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
presentControllerImpl?(actionSheet, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return
}
},
openDisclosureLink: { link in
if link == .channelLink {
let pd = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://t.me/glegramios", forceExternal: true, presentationData: pd, navigationController: nil, dismissInput: {})
return
}
if link == .chatLink {
let pd = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://t.me/glegramios_chat", forceExternal: true, presentationData: pd, navigationController: nil, dismissInput: {})
return
}
if link == .forumLink {
let pd = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: "https://t.me/glegram_forum", forceExternal: true, presentationData: pd, navigationController: nil, dismissInput: {})
return
}
if link == .betaChannel {
if let betaConfig = cachedAggregateBetaConfig(), let url = betaConfig.channelUrl, isUrlSafeForExternalOpen(url) {
let pd = context.sharedContext.currentPresentationData.with { $0 }
context.sharedContext.openExternalUrl(context: context, urlContext: .generic, url: url, forceExternal: false, presentationData: pd, navigationController: nil, dismissInput: {})
}
return
}
if link == .fakeLocationPicker {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
#if canImport(SGFakeLocation)
let pickerController = FakeLocationPickerController(presentationData: presentationData, onSave: {
reloadPromise.set(true)
})
pushControllerImpl?(pickerController)
#endif
} else if link == .appearanceTab {
pushControllerImpl?(buildGLEGramTabController(tab: .appearance, args: argumentsRef!))
} else if link == .securityTab {
pushControllerImpl?(buildGLEGramTabController(tab: .security, args: argumentsRef!))
} else if link == .otherTab {
pushControllerImpl?(buildGLEGramTabController(tab: .other, args: argumentsRef!))
} else if link == .tabOrganizer {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let tabOrganizerController = TabOrganizerController(context: context, presentationData: presentationData, onSave: {
reloadPromise.set(true)
})
pushControllerImpl?(tabOrganizerController)
} else if link == .profileCover {
pushControllerImpl?(ProfileCoverController(context: context))
} else if link == .fakeProfileSettings {
pushControllerImpl?(FakeProfileSettingsController(context: context, onSave: { reloadPromise.set(true) }))
} else if link == .feelRichAmount {
pushControllerImpl?(FeelRichAmountController(context: context, onSave: { reloadPromise.set(true) }))
} else if link == .savedDeletedMessagesList {
pushControllerImpl?(savedDeletedMessagesListController(context: context))
} else if link == .readReceiptsExclusions {
let stored = SGSimpleSettings.shared.messageReadReceiptsSendToPeerIds
var peerIds: [PeerId] = []
for key in stored {
let parts = key.split(separator: ":")
if parts.count == 2, let ns = Int32(parts[0]), let idVal = Int64(parts[1]) {
peerIds.append(PeerId(namespace: PeerId.Namespace._internalFromInt32Value(ns), id: PeerId.Id._internalFromInt64Value(idVal)))
}
}
let loadPeers: Signal<[PeerId: SelectivePrivacyPeer], NoError> = context.engine.data.get(
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.Peer.init)),
EngineDataMap(peerIds.map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
)
|> map { peerMap, participantCountMap -> [PeerId: SelectivePrivacyPeer] in
var result: [PeerId: SelectivePrivacyPeer] = [:]
for peerId in peerIds {
if let maybePeer = peerMap[peerId], let peer = maybePeer {
var participantCount: Int32?
if case let .channel(channel) = peer, case .group = channel.info {
if let maybeCount = participantCountMap[peerId], let count = maybeCount {
participantCount = Int32(count)
}
}
result[peer.id] = SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount)
}
}
return result
}
let disposable = (loadPeers |> deliverOnMainQueue |> take(1)).start(next: { initialPeers in
let title = (context.sharedContext.currentPresentationData.with { $0 }.strings.baseLanguageCode == "ru")
? "Отправлять отчёты выбранным"
: "Send read receipts to"
let controller = selectivePrivacyPeersController(
context: context,
title: title,
footer: nil,
hideContacts: false,
initialPeers: initialPeers,
initialEnableForPremium: false,
displayPremiumCategory: false,
initialEnableForBots: false,
displayBotsCategory: false,
updated: { updatedPeerIds, _, _ in
var newSet: Set<String> = []
for (peerId, _) in updatedPeerIds {
newSet.insert("\(peerId.namespace._internalGetInt32Value()):\(peerId.id._internalGetInt64Value())")
}
SGSimpleSettings.shared.messageReadReceiptsSendToPeerIds = newSet
reloadPromise.set(true)
}
)
pushControllerImpl?(controller)
})
// Keep disposable alive for the async load
_ = disposable
} else if link == .fontReplacementPicker {
let pickerController = FontReplacementPickerController(context: context, mode: .main, onSave: {
reloadPromise.set(true)
context.sharedContext.notifyFontSettingsChanged()
})
presentControllerImpl?(pickerController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else if link == .fontReplacementBoldPicker {
let pickerController = FontReplacementPickerController(context: context, mode: .bold, onSave: {
reloadPromise.set(true)
context.sharedContext.notifyFontSettingsChanged()
})
presentControllerImpl?(pickerController, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else if link == .fontReplacementBoldImportFile {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let picker = legacyICloudFilePicker(
theme: presentationData.theme,
mode: .import,
documentTypes: ["public.font", "public.truetype-ttf-font", "public.opentype"],
dismissed: {},
completion: { urls in
guard let url = urls.first else { return }
_ = url.startAccessingSecurityScopedResource()
defer { url.stopAccessingSecurityScopedResource() }
if let provider = CGDataProvider(url: url as CFURL),
let cgFont = CGFont(provider),
let name = cgFont.postScriptName as String?, !name.isEmpty {
let fileManager = FileManager.default
if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
let fontsDir = documentsURL.appendingPathComponent("SwiftgramFonts", isDirectory: true)
try? fileManager.createDirectory(at: fontsDir, withIntermediateDirectories: true)
let destURL = fontsDir.appendingPathComponent("bold.ttf")
try? fileManager.removeItem(at: destURL)
if (try? fileManager.copyItem(at: url, to: destURL)) != nil {
SGSimpleSettings.shared.fontReplacementBoldFilePath = destURL.path
}
}
CTFontManagerRegisterFontURLs([url] as CFArray, .process, true, nil)
SGSimpleSettings.shared.fontReplacementBoldName = name
context.sharedContext.notifyFontSettingsChanged()
reloadPromise.set(true)
}
}
)
presentControllerImpl?(picker, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
} else if link == .fontReplacementImportFile {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let picker = legacyICloudFilePicker(
theme: presentationData.theme,
mode: .import,
documentTypes: ["public.font", "public.truetype-ttf-font", "public.opentype"],
dismissed: {},
completion: { urls in
guard let url = urls.first else { return }
_ = url.startAccessingSecurityScopedResource()
defer { url.stopAccessingSecurityScopedResource() }
if let provider = CGDataProvider(url: url as CFURL),
let cgFont = CGFont(provider),
let name = cgFont.postScriptName as String?, !name.isEmpty {
let fileManager = FileManager.default
if let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
let fontsDir = documentsURL.appendingPathComponent("SwiftgramFonts", isDirectory: true)
try? fileManager.createDirectory(at: fontsDir, withIntermediateDirectories: true)
let destURL = fontsDir.appendingPathComponent("main.ttf")
try? fileManager.removeItem(at: destURL)
if (try? fileManager.copyItem(at: url, to: destURL)) != nil {
SGSimpleSettings.shared.fontReplacementFilePath = destURL.path
}
}
CTFontManagerRegisterFontURLs([url] as CFArray, .process, true, nil)
SGSimpleSettings.shared.fontReplacementName = name
context.sharedContext.notifyFontSettingsChanged()
reloadPromise.set(true)
}
}
)
presentControllerImpl?(picker, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}
},
action: { actionType in
guard let actionString = actionType as? String else { return }
// Per-account notification mute toggle
if actionString.hasPrefix("toggleNotificationMute_") {
let recordIdStr = String(actionString.dropFirst("toggleNotificationMute_".count))
if let recordId = Int64(recordIdStr) {
let isMuted = SGSimpleSettings.shared.isAccountNotificationMuted(recordId: recordId)
SGSimpleSettings.shared.setAccountNotificationMuted(recordId: recordId, muted: !isMuted)
reloadPromise.set(true)
}
return
}
if actionString == "clearDeletedMessages" {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let lang = presentationData.strings.baseLanguageCode
let alertController = textAlertController(
context: context,
title: i18n("Settings.DeletedMessages.Clear.Title", lang),
text: i18n("Settings.DeletedMessages.Clear.Text", lang),
actions: [
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
let _ = (SGDeletedMessages.clearAllDeletedMessages(postbox: context.account.postbox)
|> deliverOnMainQueue).start(next: { count in
reloadPromise.set(true)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let text: String
if count > 0 {
text = lang == "ru"
? "Удалено сообщений: \(count)"
: "Deleted messages: \(count)"
} else {
text = lang == "ru"
? "Нет сохранённых удалённых сообщений"
: "No saved deleted messages"
}
presentControllerImpl?(UndoOverlayController(
presentationData: presentationData,
content: .succeed(text: text, timeout: 3.0, customUndoText: nil),
elevatedLayout: false,
action: { _ in return false }
), nil)
})
}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})
]
)
presentControllerImpl?(alertController, nil)
}
if actionString == "markAllReadLocal" {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(statusController, nil)
let markItems: [(groupId: EngineChatList.Group, filterPredicate: ChatListFilterPredicate?)] = [
(.root, nil),
(.archive, nil)
]
let _ = (context.engine.messages.markAllChatsAsReadLocallyOnly(items: markItems)
|> deliverOnMainQueue).start(completed: {
statusController.dismiss()
reloadPromise.set(true)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil)
})
}
if actionString == "markAllReadServer" {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let statusController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
presentControllerImpl?(statusController, nil)
let _ = (context.engine.messages.markAllChatsAsRead()
|> deliverOnMainQueue).start(completed: {
statusController.dismiss()
reloadPromise.set(true)
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, type: .success), nil)
})
}
},
searchInput: { searchQuery in
updateState { state in
var updatedState = state
updatedState.searchQuery = searchQuery
return updatedState
}
},
iconResolver: { ref in
guard let ref = ref else { return nil }
guard let img = UIImage(bundleImageName: ref) else { return nil }
// Иконки вкладок (GLEGramTab*) масштабируем до размера как у «Канал, Чат, Форум» (~29 pt)
return scaleImageForListIcon(img, maxSize: 29.0) ?? img
}
)
argumentsRef = arguments
func buildGLEGramTabController(tab: GLEGramTab, args: SGItemListArguments<SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>) -> ViewController {
let tabSignal = combineLatest(reloadPromise.get(), statePromise.get(), context.sharedContext.presentationData, contentSettingsConfigurationPromise.get(), context.sharedContext.activeAccountsWithInfo)
|> map { _, state, presentationData, contentSettingsConfiguration, accountsWithInfo -> (ItemListControllerState, (ItemListNodeState, SGItemListArguments<SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>)) in
let lang = presentationData.strings.baseLanguageCode
let tabTitles = lang == "ru" ? ["Оформление", "Приватность", "Другие функции"] : ["Appearance", "Privacy", "Other"]
let tabTitle = tabTitles[tab.rawValue]
var tabState = state
tabState.selectedTab = tab
let accounts: [AccountInfo] = accountsWithInfo.accounts.map { info in
let recordId = info.account.id.int64
let peerId = info.account.peerId.id._internalGetInt64Value()
let name = EnginePeer(info.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
return (recordId: recordId, peerId: peerId, name: name)
}
let allEntries = gleGramEntries(presentationData: presentationData, contentSettingsConfiguration: contentSettingsConfiguration, state: tabState, mediaBoxBasePath: context.account.postbox.mediaBox.basePath, accounts: accounts)
let entriesFilteredByTab = gleGramEntriesFiltered(by: tab, entries: allEntries)
let entries = filterSGItemListUIEntrires(entries: entriesFilteredByTab, by: tabState.searchQuery)
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text(tabTitle),
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, args))
}
let tabController = ItemListController(context: context, state: tabSignal)
tabController.navigationItem.leftBarButtonItem = makeBackBarButtonItem(presentationData: context.sharedContext.currentPresentationData.with({ $0 }), controller: tabController)
return tabController
}
let signal: Signal<(ItemListControllerState, (ItemListNodeState, SGItemListArguments<SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>)), NoError> = combineLatest(reloadPromise.get(), context.sharedContext.presentationData, context.sharedContext.activeAccountsWithInfo)
|> map { _, presentationData, accountsWithInfo -> (ItemListControllerState, (ItemListNodeState, SGItemListArguments<SGBoolSetting, GLEGramSliderSetting, GLEGramOneFromManySetting, GLEGramDisclosureLink, AnyHashable>)) in
SGSimpleSettings.shared.currentAccountPeerId = "\(context.account.peerId.id._internalGetInt64Value())"
let controllerState = ItemListControllerState(
presentationData: ItemListPresentationData(presentationData),
title: .text("GLEGram"),
leftNavigationButton: nil,
rightNavigationButton: nil,
backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back)
)
let accounts: [AccountInfo] = accountsWithInfo.accounts.map { info in
let recordId = info.account.id.int64
let peerId = info.account.peerId.id._internalGetInt64Value()
let name = EnginePeer(info.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
return (recordId: recordId, peerId: peerId, name: name)
}
let entries = gleGramRootEntries(presentationData: presentationData, accounts: accounts)
let listState = ItemListNodeState(
presentationData: ItemListPresentationData(presentationData),
entries: entries,
style: .blocks,
ensureVisibleItemTag: nil,
footerItem: nil,
initialScrollToItem: nil
)
return (controllerState, (listState, arguments))
}
let controller = ItemListController(context: context, state: signal)
controller.navigationItem.leftBarButtonItem = makeBackBarButtonItem(presentationData: context.sharedContext.currentPresentationData.with({ $0 }), controller: controller)
pushControllerImpl = { [weak controller] vc in controller?.push(vc) }
presentControllerImpl = { [weak controller] c, a in
guard let controller = controller else { return }
// Present from the topmost VC in the navigation stack: when a tab controller
// is pushed, the root controller's view is removed from the hierarchy by
// UINavigationController, making its `window` nil and `present` a no-op.
if let navController = controller.navigationController as? NavigationController,
let topController = navController.viewControllers.last as? ViewController {
topController.present(c, in: .window(.root), with: a)
} else {
controller.present(c, in: .window(.root), with: a)
}
}
#if canImport(FaceScanScreen)
presentAgeVerificationImpl = { [weak controller] update in
guard let controller else {
return
}
presentAgeVerification(context: context, parentController: controller, completion: {
update()
})
}
#endif
return controller
}