Telegram-iOS/CLAUDE.md
Isaac 4ae15b42a7 Postbox -> TelegramEngine wave 3: MediaBox fetch/status/data facades + SaveToCameraRoll
Adds three thin forwarding methods on TelegramEngine.Resources
(fetch, status, data) over MediaBox, then migrates SaveToCameraRoll's
three public functions to use them, drops import Postbox from the
module (source + Bazel dep), and updates all 23 call sites across 14
caller files atomically.

Bundled: spec + fix + plan + C1 facades + C2 SaveToCameraRoll rewrite
+ BUILD dep drop + CLAUDE.md outcome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:45:34 +02:00

12 KiB

CLAUDE.md

This file provides guidance to AI assistants when working with code in this repository.

Build

The app is built using Bazel.

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. Waves landed so far:

  • Wave 1 (2026-04-16): first leaf-module cohort — 4 done, 6 abandoned.
    • Spec: docs/superpowers/specs/2026-04-16-postbox-to-telegramengine-refactor-wave-1-design.md
    • Plan: docs/superpowers/plans/2026-04-16-postbox-to-telegramengine-refactor-wave-1.md
  • Wave 2 (2026-04-17): MediaResourceEngineMediaResource facade migration — 5 TelegramEngine facades migrated, 1 utility module fully de-Postboxed, 1 consumer signal type swapped, 1 task abandoned.
    • Plan: docs/superpowers/plans/2026-04-17-mediaresource-to-enginemediaresource-wave-2.md

Rules that apply to every wave

  1. TelegramCore does not @_exported import Postbox. Once a consumer drops import Postbox, every remaining Postbox-type reference must use an engine-typealiased equivalent.
  2. Never typealias Postbox, Account, or MediaBox. These umbrella types rename without encapsulating. Narrow utility typealiases (MemoryBuffer, PostboxDecoder, PostboxEncoder, AdaptedPostboxDecoder, MediaResource, …) remain allowed and expected.
  3. No new engine wrapper structs unless the wave's spec explicitly allows — only typealiases and thin forwarding methods.
  4. 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.
  5. 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.
  6. Full project build per module. No unit tests exist in this project.
  7. TelegramCore never imports UIKit/Display. TelegramCore is shared with the Telegram-Mac codebase; its Bazel deps and 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)

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 raw MediaResource.
  • engineResource._asResource() — unwrap to the raw MediaResource.
  • EngineMediaResource.ResourceData(rawResourceData) — wrap MediaResourceData.
  • EngineMediaResource.Id(rawMediaResourceId) — wrap MediaResourceId.

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 (learned from waves 1 and 2)

The "leaf module, drop Postbox in isolation" approach only works for modules whose public API doesn't leak Postbox domain types. Most candidate leaf modules DO leak such types (postbox: Postbox / account: Account in public inits, Media/Message in public function parameters). Those modules need paired caller-migration waves, not isolated refactors.

Before selecting a wave's module list, grep each candidate for:

  • :\s*Postbox\b, :\s*Account\b, :\s*MediaBox\b in public signatures → abandon candidate
  • Media/Message as public parameter types → likely needs paired wave with callers

Inventory at execution time, not just planning time. Wave 2's SaveToCameraRoll task was planned from a narrow grep that only matched MediaResource/TelegramMediaResource and missed three postbox: Postbox public-function leaks plus multiple postbox.mediaBox.* bodies. Planning-time inventory should grep the full set \b(postbox|mediaBox|transaction|PostboxView|combinedView|MediaResource|PostboxDecoder|PostboxEncoder|MemoryBuffer)\b|^import Postbox over the module's Sources, not just the tokens specific to that wave's goal. If the planning inventory under-counts, the executor should re-inventory at Task-1 time and abandon early before editing code.

Two feasible wave shapes. Wave 1 tried "per-module Postbox drop". Wave 2 tried "per-engine-facade-API migrate MediaResource to EngineMediaResource (modify in place, update all call sites in one commit)". The second shape worked well: narrow, clean commits, no abandonment cascade. Prefer it when the refactor target is an API surface that multiple consumer modules depend on.

Wave 1 outcome (2026-04-16)

