mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-07-31 11:37:25 +02:00
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.
This commit is contained in:
@@ -300,6 +300,28 @@ public func ghostModeController(context: AccountContext) -> ViewController {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Refresh UI when Always Online is enabled externally and auto-disables Ghost Mode —
|
||||||
|
// the isEnabled flip happens in GhostModeManager from MiscSettingsManager context,
|
||||||
|
// so we need to pull fresh values from the manager.
|
||||||
|
let miscSettingsChangedSignal: Signal<Void, NoError> = Signal { subscriber in
|
||||||
|
let observer = NotificationCenter.default.addObserver(
|
||||||
|
forName: MiscSettingsManager.settingsChangedNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { _ in
|
||||||
|
updateState { state in
|
||||||
|
state.isEnabled = GhostModeManager.shared.isEnabled
|
||||||
|
state.hideReadReceipts = GhostModeManager.shared.hideReadReceipts
|
||||||
|
state.hideStoryViews = GhostModeManager.shared.hideStoryViews
|
||||||
|
state.hideOnlineStatus = GhostModeManager.shared.hideOnlineStatus
|
||||||
|
state.hideTypingIndicator = GhostModeManager.shared.hideTypingIndicator
|
||||||
|
state.forceOffline = GhostModeManager.shared.forceOffline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ActionDisposable { NotificationCenter.default.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
let _ = miscSettingsChangedSignal.start()
|
||||||
|
|
||||||
let signal = combineLatest(
|
let signal = combineLatest(
|
||||||
context.sharedContext.presentationData,
|
context.sharedContext.presentationData,
|
||||||
statePromise.get()
|
statePromise.get()
|
||||||
|
|||||||
@@ -291,12 +291,34 @@ public func miscController(context: AccountContext) -> ViewController {
|
|||||||
toggleAlwaysOnline: {
|
toggleAlwaysOnline: {
|
||||||
let newValue = !MiscSettingsManager.shared.alwaysOnline
|
let newValue = !MiscSettingsManager.shared.alwaysOnline
|
||||||
MiscSettingsManager.shared.alwaysOnline = newValue
|
MiscSettingsManager.shared.alwaysOnline = newValue
|
||||||
|
// State will be refreshed via notification if Ghost Mode got auto-disabled
|
||||||
updateState { state in
|
updateState { state in
|
||||||
state.alwaysOnline = newValue
|
state.alwaysOnline = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Refresh UI when Ghost Mode is auto-disabled by mutual exclusion —
|
||||||
|
// the toggle flip happens externally, so we must pull fresh values from the managers.
|
||||||
|
let ghostModeChangedSignal: Signal<Void, NoError> = Signal { subscriber in
|
||||||
|
let observer = NotificationCenter.default.addObserver(
|
||||||
|
forName: GhostModeManager.settingsChangedNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { _ in
|
||||||
|
updateState { state in
|
||||||
|
state.isEnabled = MiscSettingsManager.shared.isEnabled
|
||||||
|
state.bypassCopyProtection = MiscSettingsManager.shared.bypassCopyProtection
|
||||||
|
state.disableViewOnceAutoDelete = MiscSettingsManager.shared.disableViewOnceAutoDelete
|
||||||
|
state.bypassScreenshotProtection = MiscSettingsManager.shared.bypassScreenshotProtection
|
||||||
|
state.blockAds = MiscSettingsManager.shared.blockAds
|
||||||
|
state.alwaysOnline = MiscSettingsManager.shared.alwaysOnline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ActionDisposable { NotificationCenter.default.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
let _ = ghostModeChangedSignal.start()
|
||||||
|
|
||||||
let signal = combineLatest(
|
let signal = combineLatest(
|
||||||
context.sharedContext.presentationData,
|
context.sharedContext.presentationData,
|
||||||
statePromise.get()
|
statePromise.get()
|
||||||
|
|||||||
@@ -11,25 +11,35 @@ public final class GhostModeManager {
|
|||||||
// MARK: - UserDefaults Keys
|
// MARK: - UserDefaults Keys
|
||||||
|
|
||||||
private enum Keys {
|
private enum Keys {
|
||||||
static let isEnabled = "GhostMode.isEnabled"
|
static let isEnabled = "GhostMode.isEnabled"
|
||||||
static let hideReadReceipts = "GhostMode.hideReadReceipts"
|
static let hideReadReceipts = "GhostMode.hideReadReceipts"
|
||||||
static let hideStoryViews = "GhostMode.hideStoryViews"
|
static let hideStoryViews = "GhostMode.hideStoryViews"
|
||||||
static let hideOnlineStatus = "GhostMode.hideOnlineStatus"
|
static let hideOnlineStatus = "GhostMode.hideOnlineStatus"
|
||||||
static let hideTypingIndicator = "GhostMode.hideTypingIndicator"
|
static let hideTypingIndicator = "GhostMode.hideTypingIndicator"
|
||||||
static let forceOffline = "GhostMode.forceOffline"
|
static let forceOffline = "GhostMode.forceOffline"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Settings Storage
|
// MARK: - Settings Storage
|
||||||
|
|
||||||
private let defaults = UserDefaults.standard
|
private let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
// Prevents recursive mutual-exclusion calls
|
||||||
|
private var isApplyingMutualExclusion = false
|
||||||
|
|
||||||
// MARK: - Properties
|
// MARK: - Properties
|
||||||
|
|
||||||
/// Master toggle for Ghost Mode
|
/// Master toggle for Ghost Mode.
|
||||||
|
/// Enabling Ghost Mode automatically disables Always Online in MiscSettingsManager.
|
||||||
public var isEnabled: Bool {
|
public var isEnabled: Bool {
|
||||||
get { defaults.bool(forKey: Keys.isEnabled) }
|
get { defaults.bool(forKey: Keys.isEnabled) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.isEnabled)
|
defaults.set(newValue, forKey: Keys.isEnabled)
|
||||||
|
if newValue && !isApplyingMutualExclusion {
|
||||||
|
// Ghost Mode ON → disable Always Online
|
||||||
|
isApplyingMutualExclusion = true
|
||||||
|
MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion()
|
||||||
|
isApplyingMutualExclusion = false
|
||||||
|
}
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +47,7 @@ public final class GhostModeManager {
|
|||||||
/// Don't send read receipts (blue checkmarks)
|
/// Don't send read receipts (blue checkmarks)
|
||||||
public var hideReadReceipts: Bool {
|
public var hideReadReceipts: Bool {
|
||||||
get { defaults.bool(forKey: Keys.hideReadReceipts) }
|
get { defaults.bool(forKey: Keys.hideReadReceipts) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.hideReadReceipts)
|
defaults.set(newValue, forKey: Keys.hideReadReceipts)
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
@@ -46,7 +56,7 @@ public final class GhostModeManager {
|
|||||||
/// Don't send story view notifications
|
/// Don't send story view notifications
|
||||||
public var hideStoryViews: Bool {
|
public var hideStoryViews: Bool {
|
||||||
get { defaults.bool(forKey: Keys.hideStoryViews) }
|
get { defaults.bool(forKey: Keys.hideStoryViews) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.hideStoryViews)
|
defaults.set(newValue, forKey: Keys.hideStoryViews)
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
@@ -55,7 +65,7 @@ public final class GhostModeManager {
|
|||||||
/// Don't send online status
|
/// Don't send online status
|
||||||
public var hideOnlineStatus: Bool {
|
public var hideOnlineStatus: Bool {
|
||||||
get { defaults.bool(forKey: Keys.hideOnlineStatus) }
|
get { defaults.bool(forKey: Keys.hideOnlineStatus) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.hideOnlineStatus)
|
defaults.set(newValue, forKey: Keys.hideOnlineStatus)
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
@@ -64,7 +74,7 @@ public final class GhostModeManager {
|
|||||||
/// Don't send typing indicator
|
/// Don't send typing indicator
|
||||||
public var hideTypingIndicator: Bool {
|
public var hideTypingIndicator: Bool {
|
||||||
get { defaults.bool(forKey: Keys.hideTypingIndicator) }
|
get { defaults.bool(forKey: Keys.hideTypingIndicator) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.hideTypingIndicator)
|
defaults.set(newValue, forKey: Keys.hideTypingIndicator)
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
@@ -73,7 +83,7 @@ public final class GhostModeManager {
|
|||||||
/// Always appear as offline
|
/// Always appear as offline
|
||||||
public var forceOffline: Bool {
|
public var forceOffline: Bool {
|
||||||
get { defaults.bool(forKey: Keys.forceOffline) }
|
get { defaults.bool(forKey: Keys.forceOffline) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.forceOffline)
|
defaults.set(newValue, forKey: Keys.forceOffline)
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
@@ -81,72 +91,82 @@ public final class GhostModeManager {
|
|||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
/// Check if read receipts should be hidden (master + individual toggle)
|
/// 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 {
|
public var shouldHideReadReceipts: Bool {
|
||||||
return isEnabled && hideReadReceipts
|
return isEnabled && hideReadReceipts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if story views should be hidden
|
|
||||||
public var shouldHideStoryViews: Bool {
|
public var shouldHideStoryViews: Bool {
|
||||||
return isEnabled && hideStoryViews
|
return isEnabled && hideStoryViews
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if online status should be hidden
|
/// Online status is hidden only when Ghost Mode is on AND Always Online is NOT active.
|
||||||
public var shouldHideOnlineStatus: Bool {
|
public var shouldHideOnlineStatus: Bool {
|
||||||
return isEnabled && hideOnlineStatus
|
guard isEnabled && hideOnlineStatus else { return false }
|
||||||
|
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if typing indicator should be hidden
|
|
||||||
public var shouldHideTypingIndicator: Bool {
|
public var shouldHideTypingIndicator: Bool {
|
||||||
return isEnabled && hideTypingIndicator
|
return isEnabled && hideTypingIndicator
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if should force offline
|
/// Force offline only when Ghost Mode is on AND Always Online is NOT active.
|
||||||
public var shouldForceOffline: Bool {
|
public var shouldForceOffline: Bool {
|
||||||
return isEnabled && forceOffline
|
guard isEnabled && forceOffline else { return false }
|
||||||
|
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count of active features (e.g., "5/5")
|
/// Count of active features (e.g., "5/5")
|
||||||
public var activeFeatureCount: Int {
|
public var activeFeatureCount: Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
if hideReadReceipts { count += 1 }
|
if hideReadReceipts { count += 1 }
|
||||||
if hideStoryViews { count += 1 }
|
if hideStoryViews { count += 1 }
|
||||||
if hideOnlineStatus { count += 1 }
|
if hideOnlineStatus { count += 1 }
|
||||||
if hideTypingIndicator { count += 1 }
|
if hideTypingIndicator { count += 1 }
|
||||||
if forceOffline { count += 1 }
|
if forceOffline { count += 1 }
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Total number of features
|
/// Total number of features
|
||||||
public static let totalFeatureCount = 5
|
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
|
// MARK: - Initialization
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Set default values if not set
|
|
||||||
if !defaults.bool(forKey: "GhostMode.initialized") {
|
if !defaults.bool(forKey: "GhostMode.initialized") {
|
||||||
defaults.set(true, forKey: "GhostMode.initialized")
|
defaults.set(true, forKey: "GhostMode.initialized")
|
||||||
// Default: all features enabled when ghost mode is on
|
|
||||||
defaults.set(true, forKey: Keys.hideReadReceipts)
|
defaults.set(true, forKey: Keys.hideReadReceipts)
|
||||||
defaults.set(true, forKey: Keys.hideStoryViews)
|
defaults.set(true, forKey: Keys.hideStoryViews)
|
||||||
defaults.set(true, forKey: Keys.hideOnlineStatus)
|
defaults.set(true, forKey: Keys.hideOnlineStatus)
|
||||||
defaults.set(true, forKey: Keys.hideTypingIndicator)
|
defaults.set(true, forKey: Keys.hideTypingIndicator)
|
||||||
defaults.set(true, forKey: Keys.forceOffline)
|
defaults.set(true, forKey: Keys.forceOffline)
|
||||||
// Ghost mode itself is off by default
|
|
||||||
defaults.set(false, forKey: Keys.isEnabled)
|
defaults.set(false, forKey: Keys.isEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Enable All
|
// MARK: - Enable/Disable All
|
||||||
|
|
||||||
/// Enable all ghost mode features
|
/// Enable all ghost mode features.
|
||||||
|
/// Also disables Always Online (mutual exclusion).
|
||||||
public func enableAll() {
|
public func enableAll() {
|
||||||
hideReadReceipts = true
|
hideReadReceipts = true
|
||||||
hideStoryViews = true
|
hideStoryViews = true
|
||||||
hideOnlineStatus = true
|
hideOnlineStatus = true
|
||||||
hideTypingIndicator = true
|
hideTypingIndicator = true
|
||||||
forceOffline = true
|
forceOffline = true
|
||||||
isEnabled = true
|
isEnabled = true // setter handles mutual exclusion
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable all ghost mode features
|
/// Disable all ghost mode features
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// MiscSettingsManager - Central manager for Misc privacy settings
|
/// MiscSettingsManager - Central manager for Misc privacy settings
|
||||||
/// Handles: Forward bypass, View-Once persistence, Screenshot bypass
|
/// Handles: Forward bypass, View-Once persistence, Screenshot bypass, Block Ads, Always Online
|
||||||
public final class MiscSettingsManager {
|
public final class MiscSettingsManager {
|
||||||
public static let shared = MiscSettingsManager()
|
public static let shared = MiscSettingsManager()
|
||||||
|
|
||||||
private enum Keys {
|
private enum Keys {
|
||||||
static let isEnabled = "MiscSettings.isEnabled"
|
static let isEnabled = "MiscSettings.isEnabled"
|
||||||
static let bypassCopyProtection = "MiscSettings.bypassCopyProtection"
|
static let bypassCopyProtection = "MiscSettings.bypassCopyProtection"
|
||||||
static let disableViewOnceAutoDelete = "MiscSettings.disableViewOnceAutoDelete"
|
static let disableViewOnceAutoDelete = "MiscSettings.disableViewOnceAutoDelete"
|
||||||
static let bypassScreenshotProtection = "MiscSettings.bypassScreenshotProtection"
|
static let bypassScreenshotProtection = "MiscSettings.bypassScreenshotProtection"
|
||||||
static let blockAds = "MiscSettings.blockAds"
|
static let blockAds = "MiscSettings.blockAds"
|
||||||
static let alwaysOnline = "MiscSettings.alwaysOnline"
|
static let alwaysOnline = "MiscSettings.alwaysOnline"
|
||||||
}
|
}
|
||||||
|
|
||||||
private let defaults = UserDefaults.standard
|
private let defaults = UserDefaults.standard
|
||||||
|
|
||||||
|
// Prevents recursive mutual-exclusion calls
|
||||||
|
private var isApplyingMutualExclusion = false
|
||||||
|
|
||||||
// MARK: - Main Toggle
|
// MARK: - Main Toggle
|
||||||
|
|
||||||
public var isEnabled: Bool {
|
public var isEnabled: Bool {
|
||||||
@@ -64,11 +67,18 @@ public final class MiscSettingsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep online status always active
|
/// Always appear as online.
|
||||||
|
/// Enabling this automatically disables Ghost Mode (mutual exclusion).
|
||||||
public var alwaysOnline: Bool {
|
public var alwaysOnline: Bool {
|
||||||
get { defaults.bool(forKey: Keys.alwaysOnline) }
|
get { defaults.bool(forKey: Keys.alwaysOnline) }
|
||||||
set {
|
set {
|
||||||
defaults.set(newValue, forKey: Keys.alwaysOnline)
|
defaults.set(newValue, forKey: Keys.alwaysOnline)
|
||||||
|
if newValue && !isApplyingMutualExclusion {
|
||||||
|
// Always Online ON → disable Ghost Mode
|
||||||
|
isApplyingMutualExclusion = true
|
||||||
|
GhostModeManager.shared.disableForMutualExclusion()
|
||||||
|
isApplyingMutualExclusion = false
|
||||||
|
}
|
||||||
notifySettingsChanged()
|
notifySettingsChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,28 +109,39 @@ public final class MiscSettingsManager {
|
|||||||
|
|
||||||
public var activeFeatureCount: Int {
|
public var activeFeatureCount: Int {
|
||||||
var count = 0
|
var count = 0
|
||||||
if bypassCopyProtection { count += 1 }
|
if bypassCopyProtection { count += 1 }
|
||||||
if disableViewOnceAutoDelete { count += 1 }
|
if disableViewOnceAutoDelete { count += 1 }
|
||||||
if bypassScreenshotProtection { count += 1 }
|
if bypassScreenshotProtection { count += 1 }
|
||||||
if blockAds { count += 1 }
|
if blockAds { count += 1 }
|
||||||
if alwaysOnline { count += 1 }
|
if alwaysOnline { count += 1 }
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
public func enableAll() {
|
public func enableAll() {
|
||||||
bypassCopyProtection = true
|
bypassCopyProtection = true
|
||||||
disableViewOnceAutoDelete = true
|
disableViewOnceAutoDelete = true
|
||||||
bypassScreenshotProtection = true
|
bypassScreenshotProtection = true
|
||||||
blockAds = true
|
blockAds = true
|
||||||
alwaysOnline = true
|
alwaysOnline = true // setter handles mutual exclusion
|
||||||
}
|
}
|
||||||
|
|
||||||
public func disableAll() {
|
public func disableAll() {
|
||||||
bypassCopyProtection = false
|
bypassCopyProtection = false
|
||||||
disableViewOnceAutoDelete = false
|
disableViewOnceAutoDelete = false
|
||||||
bypassScreenshotProtection = false
|
bypassScreenshotProtection = false
|
||||||
blockAds = false
|
blockAds = false
|
||||||
alwaysOnline = false
|
alwaysOnline = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internal mutual exclusion (called by GhostModeManager)
|
||||||
|
|
||||||
|
/// Called by GhostModeManager when Ghost Mode is turned on.
|
||||||
|
/// Disables Always Online without triggering mutual exclusion back.
|
||||||
|
public func disableAlwaysOnlineForMutualExclusion() {
|
||||||
|
isApplyingMutualExclusion = true
|
||||||
|
defaults.set(false, forKey: Keys.alwaysOnline)
|
||||||
|
notifySettingsChanged()
|
||||||
|
isApplyingMutualExclusion = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Notification
|
// MARK: - Notification
|
||||||
@@ -134,7 +155,6 @@ public final class MiscSettingsManager {
|
|||||||
// MARK: - Init
|
// MARK: - Init
|
||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// Set default values if first launch
|
|
||||||
if !defaults.bool(forKey: "MiscSettings.initialized") {
|
if !defaults.bool(forKey: "MiscSettings.initialized") {
|
||||||
defaults.set(true, forKey: "MiscSettings.initialized")
|
defaults.set(true, forKey: "MiscSettings.initialized")
|
||||||
defaults.set(false, forKey: Keys.isEnabled)
|
defaults.set(false, forKey: Keys.isEnabled)
|
||||||
@@ -142,6 +162,7 @@ public final class MiscSettingsManager {
|
|||||||
defaults.set(true, forKey: Keys.disableViewOnceAutoDelete)
|
defaults.set(true, forKey: Keys.disableViewOnceAutoDelete)
|
||||||
defaults.set(true, forKey: Keys.bypassScreenshotProtection)
|
defaults.set(true, forKey: Keys.bypassScreenshotProtection)
|
||||||
defaults.set(true, forKey: Keys.blockAds)
|
defaults.set(true, forKey: Keys.blockAds)
|
||||||
|
defaults.set(false, forKey: Keys.alwaysOnline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,13 @@ private final class AccountPresenceManagerImpl {
|
|||||||
private let currentRequestDisposable = MetaDisposable()
|
private let currentRequestDisposable = MetaDisposable()
|
||||||
private var onlineTimer: SignalKitTimer?
|
private var onlineTimer: SignalKitTimer?
|
||||||
|
|
||||||
|
// Tracks the last app-level online value so we can refresh independently
|
||||||
private var wasOnline: Bool = false
|
private var wasOnline: Bool = false
|
||||||
|
|
||||||
|
// Observers for settings change notifications
|
||||||
|
private var ghostModeObserver: NSObjectProtocol?
|
||||||
|
private var miscSettingsObserver: NSObjectProtocol?
|
||||||
|
|
||||||
init(queue: Queue, shouldKeepOnlinePresence: Signal<Bool, NoError>, network: Network) {
|
init(queue: Queue, shouldKeepOnlinePresence: Signal<Bool, NoError>, network: Network) {
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.network = network
|
self.network = network
|
||||||
@@ -25,14 +30,37 @@ private final class AccountPresenceManagerImpl {
|
|||||||
self.shouldKeepOnlinePresenceDisposable = (shouldKeepOnlinePresence
|
self.shouldKeepOnlinePresenceDisposable = (shouldKeepOnlinePresence
|
||||||
|> distinctUntilChanged
|
|> distinctUntilChanged
|
||||||
|> deliverOn(self.queue)).start(next: { [weak self] value in
|
|> deliverOn(self.queue)).start(next: { [weak self] value in
|
||||||
guard let `self` = self else {
|
guard let self = self else { return }
|
||||||
return
|
self.wasOnline = value
|
||||||
}
|
self.refreshPresence()
|
||||||
if self.wasOnline != value {
|
|
||||||
self.wasOnline = value
|
|
||||||
self.updatePresence(value)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// React to Ghost Mode or Always Online settings changes without waiting
|
||||||
|
// for the next app focus event.
|
||||||
|
let notificationQueue = DispatchQueue.main
|
||||||
|
self.ghostModeObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: GhostModeManager.settingsChangedNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: nil
|
||||||
|
) { [weak self] _ in
|
||||||
|
notificationQueue.async {
|
||||||
|
self?.queue.async {
|
||||||
|
self?.refreshPresence()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.miscSettingsObserver = NotificationCenter.default.addObserver(
|
||||||
|
forName: MiscSettingsManager.settingsChangedNotification,
|
||||||
|
object: nil,
|
||||||
|
queue: nil
|
||||||
|
) { [weak self] _ in
|
||||||
|
notificationQueue.async {
|
||||||
|
self?.queue.async {
|
||||||
|
self?.refreshPresence()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
@@ -40,26 +68,43 @@ private final class AccountPresenceManagerImpl {
|
|||||||
self.shouldKeepOnlinePresenceDisposable?.dispose()
|
self.shouldKeepOnlinePresenceDisposable?.dispose()
|
||||||
self.currentRequestDisposable.dispose()
|
self.currentRequestDisposable.dispose()
|
||||||
self.onlineTimer?.invalidate()
|
self.onlineTimer?.invalidate()
|
||||||
|
if let observer = self.ghostModeObserver {
|
||||||
|
NotificationCenter.default.removeObserver(observer)
|
||||||
|
}
|
||||||
|
if let observer = self.miscSettingsObserver {
|
||||||
|
NotificationCenter.default.removeObserver(observer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updatePresence(_ isOnline: Bool) {
|
/// Compute the effective online state and push it to Telegram.
|
||||||
// GHOST MODE: Completely block status updates to freeze "last seen" time
|
/// Priority chain (highest → lowest):
|
||||||
if GhostModeManager.shared.shouldHideOnlineStatus {
|
/// 1. Always Online enabled → force online = true
|
||||||
|
/// 2. Ghost Mode hide online status → skip update entirely (freeze last-seen)
|
||||||
|
/// 3. Default app behaviour (wasOnline)
|
||||||
|
private func refreshPresence() {
|
||||||
|
let alwaysOnline = MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||||
|
let ghostHideOnline = GhostModeManager.shared.shouldHideOnlineStatus
|
||||||
|
|
||||||
|
if alwaysOnline {
|
||||||
|
// Always Online wins — push online regardless of Ghost Mode
|
||||||
|
sendPresenceUpdate(online: true)
|
||||||
|
} else if ghostHideOnline {
|
||||||
|
// Ghost Mode active, no Always Online — freeze presence (don't send anything)
|
||||||
self.onlineTimer?.invalidate()
|
self.onlineTimer?.invalidate()
|
||||||
self.onlineTimer = nil
|
self.onlineTimer = nil
|
||||||
return
|
} else {
|
||||||
|
// Normal mode — follow the app-level state
|
||||||
|
sendPresenceUpdate(online: wasOnline)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// ALWAYS ONLINE: Force online status when enabled
|
|
||||||
let effectiveOnline = MiscSettingsManager.shared.shouldAlwaysBeOnline ? true : isOnline
|
private func sendPresenceUpdate(online: Bool) {
|
||||||
|
|
||||||
let request: Signal<Api.Bool, MTRpcError>
|
let request: Signal<Api.Bool, MTRpcError>
|
||||||
if effectiveOnline {
|
if online {
|
||||||
|
// Keep pinging every 30 s so the server keeps us online
|
||||||
let timer = SignalKitTimer(timeout: 30.0, repeat: false, completion: { [weak self] in
|
let timer = SignalKitTimer(timeout: 30.0, repeat: false, completion: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
guard let self = self else { return }
|
||||||
return
|
self.refreshPresence()
|
||||||
}
|
|
||||||
strongSelf.updatePresence(true)
|
|
||||||
}, queue: self.queue)
|
}, queue: self.queue)
|
||||||
self.onlineTimer = timer
|
self.onlineTimer = timer
|
||||||
timer.start()
|
timer.start()
|
||||||
@@ -69,16 +114,14 @@ private final class AccountPresenceManagerImpl {
|
|||||||
self.onlineTimer = nil
|
self.onlineTimer = nil
|
||||||
request = self.network.request(Api.functions.account.updateStatus(offline: .boolTrue))
|
request = self.network.request(Api.functions.account.updateStatus(offline: .boolTrue))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.isPerformingUpdate.set(true)
|
self.isPerformingUpdate.set(true)
|
||||||
self.currentRequestDisposable.set((request
|
self.currentRequestDisposable.set((request
|
||||||
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
|> `catch` { _ -> Signal<Api.Bool, NoError> in
|
||||||
return .single(.boolFalse)
|
return .single(.boolFalse)
|
||||||
}
|
}
|
||||||
|> deliverOn(self.queue)).start(completed: { [weak self] in
|
|> deliverOn(self.queue)).start(completed: { [weak self] in
|
||||||
guard let strongSelf = self else {
|
self?.isPerformingUpdate.set(false)
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.isPerformingUpdate.set(false)
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user