mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-04-30 11:47:50 +02:00
feat: новые функции, исправлены критические ошибки сборки и баги интерфейса, больше подписей в файлах
This commit is contained in:
@@ -11,6 +11,7 @@ public final class AntiDeleteManager {
|
||||
private let defaults = UserDefaults.standard
|
||||
private let enabledKey = "antiDelete.enabled"
|
||||
private let archiveMediaKey = "antiDelete.archiveMedia"
|
||||
private let deletedMessageTransparencyKey = "antiDelete.deletedMessageTransparency"
|
||||
private let archiveKey = "antiDelete.archive"
|
||||
private let deletedIdsKey = "antiDelete.deletedIds"
|
||||
|
||||
@@ -26,6 +27,33 @@ public final class AntiDeleteManager {
|
||||
set { defaults.set(newValue, forKey: archiveMediaKey) }
|
||||
}
|
||||
|
||||
/// Минимальное значение прозрачности удалённого сообщения
|
||||
public static let minDeletedMessageTransparency: Double = 0.0
|
||||
|
||||
/// Максимальное значение прозрачности удалённого сообщения
|
||||
public static let maxDeletedMessageTransparency: Double = 0.8
|
||||
|
||||
/// Значение прозрачности удалённого сообщения по умолчанию
|
||||
public static let defaultDeletedMessageTransparency: Double = 0.45
|
||||
|
||||
/// Прозрачность удалённых сообщений (0.0 = непрозрачно, 0.8 = максимально прозрачно)
|
||||
public var deletedMessageTransparency: Double {
|
||||
get {
|
||||
let value = defaults.object(forKey: deletedMessageTransparencyKey) as? NSNumber
|
||||
let resolvedValue = value?.doubleValue ?? Self.defaultDeletedMessageTransparency
|
||||
return max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, resolvedValue))
|
||||
}
|
||||
set {
|
||||
let clampedValue = max(Self.minDeletedMessageTransparency, min(Self.maxDeletedMessageTransparency, newValue))
|
||||
defaults.set(clampedValue, forKey: deletedMessageTransparencyKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Альфа для отображения удалённых сообщений
|
||||
public var deletedMessageDisplayAlpha: Double {
|
||||
return 1.0 - self.deletedMessageTransparency
|
||||
}
|
||||
|
||||
// MARK: - Deleted Message IDs Storage
|
||||
|
||||
private var deletedMessageIds: Set<String> = []
|
||||
@@ -120,6 +148,9 @@ public final class AntiDeleteManager {
|
||||
if defaults.object(forKey: archiveMediaKey) == nil {
|
||||
defaults.set(true, forKey: archiveMediaKey)
|
||||
}
|
||||
if defaults.object(forKey: deletedMessageTransparencyKey) == nil {
|
||||
defaults.set(Self.defaultDeletedMessageTransparency, forKey: deletedMessageTransparencyKey)
|
||||
}
|
||||
loadArchive()
|
||||
loadDeletedIds()
|
||||
}
|
||||
|
||||
@@ -23,9 +23,6 @@ public final class GhostModeManager {
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
// Prevents recursive mutual-exclusion calls
|
||||
private var isApplyingMutualExclusion = false
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// Master toggle for Ghost Mode.
|
||||
@@ -34,11 +31,9 @@ public final class GhostModeManager {
|
||||
get { defaults.bool(forKey: Keys.isEnabled) }
|
||||
set {
|
||||
defaults.set(newValue, forKey: Keys.isEnabled)
|
||||
if newValue && !isApplyingMutualExclusion {
|
||||
// Ghost Mode ON → disable Always Online
|
||||
isApplyingMutualExclusion = true
|
||||
if newValue {
|
||||
// Ghost Mode ON → disable Always Online so they don't coexist in UI
|
||||
MiscSettingsManager.shared.disableAlwaysOnlineForMutualExclusion()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
notifySettingsChanged()
|
||||
}
|
||||
@@ -102,9 +97,11 @@ public final class GhostModeManager {
|
||||
}
|
||||
|
||||
/// Online status is hidden only when Ghost Mode is on AND Always Online is NOT active.
|
||||
/// Checks alwaysOnline raw value (not shouldAlwaysBeOnline) so ghost mode works
|
||||
/// even when the Misc master toggle is off.
|
||||
public var shouldHideOnlineStatus: Bool {
|
||||
guard isEnabled && hideOnlineStatus else { return false }
|
||||
return !MiscSettingsManager.shared.shouldAlwaysBeOnline
|
||||
return !MiscSettingsManager.shared.alwaysOnline
|
||||
}
|
||||
|
||||
public var shouldHideTypingIndicator: Bool {
|
||||
@@ -114,7 +111,7 @@ public final class GhostModeManager {
|
||||
/// 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
|
||||
return !MiscSettingsManager.shared.alwaysOnline
|
||||
}
|
||||
|
||||
/// Count of active features (e.g., "5/5")
|
||||
@@ -131,17 +128,6 @@ public final class GhostModeManager {
|
||||
/// 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() {
|
||||
|
||||
@@ -16,9 +16,6 @@ public final class MiscSettingsManager {
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
// Prevents recursive mutual-exclusion calls
|
||||
private var isApplyingMutualExclusion = false
|
||||
|
||||
// MARK: - Main Toggle
|
||||
|
||||
public var isEnabled: Bool {
|
||||
@@ -68,17 +65,12 @@ public final class MiscSettingsManager {
|
||||
}
|
||||
|
||||
/// Always appear as online.
|
||||
/// Enabling this automatically disables Ghost Mode (mutual exclusion).
|
||||
/// NOTE: Ghost Mode features dynamically yield to Always Online via their
|
||||
/// `shouldAlwaysBeOnline` check, so no permanent disabling is needed here.
|
||||
public var alwaysOnline: Bool {
|
||||
get { defaults.bool(forKey: Keys.alwaysOnline) }
|
||||
set {
|
||||
defaults.set(newValue, forKey: Keys.alwaysOnline)
|
||||
if newValue && !isApplyingMutualExclusion {
|
||||
// Always Online ON → disable Ghost Mode
|
||||
isApplyingMutualExclusion = true
|
||||
GhostModeManager.shared.disableForMutualExclusion()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
notifySettingsChanged()
|
||||
}
|
||||
}
|
||||
@@ -122,7 +114,7 @@ public final class MiscSettingsManager {
|
||||
disableViewOnceAutoDelete = true
|
||||
bypassScreenshotProtection = true
|
||||
blockAds = true
|
||||
alwaysOnline = true // setter handles mutual exclusion
|
||||
alwaysOnline = true
|
||||
}
|
||||
|
||||
public func disableAll() {
|
||||
@@ -136,12 +128,10 @@ public final class MiscSettingsManager {
|
||||
// MARK: - Internal mutual exclusion (called by GhostModeManager)
|
||||
|
||||
/// Called by GhostModeManager when Ghost Mode is turned on.
|
||||
/// Disables Always Online without triggering mutual exclusion back.
|
||||
/// Disables Always Online so the two modes don't coexist in the UI.
|
||||
public func disableAlwaysOnlineForMutualExclusion() {
|
||||
isApplyingMutualExclusion = true
|
||||
defaults.set(false, forKey: Keys.alwaysOnline)
|
||||
notifySettingsChanged()
|
||||
isApplyingMutualExclusion = false
|
||||
}
|
||||
|
||||
// MARK: - Notification
|
||||
|
||||
@@ -365,14 +365,46 @@ public func enqueueMessages(account: Account, peerId: PeerId, messages: [Enqueue
|
||||
} else {
|
||||
signal = .single(messages.map { (false, $0) })
|
||||
}
|
||||
|
||||
let hasMedia = messages.contains { message in
|
||||
if case let .message(_, _, _, mediaReference, _, _, _, _, _, _) = message {
|
||||
return mediaReference != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GHOSTGRAM: Send delay — write to Postbox immediately so the UI
|
||||
// clears the input field, then delay _only_ the return signal.
|
||||
// The actual network send delay is handled by scheduling: we add
|
||||
// OutgoingScheduleInfoMessageAttribute inside the transaction so
|
||||
// the message is stored as "scheduled" and Telegram server sends it
|
||||
// after the delay elapses. The message appears in Scheduled Messages
|
||||
// section for the duration of the delay.
|
||||
return signal
|
||||
|> mapToSignal { messages -> Signal<[MessageId?], NoError> in
|
||||
return account.postbox.transaction { transaction -> [MessageId?] in
|
||||
return enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: messages)
|
||||
var finalMessages = messages
|
||||
if SendDelayManager.shared.isEnabled {
|
||||
let delayInterval = hasMedia
|
||||
? SendDelayManager.mediaDelaySeconds
|
||||
: SendDelayManager.textDelaySeconds
|
||||
let scheduleTime = Int32(Date().timeIntervalSince1970) + Int32(delayInterval)
|
||||
finalMessages = messages.map { (transformed, msg) in
|
||||
let updatedMsg = msg.withUpdatedAttributes { attrs in
|
||||
var attrs = attrs
|
||||
attrs.removeAll(where: { $0 is OutgoingScheduleInfoMessageAttribute })
|
||||
attrs.append(OutgoingScheduleInfoMessageAttribute(scheduleTime: scheduleTime, repeatPeriod: nil))
|
||||
return attrs
|
||||
}
|
||||
return (transformed, updatedMsg)
|
||||
}
|
||||
}
|
||||
return enqueueMessages(transaction: transaction, account: account, peerId: peerId, messages: finalMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], threadIds: [PeerId: Int64], messages: [EnqueueMessage]) -> Signal<[MessageId], NoError> {
|
||||
let signal: Signal<[(Bool, EnqueueMessage)], NoError>
|
||||
if let transformOutgoingMessageMedia = account.transformOutgoingMessageMedia {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
/// SendDelayManager - delays outgoing messages by ~12 seconds to prevent
|
||||
/// online status from appearing after sending.
|
||||
///
|
||||
/// Delays are applied per-message at the enqueueMessages level.
|
||||
/// Media messages receive a slightly longer delay (~20 s) because upload
|
||||
/// time would otherwise reveal the send moment anyway.
|
||||
public final class SendDelayManager {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
public static let shared = SendDelayManager()
|
||||
|
||||
// MARK: - UserDefaults Keys
|
||||
|
||||
private enum Keys {
|
||||
static let isEnabled = "SendDelay.isEnabled"
|
||||
}
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
private let defaults = UserDefaults.standard
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
/// When true, all outgoing messages are delayed before being enqueued.
|
||||
public var isEnabled: Bool {
|
||||
get { defaults.bool(forKey: Keys.isEnabled) }
|
||||
set {
|
||||
defaults.set(newValue, forKey: Keys.isEnabled)
|
||||
notifySettingsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delay constants
|
||||
|
||||
/// Base delay for text-only messages.
|
||||
public static let textDelaySeconds: Double = 12.0
|
||||
|
||||
/// Delay for messages that contain media attachments.
|
||||
public static let mediaDelaySeconds: Double = 20.0
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
public static let settingsChangedNotification = Notification.Name("SendDelaySettingsChanged")
|
||||
|
||||
private func notifySettingsChanged() {
|
||||
NotificationCenter.default.post(name: SendDelayManager.settingsChangedNotification, object: nil)
|
||||
}
|
||||
}
|
||||
@@ -4233,6 +4233,9 @@ func replayFinalState(
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
let messageIds = transaction.messageIdsForGlobalIds(ids)
|
||||
for (index, messageId) in messageIds.enumerated() {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
if let message = transaction.getMessage(messageId) {
|
||||
let globalId = index < ids.count ? ids[index] : 0
|
||||
|
||||
@@ -4289,6 +4292,9 @@ func replayFinalState(
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
let messageIds = transaction.messageIdsForGlobalIds(ids)
|
||||
for messageId in messageIds {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
// Mark as deleted for icon display
|
||||
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
|
||||
|
||||
@@ -4317,6 +4323,9 @@ func replayFinalState(
|
||||
// ANTI-DELETE: Archive channel messages with full content before deletion
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
for messageId in ids {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
if let message = transaction.getMessage(messageId) {
|
||||
// Extract text content
|
||||
let textContent = message.text
|
||||
@@ -4370,6 +4379,9 @@ func replayFinalState(
|
||||
// ANTI-DELETE: Mark messages as deleted instead of removing them
|
||||
if AntiDeleteManager.shared.isEnabled {
|
||||
for messageId in ids {
|
||||
// Skip scheduled/local/quick-reply messages — they get deleted when sent, not by the remote peer
|
||||
guard messageId.namespace == Namespaces.Message.Cloud else { continue }
|
||||
|
||||
// Mark as deleted for icon display
|
||||
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
|
||||
|
||||
|
||||
@@ -82,16 +82,19 @@ private final class AccountPresenceManagerImpl {
|
||||
/// 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
|
||||
// Use raw alwaysOnline flag (not shouldAlwaysBeOnline) so it works independently
|
||||
// of the Misc master toggle. Ghost Mode's shouldHideOnlineStatus already checks
|
||||
// !MiscSettingsManager.shared.alwaysOnline internally.
|
||||
let alwaysOnline = MiscSettingsManager.shared.alwaysOnline
|
||||
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 = nil
|
||||
// Ghost Mode active: actively send offline so the server immediately
|
||||
// hides our last-seen instead of keeping the stale "online" status.
|
||||
sendPresenceUpdate(online: false)
|
||||
} else {
|
||||
// Normal mode — follow the app-level state
|
||||
sendPresenceUpdate(online: wasOnline)
|
||||
|
||||
Reference in New Issue
Block a user