From 903fb4280f2034ecd2e2bbfbcefb1d9907a6a9a2 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Wed, 20 May 2026 21:07:39 +0000 Subject: [PATCH 1/2] feat: add AyuGram-style Ghost Mode (WataGram settings) Adds a new Ghost Mode feature set with four toggles, modeled after AyuGram: - Don't send read receipts - Don't send typing/recording/sticker activity - Don't update online presence to the server - Don't mark stories as seen Storage: - WataGramSettings (Codable) in TelegramUIPreferences with 4 Bool flags - Registered as SharedDataKey #23 - updateWataGramSettingsInteractively helper, mirroring the ExperimentalUISettings pattern Plumbing: - SharedAccountContext exposes immediateWataGramSettings; loaded via Atomic + accountManager.sharedData disposable - SharedWakeupManager takes a closure getGhostHideOnline to read the flag without depending on SharedAccountContext directly UI: - WataGramSettingsController with four ItemListSwitchItem rows under a single Ghost Mode section + footer - New .wataGram case in PeerInfoSettingsSection, with a row in the advanced section right under Privacy and Security Gating: - Read receipts: ChatHistoryListNode (6 sites) and applyMaxReadIndex callsite all check ghostModeReadReceipts - Typing: ChatController activitySpace is set to nil when ghostModeTypingIndicator is on, suppressing typing/sticker/recording broadcasts uniformly - Online: SharedWakeupManager.checkTasks gates shouldKeepOnlinePresence on ghostModeOnlineStatus - Story-seen: 4 markAsSeen call sites in StoryChatContent gated on ghostModeStorySeen Co-authored-by: Seoul81 <286274373+Seoul81@users.noreply.github.com> --- .../Sources/AccountContext.swift | 1 + .../WataGram/WataGramSettingsController.swift | 163 ++++++++++++++++++ .../Sources/PeerInfoScreen.swift | 1 + .../PeerInfoScreenSettingsActions.swift | 2 + .../Sources/PeerInfoSettingsItems.swift | 3 + .../Sources/StoryChatContent.swift | 8 +- .../TelegramUI/Sources/AppDelegate.swift | 2 + .../TelegramUI/Sources/ChatController.swift | 17 +- .../Sources/ChatHistoryListNode.swift | 12 +- .../Sources/SharedAccountContext.swift | 14 ++ .../Sources/SharedWakeupManager.swift | 7 +- .../Sources/PostboxKeys.swift | 2 + .../Sources/WataGramSettings.swift | 74 ++++++++ 13 files changed, 288 insertions(+), 18 deletions(-) create mode 100644 submodules/SettingsUI/Sources/WataGram/WataGramSettingsController.swift create mode 100644 submodules/TelegramUIPreferences/Sources/WataGramSettings.swift diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 2245bafe094..5b7442ab8f1 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -1333,6 +1333,7 @@ public protocol SharedAccountContext: AnyObject { var automaticMediaDownloadSettings: Signal { get } var currentAutodownloadSettings: Atomic { get } var immediateExperimentalUISettings: ExperimentalUISettings { get } + var immediateWataGramSettings: WataGramSettings { get } var currentInAppNotificationSettings: Atomic { get } var currentMediaInputSettings: Atomic { get } var currentStickerSettings: Atomic { get } diff --git a/submodules/SettingsUI/Sources/WataGram/WataGramSettingsController.swift b/submodules/SettingsUI/Sources/WataGram/WataGramSettingsController.swift new file mode 100644 index 00000000000..2ea1c34eaf7 --- /dev/null +++ b/submodules/SettingsUI/Sources/WataGram/WataGramSettingsController.swift @@ -0,0 +1,163 @@ +import Foundation +import UIKit +import Display +import SwiftSignalKit +import TelegramCore +import TelegramPresentationData +import TelegramUIPreferences +import ItemListUI +import PresentationDataUtils +import AccountContext + +private final class WataGramSettingsControllerArguments { + let toggleReadReceipts: (Bool) -> Void + let toggleTypingIndicator: (Bool) -> Void + let toggleOnlineStatus: (Bool) -> Void + let toggleStorySeen: (Bool) -> Void + + init( + toggleReadReceipts: @escaping (Bool) -> Void, + toggleTypingIndicator: @escaping (Bool) -> Void, + toggleOnlineStatus: @escaping (Bool) -> Void, + toggleStorySeen: @escaping (Bool) -> Void + ) { + self.toggleReadReceipts = toggleReadReceipts + self.toggleTypingIndicator = toggleTypingIndicator + self.toggleOnlineStatus = toggleOnlineStatus + self.toggleStorySeen = toggleStorySeen + } +} + +private enum WataGramSettingsSection: Int32 { + case ghostMode +} + +private enum WataGramSettingsEntry: ItemListNodeEntry { + case ghostModeHeader + case readReceipts(Bool) + case typingIndicator(Bool) + case onlineStatus(Bool) + case storySeen(Bool) + case ghostModeFooter + + var section: ItemListSectionId { + return WataGramSettingsSection.ghostMode.rawValue + } + + var stableId: Int32 { + switch self { + case .ghostModeHeader: + return 0 + case .readReceipts: + return 1 + case .typingIndicator: + return 2 + case .onlineStatus: + return 3 + case .storySeen: + return 4 + case .ghostModeFooter: + return 5 + } + } + + static func <(lhs: WataGramSettingsEntry, rhs: WataGramSettingsEntry) -> Bool { + return lhs.stableId < rhs.stableId + } + + func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem { + let arguments = arguments as! WataGramSettingsControllerArguments + switch self { + case .ghostModeHeader: + return ItemListSectionHeaderItem(presentationData: presentationData, text: "GHOST MODE", sectionId: self.section) + case let .readReceipts(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Don't send read receipts", value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleReadReceipts(value) + }) + case let .typingIndicator(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Don't send typing status", value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleTypingIndicator(value) + }) + case let .onlineStatus(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Don't update online status", value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleOnlineStatus(value) + }) + case let .storySeen(value): + return ItemListSwitchItem(presentationData: presentationData, systemStyle: .glass, title: "Don't mark stories as seen", value: value, sectionId: self.section, style: .blocks, updated: { value in + arguments.toggleStorySeen(value) + }) + case .ghostModeFooter: + return ItemListTextItem(presentationData: presentationData, text: .plain("These options stop the app from sending read receipts, typing indicators, online status updates, and story view marks to Telegram servers. Other people will not know that you've read their messages or seen their stories. Note: this is a client-side modification and may technically violate the Telegram Terms of Service."), sectionId: self.section) + } + } +} + +private func wataGramSettingsControllerEntries(settings: WataGramSettings) -> [WataGramSettingsEntry] { + var entries: [WataGramSettingsEntry] = [] + entries.append(.ghostModeHeader) + entries.append(.readReceipts(settings.ghostModeReadReceipts)) + entries.append(.typingIndicator(settings.ghostModeTypingIndicator)) + entries.append(.onlineStatus(settings.ghostModeOnlineStatus)) + entries.append(.storySeen(settings.ghostModeStorySeen)) + entries.append(.ghostModeFooter) + return entries +} + +public func wataGramSettingsController(context: AccountContext) -> ViewController { + let arguments = WataGramSettingsControllerArguments( + toggleReadReceipts: { value in + let _ = updateWataGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var current = current + current.ghostModeReadReceipts = value + return current + }).startStandalone() + }, + toggleTypingIndicator: { value in + let _ = updateWataGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var current = current + current.ghostModeTypingIndicator = value + return current + }).startStandalone() + }, + toggleOnlineStatus: { value in + let _ = updateWataGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var current = current + current.ghostModeOnlineStatus = value + return current + }).startStandalone() + }, + toggleStorySeen: { value in + let _ = updateWataGramSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in + var current = current + current.ghostModeStorySeen = value + return current + }).startStandalone() + } + ) + + let signal = combineLatest(queue: .mainQueue(), + context.sharedContext.presentationData, + context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.wataGramSettings]) + ) + |> map { presentationData, sharedData -> (ItemListControllerState, (ItemListNodeState, Any)) in + let settings: WataGramSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.wataGramSettings]?.get(WataGramSettings.self) ?? .defaultSettings + + let controllerState = ItemListControllerState( + presentationData: ItemListPresentationData(presentationData), + title: .text("WataGram"), + leftNavigationButton: nil, + rightNavigationButton: nil, + backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back) + ) + let listState = ItemListNodeState( + presentationData: ItemListPresentationData(presentationData), + entries: wataGramSettingsControllerEntries(settings: settings), + style: .blocks, + animateChanges: false + ) + return (controllerState, (listState, arguments)) + } + + let controller = ItemListController(context: context, state: signal) + return controller +} diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 4dfd4770636..0f0f16e19df 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -189,6 +189,7 @@ enum PeerInfoSettingsSection { case premiumManagement case stars case ton + case wataGram } enum PeerInfoReportType { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift index c0a89120bf0..5b6d53b7520 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenSettingsActions.swift @@ -295,6 +295,8 @@ extension PeerInfoScreenNode { if let tonContext = self.controller?.tonContext { push(self.context.sharedContext.makeStarsTransactionsScreen(context: self.context, starsContext: tonContext)) } + case .wataGram: + push(wataGramSettingsController(context: self.context)) } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift index 32aa2f66199..f25d0bb5944 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoSettingsItems.swift @@ -234,6 +234,9 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_PrivacySettings, icon: PresentationResourcesSettings.security, action: { interaction.openSettings(.privacyAndSecurity) })) + items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 7, text: "WataGram", icon: PresentationResourcesSettings.security, action: { + interaction.openSettings(.wataGram) + })) items[.advanced]!.append(PeerInfoScreenDisclosureItem(id: 2, text: presentationData.strings.Settings_ChatSettings, icon: PresentationResourcesSettings.dataAndStorage, action: { interaction.openSettings(.dataAndStorage) })) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index fbda84417d6..e05cf5b498c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -1130,7 +1130,7 @@ public final class StoryContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.context.sharedContext.immediateWataGramSettings.ghostModeStorySeen { let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).startStandalone() } } @@ -1432,7 +1432,7 @@ public final class SingleStoryContentContextImpl: StoryContentContext { public func markAsSeen(id: StoryId) { if self.readGlobally { - if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.context.sharedContext.immediateWataGramSettings.ghostModeStorySeen { let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).startStandalone() } } @@ -1830,7 +1830,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.context.sharedContext.immediateWataGramSettings.ghostModeStorySeen { let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).startStandalone() } } @@ -3094,7 +3094,7 @@ public final class RepostStoriesContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory && !self.context.sharedContext.immediateWataGramSettings.ghostModeStorySeen { let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).startStandalone() } } diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 1c1631c7764..c6503a1cb0b 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -1187,6 +1187,8 @@ private func extractAccountManagerState(records: AccountRecordsView(value: WataGramSettings.defaultSettings) + public var immediateWataGramSettings: WataGramSettings { + return self.immediateWataGramSettingsValue.with { $0 } + } + private var wataGramSettingsDisposable: Disposable? + public var presentGlobalController: (ViewController, Any?) -> Void = { _, _ in } public var presentCrossfadeController: () -> Void = {} @@ -524,6 +530,14 @@ public final class SharedAccountContextImpl: SharedAccountContext { } }) + let immediateWataGramSettingsValue = self.immediateWataGramSettingsValue + self.wataGramSettingsDisposable = (self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.wataGramSettings]) + |> deliverOnMainQueue).start(next: { sharedData in + if let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.wataGramSettings]?.get(WataGramSettings.self) { + let _ = immediateWataGramSettingsValue.swap(settings) + } + }) + let _ = self.contactDataManager?.personNameDisplayOrder().start(next: { order in let _ = updateContactSettingsInteractively(accountManager: accountManager, { settings in var settings = settings diff --git a/submodules/TelegramUI/Sources/SharedWakeupManager.swift b/submodules/TelegramUI/Sources/SharedWakeupManager.swift index 4e6bb1fcc7c..22144fe9a02 100644 --- a/submodules/TelegramUI/Sources/SharedWakeupManager.swift +++ b/submodules/TelegramUI/Sources/SharedWakeupManager.swift @@ -70,6 +70,7 @@ public final class SharedWakeupManager { private var enableBackgroundTasks: Bool = false private let presentationData: () -> PresentationData? + private let getGhostHideOnline: () -> Bool private var inForeground: Bool = false private var hasActiveAudioSession: Bool = false @@ -112,7 +113,7 @@ public final class SharedWakeupManager { private var backgroundStoryProcessingTaskCancellationRequestedByApp: Bool = false private var pendingBackgroundStoryProcessingTaskTimer: SwiftSignalKit.Timer? - public init(beginBackgroundTask: @escaping (String, @escaping () -> Void) -> UIBackgroundTaskIdentifier?, endBackgroundTask: @escaping (UIBackgroundTaskIdentifier) -> Void, backgroundTimeRemaining: @escaping () -> Double, acquireIdleExtension: @escaping () -> Disposable?, activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account)]), NoError>, liveLocationPolling: Signal, watchTasks: Signal, inForeground: Signal, hasActiveAudioSession: Signal, notificationManager: SharedNotificationManager?, mediaManager: MediaManager, callManager: PresentationCallManager?, accountUserInterfaceInUse: @escaping (AccountRecordId) -> Signal, presentationData: @escaping () -> PresentationData?) { + public init(beginBackgroundTask: @escaping (String, @escaping () -> Void) -> UIBackgroundTaskIdentifier?, endBackgroundTask: @escaping (UIBackgroundTaskIdentifier) -> Void, backgroundTimeRemaining: @escaping () -> Double, acquireIdleExtension: @escaping () -> Disposable?, activeAccounts: Signal<(primary: Account?, accounts: [(AccountRecordId, Account)]), NoError>, liveLocationPolling: Signal, watchTasks: Signal, inForeground: Signal, hasActiveAudioSession: Signal, notificationManager: SharedNotificationManager?, mediaManager: MediaManager, callManager: PresentationCallManager?, accountUserInterfaceInUse: @escaping (AccountRecordId) -> Signal, presentationData: @escaping () -> PresentationData?, getGhostHideOnline: @escaping () -> Bool = { false }) { assert(Queue.mainQueue().isCurrent()) self.beginBackgroundTask = beginBackgroundTask @@ -120,6 +121,7 @@ public final class SharedWakeupManager { self.backgroundTimeRemaining = backgroundTimeRemaining self.acquireIdleExtension = acquireIdleExtension self.presentationData = presentationData + self.getGhostHideOnline = getGhostHideOnline self.accountSettingsDisposable = (activeAccounts |> mapToSignal { activeAccounts -> Signal in @@ -1146,7 +1148,8 @@ public final class SharedWakeupManager { account.shouldBeServiceTaskMaster.set(.single(.never)) } account.shouldExplicitelyKeepWorkerConnections.set(.single(tasks.backgroundAudio || tasks.importantTasks.pendingStoryCount != 0 || tasks.importantTasks.pendingMessageCount != 0)) - account.shouldKeepOnlinePresence.set(.single(primary && self.inForeground)) + let ghostHideOnline = self.getGhostHideOnline() + account.shouldKeepOnlinePresence.set(.single(primary && self.inForeground && !ghostHideOnline)) account.shouldKeepBackgroundDownloadConnections.set(.single(tasks.backgroundDownloads)) } diff --git a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift index d6f3aa3b6fc..f9447f237a2 100644 --- a/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift +++ b/submodules/TelegramUIPreferences/Sources/PostboxKeys.swift @@ -52,6 +52,7 @@ private enum ApplicationSpecificSharedDataKeyValues: Int32 { case mediaDisplaySettings = 20 case updateSettings = 21 case chatSettings = 22 + case wataGramSettings = 23 } public struct ApplicationSpecificSharedDataKeys { @@ -78,6 +79,7 @@ public struct ApplicationSpecificSharedDataKeys { public static let mediaDisplaySettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.mediaDisplaySettings.rawValue) public static let updateSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.updateSettings.rawValue) public static let chatSettings = applicationSpecificPreferencesKey(ApplicationSpecificSharedDataKeyValues.chatSettings.rawValue) + public static let wataGramSettings = applicationSpecificSharedDataKey(ApplicationSpecificSharedDataKeyValues.wataGramSettings.rawValue) } private enum ApplicationSpecificItemCacheCollectionIdValues: Int8 { diff --git a/submodules/TelegramUIPreferences/Sources/WataGramSettings.swift b/submodules/TelegramUIPreferences/Sources/WataGramSettings.swift new file mode 100644 index 00000000000..82ccdf2a6b0 --- /dev/null +++ b/submodules/TelegramUIPreferences/Sources/WataGramSettings.swift @@ -0,0 +1,74 @@ +import Foundation +import TelegramCore +import SwiftSignalKit +import Postbox + +public struct WataGramSettings: Codable, Equatable { + // Ghost Mode toggles + public var ghostModeReadReceipts: Bool + public var ghostModeTypingIndicator: Bool + public var ghostModeOnlineStatus: Bool + public var ghostModeStorySeen: Bool + + public static var defaultSettings: WataGramSettings { + return WataGramSettings( + ghostModeReadReceipts: false, + ghostModeTypingIndicator: false, + ghostModeOnlineStatus: false, + ghostModeStorySeen: false + ) + } + + public init( + ghostModeReadReceipts: Bool, + ghostModeTypingIndicator: Bool, + ghostModeOnlineStatus: Bool, + ghostModeStorySeen: Bool + ) { + self.ghostModeReadReceipts = ghostModeReadReceipts + self.ghostModeTypingIndicator = ghostModeTypingIndicator + self.ghostModeOnlineStatus = ghostModeOnlineStatus + self.ghostModeStorySeen = ghostModeStorySeen + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: StringCodingKey.self) + self.ghostModeReadReceipts = try container.decodeIfPresent(Bool.self, forKey: "ghostModeReadReceipts") ?? false + self.ghostModeTypingIndicator = try container.decodeIfPresent(Bool.self, forKey: "ghostModeTypingIndicator") ?? false + self.ghostModeOnlineStatus = try container.decodeIfPresent(Bool.self, forKey: "ghostModeOnlineStatus") ?? false + self.ghostModeStorySeen = try container.decodeIfPresent(Bool.self, forKey: "ghostModeStorySeen") ?? false + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: StringCodingKey.self) + try container.encode(self.ghostModeReadReceipts, forKey: "ghostModeReadReceipts") + try container.encode(self.ghostModeTypingIndicator, forKey: "ghostModeTypingIndicator") + try container.encode(self.ghostModeOnlineStatus, forKey: "ghostModeOnlineStatus") + try container.encode(self.ghostModeStorySeen, forKey: "ghostModeStorySeen") + } + + /// Convenience: any ghost-mode flag enabled. + public var isAnyGhostModeEnabled: Bool { + return self.ghostModeReadReceipts + || self.ghostModeTypingIndicator + || self.ghostModeOnlineStatus + || self.ghostModeStorySeen + } +} + +public func updateWataGramSettingsInteractively( + accountManager: AccountManager, + _ f: @escaping (WataGramSettings) -> WataGramSettings +) -> Signal { + return accountManager.transaction { transaction -> Void in + transaction.updateSharedData(ApplicationSpecificSharedDataKeys.wataGramSettings, { entry in + let currentSettings: WataGramSettings + if let entry = entry?.get(WataGramSettings.self) { + currentSettings = entry + } else { + currentSettings = .defaultSettings + } + return SharedPreferencesEntry(f(currentSettings)) + }) + } +} From 7413619cb61f3e564bf45b4105a44381a9993352 Mon Sep 17 00:00:00 2001 From: Kiro Agent <244629292+kiro-agent@users.noreply.github.com> Date: Wed, 20 May 2026 21:27:43 +0000 Subject: [PATCH 2/2] ci: build IPA from feat/ghost-mode on macos-26 + upload artifact --- .github/workflows/build.yml | 54 ++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7259ebd7e73..5d9ebab63a5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,16 +2,16 @@ name: CI on: push: - branches: [ master ] + branches: [ master, feat/ghost-mode ] workflow_dispatch: jobs: build: - runs-on: macos-13 + runs-on: macos-26 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: submodules: 'recursive' fetch-depth: '0' @@ -73,37 +73,29 @@ jobs: done zip -r "./$OUTPUT_PATH/Telegram.DSYMs.zip" build/DSYMs 1>/dev/null + - name: Upload IPA as workflow artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: WataGram-${{ env.BUILD_NUMBER }}-ghost-mode + path: | + /Users/Shared/telegram-ios/build/artifacts/Telegram.ipa + /Users/Shared/telegram-ios/build/artifacts/Telegram.DSYMs.zip + if-no-files-found: warn + retention-days: 14 + - name: Create Release id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: softprops/action-gh-release@v2 with: - tag_name: build-${{ env.BUILD_NUMBER }} - release_name: Telegram ${{ env.APP_VERSION }} (${{ env.BUILD_NUMBER }}) + tag_name: ghost-mode-build-${{ env.BUILD_NUMBER }} + name: WataGram ${{ env.APP_VERSION }} (${{ env.BUILD_NUMBER }}) Ghost Mode body: | - An unsigned build of Telegram for iOS ${{ env.APP_VERSION }} (${{ env.BUILD_NUMBER }}) + Unsigned WataGram (Telegram-iOS fork) build with Ghost Mode feature. + Version ${{ env.APP_VERSION }} (${{ env.BUILD_NUMBER }}) + Sideload via Sideloadly / AltStore / TrollStore. draft: false prerelease: false - - - name: Upload Release IPA - id: upload-release-ipa - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: /Users/Shared/telegram-ios/build/artifacts/Telegram.ipa - asset_name: Telegram.ipa - asset_content_type: application/zip - - - name: Upload Release DSYM - id: upload-release-dsym - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: /Users/Shared/telegram-ios/build/artifacts/Telegram.DSYMs.zip - asset_name: Telegram.DSYMs.zip - asset_content_type: application/zip + files: | + /Users/Shared/telegram-ios/build/artifacts/Telegram.ipa + /Users/Shared/telegram-ios/build/artifacts/Telegram.DSYMs.zip