Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 23 additions & 31 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions submodules/AccountContext/Sources/AccountContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,7 @@ public protocol SharedAccountContext: AnyObject {
var automaticMediaDownloadSettings: Signal<MediaAutoDownloadSettings, NoError> { get }
var currentAutodownloadSettings: Atomic<AutodownloadSettings> { get }
var immediateExperimentalUISettings: ExperimentalUISettings { get }
var immediateWataGramSettings: WataGramSettings { get }
var currentInAppNotificationSettings: Atomic<InAppNotificationSettings> { get }
var currentMediaInputSettings: Atomic<MediaInputSettings> { get }
var currentStickerSettings: Atomic<StickerSettings> { get }
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ enum PeerInfoSettingsSection {
case premiumManagement
case stars
case ton
case wataGram
}

enum PeerInfoReportType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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()
}
}
Expand Down
2 changes: 2 additions & 0 deletions submodules/TelegramUI/Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
return sharedContext.accountUserInterfaceInUse(id)
}, presentationData: {
return sharedContext.currentPresentationData.with({ $0 })
}, getGhostHideOnline: { [weak sharedContext] in
return sharedContext?.immediateWataGramSettings.ghostModeOnlineStatus ?? false
})
let sharedApplicationContext = SharedApplicationContext(sharedContext: sharedContext, notificationManager: notificationManager, wakeupManager: wakeupManager)
sharedApplicationContext.sharedContext.mediaManager.overlayMediaManager.attachOverlayMediaController(sharedApplicationContext.overlayMediaController)
Expand Down
17 changes: 11 additions & 6 deletions submodules/TelegramUI/Sources/ChatController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6289,13 +6289,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}

let activitySpace: PeerActivitySpace?
switch self.chatLocation {
case let .peer(peerId):
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .customChatContents:
if self.context.sharedContext.immediateWataGramSettings.ghostModeTypingIndicator {
// WataGram Ghost Mode: suppress typing/recording/sticker activity broadcasts
activitySpace = nil
} else {
switch self.chatLocation {
case let .peer(peerId):
activitySpace = PeerActivitySpace(peerId: peerId, category: .global)
case let .replyThread(replyThreadMessage):
activitySpace = PeerActivitySpace(peerId: replyThreadMessage.peerId, category: .thread(replyThreadMessage.threadId))
case .customChatContents:
activitySpace = nil
}
}

if let activitySpace = activitySpace {
Expand Down
Loading