Squash of 63 commits spanning waves 46-93 (plus interspersed docs commits) of the gradual Postbox->TelegramEngine consumer-side migration. Scope: 139 files changed, 2123 insertions(+), 452 deletions(-). ## Themes by wave-block **Waves 46-58 — Peer field migrations + facade additions** Foundational EnginePeer convenience init additions (PeerReference, RenderedPeer, SelectivePrivacyPeer). Multiple `peer: Peer` field migrations across PeerInfo, ChatList, and SettingsUI components. **Waves 59-73 — peer field cascade + EnginePeer wrap drops** Series of single- to two-file peer-field migrations; consumer-side wrap removal (`EnginePeer(peer)` -> direct EnginePeer use); `as? TelegramUser` cast conversion to `case let .user(...)` enum match. Wave 64: RenderedPeer convenience init. Wave 68: SelectivePrivacyPeer convenience init. **Waves 74-83 — controller-Node bridge cleanup + small migrations** Wave-71 shadow-pattern cleanup at controller->Node bridges. Migrations of ChatRecentActionsController.peer (74), PeerInfoMember (75), MentionChatInputPanelItem (76), PassportUI SecureIdAuthController (77), AccountWithInfo + ShareController (78), peerInputActivitiesPromise (79), InactiveChannel (80), BlockedPeers (81), openHashtag resolveSignal (82), NotificationExceptionsList (83). **Waves 84-90 — TelegramEngine.Resources facade migrations** Per-method Shape-A/B sweeps converting `<ctx>.account.postbox.mediaBox.X(...)` to `<ctx>.engine.resources.X(...)`. Wave 90 was a single-commit big sweep: 40 fetchedMediaResource sites in 25 files migrated to engine.resources.fetch facade in one atomic pass with first-pass-clean build. Methods covered: storeResourceData, completedResourcePath, cancelInteractiveResourceFetch, resourceRangesStatus, resourceStatus, fetch (fetchedMediaResource). **Waves 91-92 — additional type migrations** Wave 91: ItemListWebsiteItem.peer + RecentSessionsController enum-case payload + openWebSession callback Peer? -> EnginePeer?. Wave 92: ChatListController StateHolder.EntryContext status type MediaResourceStatus -> EngineMediaResource.FetchStatus. **Wave 93 — speculative `import Postbox` drop sweep** Drop import from 7 wave-touched files where it became unused; restore in 5 files where bare PeerId/Message/MediaId/StoryId references escaped the pre-flight regex. Includes one MediaId(...) -> EngineMedia.Id(...) swap in InAppPurchaseManager to unlock its import drop. ## Build state Final state at squash: clean Telegram/Telegram build at debug_sim_arm64. ## Persistent-state notes - Pre-existing WIP unchanged across the squashed range: - build-system/bazel-rules/sourcekit-bazel-bsp submodule marker - Untracked: build-system/tulsi/, submodules/TgVoip/, third-party/libx264/ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
CLAUDE.md
This file provides guidance to AI assistants when working with code in this repository.
Build
The app is built using Bazel via the Make.py wrapper. There is no selective per-module build — the only supported invocation builds the full Telegram/Telegram target.
Command:
python3 build-system/Make/Make.py --overrideXcodeVersion \
--cacheDir ~/telegram-bazel-cache \
build \
--configurationPath build-system/appstore-configuration.json \
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
--gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64
Add --continueOnError after build (forwards to bazel's --keep_going) when verifying changes that may surface errors in many files at once — it lets the full set of errors land in one pass instead of stopping at the first failing target.
The build needs TELEGRAM_CODESIGNING_GIT_PASSWORD in the environment. It is set in ~/.zshrc but Claude Code's bash tool does NOT source shell config by default. Prefix build commands with source ~/.zshrc 2>/dev/null; to pick it up.
Code Style Guidelines
- Naming: PascalCase for types, camelCase for variables/methods
- Imports: Group and sort imports at the top of files
- Error Handling: Properly handle errors with appropriate redaction of sensitive data
- Formatting: Use standard Swift/Objective-C formatting and spacing
- Types: Prefer strong typing and explicit type annotations where needed
- Documentation: Document public APIs with comments
Project Structure
- Core launch and application extensions code is in
Telegram/directory - Most code is organized into libraries in
submodules/ - External code is located in
third-party/ - No tests are used at the moment
Postbox → TelegramEngine refactor (in progress)
A gradual migration is underway to eliminate direct import Postbox from consumer submodules in favor of TelegramEngine.
Historical record: Wave-by-wave outcomes, the running tally of Postbox-free modules, and full verbose forms of the guidance subsections below live in docs/superpowers/postbox-refactor-log.md. Read that file when you need wave-specific context, a full worked example of a pattern, or the history of a particular module's migration.
Waves landed so far (as of 2026-04-24): 45 waves plus standalone cleanups. See the log file for per-wave detail; the list of still-open migration opportunities lives in the project_postbox_refactor_next_wave.md memory file.
Rules that apply to every wave
TelegramCoredoes not@_exported import Postbox. Once a consumer dropsimport Postbox, every remaining Postbox-type reference must use an engine-typealiased equivalent.- Never typealias
Postbox,Account, orMediaBox. These umbrella types rename without encapsulating. Narrow utility typealiases (MemoryBuffer,PostboxDecoder,PostboxEncoder,AdaptedPostboxDecoder,MediaResource, …) remain allowed and expected. - No new engine wrapper structs unless the wave's spec explicitly allows — only typealiases and thin forwarding methods.
- Discovery first: before adding any new engine wrapper/typealias, grep
submodules/TelegramCore/Sources/TelegramEngine/for existing equivalents. Record the search result in the commit message. - Abandonment protocol: if a module can only be refactored by violating rule 2 or by editing a module outside the current wave's list, mark the task Abandoned with a recorded reason. Do NOT substitute a new module mid-wave.
- Full project build per module. No unit tests exist in this project.
- TelegramCore never imports UIKit/Display.
TelegramCoreis shared with the Telegram-Mac codebase; its Bazeldepsand source files must not reference UIKit, Display, or any Apple-UI framework. UIKit-needing helpers (image scaling, rendering, etc.) stay in consumer-side submodules.
Engine typealias cheat sheet (existing aliases)
PeerId → EnginePeer.Id
MessageId → EngineMessage.Id
MessageIndex → EngineMessage.Index
MessageTags → EngineMessage.Tags
MessageAttribute → EngineMessage.Attribute
MessageFlags → EngineMessage.Flags
MessageForwardInfo → EngineMessage.ForwardInfo
MediaId → EngineMedia.Id
PreferencesEntry → EnginePreferencesEntry
TempBox → EngineTempBox
PinnedItemId → EngineChatList.PinnedItem.Id
MemoryBuffer → EngineMemoryBuffer (added 2026-04)
PostboxDecoder → EnginePostboxDecoder (added 2026-04)
PostboxEncoder → EnginePostboxEncoder (added 2026-04)
AdaptedPostboxDecoder → EngineAdaptedPostboxDecoder (added 2026-04)
ItemCollectionId → EngineItemCollectionId (added 2026-04-20)
FetchResourceSourceType → EngineFetchResourceSourceType (added 2026-04-20)
FetchResourceError → EngineFetchResourceError (added 2026-04-20)
For the MediaResource Postbox protocol, prefer the TelegramCore subtype TelegramMediaResource when the consumer's usage allows (note: EngineMediaResource is a wrapper class, not a typealias, so it is not interchangeable with the protocol).
MediaResource → EngineMediaResource consumer migration
EngineMediaResource is a final class in TelegramCore wrapping a MediaResource value. Unlike the typealiases above it is not interchangeable with the protocol, but it does provide wrap/unwrap helpers:
EngineMediaResource(rawResource)— wrap a rawMediaResource.engineResource._asResource()— unwrap to the rawMediaResource.EngineMediaResource.ResourceData(rawResourceData)— wrapMediaResourceData.EngineMediaResource.Id(rawMediaResourceId)— wrapMediaResourceId.
Pattern for facade functions: when a TelegramEngine.<Area> method leaks raw MediaResource in its public signature, change the facade signature in place to EngineMediaResource (and change any closure parameter types the same way). Bridge inside the facade body by calling the existing _internal_* function with engineResource._asResource() / wrapping raw inputs from inner closures with EngineMediaResource(rawResource). Update all call sites in the same commit. The _internal_* function stays on raw MediaResource — it is the Postbox-facing layer.
Do not add opt-in EngineMediaResource overloads alongside raw-MediaResource overloads. Duplicate signatures fragment the public API and leave the leak in place forever.
For consumer modules, prefer EngineMediaResource as the type in properties, locals, generic arguments and function parameters when the usage is a pure type reference. Do not try to use EngineMediaResource where a class must conform to TelegramMediaResource (Postbox protocol) or override isEqual(to: MediaResource) — those remain import Postbox.
Wave-selection guidance
Distilled lessons from waves 1–26. Each bullet below has a full-form counterpart in postbox-refactor-log.md (same subsection heading) with backstory, example scripts, and per-wave numbers.
Shape selection. The "leaf module, drop Postbox in isolation" approach (wave 1) only works when the candidate's public API doesn't leak Postbox domain types. Most candidates DO leak (postbox: Postbox / account: Account in public inits, Media/Message as public parameter types). Grep each candidate for :\s*Postbox\b, :\s*Account\b, :\s*MediaBox\b, and Media/Message as public parameter types before committing to a wave; abandon candidates whose public API leaks.
Inventory at execution time, not just planning time. Planning-time grep often undercounts. Re-inventory at Task-1 time using the full token set \b(postbox|mediaBox|transaction|PostboxView|combinedView|MediaResource|PostboxDecoder|PostboxEncoder|MemoryBuffer)\b|^import Postbox over the module's sources. If the count exceeds the plan, abandon before editing code rather than substituting a different module.
Two feasible wave shapes. Shape 1 = "per-module Postbox drop" (fragile; wave 1 lost 6 of 10 candidates). Shape 2 = "per-engine-facade-API migrate in place, update all call sites in one commit" (validated from wave 2 onward). Prefer shape 2 when the target is an API surface that multiple consumer modules depend on.
Enum-payload migrations need full case-site grep. When changing the payload type of a public enum, grep case \. / let \. / \.<caseName>\( across the enum's defining module — not just call sites of the facade that returns it. Wave 4 undercounted by 6 sites (shortcut constructions and destructures inside the same file as the facade) because the inventory only grepped facade callers.
Unused-import sweeps (wave-shape applied in waves 6, 14). Speculatively drop ^import Postbox$ from every candidate file, build with --continueOnError, extract failing files and restore their imports, iterate. After a few iterations, do pattern-based preemptive restores for files naming Postbox-only symbols (MediaBox, PostboxCoding, PostboxDecoder, PostboxEncoder, TempBoxFile, ValueBoxKey, Postbox\b, PeerId, MessageId, MediaId, MessageIndex, MessageAndThreadId, PeerNameIndex). Scope never leaves the consumer-module candidate set — halt if errors surface in TelegramCore / Postbox / TelegramApi. Run a matching BUILD-dep sweep immediately after (near-zero execution risk). Full methodology, scripts, and iteration-count history in the log.
Public-Postbox-type inventory (wave-11-pattern planning). Grep candidate modules against the full Postbox public-types allowlist, not just the pattern's target tokens. Waves before 16 missed types like EngineMessageHistoryThread.Info (Postbox-defined despite its "Engine" prefix) and PeerStoryStats. "Engine"-prefixed types can still be Postbox-defined — grep for the defining module, don't trust naming. Build allowlist with grep -rhE "^public\s+(class|struct|enum|protocol|typealias)\s+\w+" submodules/Postbox/Sources/ | awk '{print $3}' | sed 's/[(:<].*//' | sort -u, then grep candidates against it. Full script in the log.
Wave-shape G: facade addition + consumer sweep in one commit (validated across waves 19–26). Recipe:
- Target a
MediaBoxmethod whose Postbox signature uses clean leaf types (MediaResourceId,Data,String,Bool) and whose return type is either non-Postbox or has an existingEngine*wrapper. - Pre-flight inventory: classify each call site as Shape A (
context.account.postbox.mediaBox.X(...), migratable), Shape B (different overload viaAccountContext, migratable), Shape C (rawaccount: Accountlocal, skip — needs per-module rework), Shape D (self.postboxstored field, skip). Also check foraccountManager.mediaBox.X(...)— a separate migration path. - Design facade with
EngineMediaResource.IdorEngineMediaResourceparameters and engine-or-clean return types; preserve default argument values. - WIP-interference check:
git status --short | grep -v "^??"— if any Shape-A site is in a WIP file, either skip those sites or wait. - Name-collision check: if the facade signature names a Swift stdlib type with availability restrictions (
RangeSet, iOS 18+), verify the third-party module import is present inTelegramEngineResources.swift. - Batch duplicate call expressions with
replace_all=true. - Cheapness: 5–50 sites per wave, single atomic commit, expected first-pass-clean build. If post-migration grep for the migrated expression returns empty (excluding Shape C/D) and build is green, commit.
Full per-shape recipe and wave-specific examples in the log.
TelegramEngine.Resources facade inventory (as of wave 32)
All mediaBox methods with clean signatures (no Postbox-protocol leaks, no complex return-type migrations) have been migrated to TelegramEngine.Resources. Quick reference for consumers — all of these live in submodules/TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:
| Facade | Wave | Wraps |
|---|---|---|
fetch(reference:userLocation:userContentType:) |
3 | fetchedMediaResource |
status(resource:) |
3 | MediaBox.resourceStatus (resource-based) |
status(id:, resourceSize:) |
32 | MediaBox.resourceStatus(_ id:, resourceSize:) |
data(resource:, pathExtension:, waitUntilFetchStatus:) |
3 | MediaBox.resourceData (resource-based) |
data(id:, attemptSynchronously:) |
3 | MediaBox.resourceData (id-based, defaults to .complete(waitUntilFetchStatus: false)) |
custom(id:, fetch:, cacheTimeout:, attemptSynchronously:) |
pre-wave-21 | MediaBox.customResourceData |
httpData(url:, preserveExactUrl:) |
pre-wave-21 | fetchHttpResource |
shortLivedResourceCachePathPrefix(id:) |
19 | MediaBox.shortLivedResourceCachePathPrefix |
completedResourcePath(id:, pathExtension:) |
21 | MediaBox.completedResourcePath(id:, pathExtension:) |
storeResourceData(id:, data:, synchronous:) |
22 | MediaBox.storeResourceData(_ id:, data:, synchronous:) |
cancelInteractiveResourceFetch(id:) |
23 | MediaBox.cancelInteractiveResourceFetch(resourceId:) |
moveResourceData(id:, toTempPath:) |
24 | MediaBox.moveResourceData(_ id:, toTempPath:) |
moveResourceData(from:, to:, synchronous:) |
24 | MediaBox.moveResourceData(from:, to:, synchronous:) |
copyResourceData(id:, fromTempPath:) |
25 | MediaBox.copyResourceData(_ id:, fromTempPath:) |
copyResourceData(from:, to:, synchronous:) |
25 | MediaBox.copyResourceData(from:, to:, synchronous:) |
resourceRangesStatus(resource:) |
26 | MediaBox.resourceRangesStatus(_ resource:) |
removeCachedResources(ids:, force:, notify:) |
26 | MediaBox.removeCachedResources(_ ids:, force:, notify:) |
Facade-shape convention: all of these take EngineMediaResource.Id or EngineMediaResource (never raw MediaResourceId/MediaResource). Return types either don't leak Postbox (Void, String, String?, Signal<RangeSet<Int64>, NoError>, Signal<Float, NoError>) or wrap via TelegramCore type (Signal<EngineMediaResource.ResourceData, NoError>).
Swift-stdlib-vs-third-party-module name collisions (learned in wave 26): RangeSet<Int64> collides with Swift stdlib's RangeSet (iOS 18+ only). Fix: import RangeSet at the file top of any TelegramCore file that names RangeSet in a signature. TelegramCore/BUILD already depends on //submodules/Utils/RangeSet:RangeSet. Future facade additions in TelegramEngineResources.swift should re-check this if new signature types are introduced.