chore: migrate to new version + fixed several critical bugs

- Migrated project to latest Telegram iOS base (v12.3.2+)
- Fixed circular dependency between GhostModeManager and MiscSettingsManager
- Fixed multiple Bazel build configuration errors (select() default conditions)
- Fixed duplicate type definitions in PeerInfoScreen
- Fixed swiftmodule directory resolution in build scripts
- Added Ghostgram Settings tab in main Settings menu with all 5 features
- Cleared sensitive credentials from config.json (template-only now)
- Excluded bazel-cache from version control
This commit is contained in:
ichmagmaus 812
2026-02-23 23:04:32 +01:00
parent 703e291bcb
commit db53826061
1017 changed files with 62337 additions and 40559 deletions
@@ -1020,6 +1020,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
let peerId = PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId))
updatedState.updateMinAvailableMessage(MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: minId))
case let .updateDeleteMessages(messages, _, _):
// Note: Actual archiving happens in DeleteMessagesWithGlobalIds handler
// where we have access to transaction and can get full message content
updatedState.deleteMessagesWithGlobalIds(messages)
case let .updatePinnedMessages(flags, peer, messages, _, _):
let peerId = peer.peerId
@@ -1906,6 +1908,8 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
}
case let .updateStarGiftAuctionUserState(giftId, userState):
updatedState.updateStarGiftAuctionMyState(giftId: giftId, state: GiftAuctionContext.State.MyState(apiAuctionUserState: userState))
case let .updateEmojiGameInfo(info):
updatedState.updateEmojiGameInfo(info: EmojiGameInfo(apiEmojiGameInfo: info))
default:
break
}
@@ -3629,7 +3633,7 @@ private func optimizedOperations(_ operations: [AccountStateMutationOperation])
var currentAddQuickReplyMessages: OptimizeAddMessagesState?
for operation in operations {
switch operation {
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState:
case .DeleteMessages, .DeleteMessagesWithGlobalIds, .EditMessage, .UpdateMessagePoll, .UpdateMessageReactions, .UpdateMedia, .MergeApiChats, .MergeApiUsers, .MergePeerPresences, .UpdatePeer, .ReadInbox, .ReadOutbox, .ReadGroupFeedInbox, .ResetReadState, .ResetIncomingReadState, .UpdatePeerChatUnreadMark, .ResetMessageTagSummary, .UpdateNotificationSettings, .UpdateGlobalNotificationSettings, .UpdateSecretChat, .AddSecretMessages, .ReadSecretOutbox, .AddPeerInputActivity, .AddPeerLiveTypingDraftUpdate, .UpdateCachedPeerData, .UpdatePinnedItemIds, .UpdatePinnedSavedItemIds, .UpdatePinnedTopic, .UpdatePinnedTopicOrder, .ReadMessageContents, .UpdateMessageImpressionCount, .UpdateMessageForwardsCount, .UpdateInstalledStickerPacks, .UpdateRecentGifs, .UpdateChatInputState, .UpdateCall, .AddCallSignalingData, .UpdateLangPack, .UpdateMinAvailableMessage, .UpdateIsContact, .UpdatePeerChatInclusion, .UpdatePeersNearby, .UpdateTheme, .SyncChatListFilters, .UpdateChatListFilter, .UpdateChatListFilterOrder, .UpdateReadThread, .UpdateMessagesPinned, .UpdateGroupCallParticipants, .UpdateGroupCall, .UpdateGroupCallChainBlocks, .UpdateGroupCallMessage, .UpdateGroupCallOpaqueMessage, .UpdateAutoremoveTimeout, .UpdateAttachMenuBots, .UpdateAudioTranscription, .UpdateConfig, .UpdateExtendedMedia, .ResetForumTopic, .UpdateStory, .UpdateReadStories, .UpdateStoryStealthMode, .UpdateStorySentReaction, .UpdateNewAuthorization, .UpdateWallpaper, .UpdateStarsBalance, .UpdateStarsRevenueStatus, .UpdateStarsReactionsDefaultPrivacy, .ReportMessageDelivery, .UpdateMonoForumNoPaidException, .UpdateStarGiftAuctionState, .UpdateStarGiftAuctionMyState, .UpdateEmojiGameInfo:
if let currentAddMessages = currentAddMessages, !currentAddMessages.messages.isEmpty {
result.append(.AddMessages(currentAddMessages.messages, currentAddMessages.location))
}
@@ -3772,6 +3776,7 @@ func replayFinalState(
var reportMessageDelivery = Set<MessageId>()
var updatedStarGiftAuctionState: [Int64: GiftAuctionContext.State.AuctionState] = [:]
var updatedStarGiftAuctionMyState: [Int64: GiftAuctionContext.State.MyState] = [:]
var updatedEmojiGameInfo: EmojiGameInfo?
var holesFromPreviousStateMessageIds: [MessageId] = []
var clearHolesFromPreviousStateForChannelMessagesWithPts: [PeerIdAndMessageNamespace: Int32] = [:]
@@ -4224,18 +4229,166 @@ func replayFinalState(
}
}
case let .DeleteMessagesWithGlobalIds(ids):
var resourceIds: [MediaResourceId] = []
transaction.deleteMessagesWithGlobalIds(ids, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
// ANTI-DELETE: Archive messages with full content before deletion
if AntiDeleteManager.shared.isEnabled {
let messageIds = transaction.messageIdsForGlobalIds(ids)
for (index, messageId) in messageIds.enumerated() {
if let message = transaction.getMessage(messageId) {
let globalId = index < ids.count ? ids[index] : 0
// Extract text content
let textContent = message.text
// Extract media description
var mediaDesc: String? = nil
for media in message.media {
switch media {
case let image as TelegramMediaImage:
mediaDesc = "📷 Photo"
if let largest = image.representations.last {
mediaDesc = "📷 Photo \(largest.dimensions.width)x\(largest.dimensions.height)"
}
case let file as TelegramMediaFile:
if file.isVideo {
mediaDesc = "🎬 Video"
} else if file.isVoice {
mediaDesc = "🎤 Voice Message"
} else if file.isInstantVideo {
mediaDesc = "📹 Video Message"
} else if file.isSticker {
mediaDesc = "🎭 Sticker"
} else {
mediaDesc = "📎 \(file.fileName ?? "File")"
}
case is TelegramMediaContact:
mediaDesc = "👤 Contact"
case is TelegramMediaMap:
mediaDesc = "📍 Location"
case let poll as TelegramMediaPoll:
mediaDesc = "📊 Poll: \(poll.text)"
default:
break
}
}
AntiDeleteManager.shared.archiveMessage(
globalId: globalId,
peerId: messageId.peerId.toInt64(),
messageId: messageId.id,
timestamp: message.timestamp,
authorId: message.author?.id.toInt64(),
text: textContent,
forwardAuthorId: message.forwardInfo?.author?.id.toInt64(),
mediaDescription: mediaDesc
)
}
}
}
// ANTI-DELETE: Mark messages as deleted instead of removing them
if AntiDeleteManager.shared.isEnabled {
let messageIds = transaction.messageIdsForGlobalIds(ids)
for messageId in messageIds {
// Mark as deleted for icon display
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
transaction.updateMessage(messageId, update: { currentMessage in
var attributes = currentMessage.attributes
// Don't add duplicate DeletedMessageAttribute
if !attributes.contains(where: { $0 is DeletedMessageAttribute }) {
attributes.append(DeletedMessageAttribute(deletedAt: Int32(Date().timeIntervalSince1970)))
}
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
// Keep original text, no prefix needed - icon will show deleted status
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
} else {
var resourceIds: [MediaResourceId] = []
transaction.deleteMessagesWithGlobalIds(ids, forEachMedia: { media in
addMessageMediaResourceIdsToRemove(media: media, resourceIds: &resourceIds)
})
if !resourceIds.isEmpty {
let _ = mediaBox.removeCachedResources(Array(Set(resourceIds)), force: true).start()
}
}
deletedMessageIds.append(contentsOf: ids.map { .global($0) })
case let .DeleteMessages(ids):
_internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in
addMessageThreadStatsDifference(threadKey: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false)
})
// ANTI-DELETE: Archive channel messages with full content before deletion
if AntiDeleteManager.shared.isEnabled {
for messageId in ids {
if let message = transaction.getMessage(messageId) {
// Extract text content
let textContent = message.text
// Extract media description
var mediaDesc: String? = nil
for media in message.media {
switch media {
case let image as TelegramMediaImage:
mediaDesc = "📷 Photo"
if let largest = image.representations.last {
mediaDesc = "📷 Photo \(largest.dimensions.width)x\(largest.dimensions.height)"
}
case let file as TelegramMediaFile:
if file.isVideo {
mediaDesc = "🎬 Video"
} else if file.isVoice {
mediaDesc = "🎤 Voice Message"
} else if file.isInstantVideo {
mediaDesc = "📹 Video Message"
} else if file.isSticker {
mediaDesc = "🎭 Sticker"
} else {
mediaDesc = "📎 \(file.fileName ?? "File")"
}
case is TelegramMediaContact:
mediaDesc = "👤 Contact"
case is TelegramMediaMap:
mediaDesc = "📍 Location"
case let poll as TelegramMediaPoll:
mediaDesc = "📊 Poll: \(poll.text)"
default:
break
}
}
AntiDeleteManager.shared.archiveMessage(
globalId: messageId.id, // Use message id as globalId for channel messages
peerId: messageId.peerId.toInt64(),
messageId: messageId.id,
timestamp: message.timestamp,
authorId: message.author?.id.toInt64(),
text: textContent,
forwardAuthorId: message.forwardInfo?.author?.id.toInt64(),
mediaDescription: mediaDesc
)
}
}
}
// ANTI-DELETE: Mark messages as deleted instead of removing them
if AntiDeleteManager.shared.isEnabled {
for messageId in ids {
// Mark as deleted for icon display
AntiDeleteManager.shared.markAsDeleted(peerId: messageId.peerId.toInt64(), messageId: messageId.id)
transaction.updateMessage(messageId, update: { currentMessage in
var attributes = currentMessage.attributes
// Don't add duplicate DeletedMessageAttribute
if !attributes.contains(where: { $0 is DeletedMessageAttribute }) {
attributes.append(DeletedMessageAttribute(deletedAt: Int32(Date().timeIntervalSince1970)))
}
let storeForwardInfo = currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init)
// Keep original text, no prefix needed - icon will show deleted status
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
} else {
_internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: ids, manualAddMessageThreadStatsDifference: { id, add, remove in
addMessageThreadStatsDifference(threadKey: id, remove: remove, addedMessagePeer: nil, addedMessageId: nil, isOutgoing: false)
})
}
deletedMessageIds.append(contentsOf: ids.map { .messageId($0) })
case let .UpdateMinAvailableMessage(id):
if let message = transaction.getMessage(id) {
@@ -4294,6 +4447,14 @@ func replayFinalState(
updatedAttributes.append(translation)
}
}
} else {
// GHOSTGRAM: Save original text before edit
EditHistoryManager.shared.saveOriginalText(
peerId: id.peerId.toInt64(),
messageId: id.id,
originalText: previousMessage.text,
editDate: Int32(Date().timeIntervalSince1970)
)
}
if let previousFactCheckAttribute = previousMessage.attributes.first(where: { $0 is FactCheckMessageAttribute }) as? FactCheckMessageAttribute, let updatedFactCheckAttribute = message.attributes.first(where: { $0 is FactCheckMessageAttribute }) as? FactCheckMessageAttribute {
@@ -5339,6 +5500,8 @@ func replayFinalState(
updatedStarGiftAuctionState[giftId] = state
case let .UpdateStarGiftAuctionMyState(giftId, state):
updatedStarGiftAuctionMyState[giftId] = state
case let .UpdateEmojiGameInfo(info):
updatedEmojiGameInfo = info
}
}
@@ -5889,6 +6052,7 @@ func replayFinalState(
reportMessageDelivery: reportMessageDelivery,
addedConferenceInvitationMessagesIds: addedConferenceInvitationMessagesIds,
updatedStarGiftAuctionState: updatedStarGiftAuctionState,
updatedStarGiftAuctionMyState: updatedStarGiftAuctionMyState
updatedStarGiftAuctionMyState: updatedStarGiftAuctionMyState,
updatedEmojiGameInfo: updatedEmojiGameInfo
)
}
@@ -374,6 +374,7 @@ public final class AccountStateManager {
private let appliedQtsPromise = Promise<Int32?>(nil)
private let appliedQtsDisposable = MetaDisposable()
private let reportMessageDeliveryDisposable = DisposableSet()
private let updateEmojiGameInfoDisposable = MetaDisposable()
let updateConfigRequested: (() -> Void)?
let isPremiumUpdated: (() -> Void)?
@@ -414,6 +415,7 @@ public final class AccountStateManager {
self.appliedMaxMessageIdDisposable.dispose()
self.appliedQtsDisposable.dispose()
self.reportMessageDeliveryDisposable.dispose()
self.updateEmojiGameInfoDisposable.dispose()
}
public func reset() {
@@ -1137,6 +1139,11 @@ public final class AccountStateManager {
if !events.updatedStarGiftAuctionMyState.isEmpty {
strongSelf.notifyUpdatedStarGiftAuctionMyState(events.updatedStarGiftAuctionMyState)
}
if let updatedEmojiGameInfo = events.updatedEmojiGameInfo {
strongSelf.updateEmojiGameInfoDisposable.set(strongSelf.postbox.transaction({ transaction in
updateEmojiGameInfo(transaction: transaction, { _ in return updatedEmojiGameInfo })
}).start())
}
if !events.updatedCalls.isEmpty {
for call in events.updatedCalls {
strongSelf.callSessionManager?.updateSession(call, completion: { _ in })
@@ -125,6 +125,7 @@ final class AccountTaskManager {
tasks.add(managedPeerColorUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
tasks.add(managedStarGiftsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
tasks.add(managedSavedMusicIdsUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
tasks.add(managedEmojiGameUpdates(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
self.managedTopReactionsDisposable.set(managedTopReactions(postbox: self.stateManager.postbox, network: self.stateManager.network).start())
@@ -2173,7 +2173,7 @@ public final class AccountViewTracker {
fixedCombinedReadStates: .peer([peerId: CombinedPeerReadState(states: [
(Namespaces.Message.Cloud, PeerReadState.idBased(maxIncomingReadId: Int32.max - 1, maxOutgoingReadId: Int32.max - 1, maxKnownId: Int32.max - 1, count: 0, markedUnread: false))
])]),
topTaggedMessageIdNamespaces: [],
topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud],
tag: tag,
appendMessagesFromTheSameGroup: false,
namespaces: .not(Namespaces.Message.allNonRegular),
@@ -2201,7 +2201,7 @@ public final class AccountViewTracker {
count: count,
trackHoles: trackHoles,
fixedCombinedReadStates: nil,
topTaggedMessageIdNamespaces: [],
topTaggedMessageIdNamespaces: [Namespaces.Message.Cloud],
tag: tag,
appendMessagesFromTheSameGroup: false,
namespaces: .not(Namespaces.Message.allNonRegular),
@@ -104,7 +104,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
var updatedTimestamp: Int32?
if let apiMessage = apiMessage {
switch apiMessage {
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
updatedTimestamp = date
case .messageEmpty:
break
@@ -400,7 +400,7 @@ func applyUpdateGroupMessages(postbox: Postbox, stateManager: AccountStateManage
} else if let message = messages.first, let apiMessage = result.messages.first {
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
namespace = Namespaces.Message.ScheduledCloud
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
namespace = Namespaces.Message.ScheduledCloud
}
}
@@ -43,8 +43,18 @@ private final class AccountPresenceManagerImpl {
}
private func updatePresence(_ isOnline: Bool) {
// GHOST MODE: Completely block status updates to freeze "last seen" time
if GhostModeManager.shared.shouldHideOnlineStatus {
self.onlineTimer?.invalidate()
self.onlineTimer = nil
return
}
// ALWAYS ONLINE: Force online status when enabled
let effectiveOnline = MiscSettingsManager.shared.shouldAlwaysBeOnline ? true : isOnline
let request: Signal<Api.Bool, MTRpcError>
if isOnline {
if effectiveOnline {
let timer = SignalKitTimer(timeout: 30.0, repeat: false, completion: { [weak self] in
guard let strongSelf = self else {
return
@@ -479,6 +479,26 @@ private func _internal_clearHistory(transaction: Transaction, postbox: Postbox,
|> `catch` { _ -> Signal<Void, NoError> in
return .complete()
}
} else if let threadId = operation.threadId {
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
return network.request(Api.functions.messages.deleteTopicHistory(peer: inputPeer, topMsgId: Int32(clamping: threadId)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.AffectedHistory?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
if let result = result {
switch result {
case let .affectedHistory(pts, ptsCount, _):
stateManager.addUpdateGroups([.updatePts(pts: pts, ptsCount: ptsCount)])
return .complete()
}
} else {
return .complete()
}
}
} else {
return requestClearHistory(postbox: postbox, network: network, stateManager: stateManager, inputPeer: inputPeer, maxId: operation.topMessageId.id, justClear: true, minTimestamp: operation.minTimestamp, maxTimestamp: operation.maxTimestamp, type: operation.type)
}
@@ -0,0 +1,104 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum EmojiGameInfo: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case type
case info
}
public struct Info: Codable, Equatable {
public let gameHash: String
public let previousStake: Int64
public let currentStreak: Int32
public let parameters: [Int32]
public let playsLeft: Int32?
}
case available(Info)
case unavailable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(Int32.self, forKey: .type)
switch type {
case 1:
self = .available(try container.decode(Info.self, forKey: .info))
default:
self = .unavailable
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .available(info):
try container.encode(Int32(1), forKey: .type)
try container.encode(info, forKey: .info)
case .unavailable:
try container.encode(Int32(0), forKey: .type)
}
}
}
extension EmojiGameInfo {
init(apiEmojiGameInfo: Api.messages.EmojiGameInfo) {
switch apiEmojiGameInfo {
case let .emojiGameDiceInfo(_, gameHash, prevStake, currentStreak, params, playsLeft):
self = .available(Info(gameHash: gameHash, previousStake: prevStake, currentStreak: currentStreak, parameters: params, playsLeft: playsLeft))
case .emojiGameUnavailable:
self = .unavailable
}
}
}
public func currentEmojiGameInfo(transaction: Transaction) -> EmojiGameInfo {
if let entry = transaction.getPreferencesEntry(key: PreferencesKeys.emojiGameInfo())?.get(EmojiGameInfo.self) {
return entry
} else {
return .unavailable
}
}
func updateEmojiGameInfo(transaction: Transaction, _ f: (EmojiGameInfo) -> EmojiGameInfo) {
let current = currentEmojiGameInfo(transaction: transaction)
let updated = f(current)
if updated != current {
transaction.setPreferencesEntry(key: PreferencesKeys.emojiGameInfo(), value: PreferencesEntry(updated))
}
}
func updateEmojiGameInfoOnce(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
return network.request(Api.functions.messages.getEmojiGameInfo())
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.EmojiGameInfo?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<Void, NoError> in
guard let result else {
return .complete()
}
return postbox.transaction { transaction -> Void in
let info = EmojiGameInfo(apiEmojiGameInfo: result)
updateEmojiGameInfo(transaction: transaction) { _ in
return info
}
}
}
}
func managedEmojiGameUpdates(postbox: Postbox, network: Network) -> Signal<Void, NoError> {
let poll = Signal<Void, NoError> { subscriber in
return updateEmojiGameInfoOnce(postbox: postbox, network: network).start(completed: {
subscriber.putCompletion()
})
}
return (poll |> then(.complete() |> suspendAwareDelay(1.0 * 60.0 * 60.0, queue: Queue.concurrentDefaultQueue()))) |> restart
}
@@ -142,6 +142,11 @@ private func actionFromActivity(_ activity: PeerInputActivity?) -> Api.SendMessa
}
private func requestActivity(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadId: Int64?, activity: PeerInputActivity?) -> Signal<Void, NoError> {
// GHOST MODE: Block typing indicator
if GhostModeManager.shared.shouldHideTypingIndicator {
return .complete()
}
return postbox.transaction { transaction -> Signal<Void, NoError> in
if let peer = transaction.getPeer(peerId) {
if peerId == accountPeerId {
@@ -119,6 +119,11 @@ func managedSynchronizeViewStoriesOperations(postbox: Postbox, network: Network,
}
private func pushStoriesAreSeen(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: SynchronizeViewStoriesOperation) -> Signal<Void, NoError> {
// GHOST MODE: Don't send story view notifications
if GhostModeManager.shared.shouldHideStoryViews {
return .complete()
}
guard let inputPeer = apiInputPeer(peer) else {
return .complete()
}
@@ -2076,7 +2076,7 @@ public final class PendingMessageManager {
if message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
isScheduled = true
}
if case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage {
if case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage {
if (flags2 & (1 << 4)) != 0 {
isScheduled = true
}
@@ -2120,7 +2120,7 @@ public final class PendingMessageManager {
namespace = Namespaces.Message.QuickReplyCloud
} else if let apiMessage = result.messages.first, message.scheduleTime != nil && message.scheduleTime == apiMessage.timestamp {
namespace = Namespaces.Message.ScheduledCloud
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
} else if let apiMessage = result.messages.first, case let .message(_, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) = apiMessage, (flags2 & (1 << 4)) != 0 {
namespace = Namespaces.Message.ScheduledCloud
}
}
@@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 220
return 221
}
public func parseMessage(_ data: Data!) -> Any! {
@@ -219,6 +219,11 @@ private func validatePeerReadState(network: Network, postbox: Postbox, stateMana
}
private func pushPeerReadState(network: Network, postbox: Postbox, stateManager: AccountStateManager, peerId: PeerId, readState: PeerReadState) -> Signal<PeerReadState, PeerReadStateValidationError> {
// GHOST MODE: Block read receipts (blue checkmarks) for non-secret chats
if peerId.namespace != Namespaces.Peer.SecretChat && GhostModeManager.shared.shouldHideReadReceipts {
return .single(readState)
}
if peerId.namespace == Namespaces.Peer.SecretChat {
return inputSecretChat(postbox: postbox, peerId: peerId)
|> mapToSignal { inputPeer -> Signal<PeerReadState, PeerReadStateValidationError> in
@@ -58,7 +58,7 @@ class UpdateMessageService: NSObject, MTMessageService {
self.putNext(groups)
}
case let .updateShortChatMessage(flags, id, fromId, chatId, message, pts, ptsCount, date, fwdFrom, viaBotId, replyHeader, entities, ttlPeriod):
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: .peerUser(userId: fromId), fromBoostsApplied: nil, peerId: Api.Peer.peerChat(chatId: chatId), savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
@@ -74,7 +74,7 @@ class UpdateMessageService: NSObject, MTMessageService {
let generatedPeerId = Api.Peer.peerUser(userId: userId)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil)
let generatedMessage = Api.Message.message(flags: flags, flags2: 0, id: id, fromId: generatedFromId, fromBoostsApplied: nil, peerId: generatedPeerId, savedPeerId: nil, fwdFrom: fwdFrom, viaBotId: viaBotId, viaBusinessBotId: nil, replyTo: replyHeader, date: date, message: message, media: Api.MessageMedia.messageMediaEmpty, replyMarkup: nil, entities: entities, views: nil, forwards: nil, replies: nil, editDate: nil, postAuthor: nil, groupedId: nil, reactions: nil, restrictionReason: nil, ttlPeriod: ttlPeriod, quickReplyShortcutId: nil, effect: nil, factcheck: nil, reportDeliveryUntilDate: nil, paidMessageStars: nil, suggestedPost: nil, scheduleRepeatPeriod: nil, summaryFromLanguage: nil)
let update = Api.Update.updateNewMessage(message: generatedMessage, pts: pts, ptsCount: ptsCount)
let groups = groupUpdates([update], users: [], chats: [], date: date, seqRange: nil)
if groups.count != 0 {
@@ -104,7 +104,7 @@ extension Api.MessageMedia {
extension Api.Message {
var rawId: Int32 {
switch self {
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return id
case let .messageEmpty(_, id, _):
return id
@@ -115,7 +115,7 @@ extension Api.Message {
func id(namespace: MessageId.Namespace = Namespaces.Message.Cloud) -> MessageId? {
switch self {
case let .message(_, flags2, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, flags2, id, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
var namespace = namespace
if (flags2 & (1 << 4)) != 0 {
namespace = Namespaces.Message.ScheduledCloud
@@ -136,7 +136,7 @@ extension Api.Message {
var peerId: PeerId? {
switch self {
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, messagePeerId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
let peerId: PeerId = messagePeerId.peerId
return peerId
case let .messageEmpty(_, _, peerId):
@@ -149,7 +149,7 @@ extension Api.Message {
var timestamp: Int32? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, date, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return date
case let .messageService(_, _, _, _, _, _, date, _, _, _):
return date
@@ -160,7 +160,7 @@ extension Api.Message {
var preCachedResources: [(MediaResource, Data)]? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return media?.preCachedResources
default:
return nil
@@ -169,7 +169,7 @@ extension Api.Message {
var preCachedStories: [StoryId: Api.StoryItem]? {
switch self {
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .message(_, _, _, _, _, _, _, _, _, _, _, _, _, media, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return media?.preCachedStories
default:
return nil
@@ -32,9 +32,10 @@ public struct UserLimitsConfiguration: Equatable {
public static var defaultValue: UserLimitsConfiguration {
return UserLimitsConfiguration(
maxPinnedChatCount: 5,
maxPinnedSavedChatCount: 5,
maxArchivedPinnedChatCount: 100,
// GHOSTGRAM: Unlimited pinned chats bypass
maxPinnedChatCount: 99,
maxPinnedSavedChatCount: 99,
maxArchivedPinnedChatCount: 99,
maxChannelsCount: 500,
maxPublicLinksCount: 10,
maxSavedGifCount: 200,
@@ -145,9 +146,10 @@ extension UserLimitsConfiguration {
}
}
self.maxPinnedChatCount = getValue("dialogs_pinned_limit", orElse: defaultValue.maxPinnedChatCount)
self.maxPinnedSavedChatCount = getValue("saved_dialogs_pinned_limit", orElse: defaultValue.maxPinnedSavedChatCount)
self.maxArchivedPinnedChatCount = getValue("dialogs_folder_pinned_limit", orElse: defaultValue.maxArchivedPinnedChatCount)
// GHOSTGRAM: Force unlimited pinned chats (ignore server limits)
self.maxPinnedChatCount = 99
self.maxPinnedSavedChatCount = 99
self.maxArchivedPinnedChatCount = 99
self.maxChannelsCount = getValue("channels_limit", orElse: defaultValue.maxChannelsCount)
self.maxPublicLinksCount = getValue("channels_public_limit", orElse: defaultValue.maxPublicLinksCount)
self.maxSavedGifCount = getValue("saved_gifs_limit", orElse: defaultValue.maxSavedGifCount)