Files
ghostgram/submodules/TelegramCore/Sources/GhostMode/GhostModeManager.swift
T
ichmagmaus 812 019945f9da fix(ghostgram): fix Always Online + implement Ghost Mode mutual exclusion
Bugs fixed:
- Always Online had no effect after toggle — updatePresence was only called
  on app focus changes, not on settings changes. Fixed by subscribing to
  MiscSettingsManager notifications in AccountPresenceManagerImpl.

- Ghost Mode + Always Online conflict: if Ghost Mode was enabled, the early
  return in updatePresence completely blocked Always Online logic.

Changes:
- ManagedAccountPresence: priority chain Always Online > Ghost Mode > default.
  Subscribes to GhostMode/MiscSettings notifications, refreshes presence
  immediately on any change. 30s keep-alive timer for Always Online.
- GhostModeManager: enabling Ghost Mode auto-disables alwaysOnline via
  disableAlwaysOnlineForMutualExclusion(). No recursion via guard flag.
- MiscSettingsManager: enabling alwaysOnline auto-disables Ghost Mode via
  disableForMutualExclusion(). No recursion via guard flag.
- MiscController: subscribes to GhostModeManager notifications to refresh
  UI when Ghost Mode is auto-disabled by Always Online.
- GhostModeController: subscribes to MiscSettings notifications to refresh
  UI when Ghost Mode is auto-disabled by Always Online.
2026-02-23 23:17:25 +01:00

185 lines
6.3 KiB
Swift

import Foundation
/// GhostModeManager - Central manager for Ghost Mode privacy settings
/// Controls all privacy features: hide read receipts, typing indicator, online status, story views
public final class GhostModeManager {
// MARK: - Singleton
public static let shared = GhostModeManager()
// MARK: - UserDefaults Keys
private enum Keys {
static let isEnabled = "GhostMode.isEnabled"
static let hideReadReceipts = "GhostMode.hideReadReceipts"
static let hideStoryViews = "GhostMode.hideStoryViews"
static let hideOnlineStatus = "GhostMode.hideOnlineStatus"
static let hideTypingIndicator = "GhostMode.hideTypingIndicator"
static let forceOffline = "GhostMode.forceOffline"
}
// MARK: - Settings Storage
private let defaults = UserDefaults.standard
// Prevents recursive mutual-exclusion calls
private var isApplyingMutualExclusion = false
// MARK: - Properties
/// Master toggle for Ghost Mode.
/// Enabling Ghost Mode automatically disables Always Online in MiscSettingsManager.
public var isEnabled: Bool {
get { defaults.bool(forKey: Keys.isEnabled) }
set {
defaults.set(newValue, forKey: Keys.isEnabled)
if newValue && !isApplyingMutualExclusion {
// Ghost Mode ON disable Always Online
isApplyingMutualExclusion = true
MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion()
isApplyingMutualExclusion = false
}
notifySettingsChanged()
}
}
/// Don't send read receipts (blue checkmarks)
public var hideReadReceipts: Bool {
get { defaults.bool(forKey: Keys.hideReadReceipts) }
set {
defaults.set(newValue, forKey: Keys.hideReadReceipts)
notifySettingsChanged()
}
}
/// Don't send story view notifications
public var hideStoryViews: Bool {
get { defaults.bool(forKey: Keys.hideStoryViews) }
set {
defaults.set(newValue, forKey: Keys.hideStoryViews)
notifySettingsChanged()
}
}
/// Don't send online status
public var hideOnlineStatus: Bool {
get { defaults.bool(forKey: Keys.hideOnlineStatus) }
set {
defaults.set(newValue, forKey: Keys.hideOnlineStatus)
notifySettingsChanged()
}
}
/// Don't send typing indicator
public var hideTypingIndicator: Bool {
get { defaults.bool(forKey: Keys.hideTypingIndicator) }
set {
defaults.set(newValue, forKey: Keys.hideTypingIndicator)
notifySettingsChanged()
}
}
/// Always appear as offline
public var forceOffline: Bool {
get { defaults.bool(forKey: Keys.forceOffline) }
set {
defaults.set(newValue, forKey: Keys.forceOffline)
notifySettingsChanged()
}
}
// MARK: - Computed Properties
/// Returns true only when Ghost Mode is enabled AND the individual toggle is on.
/// NOTE: Always Online takes precedence if Always Online is active, online status is never hidden.
public var shouldHideReadReceipts: Bool {
return isEnabled && hideReadReceipts
}
public var shouldHideStoryViews: Bool {
return isEnabled && hideStoryViews
}
/// Online status is hidden only when Ghost Mode is on AND Always Online is NOT active.
public var shouldHideOnlineStatus: Bool {
guard isEnabled && hideOnlineStatus else { return false }
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
}
public var shouldHideTypingIndicator: Bool {
return isEnabled && hideTypingIndicator
}
/// Force offline only when Ghost Mode is on AND Always Online is NOT active.
public var shouldForceOffline: Bool {
guard isEnabled && forceOffline else { return false }
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
}
/// Count of active features (e.g., "5/5")
public var activeFeatureCount: Int {
var count = 0
if hideReadReceipts { count += 1 }
if hideStoryViews { count += 1 }
if hideOnlineStatus { count += 1 }
if hideTypingIndicator { count += 1 }
if forceOffline { count += 1 }
return count
}
/// Total number of features
public static let totalFeatureCount = 5
// MARK: - Internal mutual exclusion (called by MiscSettingsManager)
/// Called by MiscSettingsManager when Always Online is turned on.
/// Disables Ghost Mode without triggering mutual exclusion back.
public func disableForMutualExclusion() {
isApplyingMutualExclusion = true
defaults.set(false, forKey: Keys.isEnabled)
notifySettingsChanged()
isApplyingMutualExclusion = false
}
// MARK: - Initialization
private init() {
if !defaults.bool(forKey: "GhostMode.initialized") {
defaults.set(true, forKey: "GhostMode.initialized")
defaults.set(true, forKey: Keys.hideReadReceipts)
defaults.set(true, forKey: Keys.hideStoryViews)
defaults.set(true, forKey: Keys.hideOnlineStatus)
defaults.set(true, forKey: Keys.hideTypingIndicator)
defaults.set(true, forKey: Keys.forceOffline)
defaults.set(false, forKey: Keys.isEnabled)
}
}
// MARK: - Enable/Disable All
/// Enable all ghost mode features.
/// Also disables Always Online (mutual exclusion).
public func enableAll() {
hideReadReceipts = true
hideStoryViews = true
hideOnlineStatus = true
hideTypingIndicator = true
forceOffline = true
isEnabled = true // setter handles mutual exclusion
}
/// Disable all ghost mode features
public func disableAll() {
isEnabled = false
}
// MARK: - Notifications
public static let settingsChangedNotification = Notification.Name("GhostModeSettingsChanged")
private func notifySettingsChanged() {
NotificationCenter.default.post(name: GhostModeManager.settingsChangedNotification, object: nil)
}
}