4 modules done: ChatInterfaceState, ChatSendMessageActionUI, ContactListUI, DrawingUI. 6 modules abandoned with recorded reasons in the wave-1 plan: ActionSheetPeerItem, ChatListSearchRecentPeersNode, DirectMediaImageCache, FetchManagerImpl, GalleryData, ICloudResources.

Wave 2 outcome (2026-04-17)

5 TelegramEngine facades migrated to EngineMediaResource (signatures changed in place; _internal_* Postbox layer unchanged):

  • TelegramEngine.Peers.uploadedPeerPhoto, uploadedPeerVideo, updatePeerPhoto
  • TelegramEngine.AccountData.updateAccountPhoto, updateFallbackPhoto
  • TelegramEngine.Contacts.updateContactPhoto
  • TelegramEngine.Auth.uploadedPeerVideo

1 consumer submodule fully de-Postboxed: MapResourceToAvatarSizes (signature changed from (postbox: Postbox, resource: MediaResource, …) to (engine: TelegramEngine, resource: EngineMediaResource, …); 27 call sites migrated).

1 consumer signal type swapped: AuthorizationUI/AuthorizationSequenceController.swift (Signal<TelegramMediaResource?>Signal<EngineMediaResource?>).

1 task abandoned with recorded reason in the wave-2 plan: SaveToCameraRoll (full-module Postbox coupling, needs its own wave).

Wave 3 outcome (2026-04-18)

3 thin forwarders added on TelegramEngine.Resources over MediaBox:

  • fetch(reference:userLocation:userContentType:)Signal<FetchResourceSourceType, FetchResourceError> (Postbox return types remain a documented accepted leak)
  • status(resource: EngineMediaResource)Signal<EngineMediaResource.FetchStatus, NoError>
  • data(resource: EngineMediaResource, pathExtension:, waitUntilFetchStatus:)Signal<EngineMediaResource.ResourceData, NoError> (takes a Bool rather than exposing ResourceDataRequestOption, per YAGNI)

1 consumer submodule fully de-Postboxed: SaveToCameraRoll. Public signatures changed from (context:, postbox: Postbox, userLocation:, …) to (context:, userLocation:, …); FetchMediaDataState.data payload changed from MediaResourceData to EngineMediaResource.ResourceData; internals rewired through context.engine.resources.*. 23 call sites across 14 files migrated atomically with the module.

Pre-flight verified that ShareController.swift:2406's self.currentContext.stateManager.postbox is equivalent to context.account.postbox in the ShareControllerAppAccountContext path (because AccountStateManager is constructed with the account's own postbox), so the postbox: argument could be dropped without behavior change.

No tasks abandoned. Shape validated: "per-engine-facade-API migration + full consumer module rewrite" (the wave-2 shape, scaled up to a full module drop).

Plan: docs/superpowers/plans/2026-04-18-postbox-to-telegramengine-wave-3.md

Modules currently free of import Postbox (running tally)

Consumer modules that no longer import Postbox, across all waves and standalone commits:

  • ChatInterfaceState (wave 1)
  • ChatSendMessageActionUI (wave 1)
  • ContactListUI (wave 1)
  • DrawingUI (wave 1)
  • StickerPeekUI (standalone cleanup, 2026-04-17 — import was unused)
  • PromptUI (standalone cleanup)
  • PresentationDataUtils (standalone cleanup)
  • MapResourceToAvatarSizes (wave 2)
  • SaveToCameraRoll (wave 3)

Known future-wave candidates

Surfaced by the wave-2 final review:

  • TelegramEngine.Stickers.uploadSticker(peer: Peer, resource: MediaResource, thumbnail: MediaResource?, …) — same MediaResource migration as wave 2, plus peer: Peer which would naturally migrate to EnginePeer at the same time. Self-contained to a small number of call sites.
  • submodules/TelegramCore/Sources/TelegramEngine/SecureId/UploadSecureIdFile.swift: public func uploadSecureIdFile(…, postbox: Postbox, …, resource: MediaResource) — rule-2-sensitive (umbrella-type leak). Needs a paired wave with its caller(s).
  • Classes conforming to TelegramMediaResource (need isEqual(to: MediaResource) override) remain permanently blocked from consumer-side migration: ICloudFileResource, InstantPageExternalMediaResource, VideoLibraryMediaResource, YoutubeEmbedStoryboardMediaResource. Either move the class into TelegramCore or keep import Postbox in its module.

Build environment quirk

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.