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>
18 KiB
Wave 50: enclosingPeer Peer? → EnginePeer? Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Migrate the PeerInfo members chain's enclosingPeer field from raw Postbox Peer? to EnginePeer? (wave 50 of the Postbox → TelegramEngine refactor).
Architecture: Cross-file private struct-field migration with stored-form ratchet. Edits stay inside submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/. Replaces as? TelegramChannel / as? TelegramGroup casts with case let .channel(...) / case let .legacyGroup(...) (wave-41/45 idiom), drops is TelegramChannel checks for case .channel = ... (wave-41 always-false-warning fix), and removes 5 internal _asPeer() / EnginePeer(...) / flatMap(EnginePeer.init) bridges. The engine.data subscription at PIMP:354 already returns EnginePeer? — this wave closes the demote-then-promote ratchet.
Tech Stack: Swift, Bazel via Make.py, no unit tests (per CLAUDE.md). Verification is the full-project debug-sim-arm64 build with --continueOnError.
Iteration budget: 1–2 (target first-pass-clean; recent first-pass-clean streak: waves 42, 43*, 45, 46, 48, 49 — *wave 43 took 2 iterations).
Note on TDD: This project has no unit tests (CLAUDE.md "No tests are used at the moment"). The standard TDD test-first cycle in the skill template does not apply. Each task instead writes the edits, then verifies via Bazel build + residue grep.
File Structure
| File | Role | Changes |
|---|---|---|
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift (PSMI) |
List-item view-model + node | Type-change stored field + init param; 4 cast/is-check rewrites; 1 flatMap(EnginePeer.init) simplification |
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift (PIMP) |
Members-pane node + helpers | 3 func sigs + 1 stored field type-change; 4 cast/is-check rewrites; 1 EnginePeer(...) wrap drop; 2 _asPeer() drops |
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift (PSPB) |
Profile-items builder (non-settings members section) | 1 boundary _asPeer() drop at the call site that constructs the migrated init |
No public-API ripple — PeerInfoScreenMemberItem and PeerInfoMembersPaneNode are local to the PeerInfoScreen module.
Task 1: PSMI.swift — type changes + cast/is-check rewrites + flatMap simplification
Files:
- Modify:
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift
Edits in this task: 7 (1 stored-field type, 1 init-param type, 2 cast→case-let, 2 is→case, 1 flatMap simplification).
- Step 1: Change stored field type at line 23
Find:
let enclosingPeer: Peer?
Replace with:
let enclosingPeer: EnginePeer?
- Step 2: Change init parameter type at line 34
Find:
enclosingPeer: Peer?,
Replace with:
enclosingPeer: EnginePeer?,
- Step 3: Rewrite cast at line 152 (TelegramChannel)
Find:
if let channel = item.enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) {
Replace with:
if case let .channel(channel) = item.enclosingPeer, channel.hasPermission(.editRank) {
- Step 4: Rewrite cast at line 154 (TelegramGroup)
Find:
} else if let group = item.enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) {
Replace with:
} else if case let .legacyGroup(group) = item.enclosingPeer, !group.hasBannedPermission(.banEditRank) {
- Step 5: Simplify flatMap at line 178
Find:
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer.flatMap(EnginePeer.init), member: item.member)
Replace with:
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member)
- Step 6: Rewrite is-check at line 181
Find:
if actions.contains(.promote) && item.enclosingPeer is TelegramChannel {
Replace with:
if actions.contains(.promote), case .channel = item.enclosingPeer {
- Step 7: Rewrite is-check at line 187
Find:
if item.enclosingPeer is TelegramChannel {
Replace with:
if case .channel = item.enclosingPeer {
Task 2: PIMP.swift — signatures + stored field + body rewrites + demotion drops
Files:
- Modify:
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift
Edits in this task: 11 (3 func sigs + 1 stored-field type + 4 cast/is rewrites + 1 EnginePeer wrap drop + 2 _asPeer() drops).
- Step 1: Change
func item(...)signature at line 92
Find:
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem {
Replace with:
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem {
- Step 2: Rewrite cast at line 113 (TelegramChannel, non-optional context)
Find:
if let channel = enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) {
Replace with:
if case let .channel(channel) = enclosingPeer, channel.hasPermission(.editRank) {
- Step 3: Rewrite cast at line 115 (TelegramGroup, non-optional context)
Find:
} else if let group = enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) {
Replace with:
} else if case let .legacyGroup(group) = enclosingPeer, !group.hasBannedPermission(.banEditRank) {
- Step 4: Drop the
EnginePeer(...)wrap at line 139
Find:
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: EnginePeer(enclosingPeer), member: member)
Replace with:
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member)
availableActionsForMemberOfPeer takes peer: EnginePeer? (PeerInfoData.swift:2314); Swift auto-wraps the non-optional enclosingPeer: EnginePeer to optional.
- Step 5: Rewrite is-check at line 142 (non-optional context)
Find:
if actions.contains(.promote) && enclosingPeer is TelegramChannel {
Replace with:
if actions.contains(.promote), case .channel = enclosingPeer {
- Step 6: Rewrite is-check at line 148 (non-optional context)
Find:
if enclosingPeer is TelegramChannel {
Replace with:
if case .channel = enclosingPeer {
- Step 7: Change
preparedTransitionsignature at line 271
Find:
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction {
Replace with:
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction {
- Step 8: Change stored field type at line 293
Find:
private var enclosingPeer: Peer?
Replace with:
private var enclosingPeer: EnginePeer?
- Step 9: Drop
_asPeer()at line 361
Find:
strongSelf.enclosingPeer = enclosingPeer._asPeer()
Replace with:
strongSelf.enclosingPeer = enclosingPeer
- Step 10: Drop
_asPeer()at line 363
Find:
strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), state: state, presentationData: presentationData)
Replace with:
strongSelf.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData)
- Step 11: Change
updateStatesignature at line 442
Find:
private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) {
Replace with:
private func updateState(enclosingPeer: EnginePeer, state: PeerInfoMembersState, presentationData: PresentationData) {
The pass-through call sites at PIMP:275, :276, :437, :438, :451, :485 require no edit — types flow through transparently.
Task 3: PSPB.swift — boundary lift at members-section call site
Files:
- Modify:
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift
Edits in this task: 1.
- Step 1: Drop
_asPeer()at line 852
Find:
items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer._asPeer(), member: member, isAccount: false, action: isAccountPeer ? { _ in
Replace with:
items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer, member: member, isAccount: false, action: isAccountPeer ? { _ in
peer here is the closure-bound EnginePeer from the data.peer source pipeline (PeerInfoScreenData.peer: EnginePeer? post-wave-42, unwrapped to non-optional EnginePeer and being passed to a now-EnginePeer? param — auto-promotes to optional).
The other PeerInfoScreenMemberItem(...) construction at PeerInfoSettingsItems.swift:132 passes enclosingPeer: nil, which is valid for either optional type — no edit.
Task 4: Full-project Bazel build
Files: none (verification only).
- Step 1: Run the build with
--continueOnError
Run:
source ~/.zshrc 2>/dev/null; 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 --continueOnError
Expected: clean build (bazel build complete or equivalent green output).
- Step 2: If build fails, triage iteration
If errors land in PeerInfoScreenMemberItem.swift or PeerInfoMembersPane.swift or PeerInfoProfileItems.swift:
- Read the failing line.
- Common failure modes from prior waves:
- Always-false
iswarning under-warnings-as-errors: leftoveris TelegramXnot converted in step. Re-grepenclosingPeer is Telegramover the 3 files. - Always-failing
as?cast warning: leftoveras? TelegramXnot converted. Re-grepenclosingPeer.*as\?. - Type mismatch on closure-capture alias: a
strongSelf.enclosingPeerorself.enclosingPeersite missed a_asPeer()drop. Re-grepenclosingPeer\._asPeer\|EnginePeer\(enclosingPeer. - Unused variable warning: a binding from
case let .channel(channel)not actually used. Re-read the body.
- Always-false
Fix in place and re-run step 1. Budget: 2 iterations.
If errors land outside those 3 files: STOP. The wave was supposed to be self-contained. Re-read the spec, identify the missed call site, decide whether to add it or abandon the wave.
Task 5: Post-edit residue grep
Files: none (verification only).
- Step 1: Bridge residue grep
Run:
grep -rnE "enclosingPeer\._asPeer|EnginePeer\(enclosingPeer\)|enclosingPeer\.flatMap\(EnginePeer" \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/
Expected: empty output.
- Step 2: Cast/is-check residue grep
Run:
grep -rnE "enclosingPeer.*as\? TelegramChannel|enclosingPeer.*as\? TelegramGroup|enclosingPeer is TelegramChannel" \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/
Expected: empty output.
- Step 3: Sanity check —
enclosingPeerreferences should now exclusively type-resolve to EnginePeer
Run:
grep -nE ": Peer\b" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift
Expected: no enclosingPeer: Peer or enclosingPeer: Peer? annotations remain. (Other : Peer annotations on unrelated symbols are fine.)
Task 6: Commit the wave
Files: none (git only).
- Step 1: Stage the 3 modified files
git add \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift
- Step 2: Confirm staging is clean
git status --short | grep -v "^??"
Expected output: only the 3 staged files (lines starting with M or A ). If other modified files appear, they predate the wave (per CLAUDE.md memory: build-system/bazel-rules/sourcekit-bazel-bsp submodule marker is pre-existing WIP).
- Step 3: Commit
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 50
Migrate enclosingPeer Peer? -> EnginePeer? across PeerInfoScreenMemberItem
+ PeerInfoMembersPaneNode + 1 PSPB call site. 19 edits / 3 files.
Drops 5 internal bridges: 2 _asPeer() demotions at PIMP:361/363, 1
EnginePeer(enclosingPeer) wrap at PIMP:139, 1 flatMap(EnginePeer.init)
at PSMI:178, 1 boundary _asPeer() lift at PSPB:852.
Closes the wave-48-pattern internal-demotion-and-external-re-promotion
ratchet at PIMP:354-363 (engine.data subscription returns EnginePeer?,
previously demoted to Peer? at storage).
All `as? TelegramChannel` / `as? TelegramGroup` casts converted to
`case let .channel(...)` / `case let .legacyGroup(...)` (wave-41/45
idiom). All `is TelegramChannel` checks converted to
`case .channel = ...` (wave-41 always-false-warning fix).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
- Step 4: Verify commit
git log --oneline -1
Expected: shows the wave 50 commit.
Task 7: Update outcome log + memory
Files:
-
Modify:
docs/superpowers/postbox-refactor-log.md -
Modify:
~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md -
Step 1: Append wave 50 outcome to refactor log
Add a "Wave 50 outcome" entry at the appropriate chronological position in docs/superpowers/postbox-refactor-log.md. Use the wave 49 outcome entry as the template. Include:
-
Commit hash (from Task 6 step 4).
-
Iteration count (1 if first-pass-clean; 2 if Task 4 step 2 fired once).
-
Net-bridge accounting: −5 internal bridges (2
_asPeer()+ 1EnginePeer(...)wrap + 1flatMap(EnginePeer.init)+ 1 boundary_asPeer()lift). 0 ADD wraps. 0 boundary lifts net new. -
Bazel build duration (from Task 4 step 1 output).
-
Any wave-specific lessons surfaced.
-
Step 2: Update wave-50-next-wave memory
Edit ~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md:
- Promote wave 50 outcome line into the "Latest commits" section using the format of the wave 49 entry.
- Update the top frontmatter
descriptionto reflect wave 50 landed and propose wave 51. - Promote the wave-51 candidate (
PeerInfoGroupsInCommonPaneNode.PeerEntry.peer: Peer → EnginePeer) to the top of the "Wave 51 candidates" section, replacing the now-stale "Wave 50 candidates" header. Re-run the broader grep if needed:
grep -rnE "^\s*(let|var|public let|public var|private let|private var) [a-zA-Z_]+: Peer\??$|^\s*(let|var|public let|public var|private let|private var) [a-zA-Z_]+: Peer\? = " \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ --include="*.swift" | grep -v "EnginePeer"
- Step 3: Commit the doc update
git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 50 outcome
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
(Memory file updates are not committed — they live outside the repo.)
Net delta projection (from spec)
| Category | Count | Sites |
|---|---|---|
| Internal bridge drops | −5 | PIMP:361, PIMP:363, PIMP:139, PSMI:178, PSPB:852 |
| Boundary lifts (net new) | 0 | source pipeline already EnginePeer? |
| ADD wraps | 0 | no Peer-only property accesses on bare enclosingPeer |
| Cast→case-let conversions | 4 | PSMI:152/154, PIMP:113/115 |
is→case conversions |
4 | PSMI:181/187, PIMP:142/148 |
| Type annotations updated | 6 | PSMI:23/34, PIMP:92/271/293/442 |
Total commit footprint: 19 line edits across 3 files, plus a docs commit for the outcome log.