diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md b/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md new file mode 100644 index 0000000000..68d5d0311b --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md @@ -0,0 +1,347 @@ +# Wave 103: ChatRecentActionsControllerNode peer 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 `ChatRecentActionsControllerNode`'s stored `peer: Peer` field to `EnginePeer`, dropping the `_asPeer()` boundary call at the single caller site (wave 103 of the Postbox → TelegramEngine refactor). + +**Architecture:** Wave-71-shadow close. Single-file private stored-form migration plus a 1-line caller drop. The caller (`ChatRecentActionsController`) already holds `peer: EnginePeer` and demotes once before passing into the node init. The wave drops the demotion and rewrites 3 `as? TelegramChannel` downcasts inside the node body to `case let .channel(...)` (wave-41/45 idiom). All scope is within `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/`. + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests (per `CLAUDE.md`). Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1 (target first-pass-clean given the 7-edit scope and validated pre-flight grep). + +**Note on TDD:** This project has no unit tests. The standard TDD test-first cycle does not apply. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift` (CRACN) | Recent-actions screen controller node | Drop `import Postbox`, retype stored field + init param, rewrite 3 `as? TelegramChannel` downcasts (6 edits) | +| `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift` (CRAC) | Recent-actions screen controller (caller) | Drop `_asPeer()` at the node init (1 edit) | + +No public-API ripple — `ChatRecentActionsControllerNode` is local to the module and has a single caller verified by grep. + +--- + +## Task 1: CRACN.swift — drop `import Postbox` + type changes + downcast rewrites + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift` + +**Edits in this task:** 6 (1 import drop, 1 stored-field retype, 1 init-param retype, 3 cast → case-let). + +- [ ] **Step 1: Drop `import Postbox` at line 5** + +Find: + +```swift +import Postbox +``` + +Replace with: (delete the line entirely) + +This file imports `TelegramCore` at line 4, which provides the `EnginePeer` type and the typealiases needed for the rest of this task. + +- [ ] **Step 2: Retype stored field at line 46** + +Find: + +```swift + private let peer: Peer +``` + +Replace with: + +```swift + private let peer: EnginePeer +``` + +- [ ] **Step 3: Retype init parameter at line 111** + +Find: + +```swift + init(context: AccountContext, controller: ChatRecentActionsController, peer: Peer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { +``` + +Replace with: + +```swift + init(context: AccountContext, controller: ChatRecentActionsController, peer: EnginePeer, presentationData: PresentationData, pushController: @escaping (ViewController) -> Void, presentController: @escaping (ViewController, PresentationContextType, Any?) -> Void, getNavigationController: @escaping () -> NavigationController?) { +``` + +- [ ] **Step 4: Rewrite downcast at line 899** + +Find: + +```swift + if let peer = strongSelf.peer as? TelegramChannel { +``` + +Replace with: + +```swift + if case let .channel(peer) = strongSelf.peer { +``` + +The bound name `peer` is preserved so the inner block (`switch peer.info { case .group: ... }`) ports verbatim. `case let .channel(peer)` binds `peer: TelegramChannel` directly (the associated value of `EnginePeer.channel`). + +- [ ] **Step 5: Rewrite downcast at line 948** + +Find: + +```swift + if let channel = self.peer as? TelegramChannel, case .broadcast = channel.info { +``` + +Replace with: + +```swift + if case let .channel(channel) = self.peer, case .broadcast = channel.info { +``` + +The compound condition (`, case .broadcast = channel.info`) ports verbatim because the bound `channel` is still `TelegramChannel`-typed. + +- [ ] **Step 6: Rewrite downcast at line 1088** + +Find: + +```swift + if let channel = self.peer as? TelegramChannel { +``` + +Replace with: + +```swift + if case let .channel(channel) = self.peer { +``` + +The inner block (`channel.hasPermission(.banMembers)`, `case .broadcast = channel.info`) ports verbatim. + +The `self.peer.id` accesses at lines 145, 161, 1138, 1490 require no edit — `EnginePeer.id` is a typealiased `PeerId`, identical at the call sites. + +--- + +## Task 2: CRAC.swift — drop boundary `_asPeer()` + +**Files:** +- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift` + +**Edits in this task:** 1. + +- [ ] **Step 1: Drop `_asPeer()` at line 277** + +Find: + +```swift + self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer._asPeer(), presentationData: self.presentationData, pushController: { [weak self] c in +``` + +Replace with: + +```swift + self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in +``` + +`ChatRecentActionsController.peer` is already declared `EnginePeer` at line 42 (`public init(context: AccountContext, peer: EnginePeer, ...)`) — the type carries through to the now-`EnginePeer`-typed init parameter. + +--- + +## Task 3: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build** + +Run: + +```sh +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 +``` + +Expected: clean build (`bazel build complete` or equivalent green output). No `--continueOnError` because the small scope makes the first error informative. + +Build cost projection: consumer-only, ~25s. If it exceeds ~60s, suspect a cascade leak. + +- [ ] **Step 2: If build fails, triage iteration** + +If errors land in `ChatRecentActionsControllerNode.swift` or `ChatRecentActionsController.swift`: +- Read the failing line. +- Common failure modes from prior waves: + - **Always-false `is` warning under `-warnings-as-errors`:** none expected here (pre-flight grep confirmed no `is TelegramChannel` checks on `self.peer`). If one surfaces anyway, convert to `case .channel = self.peer`. + - **Always-failing `as?` cast warning:** leftover `as? TelegramX` not converted in step 4/5/6. Re-grep `(self|strongSelf)\.peer as\?` over the file. + - **Type mismatch on closure-capture alias:** none expected here (pre-flight grep confirmed only `strongSelf.peer` and `self.peer` aliases, both ride the type change). + - **Type mismatch on `.id` access:** would indicate a regression in the `EnginePeer.Id` typealias — STOP and re-read CLAUDE.md, this is not a wave-103 issue. + - **Unused-variable warning under `-warnings-as-errors`:** a `case let .channel(peer)` binding not used inside the body. Re-read step 4/5/6 — if the inner block never references the bound name, switch to `case .channel = ...` and remove the binding. + +Fix in place and re-run step 1. Budget: 2 iterations. + +If errors land outside those 2 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 4: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Cast residue grep** + +Run: + +```sh +grep -nE "(self|strongSelf)\.peer as\? Telegram(Channel|Group|User)" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ +``` + +Expected: empty output. + +- [ ] **Step 2: Boundary `_asPeer()` residue grep** + +Run: + +```sh +grep -nE "self\.peer\._asPeer\(\)" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ +``` + +Expected: empty output. + +- [ ] **Step 3: `import Postbox` residue grep** + +Run: + +```sh +grep -rn "^import Postbox$" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ +``` + +Expected: empty output. The module is now Postbox-import-free. + +- [ ] **Step 4: Sanity check — `peer: Peer` annotations** + +Run: + +```sh +grep -nE "peer: Peer\b" \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift +``` + +Expected: empty output. (The 3 `as? TelegramChannel` downcasts on `self.peer` were the only sources; both `peer: Peer` annotations on stored field and init param are now `peer: EnginePeer`.) + +--- + +## Task 5: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 2 modified files** + +```sh +git add \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift \ + submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected output: only the 2 staged files (lines starting with `M `). 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** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 103 + +Migrate ChatRecentActionsControllerNode.peer Peer -> EnginePeer. +Closes the wave-71 shadow: caller already held EnginePeer and demoted +at the boundary. 7 edits / 2 files. + +Drops 1 boundary _asPeer() at ChatRecentActionsController:277, drops +import Postbox at ChatRecentActionsControllerNode:5, rewrites 3 +`as? TelegramChannel` downcasts to `case let .channel(...)` (wave-41/45 +idiom). + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 103 commit as HEAD. + +--- + +## Task 6: 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` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 103 outcome to refactor log** + +Append a "Wave 103 outcome" entry at the chronological end of `docs/superpowers/postbox-refactor-log.md`. Use the most recent wave-outcome entry as a structural template. Include: +- Commit hash (from Task 5 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired). +- Net-bridge accounting: −1 boundary `_asPeer()` (CRAC:277), −1 `import Postbox` (CRACN:5). 0 ADD wraps. 3 cast → case-let conversions (CRACN:899/948/1088). +- Bazel build duration (from Task 3 step 1 output). +- Wave-shape note: wave-71-shadow close, single-iter target validated. + +- [ ] **Step 2: Update next-wave memory** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`: +- Add the wave 103 outcome line into the recent-waves section (commit hash + 7-edit / 2-file / 1-iter summary). +- Remove the now-stale `ChatRecentActionsControllerNode.peer: Peer -> EnginePeer` candidate line (currently bullet 5 in the candidates list). +- Update the top frontmatter `description` to reflect wave 103 landed and propose wave 104. +- Promote the next candidate (likely one of: `cachedResourceRepresentation` foundational facade, `RenderedPeer` cascade kickoff, `SelectivePrivacyPeer` foundational, or another Shape-C/D mini-refactor) to the top of the candidates list. + +- [ ] **Step 3: Update MEMORY.md index** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md`: +- Update the `[Postbox refactor next wave]` line to mention wave 103 landed and shift the "Wave 103+ Shape-C/D candidates" framing forward to "Wave 104+ candidates". + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 103 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates at `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/` are not committed — they live outside the repo.) + +--- + +## Net delta projection (from spec) + +| Category | Count | Sites | +|---|---|---| +| Internal bridge drops | −1 | CRAC:277 (`_asPeer()`) | +| `import Postbox` drops | −1 | CRACN:5 | +| ADD wraps | 0 | no Peer-only property accesses on bare `self.peer` | +| Cast → case-let conversions | 3 | CRACN:899, CRACN:948, CRACN:1088 | +| Type annotations updated | 2 | CRACN:46 (stored field), CRACN:111 (init param) | +| Postbox-free module count | +1 | `Components/Chat/ChatRecentActionsController/` joins the list | + +**Total commit footprint:** 7 line edits across 2 files, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md b/docs/superpowers/plans/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md new file mode 100644 index 0000000000..efc202c33d --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md @@ -0,0 +1,260 @@ +# Wave 103 (retry): accountManager.mediaBox.storeResourceData drain 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:** Drain 5 remaining `accountManager.mediaBox.storeResourceData(...)` Shape-A sites against the wave-94 `AccountManagerResources.storeResourceData(id:data:synchronous:)` facade. Wave 103 (retry) of the Postbox → TelegramEngine refactor, after the abandonment of the original wave-103 plan. + +**Architecture:** Wave-shape-G drain. Pure call-site rewrite; no facade addition, no TelegramCore touch, no public-API change. 5 sites across 2 consumer files (`ThemeUpdateManager.swift`, `WallpaperResources.swift`) migrated via 3 `Edit` calls (1 single + 2 `replace_all=true` batches). + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests (per `CLAUDE.md`). Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1 (target first-pass-clean given mechanical scope and validated facade). + +**Note on TDD:** This project has no unit tests. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/TelegramUI/Sources/ThemeUpdateManager.swift` | Theme-update background sync | 1 site migrated | +| `submodules/WallpaperResources/Sources/WallpaperResources.swift` | Wallpaper resource pipeline | 4 sites migrated via 2 `replace_all=true` batches | + +No public-API ripple — both files are leaf consumers of the wave-94 facade. + +--- + +## Task 1: ThemeUpdateManager.swift — single-site migration + +**Files:** +- Modify: `submodules/TelegramUI/Sources/ThemeUpdateManager.swift` + +**Edits in this task:** 1. + +- [ ] **Step 1: Migrate the storeResourceData call at line 112** + +Find: + +```swift + accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) +``` + +Replace with: + +```swift + accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true) +``` + +`accountManager` here is closure-captured from `presentationThemeSettingsUpdated(_:)` scope, typed `AccountManager`. The facade is exposed via `public extension AccountManager { var resources: AccountManagerResources }`. + +--- + +## Task 2: WallpaperResources.swift — two batched migrations + +**Files:** +- Modify: `submodules/WallpaperResources/Sources/WallpaperResources.swift` + +**Edits in this task:** 2 (each `replace_all=true`, covering 2 sites apiece). + +- [ ] **Step 1: Migrate the `reference.resource.id` pattern (lines 973, 1214)** + +Use `Edit` with `replace_all=true`: + +Find: + +```swift +accountManager.mediaBox.storeResourceData(reference.resource.id, data: data) +``` + +Replace with: + +```swift +accountManager.resources.storeResourceData(id: EngineMediaResource.Id(reference.resource.id), data: data) +``` + +Both sites share identical text (verified by pre-flight grep). `replace_all=true` handles both atomically. + +- [ ] **Step 2: Migrate the `file.file.resource.id` pattern (lines 1260, 1523)** + +Use `Edit` with `replace_all=true`: + +Find: + +```swift +accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData) +``` + +Replace with: + +```swift +accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData) +``` + +Both sites share identical text. `replace_all=true` handles both atomically. + +--- + +## Task 3: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build** + +```sh +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 +``` + +Expected: clean build (`bazel build complete` / `INFO: Build completed successfully`). No `--continueOnError` because the small scope makes the first error informative. + +Build cost projection: WallpaperResources is foundational with wide rebuild fan-out; expect ~30-90s. + +- [ ] **Step 2: If build fails, triage iteration** + +Common failure modes: +- **`EngineMediaResource.Id` not in scope** — verify `import TelegramCore` is at the top of the failing file (it should be — pre-flight inventoried both files have it). If absent, add it. +- **Type mismatch on `id:` parameter** — would suggest an unexpected `MediaResourceId` subtype. STOP and re-read; the migration assumed `MediaResource.id: MediaResourceId` for both `reference.resource` and `file.file.resource`. Both should resolve to `MediaResourceId` per Postbox protocol. +- **`accountManager.resources` not in scope** — the `public extension AccountManager` exists in TelegramCore (wave 94). If unreachable, the consumer's BUILD might be missing a TelegramCore dep — but both files already use TelegramCore types, so this should not happen. STOP if it does. + +If errors land outside those 2 files: **STOP and report BLOCKED**. The wave is supposed to be self-contained. + +Fix in place and re-run step 1. Budget: 2 iterations. + +--- + +## Task 4: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Verify zero remaining `accountManager.mediaBox.storeResourceData` in the 2 touched files** + +Run: + +```sh +grep -rn "accountManager\.mediaBox\.storeResourceData" \ + submodules/TelegramUI/Sources/ThemeUpdateManager.swift \ + submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: empty output. + +--- + +## Task 5: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 2 modified files** + +```sh +git add \ + submodules/TelegramUI/Sources/ThemeUpdateManager.swift \ + submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected output: only the 2 staged files (lines starting with `M `). The line `m build-system/bazel-rules/sourcekit-bazel-bsp` is pre-existing WIP and should NOT appear in the staged list. + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 103 (retry) + +Drain 5 accountManager.mediaBox.storeResourceData(...) Shape-A sites +that the wave-94/95-99 sweep missed. All 5 migrated to +accountManager.resources.storeResourceData(id: EngineMediaResource.Id(...)) +against the existing wave-94 facade. + +Sites: ThemeUpdateManager:112 (with synchronous: true), +WallpaperResources:973, 1214 (reference.resource.id pattern, replace_all), +WallpaperResources:1260, 1523 (file.file.resource.id pattern, replace_all). + +5 sites / 2 files / 3 Edit calls. Consumer-only build. + +Wave-103 retry after the abandonment of ChatRecentActionsControllerNode +peer migration; see postbox-refactor-log "Wave 103 outcome" for the +forensics. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 103 (retry) commit as HEAD. + +--- + +## Task 6: 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` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 103 (retry) outcome to refactor log** + +Append a "Wave 103 (retry) outcome" entry to `docs/superpowers/postbox-refactor-log.md`. Include: +- Commit hash (from Task 5 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired). +- Bazel build duration. +- Net-delta accounting: −5 raw `mediaBox.X` accesses, +5 facade calls, +5 `EngineMediaResource.Id(...)` wraps (canonical engine-side, not Postbox bridges). +- Wave-shape note: G drain, validates the wave-94 facade across an additional 2-module footprint. + +- [ ] **Step 2: Update next-wave memory** + +Edit `project_postbox_refactor_next_wave.md`: +- Add wave 103 (retry) outcome line into the recent-waves section. +- Mark the 5 sites as drained; remove from candidate inventories (the file currently lists "Wave 95+ candidates" with stale storeResourceData entries — clean those up). +- Update the top frontmatter `description` to reflect wave 103 (retry) landed. +- Promote next candidate. Options: 7-site `resourceData(...)` drain (would need a new facade method or use existing `data(resource:)`), DirectMediaImageCache Shape-C/D, or pivot to a foundational wave. + +- [ ] **Step 3: Update MEMORY.md index** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md`: +- Update the `[Postbox refactor next wave]` line to mention wave 103 (retry) landed. + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 103 (retry) outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates are not committed — they live outside the repo.) + +--- + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| Raw `mediaBox.X` access drops | −5 | TUM:112 + WR:973, 1214, 1260, 1523 | +| Facade calls added | +5 | same sites, migrated form | +| `EngineMediaResource.Id(...)` wraps | +5 | canonical engine-side constructs (not Postbox bridges) | +| `import Postbox` drops | 0 | both files retain Postbox import for unrelated symbols | +| Postbox-free module count | 0 | no module dropped from the import list | + +**Total commit footprint:** 5 line edits (3 Edit calls) across 2 files, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md b/docs/superpowers/plans/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md new file mode 100644 index 0000000000..de643a1f87 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md @@ -0,0 +1,331 @@ +# Wave 104: accountManager.mediaBox.resourceData drain (3 clean sites) 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:** Drain 3 of 8 `accountManager.mediaBox.resourceData(...)` Shape-A sites against the existing wave-32 / wave-94 `AccountManagerResources.data(resource:)` facade. Wave 104 of the Postbox → TelegramEngine refactor. + +**Architecture:** Wave-shape-G drain with a documented consumer field rename. Single-file consumer migration in `submodules/WallpaperResources/Sources/WallpaperResources.swift`. 3 call rewrites + 3 consumer-side `.complete` → `.isComplete` renames, 6 Edit calls total. The remaining 5 of the original 8 `resourceData` candidates are deferred (2 cross a `MediaResourceData` flow-out cascade, 3 are coupled to postbox-side via `combineLatest` typed tuples). + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests. Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1 (target first-pass-clean given verified pre-flight inventory). + +**Note on TDD:** This project has no unit tests. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Changes | +|---|---|---| +| `submodules/WallpaperResources/Sources/WallpaperResources.swift` | Wallpaper resource pipeline | 3 call rewrites + 3 consumer renames | + +No public-API ripple — leaf-consumer migration against an existing facade. + +--- + +## Task 1: WallpaperResources.swift — call rewrites (3 edits) + +**Files:** +- Modify: `submodules/WallpaperResources/Sources/WallpaperResources.swift` + +**Edits in this task:** 3. + +- [ ] **Step 1: Migrate the call at line 957 (`reference.resource` argument)** + +Find: + +```swift + let maybeFetched = accountManager.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) +``` + +Replace with: + +```swift + let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(reference.resource), attemptSynchronously: synchronousLoad) +``` + +Note: `waitUntilFetchStatus: false` is omitted because the facade default is `false`. The site explicitly passed `false`, so behavior is preserved. + +- [ ] **Step 2: Migrate the call at line 1164 (`fileReference.media.resource` argument)** + +Find: + +```swift + let maybeFetched = accountManager.mediaBox.resourceData(fileReference.media.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) +``` + +Replace with: + +```swift + let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(fileReference.media.resource), attemptSynchronously: synchronousLoad) +``` + +Same `waitUntilFetchStatus: false` omission rationale. + +- [ ] **Step 3: Migrate the call at line 1264 (`file.file.resource` argument, no option)** + +Find: + +```swift + return accountManager.mediaBox.resourceData(file.file.resource) +``` + +Replace with: + +```swift + return accountManager.resources.data(resource: EngineMediaResource(file.file.resource)) +``` + +The original used the underlying `MediaBox.resourceData(_ resource:)` overload's defaults — facade defaults match exactly (`pathExtension: nil`, `waitUntilFetchStatus: false`, `attemptSynchronously: false`). + +--- + +## Task 2: WallpaperResources.swift — consumer-side `.complete` → `.isComplete` renames (3 edits) + +**Files:** +- Modify: `submodules/WallpaperResources/Sources/WallpaperResources.swift` + +**Edits in this task:** 3. + +`EngineMediaResource.ResourceData` exposes `.isComplete` (renamed from `MediaResourceData.complete`). All three migrated call sites have a single consumer-side `.complete` access on the migrated result that needs renaming. + +- [ ] **Step 1: Rename `maybeData.complete` at line 961 (consumer of site 957)** + +Find: + +```swift + if maybeData.complete { +``` + +Replace with: + +```swift + if maybeData.isComplete { +``` + +The leading whitespace (8 spaces) must match exactly. + +- [ ] **Step 2: Rename `maybeData.complete` at line 1168 (consumer of site 1164)** + +Find: + +```swift + if maybeData.complete && isSupportedTheme { +``` + +Replace with: + +```swift + if maybeData.isComplete && isSupportedTheme { +``` + +The leading whitespace (16 spaces) must match exactly. + +- [ ] **Step 3: Rename `data.complete` at line 1266 (consumer of site 1264)** + +Find: + +```swift + if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { +``` + +Replace with: + +```swift + if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { +``` + +The leading whitespace (36 spaces) must match exactly. + +The `data.path` access on the same line is unchanged — both `MediaResourceData.path` and `EngineMediaResource.ResourceData.path` are `String`. + +--- + +## Sites NOT touched (deferred) + +For the implementer's awareness — these `.complete` accesses on UNRELATED bindings stay raw and are NOT to be renamed: + +- `WallpaperResources.swift:968` — `return data.complete ? try? Data(contentsOf: URL(fileURLWithPath: data.path)) : nil` — this `data` is bound from `account.postbox.mediaBox.resourceData(...)` (postbox-side, not migrated). STAYS `.complete`. +- Other `.complete` accesses elsewhere in the file that aren't on the 3 migrated bindings — STAY. + +The 3 renames target only the 3 specific lines listed in Task 2 steps 1-3. Do NOT use `replace_all=true` for renames — bindings differ per scope. + +--- + +## Task 3: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build** + +```sh +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 +``` + +Expected: clean build (`bazel build complete` / `INFO: Build completed successfully`). No `--continueOnError`. Build cost projection: ~30-60s (consumer-only, foundational module rebuild fan-out). + +- [ ] **Step 2: If build fails, triage iteration** + +Common failure modes: +- **`EngineMediaResource` constructor not found** — verify `import TelegramCore` at the top of WallpaperResources.swift (it should already be there). If missing, add it. +- **Type mismatch on `resource:` parameter** — would suggest the argument expression isn't `MediaResource`-typed. STOP and check the actual type at the failing site. +- **Type mismatch on `.isComplete` rename** — if the closure parameter binding is somehow inferred wrong (e.g., Swift inferred the OLD `MediaResourceData` type because the call rewrite didn't take effect), the rename will fail. Re-read the diff and verify the call rewrite landed. +- **`data.path` type mismatch** — should not happen; both types expose `path: String`. If it does, STOP and re-read. + +If errors land outside WallpaperResources.swift: STOP and report BLOCKED. The wave is supposed to be self-contained. + +Iteration budget: 2. + +--- + +## Task 4: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Verify the 3 migrated call sites are gone** + +Run: + +```sh +grep -nE "accountManager\.mediaBox\.resourceData\(" submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: exactly 3 lines remaining (L33, L59, L401 — the deferred combineLatest sites). The migrated lines (originally 957, 1164, 1264) should NOT appear. + +- [ ] **Step 2: Verify the 3 renames are applied** + +Run: + +```sh +grep -nE "maybeData\.complete\b" submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: empty output. Both `maybeData.complete` accesses (originally L961, L1168) should be gone. + +```sh +grep -nE "if data\.complete," submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: no line at L1266 (the migrated site). Other `data.complete` accesses on postbox-side bindings (e.g., L968) may remain — those are out of scope. + +--- + +## Task 5: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 1 modified file** + +```sh +git add submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +- [ ] **Step 2: Confirm staging is clean** + +```sh +git status --short | grep -v "^??" +``` + +Expected: only the 1 staged file (line starting with `M `). The line `m build-system/bazel-rules/sourcekit-bazel-bsp` is pre-existing WIP and should NOT appear in the staged list. + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 104 + +Drain 3 accountManager.mediaBox.resourceData(...) Shape-A sites against +the existing wave-32 / wave-94 AccountManagerResources.data(resource:) +facade. Sites: WallpaperResources:957 (reference.resource), :1164 +(fileReference.media.resource), :1264 (file.file.resource). + +Migration: accountManager.mediaBox.resourceData(X, option: .complete( +waitUntilFetchStatus: false)[, attemptSynchronously: Y]) -> accountManager +.resources.data(resource: EngineMediaResource(X)[, attemptSynchronously: +Y]). Plus 3 consumer-side .complete -> .isComplete renames at L961, +L1168, L1266 to match EngineMediaResource.ResourceData field name. + +3 sites / 1 file / 6 Edit calls. Consumer-only build. + +Deferred: 2 sites in FetchCachedRepresentations.swift (482, 490) flow +data: MediaResourceData into fetchCachedScaled*Representation cascade; +3 sites in WallpaperResources (33, 59, 401) coupled to postbox-side via +combineLatest typed tuples. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +Expected: shows the wave 104 commit as HEAD. + +--- + +## Task 6: 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` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 104 outcome to refactor log** + +Append a "Wave 104 outcome" entry to `docs/superpowers/postbox-refactor-log.md` matching the format of "Wave 103 (retry) outcome". Include: +- Commit hash (from Task 5 step 4). +- Iteration count (1 if first-pass-clean; 2 if Task 3 step 2 fired). +- Bazel build duration (from Task 3 step 1 output). +- Net-delta accounting: −3 raw `mediaBox.X` accesses, +3 facade calls, +3 `EngineMediaResource(...)` wraps, +3 consumer field renames. +- Wave-shape note: G drain with documented consumer field rename. The pre-flight identified a `MediaResourceData`-typed-function-parameter barrier (`fetchCachedScaled*Representation` family) that forced 2 sites into the deferred bucket — illustrates the wave-71-shadow lesson applied to result-type cascades, not just peer migrations. + +- [ ] **Step 2: Update next-wave memory** + +Edit `project_postbox_refactor_next_wave.md`: +- Add wave 104 outcome line into the recent-waves section. +- Update accountManager-side facade drain status table: `resourceData` count drops from 8 → 5 (3 drained, 5 deferred). +- Add a new section (or extend an existing one) documenting the "Postbox-typed-function-parameter barrier" pattern, with `Message.peers: SimpleDictionary` (wave-103 lesson) and now `fetchCachedScaled*Representation(resourceData: MediaResourceData)` as the two known instances. + +- [ ] **Step 3: Update MEMORY.md index** + +Edit `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md`: +- Update the `[Postbox refactor next wave]` line to mention wave 104 landed. + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 104 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates are not committed — they live outside the repo.) + +--- + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| Raw `mediaBox.X` access drops | −3 | WR:957, 1164, 1264 | +| Facade calls added | +3 | same sites, migrated form | +| `EngineMediaResource(...)` wraps | +3 | canonical engine-side, not Postbox bridges | +| Consumer field renames | +3 | WR:961 (`maybeData.complete` → `.isComplete`), WR:1168 (same), WR:1266 (`data.complete` → `.isComplete`) | +| `import Postbox` drops | 0 | WallpaperResources retains import for unrelated symbols | + +**Total commit footprint:** 6 line edits in 1 file, plus a docs commit for the outcome log. diff --git a/docs/superpowers/plans/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md b/docs/superpowers/plans/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md new file mode 100644 index 0000000000..a191faea48 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md @@ -0,0 +1,489 @@ +# Wave 105: DeviceContactInfoSubject enum payload 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. + +**Goal:** Migrate `DeviceContactInfoSubject` enum's 3 case payloads + 2 callback signatures + 1 computed property from raw Postbox `Peer?` to `EnginePeer?`. Wave 105 of the Postbox → TelegramEngine refactor. + +**Architecture:** Multi-module enum-payload migration (wave-91 shape). 17 edits across 5 files. AccountContext.swift hosts the enum + property. DeviceContactInfoController.swift is the primary consumer. 4 construction sites in TelegramUI/PeerInfoUI/StoryContainerScreen/ChatController. Net wrap delta: −8 (drops 10, adds 2 at Chat-side construction barriers documented per spec). + +**Tech Stack:** Swift, Bazel via `Make.py`, no unit tests. Verification is the full-project debug-sim-arm64 build. + +**Iteration budget:** 1-3 (wave-91 precedent: 2 iter for similar shape). + +**Note on TDD:** No unit tests in this project. Each task writes the edits, then verifies via Bazel build + residue grep. + +--- + +## File Structure + +| File | Role | Edits | +|---|---|---| +| `submodules/AccountContext/Sources/AccountContext.swift` | Enum definition + computed property | 4 type-line edits | +| `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` | Primary consumer | 9 edits (5 `_asPeer` drops + 3 `.flatMap` simplifications + 1 downcast rewrite) | +| `submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift` | Chat-side construction (Pattern E ADD bridges) | 1 Edit (replace_all=true covers 2 sites) | +| `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` | Story-side construction | 1 edit | +| `submodules/TelegramUI/Sources/OpenChatMessage.swift` | OpenChatMessage construction | 1 edit | + +--- + +## Task 1: AccountContext.swift — enum + computed property type changes + +**File:** `submodules/AccountContext/Sources/AccountContext.swift` + +- [ ] **Step 1: Migrate the 3 enum case payloads (single Edit covers consecutive lines)** + +Find: + +```swift +public enum DeviceContactInfoSubject { + case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void) + case create(peer: Peer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) + + public var peer: Peer? { +``` + +Replace with: + +```swift +public enum DeviceContactInfoSubject { + case vcard(EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: EnginePeer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (EnginePeer?, DeviceContactExtendedData) -> Void) + case create(peer: EnginePeer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (EnginePeer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) + + public var peer: EnginePeer? { +``` + +This single Edit covers all 4 type-line changes in `AccountContext.swift`. The `contactData: DeviceContactExtendedData` computed property (lines 719-727) is unaffected. + +--- + +## Task 2: DeviceContactInfoController.swift — Pattern D downcast rewrite (1 edit) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Rewrite the `as? TelegramUser` downcast at line 849** + +Find: + +```swift + if let peer = peer as? TelegramUser { +``` + +Replace with: + +```swift + if case let .user(peer) = peer { +``` + +The leading whitespace (8 spaces) must match exactly. The outer `peer: EnginePeer?` (from `case let .create(peer, ...) = subject` at L845) is shadowed inside the if-body by `peer: TelegramUser` (the `.user` case associated value). Inner body access (`peer.firstName`, `peer.lastName`, `peer.phone`) works on the rebinding. + +--- + +## Task 3: DeviceContactInfoController.swift — Pattern C `.flatMap` simplifications (3 edits) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Simplify `.vcard` case body at line 942** + +Find: + +```swift + case let .vcard(peer, id, data): + contactData = .single((peer.flatMap(EnginePeer.init), id, data)) +``` + +Replace with: + +```swift + case let .vcard(peer, id, data): + contactData = .single((peer, id, data)) +``` + +- [ ] **Step 2: Simplify `.filter` case body at line 944** + +Find: + +```swift + case let .filter(peer, id, data, _): + contactData = .single((peer.flatMap(EnginePeer.init), id, data)) +``` + +Replace with: + +```swift + case let .filter(peer, id, data, _): + contactData = .single((peer, id, data)) +``` + +- [ ] **Step 3: Simplify `.create` case body at line 946** + +Find: + +```swift + case let .create(peer, data, share, shareViaExceptionValue, _): + contactData = .single((peer.flatMap(EnginePeer.init), nil, data)) +``` + +Replace with: + +```swift + case let .create(peer, data, share, shareViaExceptionValue, _): + contactData = .single((peer, nil, data)) +``` + +After Task 1's enum migration, the destructured `peer: EnginePeer?` is the target type — `.flatMap(EnginePeer.init)` becomes a redundant round-trip. + +--- + +## Task 4: DeviceContactInfoController.swift — Pattern B `_asPeer` drops at completion calls (2 edits) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Drop `_asPeer()` at completion call line 1105** + +Find: + +```swift + completion(peerAndContactData.0?._asPeer(), filteredData) +``` + +Replace with: + +```swift + completion(peerAndContactData.0, filteredData) +``` + +`peerAndContactData.0` is `EnginePeer?` from the typed signal at L939. Completion's first parameter type changes from `Peer?` to `EnginePeer?` per Task 1. + +- [ ] **Step 2: Drop `_asPeer()` at completion call line 1224** + +Find: + +```swift + completion(contactIdAndData.2?._asPeer(), contactIdAndData.0, contactIdAndData.1) +``` + +Replace with: + +```swift + completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1) +``` + +`contactIdAndData.2` is `EnginePeer?` per the typed signal `(DeviceContactStableId, DeviceContactExtendedData, EnginePeer?)?` declared at L1175. + +--- + +## Task 5: DeviceContactInfoController.swift — Pattern A `_asPeer` drops at construction (3 edits) + +**File:** `submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift` + +- [ ] **Step 1: Drop `_asPeer()` at line 1289** + +Find: + +```swift + replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer?._asPeer(), contactId, contactData), completed: nil, cancelled: nil)) +``` + +Replace with: + +```swift + replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer, contactId, contactData), completed: nil, cancelled: nil)) +``` + +- [ ] **Step 2: Drop `_asPeer()` at line 1443** + +Find: + +```swift + parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in +``` + +Replace with: + +```swift + parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in +``` + +- [ ] **Step 3: Drop `_asPeer()` at line 1489** + +Find: + +```swift + controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in +``` + +Replace with: + +```swift + controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in +``` + +All 3 sites have `peer` source already typed as `EnginePeer?` per inventory. + +--- + +## Task 6: ChatControllerOpenAttachmentMenu.swift — Pattern E ADD wraps (1 Edit, 2 sites via replace_all=true) + +**File:** `submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift` + +- [ ] **Step 1: Add `.flatMap(EnginePeer.init)` wrap at lines 683 and 1850** + +Use Edit with `replace_all=true`. Find: + +```swift +subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in +``` + +Replace with: + +```swift +subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in +``` + +`replace_all=true` is required — both sites at L683 and L1850 share identical text. The upstream signal type is `(Peer?, DeviceContactExtendedData?)` (verified at L634 and L1822); `.flatMap(EnginePeer.init)` wraps `Peer?` to `EnginePeer?` to satisfy the migrated `.filter(peer: EnginePeer?, ...)` signature. + +--- + +## Task 7: StoryItemSetContainerViewSendMessage.swift — Pattern A `_asPeer` drop (1 edit) + +**File:** `submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift` + +- [ ] **Step 1: Drop `_asPeer()` at line 2132** + +Find: + +```swift + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in +``` + +Replace with: + +```swift + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in +``` + +`peerAndContactData.0` is `EnginePeer?` from the typed signal at this site (the presence of `?._asPeer()` confirms it). + +--- + +## Task 8: OpenChatMessage.swift — Pattern A `_asPeer` drop (1 edit) + +**File:** `submodules/TelegramUI/Sources/OpenChatMessage.swift` + +- [ ] **Step 1: Drop `_asPeer()` at line 443** + +Find: + +```swift + let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer?._asPeer(), nil, contactData), completed: nil, cancelled: nil) +``` + +Replace with: + +```swift + let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer, nil, contactData), completed: nil, cancelled: nil) +``` + +`peer` source is already `EnginePeer?` (the `?._asPeer()` confirms the source type). + +--- + +## Task 9: Full-project Bazel build + +**Files:** none (verification only). + +- [ ] **Step 1: Run the build with `--continueOnError`** + +```sh +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 +``` + +`--continueOnError` enabled — multi-module wave; surface all errors at once if iter-1 fails. + +Expected: clean build. AccountContext is foundational; expect 60-180s build cost. + +- [ ] **Step 2: If build fails, triage iteration** + +Common failure modes (per wave-91 precedent): +- **Type mismatch on a destructured `peer`** — a destructure body may use `peer.X` where `X` is a Peer-protocol-only method not on EnginePeer. Pre-flight inventory found ZERO such sites, but verify the failing line. +- **`.id` access on EnginePeer? doesn't compile** — would indicate an EnginePeer.Id typealias regression (very unlikely; would have failed all prior waves). +- **`case let .user(peer) = peer` doesn't compile** — verify the outer `peer` is `EnginePeer?` (after migration) and not still `Peer?`. +- **A construction site missed an `_asPeer()` drop** — re-grep `_asPeer\(\)` over the 5 touched files. +- **Hidden `Peer?`-typed completion call site** — would indicate an unmigrated callback consumer. Re-grep across consumer module sources. + +If errors land outside the 5 touched files: STOP and report BLOCKED — the wave is supposed to be self-contained. + +Iteration budget: 3. + +--- + +## Task 10: Post-edit residue grep + +**Files:** none (verification only). + +- [ ] **Step 1: Construction-site `_asPeer` residue (expected empty)** + +```sh +grep -nE "subject:\s*\.(vcard|filter|create)\(.*_asPeer\(\)" \ + submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \ + submodules/TelegramUI/Sources/OpenChatMessage.swift +``` + +- [ ] **Step 2: Completion `_asPeer` residue (expected empty)** + +```sh +grep -nE "completion\(.*_asPeer\(\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +``` + +- [ ] **Step 3: `.flatMap(EnginePeer.init)` simplification residue (expected empty)** + +```sh +grep -nE "peer\.flatMap\(EnginePeer\.init\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +``` + +- [ ] **Step 4: Downcast residue (expected empty)** + +```sh +grep -nE "peer as\? TelegramUser" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +``` + +- [ ] **Step 5: ADD wraps applied (expected 2 lines)** + +```sh +grep -nE "peerAndContactData\.0\.flatMap\(EnginePeer\.init\)" submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +``` + +Expected: 2 lines (originally L683 and L1850, line numbers may have shifted slightly). + +--- + +## Task 11: Commit the wave + +**Files:** none (git only). + +- [ ] **Step 1: Stage the 5 modified files** + +```sh +git add \ + submodules/AccountContext/Sources/AccountContext.swift \ + submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \ + submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \ + submodules/TelegramUI/Sources/OpenChatMessage.swift +``` + +- [ ] **Step 2: Confirm staging** + +```sh +git status --short | grep -v "^??" +``` + +Expected: 5 staged files (lines starting with `M `). The pre-existing `m build-system/bazel-rules/sourcekit-bazel-bsp` WIP marker should NOT appear in staged. + +- [ ] **Step 3: Commit** + +```sh +git commit -m "$(cat <<'EOF' +Postbox -> TelegramEngine wave 105 + +Migrate DeviceContactInfoSubject enum 3 case Peer? payloads + 2 callback +(Peer?, ...) -> Void signatures + 1 computed peer: Peer? property to +EnginePeer?. Wave-91-pattern multi-module enum-payload migration. + +Drops 10 wraps: +- 5 _asPeer() at construction sites: DeviceContactInfoController:1289, + 1443, 1489 + StoryItemSetContainerViewSendMessage:2132 + + OpenChatMessage:443. +- 2 _asPeer() at completion-call sites: + DeviceContactInfoController:1105, 1224. +- 3 .flatMap(EnginePeer.init) simplifications at + DeviceContactInfoController:942, 944, 946. + +Adds 2 ADD bridges: ChatControllerOpenAttachmentMenu:683, 1850 — both +construct .filter(peer:) from peerAndContactData.0 typed (Peer?, ...); +.flatMap(EnginePeer.init) wraps to EnginePeer?. Net wrap delta: -8. + +Plus 1 downcast rewrite: DeviceContactInfoController:849 — `if let peer += peer as? TelegramUser` to `if case let .user(peer) = peer`. + +5 files / 17 edits. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +- [ ] **Step 4: Verify commit** + +```sh +git log --oneline -1 +``` + +--- + +## Task 12: 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` +- Modify: `~/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/MEMORY.md` + +- [ ] **Step 1: Append wave 105 outcome to refactor log** + +Include: +- Commit hash (from Task 11 step 4). +- Iteration count (1 if first-pass-clean; 2-3 if Task 9 step 2 fired). +- Bazel build duration. +- Net-delta accounting: −10 wrap drops, +2 ADD wraps, +1 downcast rewrite. Net −8 wraps. +- Wave-shape note: wave-91-pattern multi-module enum-payload migration with full pre-flight inventory clearing layers 1-4 of the wave-71-shadow checklist. Documents the value of thorough pre-flight inventory: 17 mechanical edits with 0 surprises. + +- [ ] **Step 2: Update next-wave memory** + +Edit `project_postbox_refactor_next_wave.md`: +- Add wave 105 outcome line into the recent-waves section. +- Mark `DeviceContactInfoSubject` candidate as drained (currently bullet 9 in deferred list). +- Promote next candidate. + +- [ ] **Step 3: Update MEMORY.md index** + +Update the `[Postbox refactor next wave]` line. + +- [ ] **Step 4: Commit the doc update** + +```sh +git add docs/superpowers/postbox-refactor-log.md +git commit -m "$(cat <<'EOF' +docs: log wave 105 outcome + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +(Memory file updates not committed — they live outside the repo.) + +--- + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| `_asPeer()` drops at construction | −5 | DCIC:1289, 1443, 1489 + SISCVSM:2132 + OCM:443 | +| `_asPeer()` drops at completion calls | −2 | DCIC:1105, 1224 | +| `.flatMap(EnginePeer.init)` simplifications | −3 | DCIC:942, 944, 946 | +| `.flatMap(EnginePeer.init)` ADD wraps | +2 | CCOAM:683, 1850 | +| Downcast → case-let | +1 | DCIC:849 | +| Type annotations migrated | 4 | AccountContext: 3 enum cases + 1 computed property | + +**Total commit footprint:** 17 line edits across 5 files, plus a docs commit for the outcome log. + +**Net wrap delta:** **−8** (the wave's headline metric). diff --git a/docs/superpowers/postbox-refactor-log.md b/docs/superpowers/postbox-refactor-log.md index ad0a828b79..6f2a69fe99 100644 --- a/docs/superpowers/postbox-refactor-log.md +++ b/docs/superpowers/postbox-refactor-log.md @@ -1871,6 +1871,81 @@ All four forwarding implementations live in `submodules/TelegramCore/Sources/Uti --- +## Wave 103 outcome (2026-04-26): ABANDONED + +`ChatRecentActionsControllerNode.peer: Peer → EnginePeer` (wave-71-shadow close). Implementation built and committed (`e60a8692a7`, build clean at iter-3 / 41s), then **reverted** (`git reset --hard HEAD~1`) after pre-flight failure surfaced. + +**Spec promised:** −1 boundary `_asPeer()` (CRAC:277) + −1 `import Postbox` (CRACN:5) + 0 ADD wraps. 7 edits / 2 files / 1 iter. + +**Actual outcome before revert:** −1 boundary `_asPeer()` (CRAC:277) + −1 `EnginePeer(strongSelf.peer)` wrap (CRACN:535, bonus) + 0 `import Postbox` drops (raw `Message`/`MessageId` references for `AdminLogEventAction` payloads block the drop) + **+2 ADD `_asPeer()` wraps** (CRACN:228 for `canSetupAutoremoveTimeout` Peer-protocol extension, CRACN:737 for `chatRecentActionsHistoryPreparedTransition(peer: Peer)` helper). Net wrap delta: **0**, not the promised −1. Net `import Postbox` drop: **0**, not the promised −1. + +**Why "extend in follow-up" was rejected:** Migrating `chatRecentActionsHistoryPreparedTransition(peer:)` to `EnginePeer` to drop the CRACN:737 ADD bridge would cascade into `ChatRecentActionsEntry.item(peer:)`. Inventory found 75 ADD-bridge sites in the helper body: 72 `peers[peer.id] = peer` stores into a `SimpleDictionary` (the type required by `Message(...)` constructor's `peers:` parameter) plus 3 `filterMessageChannelPeer(peer)` calls. Net cost of dropping 1 bridge: **+74 new bridges**. Catastrophic regression. + +**Pre-flight failure modes (lessons):** + +- **Pre-flight grep for `self..X` access must enumerate Peer-protocol extension methods, not just downcast patterns.** Wave-103 spec grepped `self.peer as\?` and `self.peer\.id\b` but missed `self.peer.canSetupAutoremoveTimeout(...)` at CRACN:228. `canSetupAutoremoveTimeout` is a `public extension Peer` method in `AccountContext/Sources/ChatController.swift:1013` — not on `EnginePeer`. **Mitigation:** future wave specs that retype a stored field from `Peer` to `EnginePeer` MUST grep for ALL `self..(` patterns and verify each method is reachable on `EnginePeer` (not just on the `Peer` protocol). Build a method-allowlist for `EnginePeer` from `extension EnginePeer` declarations in TelegramCore and intersect. + +- **Pre-flight grep for `` flowing into Peer-typed function parameters.** Wave-103 spec missed that `peer` was passed to `chatRecentActionsHistoryPreparedTransition(peer: Peer)` at CRACN:737. The helper sits in a different file (`ChatRecentActionsHistoryTransition.swift`) but the same submodule. **Mitigation:** grep `: peer\b|, peer:|peer: peer\b|peer:\s*self\.peer` across the entire submodule, classify each call site as (a) target accepts EnginePeer (clean), (b) target accepts Peer (would need `_asPeer()` ADD or co-migration of target), (c) target accepts `EnginePeer(peer)` wrap (clean drop). + +- **Wave-71-shadow close is NOT always cheap.** The implicit assumption ("caller already has EnginePeer; just drop the boundary `_asPeer()` and retype the storage") only holds when ALL of: (a) every `self..X` access has an EnginePeer-compatible counterpart, (b) every function call passing `self.` accepts EnginePeer, (c) no protocol-conformance constraint on the stored field exists. Wave 71 itself worked because `peerInfoControllerImpl` was a single private function with mechanical body. Wave 103 failed because the file participates in a deep helper-cascade that builds Postbox `Message` values — and `Message`'s `peers: SimpleDictionary` constructor parameter is a hard barrier. + +- **The `Message(... peers: SimpleDictionary, ...)` constructor is a hard wave barrier.** Any helper that builds Message values from a peer-typed parameter has Cat-3 ADD-bridge sites everywhere it stores the migrated peer into the peers dict. ChatRecentActionsHistoryTransition.swift has 70+ such constructions. Future waves should treat Message-building helpers as red-flagged candidates and pre-flight inventory their `peers[X.id] = X` store sites before promising any `peer: Peer → EnginePeer` migration upstream. + +- **Spec self-review must include an "ADD wraps inventory" pass.** The wave-103 spec claimed "ADD wraps: 0" but the pre-flight grep didn't actually verify this — only the cast/`is` patterns were enumerated. Future spec template: a dedicated "ADD-wrap risk grep" section listing the regex patterns checked (Peer-protocol method calls, function calls accepting `: Peer`, dictionary stores into `[PeerId: Peer]`). + +**Outcome:** wave-103 candidate marked **abandoned** in `project_postbox_refactor_next_wave.md`. Spec and plan docs retained at `docs/superpowers/specs/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node-design.md` and `docs/superpowers/plans/2026-04-26-postbox-wave-103-chat-recent-actions-controller-node.md` as record of the failed attempt. + +--- + +## Wave 103 (retry) outcome (2026-04-26) + +`accountManager.mediaBox.storeResourceData(...)` Shape-A drain against the wave-94 `AccountManagerResources.storeResourceData(id:data:synchronous:)` facade. 5 sites / 2 files / 3 Edit calls (1 single + 2 `replace_all=true` batches) / **first-pass-clean** Bazel 29.5s (warm cache). Commit `92230b0691`. Sites: ThemeUpdateManager:112 (with `synchronous: true`), WallpaperResources:973+1214 (`reference.resource.id` pattern, replace_all), WallpaperResources:1260+1523 (`file.file.resource.id` pattern, replace_all). + +**Net delta:** −5 raw `mediaBox.X` accesses, +5 facade calls, +5 `EngineMediaResource.Id(...)` wraps (canonical engine-side, not Postbox bridges). + +**Lesson reinforced:** wave-shape-G drain against an existing facade is the cheapest reliable wave shape. 1-iter, ~30s build, single atomic commit. Pre-flight risk inventory takes ~5 minutes; implementation takes ~30s. Use this shape after a difficult abandonment to rebuild momentum. + +**Wave-shape-G drain after a failed wave-71-shadow:** the contrast between the abandoned wave-103 (`peer: Peer → EnginePeer` field migration with hidden 75-site cascade) and the retry (5-site call-rewrite drain) illustrates why facade-drain waves and field-migration waves are categorically different. Drains have bounded scope (only sites matching a literal text pattern); field migrations have unbounded scope (any consumer of any field-typed value). Future wave selection should prefer drains when the goal is consistent forward progress and reserve field migrations for sessions with explicit budget for cascade investigation. + +--- + +## Wave 104 outcome (2026-04-26) + +`accountManager.mediaBox.resourceData(...)` Shape-A drain (3 of 8 candidate sites) against the wave-32 / wave-94 `AccountManagerResources.data(resource:)` facade. 1 file / 6 Edit calls (3 call rewrites + 3 consumer-side `.complete` → `.isComplete` renames) / **first-pass-clean** Bazel 11.7s. Commit `08fc3f721e`. Sites: WallpaperResources:957 (`reference.resource`), :1164 (`fileReference.media.resource`), :1264 (`file.file.resource`). + +**Net delta:** −3 raw `mediaBox.X` accesses, +3 facade calls, +3 `EngineMediaResource(...)` wraps, +3 consumer field renames (`.complete` → `.isComplete` to match `EngineMediaResource.ResourceData`'s renamed field). + +**Deferred (from the original 8-site candidate set):** +- 2 sites in `FetchCachedRepresentations.swift:482, 490` — flow `data: MediaResourceData` into `fetchCachedScaledImageRepresentation` / `fetchCachedBlurredWallpaperRepresentation` (raw-`MediaResourceData`-typed `resourceData:` parameter). Migration would cascade those functions OR require boundary `MediaResourceData` reconstruction. Defer. +- 3 sites in `WallpaperResources.swift:33, 59, 401` — coupled to postbox-side via `combineLatest(accountManager.mediaBox.resourceData(X), account.postbox.mediaBox.resourceData(X))` returning typed `Signal<(MediaResourceData, MediaResourceData), NoError>`. Migrating one side without the other breaks the tuple type. Defer until postbox-side is also drainable or a paired-resource facade is designed. + +**Lesson — "Postbox-typed-function-parameter barrier" pattern (generalized):** wave 103's abandonment introduced this concept for `Message(... peers: SimpleDictionary, ...)` — any helper that builds a `Message` value forces ADD bridges at every `peers[X.id] = X` store. Wave 104 finds the same pattern at the result-type-flow level: `fetchCachedScaled*Representation(resourceData: MediaResourceData)` is a barrier that forces ADD bridges at every site whose closure flows the migrated result into it. Both are instances of "a Postbox-typed function parameter blocks upstream migration of values that flow into it." Pre-flight inventory must enumerate not just type-level uses of the migrated symbol but also the function-parameter-typed barriers it transitively flows into. + +**Lesson — "field rename at wrapper-type boundaries":** `EngineMediaResource.ResourceData.isComplete` is renamed from `MediaResourceData.complete`. Each migrated call site has a paired consumer rename. This is a small but mandatory step the spec must call out per-site; an undocumented rename would surface as a build error referencing a property that doesn't exist on the new wrapper. The rename is verifiable per-site by reading the closure body that consumes the result. + +--- + +## Wave 105 outcome (2026-04-26) + +`DeviceContactInfoSubject` enum-payload Peer? → EnginePeer? migration. 5 files / 17 edits / **first-pass-clean** Bazel 203s (foundational AccountContext touch). Commit `0c76724409`. Wave-91-pattern multi-module enum-payload + completion-callback signature migration. + +**Type changes:** 3 enum case payloads (`Peer?` → `EnginePeer?`), 2 completion-callback signatures (`(Peer?, ...) -> Void` → `(EnginePeer?, ...) -> Void`), 1 computed property (`peer: Peer?` → `peer: EnginePeer?`). All in `AccountContext.swift`. + +**Net delta:** −10 wraps dropped, +2 wraps added (Pattern E ADD bridges at Chat-side construction sites where `peerAndContactData.0` is raw `Peer?`), +1 downcast → case-let conversion. **Net wrap delta: −8.** + +Per-pattern breakdown: +- Pattern A (5 sites): `_asPeer()` drops at construction sites where source is already `EnginePeer?`. DeviceContactInfoController.swift:1289, 1443, 1489 + StoryItemSetContainerViewSendMessage.swift:2132 + OpenChatMessage.swift:443. +- Pattern B (2 sites): `_asPeer()` drops at completion-call sites. DeviceContactInfoController.swift:1105, 1224. +- Pattern C (3 sites): `.flatMap(EnginePeer.init)` simplifications when destructured `peer` is already `EnginePeer?` post-migration. DeviceContactInfoController.swift:942, 944, 946. +- Pattern D (1 site): downcast `as? TelegramUser` → `case let .user(peer)`. DeviceContactInfoController.swift:849. +- Pattern E (2 sites): ADD `.flatMap(EnginePeer.init)` wraps at construction sites where source is raw `Peer?`. ChatControllerOpenAttachmentMenu.swift:683, 1850. + +**Lesson — "thorough pre-flight inventory pays for itself":** wave 105 was the first wave-71-shadow-style (field/payload migration with cascade risk) attempted after the wave-103 abandonment forced a discipline reset. Pre-flight inventory took ~15 minutes (full 4-layer wave-71-shadow-checklist sweep, including verifying 8 destructure sites + 5 callback consumers + 3 `subject.peer` accesses + 12 construction sites across 8 files). The inventory caught the 2 ADD bridges at Chat sites that an Explore-agent pass had initially miscategorized — verified by direct grep of `peerAndContactData` source signal types. Cost-of-care: 15 minutes of pre-flight + 5 minutes of one-line spec fix vs. the wave-103 cost of a build-and-revert cycle. Recommendation: apply the same discipline to every future wave-71-shadow candidate. The inventory cost is a tiny fraction of the abandoned-wave cost. + +**Lesson — "documented ADD bridges are net-positive":** ADD bridges are not always disqualifying. The wave-71-shadow risk feedback emphasizes inventorying them, not avoiding them entirely. When the migration delivers net-negative wrap delta even after accounting for the ADD bridges (here: −10 drops vs. +2 adds = −8 net), the wave is still worth doing. The ADD bridges are also documented in the spec and commit message, so future waves migrating the upstream `peerAndContactData` source can drop them as part of that migration's natural cascade. + +--- + ## Modules currently free of `import Postbox` (running tally) Consumer modules that no longer import Postbox, across all waves and standalone commits: diff --git a/docs/superpowers/specs/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md b/docs/superpowers/specs/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md new file mode 100644 index 0000000000..bc24de86be --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-postbox-wave-103-retry-account-manager-store-resource-data-drain.md @@ -0,0 +1,149 @@ +# Wave 103 (retry) — accountManager.mediaBox.storeResourceData drain + +**Date:** 2026-04-26 +**Pattern:** wave-shape-G drain of an existing TelegramCore facade (the wave-94 `AccountManagerResources.storeResourceData(id:data:synchronous:)`). +**Module:** `submodules/TelegramUI/Sources/ThemeUpdateManager.swift` + `submodules/WallpaperResources/Sources/WallpaperResources.swift` only — no TelegramCore touch, no public-API change. + +## Goal + +Drain the 5 remaining `accountManager.mediaBox.storeResourceData(...)` Shape-A sites that the wave-94/95-99 sweep didn't catch. Migrate each to `accountManager.resources.storeResourceData(id: EngineMediaResource.Id(...), data: ..., synchronous: ...)` against the existing wave-94 facade. Net effect: −5 raw `accountManager.mediaBox.X` accesses, +5 facade calls. Consumer-only build. + +This is the wave-103 retry after the abandonment of `ChatRecentActionsControllerNode.peer` migration (see `postbox-refactor-log.md` "Wave 103 outcome (2026-04-26): ABANDONED"). + +## Wave-71-shadow risk inventory (per `feedback_wave71_shadow_risk.md`) + +| Layer | Applicable? | Notes | +|---|---|---| +| 1. Downcasts (`as?` / `is`) | N/A | No Peer migration, no type-level change | +| 2. Peer-protocol extension method calls | N/A | No stored field retype | +| 3. Field flow into Peer-typed function parameters | N/A | No `peer` param involved | +| 4. Message-builder cascade via `SimpleDictionary` | N/A | No `Message(...)` construction touched | + +Wave shape (call-site rewrite against an existing facade) is orthogonal to the wave-71-shadow risk layers. The wave-94 lesson and wave-shape-G recipe are the relevant precedents. + +## Sites (5 total) + +### ThemeUpdateManager.swift (1 site) + +| Line | Existing | Migrated | +|---|---|---| +| 112 | `accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true)` | `accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true)` | + +`accountManager` flows from the enclosing `presentationThemeSettingsUpdated(_:)` method's closure-captured scope. `accountManager: AccountManager` typed (Shape-A). + +### WallpaperResources.swift (4 sites) + +All four sites use the same call-text pattern (same arity, no `synchronous:` arg) but different argument expressions: + +| Line | Argument expression | +|---|---| +| 973 | `reference.resource.id, data: data` | +| 1214 | `reference.resource.id, data: data` | +| 1260 | `file.file.resource.id, data: fullSizeData` | +| 1523 | `file.file.resource.id, data: fullSizeData` | + +Lines 973 and 1214 share identical text (`accountManager.mediaBox.storeResourceData(reference.resource.id, data: data)`) — `Edit replace_all=true` bundles them. Lines 1260 and 1523 share identical text (`accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData)`) — same. + +Each migrated to: `accountManager.resources.storeResourceData(id: EngineMediaResource.Id(), data: )`. + +`accountManager` flows from `wallpaperDatas(account:accountManager:...)` and other public functions in the file, all parameter-typed `AccountManager` (Shape-A). + +## Edit patterns + +### A. ThemeUpdateManager (1 site) + +Single Edit: + +| File:Line | Before | After | +|---|---|---| +| ThemeUpdateManager.swift:112 | `accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true)` | `accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true)` | + +### B. WallpaperResources (4 sites in 2 replace_all batches) + +Two `Edit` calls, each with `replace_all=true`: + +| Pattern | Before | After | +|---|---|---| +| Pattern 1 (lines 973, 1214) | `accountManager.mediaBox.storeResourceData(reference.resource.id, data: data)` | `accountManager.resources.storeResourceData(id: EngineMediaResource.Id(reference.resource.id), data: data)` | +| Pattern 2 (lines 1260, 1523) | `accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData)` | `accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData)` | + +**Total edits:** 3 Edit calls (1 single + 2 replace_all batches), 5 sites migrated. + +## Facade signature reference + +From `submodules/TelegramCore/Sources/AccountManager/AccountManagerResources.swift` (added wave 94): + +```swift +public func storeResourceData(id: EngineMediaResource.Id, data: Data, synchronous: Bool = false) { + self.mediaBox.storeResourceData(MediaResourceId(id.stringRepresentation), data: data, synchronous: synchronous) +} +``` + +`EngineMediaResource.Id(_ id: MediaResourceId)` constructor at `TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:179`. + +`accountManager.resources` is a computed property that constructs a fresh `AccountManagerResources` wrapper holding only a `MediaBox` reference — cheap. + +## Risk register + +| Risk | Mitigation | +|---|---| +| `replace_all=true` matching the wrong site | Two patterns are scoped narrowly enough (full call expressions including the closing paren). Pre-flight grep confirmed exactly 2 instances of each pattern across the file. | +| `EngineMediaResource.Id(...)` constructor missing for the argument expression's type | Verified: `init(_ id: MediaResourceId)` exists. `MediaResource.id` returns `MediaResourceId` per Postbox protocol. Construction is canonical. | +| `synchronous:` default mismatch | Facade default is `synchronous: false`, matching `MediaBox.storeResourceData`'s underlying default. Sites without explicit `synchronous:` keep behavior. | +| Build cascade beyond touched files | Consumer-only — both files are leaf consumers (no public re-export of touched symbols). No TelegramCore touch. WallpaperResources is foundational so its rebuild fans out, but the public API is unchanged so dependent modules don't need recompilation. | +| WIP-interference at staging | Pre-existing WIP markers (`build-system/bazel-rules/sourcekit-bazel-bsp` + 3 untracked dirs) are in unrelated paths — no overlap. Stage by explicit file list. | + +## Wave shape + +**Classification:** wave-shape-G drain of an existing TelegramCore facade (waves 84-93 cohort, validated wave 94 + waves 95-99 drains). +**Iteration budget:** 1 (target first-pass-clean given mechanical scope and 5-site footprint). +**Subagent dispatch:** not needed — 3 Edit calls is single-implementer scope. + +## Verification + +### Build + +```sh +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 +``` + +(No `--continueOnError` — small atomic scope.) WallpaperResources is a foundational submodule with a wide rebuild fan-out, but its public API is unchanged so dependents don't recompile. Build cost projection: ~30-60s. + +### Post-edit residue grep (expect empty) + +```sh +grep -rn "accountManager\.mediaBox\.storeResourceData" \ + submodules/TelegramUI/Sources/ThemeUpdateManager.swift \ + submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: empty output across both files. + +## Net delta projection + +- **Raw `mediaBox.X` accesses:** −5 +- **Facade `resources.X` calls:** +5 +- **`EngineMediaResource.Id(...)` wraps:** +5 (these are canonical engine-side constructs, not Postbox bridges — they don't count as `_asPeer()`-style ADD wraps) +- **`import Postbox` drops:** 0 (both files retain `import Postbox` for unrelated symbols — this wave doesn't promise an import drop) +- **Postbox-free module count:** 0 net change + +## Out of scope + +- 7 `accountManager.mediaBox.resourceData(...)` sites — could use the existing `AccountManagerResources.data(resource:)` facade or a future `data(id:)` facade. Defer to a future drain wave. +- 22 `accountManager.mediaBox.cachedResourceRepresentation(...)` sites — Option A holdover, blocked by `CachedMediaResourceRepresentation` Postbox protocol leak. Needs facade-design pass. +- 3 `accountManager.mediaBox.storeCachedResourceRepresentation(...)` sites — same blocker. +- 2 `accountManager.mediaBox.cachedRepresentationCompletePath(...)` sites — same blocker. +- The `accountManager.mediaBox.cachedResourceRepresentation(...)` call at WallpaperResources:1261 and :1524 — directly adjacent to two of our migrated sites but blocked. Leave in place; the migrated `storeResourceData` call directly above it does not depend on it. + +## Memory file update + +After landing, update `project_postbox_refactor_next_wave.md`: +- Add wave 103 (retry) outcome line into the recent-waves section. +- Mark the 5 sites as drained; remove from candidate inventories. +- Promote the next candidate (likely the 7-site `resourceData` drain or one of the foundational waves). diff --git a/docs/superpowers/specs/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md b/docs/superpowers/specs/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md new file mode 100644 index 0000000000..defde5f1a7 --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-postbox-wave-104-account-manager-resource-data-drain-3-sites.md @@ -0,0 +1,148 @@ +# Wave 104 — accountManager.mediaBox.resourceData drain (3 clean sites) + +**Date:** 2026-04-26 +**Pattern:** wave-shape-G drain of an existing TelegramCore facade (the wave-32 / wave-94 `AccountManagerResources.data(resource:pathExtension:waitUntilFetchStatus:attemptSynchronously:)`) with a documented field rename at consumer sites (`.complete` → `.isComplete`). +**Module:** `submodules/WallpaperResources/Sources/WallpaperResources.swift` only. + +## Goal + +Drain 3 of 8 `accountManager.mediaBox.resourceData(...)` Shape-A sites against the existing facade. Net effect: −3 raw `accountManager.mediaBox.X` accesses, +3 facade calls, +3 `EngineMediaResource(...)` wraps, +3 consumer `.complete` → `.isComplete` renames. + +The remaining 5 sites are deferred: 2 (`FetchCachedRepresentations.swift:482, 490`) flow `data: MediaResourceData` into `fetchCachedScaledImageRepresentation` / `fetchCachedBlurredWallpaperRepresentation` — both expect raw Postbox `MediaResourceData`, so migration would force a cascade or boundary reconstruction. 3 (`WallpaperResources.swift:33, 59, 401`) are coupled to postbox-side via `combineLatest(accountManager.mediaBox.resourceData, account.postbox.mediaBox.resourceData)` returning typed `Signal<(MediaResourceData, MediaResourceData), NoError>` — migrating one side without the other breaks the tuple type. + +## Wave-71-shadow risk inventory (per `feedback_wave71_shadow_risk.md`) + +| Layer | Applicable? | Notes | +|---|---|---| +| 1. Downcasts | N/A | No type-level migration | +| 2. Peer-protocol extension method calls | N/A | Not a peer migration | +| 3. Field flow into Peer-typed function parameters | Adapted: result-type flow into MediaResourceData-typed params | **Cleared:** all 3 sites consume `maybeData.complete` and `maybeData.path` inline within the closure — no flow-out to functions taking raw `MediaResourceData`. Sites that DO flow out (482/490) are deferred. | +| 4. Message-builder cascade | N/A | No `Message(...)` construction touched | + +The "data: MediaResourceData" parameter at `fetchCachedScaledImageRepresentation:311` / `fetchCachedBlurredWallpaperRepresentation:453, 502` is the analogue of the wave-103 `Message.peers` constructor barrier — a Postbox-typed function-parameter barrier that forces ADD bridges if upstream migrates. The 3 chosen sites do not cross this barrier; the deferred 2 sites do. + +## Sites (3 total) + +### Call rewrites + +| Line | Existing call | Migrated call | +|---|---|---| +| 957 | `let maybeFetched = accountManager.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)` | `let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(reference.resource), attemptSynchronously: synchronousLoad)` | +| 1164 | `let maybeFetched = accountManager.mediaBox.resourceData(fileReference.media.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad)` | `let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(fileReference.media.resource), attemptSynchronously: synchronousLoad)` | +| 1264 | `return accountManager.mediaBox.resourceData(file.file.resource)` | `return accountManager.resources.data(resource: EngineMediaResource(file.file.resource))` | + +**`waitUntilFetchStatus: false` is omitted** in the migrated form — the facade signature has `waitUntilFetchStatus: Bool = false` as default. Sites 957/1164 explicitly pass `false`; site 1264 uses the underlying default. + +### Consumer-side renames (`.complete` → `.isComplete`) + +| Line | Existing | Migrated | +|---|---|---| +| 961 | ` if maybeData.complete {` | ` if maybeData.isComplete {` | +| 1168 | ` if maybeData.complete && isSupportedTheme {` | ` if maybeData.isComplete && isSupportedTheme {` | +| 1266 | ` if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {` | ` if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) {` | + +### Sites NOT migrated (deferred) + +- L33, L59, L401 — `combineLatest(accountManager.mediaBox.resourceData(X), account.postbox.mediaBox.resourceData(X))` (typed tuple return) +- L482, L490 (in FetchCachedRepresentations.swift, not WallpaperResources.swift) — `data: MediaResourceData` flow-out cascade +- The closure bodies at sites 957, 1164 contain INNER `account.postbox.mediaBox.resourceData(...)` calls and inner `data.complete` accesses on a different binding (the postbox-side result) — those stay raw and are NOT touched by this wave. Only the OUTER `maybeFetched`-typed result is migrated. + +## Type reference + +Facade signature (existing, wave-32 / wave-94): + +```swift +public func data( + resource: EngineMediaResource, + pathExtension: String? = nil, + waitUntilFetchStatus: Bool = false, + attemptSynchronously: Bool = false +) -> Signal +``` + +`EngineMediaResource.ResourceData` (final class at `TelegramCore/Sources/TelegramEngine/Resources/TelegramEngineResources.swift:149`): +- `public let path: String` (matches `MediaResourceData.path`) +- `public let availableSize: Int64` +- `public let isComplete: Bool` (renamed from `MediaResourceData.complete`) + +`EngineMediaResource(_ resource: MediaResource)` constructor — canonical wrap (CLAUDE.md cheat sheet). + +## Edit patterns + +6 separate Edit calls in 1 file. No `replace_all=true` opportunity — each call/rename has unique surrounding text. + +**Order (recommended):** call rewrites first (3 edits), then consumer renames (3 edits). This sequence keeps the file in a half-migrated but compilable state between batches if interrupted (call site uses new facade, consumer still on old field name → swift compile error caught quickly). + +Alternative order: per-site bundled (call + rename pair, then next pair) — also fine. + +## Risk register + +| Risk | Mitigation | +|---|---| +| `EngineMediaResource(rawResource)` constructor missing | Verified: constructor exists per CLAUDE.md cheat sheet ("EngineMediaResource(rawResource) — wrap a raw MediaResource"). | +| `.path` field mismatch | Verified: both `MediaResourceData.path` and `EngineMediaResource.ResourceData.path` are `String`. No edit needed at any `data.path` usage site. | +| `.availableSize` not exposed by `MediaResourceData` | None of the 3 consumers use `.availableSize`. Only `.complete` (renamed) and `.path` (unchanged) are used. | +| Inner `data.complete` accesses on postbox-side bindings get renamed by accident | The 3 renames are on distinct bindings (`maybeData`, `maybeData`, `data`) within distinct outer scopes. The inner `data.complete` at L968 (postbox-side closure body inside site 957) is on a DIFFERENT `data` binding — its surrounding text differs (`return data.complete ? try? Data(...)` vs the migrated `if data.complete, let imageData = try? Data(...)`). Each Edit's `old_string` includes enough surrounding text to disambiguate. | +| `Signal.complete()` confused with field rename | The renames target `.complete` (property access). `Signal.complete()` is a method call, syntactically distinct (`return .complete()`). No regex collision. | +| `attemptSynchronously: synchronousLoad` arg flows | Facade exposes `attemptSynchronously: Bool = false`. Site 957/1164 pass `synchronousLoad` (a function param of the same name, Bool-typed) — flows through unchanged. | +| Build cascade beyond touched file | WallpaperResources is foundational with wide rebuild fan-out, but the public API is unchanged so dependents don't recompile. Build cost projection: ~30-60s. | + +## Wave shape + +**Classification:** wave-shape-G drain of an existing TelegramCore facade with a documented consumer field rename. Mid-difficulty between wave-103-retry (pure mechanical) and wave-71-shadow (cascade-prone). +**Iteration budget:** 1 (target first-pass-clean given small footprint and verified pre-flight inventory). +**Subagent dispatch:** not needed — 6 edits in 1 file is single-implementer scope. + +## Verification + +### Build + +```sh +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 +``` + +(No `--continueOnError` — small atomic scope.) Build cost projection: ~30-60s. + +### Post-edit residue grep (expect specific output, NOT empty) + +```sh +grep -rn "accountManager\.mediaBox\.resourceData" submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: 3 lines remaining (L33, L59, L401 — the deferred combineLatest sites). Sites 957, 1164, 1264 should NOT appear. + +```sh +grep -nE "maybeData\.complete\b|^.*\bdata\.complete\b" submodules/WallpaperResources/Sources/WallpaperResources.swift +``` + +Expected: a small number of lines remaining at unrelated sites (the postbox-side inner closure at site-957's L968 still uses `data.complete` on a postbox-side binding — that stays). Lines 961, 1168, 1266 should NOT appear. + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| Raw `mediaBox.resourceData` accesses dropped | −3 | WR:957, 1164, 1264 | +| Facade calls added | +3 | same sites, migrated form | +| `EngineMediaResource(...)` wraps added | +3 | canonical engine-side wraps, not Postbox bridges | +| Consumer `.complete` → `.isComplete` renames | +3 | WR:961, 1168, 1266 | +| `import Postbox` drops | 0 | WallpaperResources retains Postbox import for unrelated symbols | +| Postbox-free module count | 0 | unchanged | + +## Out of scope + +- Sites 482/490 in FetchCachedRepresentations.swift — `data: MediaResourceData` cascade through `fetchCachedScaled*Representation` family. Defer to a session that designs the appropriate facade or migrates the cascade as a co-wave. +- Sites 33/59/401 in WallpaperResources.swift — `combineLatest(accountManager.mediaBox.resourceData, account.postbox.mediaBox.resourceData)` typed-tuple coupling. Defer until postbox-side `account.postbox.mediaBox.resourceData` is also drainable (Shape-C territory) or a paired-resource facade is designed. +- The 22 `cachedResourceRepresentation` accountManager-side sites — blocked by `CachedMediaResourceRepresentation` Postbox protocol leak. + +## Memory file update + +After landing, update `project_postbox_refactor_next_wave.md`: +- Add wave 104 outcome line into the recent-waves section. +- Update accountManager-side facade drain status table: `resourceData` count drops from 8 → 5 (3 drained, 5 deferred). +- Note the `fetchCachedScaled*Representation` cascade barrier — adds it to the list of "Postbox-typed-function-parameter barriers" alongside `Message.peers: SimpleDictionary`. diff --git a/docs/superpowers/specs/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md b/docs/superpowers/specs/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md new file mode 100644 index 0000000000..5147af412a --- /dev/null +++ b/docs/superpowers/specs/2026-04-26-postbox-wave-105-device-contact-info-subject-engine-peer.md @@ -0,0 +1,183 @@ +# Wave 105 — DeviceContactInfoSubject enum payload Peer? → EnginePeer? + +**Date:** 2026-04-26 +**Pattern:** Multi-module enum-payload migration with completion-callback signature change (wave-91 shape — `ItemListWebsiteItem.peer + RecentSessionsController.website case payload + openWebSession callback`). +**Modules:** `AccountContext` (enum + computed property), `PeerInfoUI` (`DeviceContactInfoController.swift` primary consumer), `TelegramUI` (4 construction sites across 4 files). + +## Goal + +Migrate `DeviceContactInfoSubject` enum's 3 case payloads from `Peer?` to `EnginePeer?`, plus 2 callback signatures (`.filter`'s `(Peer?, DeviceContactExtendedData) -> Void` and `.create`'s `(Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void`) and the public `peer: Peer?` computed property. Net effect: −10 wraps dropped, +2 wraps added (Chat-side construction barriers), +1 `as? TelegramUser` → `case let .user(...)` rewrite. **Net wrap delta: −8.** + +## Wave-71-shadow risk inventory (per `feedback_wave71_shadow_risk.md`) + +| Layer | Result | Notes | +|---|---|---| +| 1. Downcasts (`as?` / `is`) on the migrated value | **1 site** | `DeviceContactInfoController.swift:849` — `if let peer = peer as? TelegramUser` becomes `if case let .user(peer) = peer`. Inner body accesses `peer.firstName`, `peer.lastName`, `peer.phone` — all `TelegramUser` fields, work after rebinding via case-let. | +| 2. Peer-protocol extension method calls | **0 blockers** | Inventory pass found ALL consumer-side access on the migrated bindings is `.id` only. No `Peer`-protocol-only methods (no `canSetupAutoremoveTimeout`, `displayTitle`, `addressName`, etc. on the migrated bindings). | +| 3. Field flow into `Peer`-typed function parameters | **2 ADD bridges** | `ChatControllerOpenAttachmentMenu.swift:683` and `:1850` — both pass `peerAndContactData.0` directly to `.filter(peer:)` constructor. The upstream signal type is explicitly `(Peer?, DeviceContactExtendedData?)` (see L634, L1822). After migration, the construction must wrap: `peerAndContactData.0.flatMap(EnginePeer.init)`. **Accepted barrier — net-negative wave delta still wins.** | +| 4. `Message`-builder / `SimpleDictionary` barriers | **0** | No `Message(...)` constructor calls or dict-store patterns on the migrated bindings. | + +The 2 ADD bridges in Layer 3 are the only wave cost; net delta after accounting for them is still −8. + +## Type changes + +### AccountContext.swift (lines 703-718) + +| Line | Before | After | +|---|---|---| +| 704 | `case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData)` | `case vcard(EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData)` | +| 705 | `case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void)` | `case filter(peer: EnginePeer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (EnginePeer?, DeviceContactExtendedData) -> Void)` | +| 706 | `case create(peer: Peer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void)` | `case create(peer: EnginePeer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (EnginePeer?, DeviceContactStableId, DeviceContactExtendedData) -> Void)` | +| 708 | `public var peer: Peer? {` | `public var peer: EnginePeer? {` | + +The `contactData: DeviceContactExtendedData` computed property at L719 is unchanged. + +## Edit patterns + +### Pattern A — `_asPeer()` drops at construction sites (5 sites) + +| File:Line | Before | After | +|---|---|---| +| DeviceContactInfoController.swift:1289 | `subject: .vcard(peer?._asPeer(), contactId, contactData)` | `subject: .vcard(peer, contactId, contactData)` | +| DeviceContactInfoController.swift:1443 | `subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in` | `subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in` | +| DeviceContactInfoController.swift:1489 | `subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in` | `subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in` | +| StoryItemSetContainerViewSendMessage.swift:2132 | `subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in` | `subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in` | +| OpenChatMessage.swift:443 | `subject: .vcard(peer?._asPeer(), nil, contactData)` | `subject: .vcard(peer, nil, contactData)` | + +Each source `peer` is already `EnginePeer?` (verified per-site: line 1289 in `addContactToExisting` callback already typed `(EnginePeer?, ...)` at L1409; line 1443 in `dataSignal` callback that returns `(EnginePeer?, ...)`; line 1489 in `addContactOptionsController(peer: EnginePeer?, ...)`; line 2132 in `(EnginePeer?, ...)` signal; line 443 in `peer: EnginePeer?` source). + +### Pattern B — `_asPeer()` drops at completion-call sites (2 sites) + +| File:Line | Before | After | +|---|---|---| +| DeviceContactInfoController.swift:1105 | `completion(peerAndContactData.0?._asPeer(), filteredData)` | `completion(peerAndContactData.0, filteredData)` | +| DeviceContactInfoController.swift:1224 | `completion(contactIdAndData.2?._asPeer(), contactIdAndData.0, contactIdAndData.1)` | `completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1)` | + +The completion's first parameter type changes from `Peer?` to `EnginePeer?` per the enum migration. Source values (`peerAndContactData.0` and `contactIdAndData.2`) are already `EnginePeer?` (typed signal pipelines), so dropping `_asPeer()` is a clean simplification. + +### Pattern C — `.flatMap(EnginePeer.init)` simplifications (3 sites) + +DeviceContactInfoController.swift:941-946 region: + +| File:Line | Before | After | +|---|---|---| +| 941-942 | `case let .vcard(peer, id, data):\n contactData = .single((peer.flatMap(EnginePeer.init), id, data))` | `case let .vcard(peer, id, data):\n contactData = .single((peer, id, data))` | +| 943-944 | `case let .filter(peer, id, data, _):\n contactData = .single((peer.flatMap(EnginePeer.init), id, data))` | `case let .filter(peer, id, data, _):\n contactData = .single((peer, id, data))` | +| 945-946 | `case let .create(peer, data, share, shareViaExceptionValue, _):\n contactData = .single((peer.flatMap(EnginePeer.init), nil, data))` | `case let .create(peer, data, share, shareViaExceptionValue, _):\n contactData = .single((peer, nil, data))` | + +After migration, the destructured `peer: EnginePeer?` is already the target type — the `.flatMap(EnginePeer.init)` round-trip becomes redundant. + +### Pattern D — Downcast → case-let (1 site) + +| File:Line | Before | After | +|---|---|---| +| DeviceContactInfoController.swift:849 | `if let peer = peer as? TelegramUser {` | `if case let .user(peer) = peer {` | + +The outer `peer` is bound from `case let .create(peer, contactData, _, _, _) = subject` at L845, which becomes `EnginePeer?` post-migration. `case let .user(peer) = peer` rebinds the inner `peer` to `TelegramUser` (the `.user` case associated value). Inner body accesses `peer.firstName`, `peer.lastName`, `peer.phone` — all `TelegramUser` instance methods/properties, work transparently after rebinding. + +### Pattern E — ADD wraps at Chat-side construction (2 sites) + +| File:Line | Before | After | +|---|---|---| +| ChatControllerOpenAttachmentMenu.swift:683 | `subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in` | `subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in` | +| ChatControllerOpenAttachmentMenu.swift:1850 | `subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in` | `subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in` | + +Both sites have identical text — `Edit replace_all=true` bundles them. The upstream signal type is explicitly `(Peer?, DeviceContactExtendedData?)` (verified at L634 and L1822). The `.flatMap(EnginePeer.init)` wraps the optional `Peer?` to optional `EnginePeer?`. + +### Pattern F — Pass-through (no edit needed) + +These flow transparently through the type change: +- DeviceContactInfoController.swift:897, 1041, 1047 — `subject.peer` access (returns `EnginePeer?` post-migration, consumers use `.id` or `if let peer = subject.peer`) +- DeviceContactInfoController.swift:1041 — `.create(peer: subject.peer, ...)` — both sides EnginePeer? after migration +- DeviceContactInfoController.swift:1149-1163, 1183-1189 — destructured `peer` from `.create` becomes `EnginePeer?`, body accesses `peer.id` and passes to `completion(peer, ...)` (now `EnginePeer?`-accepting) +- ContactsController.swift:312, 785; OpenAddContact.swift:32; ComposeController.swift:220; ShareExtensionContext.swift:532 — `peer: nil` or `.vcard(nil, ...)` constructions, `nil` works for both optional types +- All callback consumer bodies that use `peer?.id` (StoryItemSetContainerViewSendMessage:2141, ChatControllerOpenAttachmentMenu:689, :1856) — `EnginePeer?.id` is `EnginePeer.Id` typealiased to `PeerId`, identical at usage sites + +**Total edits: 17 across 5 files.** AccountContext.swift (4) + DeviceContactInfoController.swift (9) + ChatControllerOpenAttachmentMenu.swift (1 with replace_all) + StoryItemSetContainerViewSendMessage.swift (1) + OpenChatMessage.swift (1) = ~16 Edit calls. + +## Risk register + +| Risk | Mitigation | +|---|---| +| `_asPeer()` source not actually `EnginePeer?` at one of the drop sites | Per-site source typing verified during inventory: 1289 (addContactToExisting callback typed `(EnginePeer?, ...)` at L1409), 1443 (dataSignal returns `(EnginePeer?, ...)`), 1489 (function param `peer: EnginePeer?` at L1481), 2132 (signal callback typed `(EnginePeer?, ...)`), 443 (local `peer: EnginePeer?`). All confirmed. | +| `subject.peer` consumers break | 3 access sites, all pattern `if let peer = subject.peer { ... peer.id ... }`. Body uses `.id` (transparent). | +| Closure capture aliases of destructured `peer` flow into untyped contexts | Inventory found 8 destructure sites; all body uses are `.id` access or pass-through to completion calls (whose signature also migrates). | +| Build cascade through AccountContext consumers | AccountContext is foundational. The enum + computed property changes cascade ALL consumers. Build cost projection: 60-180s. | +| `case let .user(peer)` rebinding shadow at L849 | The outer `peer` (EnginePeer?) is shadowed by the inner `peer` (TelegramUser). Inner body uses `peer.firstName`, `peer.lastName`, `peer.phone` — all TelegramUser fields. No reference to the outer EnginePeer? inside the if-body. Safe. | +| `.flatMap(EnginePeer.init)` simplification leaves wrong type | After migration, destructured `peer: EnginePeer?`. `.flatMap(EnginePeer.init)` would re-wrap to `EnginePeer?` (a no-op). Dropping is safe. | +| Pre-existing `import Postbox` removable from any of the 5 touched files | `import Postbox` should NOT be dropped speculatively — these files use Postbox for unrelated symbols (most consumers retain `Peer` references for non-DeviceContactInfoSubject paths). Defer Postbox-import drops to dedicated cleanup waves. | + +## Wave shape + +**Classification:** wave-91-pattern multi-module enum-payload + callback-signature migration. +**Iteration budget:** 1-3 (target 1; wave 91 took 2; this wave is similar size, slightly more complex). +**Subagent dispatch:** not needed — 17 edits in 5 files is single-implementer scope, but coordinator should review the diff carefully before commit given multi-module footprint. + +## Verification + +### Build + +```sh +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 +``` + +`--continueOnError` flag enabled given multi-module scope — surface all errors at once if iter-1 fails. + +### Post-edit residue grep (expect specific patterns) + +```sh +# Construction-site _asPeer drops complete: +grep -nE "subject:\s*\.(vcard|filter|create)\(.*_asPeer\(\)" \ + submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift \ + submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift \ + submodules/TelegramUI/Sources/OpenChatMessage.swift +# Expected: empty. + +# Completion _asPeer drops complete: +grep -nE "completion\(.*_asPeer\(\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +# Expected: empty. + +# .flatMap(EnginePeer.init) simplifications complete in DeviceContactInfoController: +grep -nE "peer\.flatMap\(EnginePeer\.init\)" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +# Expected: empty. + +# Downcast rewrite complete: +grep -nE "peer as\? TelegramUser" submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +# Expected: empty. + +# ADD wraps present at the 2 Chat sites: +grep -nE "peerAndContactData\.0\.flatMap\(EnginePeer\.init\)" submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +# Expected: 2 lines (683 and 1850, line numbers may shift slightly). +``` + +## Net delta projection + +| Category | Count | Sites | +|---|---|---| +| `_asPeer()` drops at construction | −5 | DeviceContactInfoController:1289, 1443, 1489 + StoryItemSetContainerViewSendMessage:2132 + OpenChatMessage:443 | +| `_asPeer()` drops at completion calls | −2 | DeviceContactInfoController:1105, 1224 | +| `.flatMap(EnginePeer.init)` simplifications | −3 | DeviceContactInfoController:942, 944, 946 | +| `EnginePeer.init` wraps added (Pattern E) | +2 | ChatControllerOpenAttachmentMenu:683, 1850 | +| Downcast → case-let conversions | +1 | DeviceContactInfoController:849 | +| Type annotations migrated | 4 | AccountContext: 3 enum cases + 1 computed property | + +**Net wrap delta:** **−8** (10 drops minus 2 adds). + +## Out of scope + +- `import Postbox` drops in any of the 5 touched files — they use Postbox for unrelated symbols. Defer to dedicated cleanup waves. +- Migrating the `peerAndContactData` upstream signal type from `(Peer?, DeviceContactExtendedData?)` to `(EnginePeer?, ...)` — would drop the 2 ADD bridges at Chat sites but cascades into multiple closures. Separate wave. +- `addContactToExisting`'s internal completion call sites — already typed `(EnginePeer?, ...)` per L1409, no migration needed in this wave. + +## Memory file update + +After landing, update `project_postbox_refactor_next_wave.md`: +- Add wave 105 outcome line into the recent-waves section. +- Mark `DeviceContactInfoSubject` candidate as drained. +- Note the wave-91-shape success — multi-module enum-payload migrations remain viable when pre-flight inventory clears layers 1-4. diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 4f79cd0036..66716ccf1a 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -701,11 +701,11 @@ public final class NavigateToChatControllerParams { } public enum DeviceContactInfoSubject { - case vcard(Peer?, DeviceContactStableId?, DeviceContactExtendedData) - case filter(peer: Peer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (Peer?, DeviceContactExtendedData) -> Void) - case create(peer: Peer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (Peer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) - - public var peer: Peer? { + case vcard(EnginePeer?, DeviceContactStableId?, DeviceContactExtendedData) + case filter(peer: EnginePeer?, contactId: DeviceContactStableId?, contactData: DeviceContactExtendedData, completion: (EnginePeer?, DeviceContactExtendedData) -> Void) + case create(peer: EnginePeer?, contactData: DeviceContactExtendedData, isSharing: Bool, shareViaException: Bool, completion: (EnginePeer?, DeviceContactStableId, DeviceContactExtendedData) -> Void) + + public var peer: EnginePeer? { switch self { case let .vcard(peer, _, _): return peer diff --git a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift index 0bd450fc56..c699a9fe67 100644 --- a/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift +++ b/submodules/PeerInfoUI/Sources/DeviceContactInfoController.swift @@ -846,7 +846,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, var peerPhoneNumber: String? var firstName = contactData.basicData.firstName var lastName = contactData.basicData.lastName - if let peer = peer as? TelegramUser { + if case let .user(peer) = peer { firstName = peer.firstName ?? "" lastName = peer.lastName ?? "" if let phone = peer.phone { @@ -939,11 +939,11 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, var shareViaException = false switch subject { case let .vcard(peer, id, data): - contactData = .single((peer.flatMap(EnginePeer.init), id, data)) + contactData = .single((peer, id, data)) case let .filter(peer, id, data, _): - contactData = .single((peer.flatMap(EnginePeer.init), id, data)) + contactData = .single((peer, id, data)) case let .create(peer, data, share, shareViaExceptionValue, _): - contactData = .single((peer.flatMap(EnginePeer.init), nil, data)) + contactData = .single((peer, nil, data)) isShare = share shareViaException = shareViaExceptionValue } @@ -1102,7 +1102,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, } else if case let .filter(_, _, _, completion) = subject { let filteredData = filteredContactData(contactData: peerAndContactData.2, excludedComponents: state.excludedComponents) rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.ShareMenu_Send), style: .bold, enabled: !filteredData.basicData.phoneNumbers.isEmpty, action: { - completion(peerAndContactData.0?._asPeer(), filteredData) + completion(peerAndContactData.0, filteredData) dismissImpl?(true) }) } else if case let .create(createForPeer, _, _, _, completion) = subject { @@ -1221,7 +1221,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, return state } if let contactIdAndData = contactIdAndData { - completion(contactIdAndData.2?._asPeer(), contactIdAndData.0, contactIdAndData.1) + completion(contactIdAndData.2, contactIdAndData.0, contactIdAndData.1) } completed?() dismissImpl?(true) @@ -1286,7 +1286,7 @@ public func deviceContactInfoController(context: ShareControllerAccountContext, return } addContactToExisting(context: accountContext, parentController: controller, contactData: subject.contactData, completion: { peer, contactId, contactData in - replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer?._asPeer(), contactId, contactData), completed: nil, cancelled: nil)) + replaceControllerImpl?(deviceContactInfoController(context: context, environment: environment, subject: .vcard(peer, contactId, contactData), completed: nil, cancelled: nil)) }) } openChatImpl = { [weak controller] peerId in @@ -1440,7 +1440,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie let _ = (dataSignal |> deliverOnMainQueue).start(next: { peer, stableId in guard let stableId = stableId else { - parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in + parentController.present(deviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in }), completed: nil, cancelled: nil), in: .window(.root)) return } @@ -1486,7 +1486,7 @@ func addContactOptionsController(context: AccountContext, peer: EnginePeer?, con controller.setItemGroups([ ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Profile_CreateNewContact, action: { [weak controller] in - controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer?._asPeer(), contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in + controller?.present(context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: context), environment: ShareControllerAppEnvironment(sharedContext: context.sharedContext), subject: .create(peer: peer, contactData: contactData, isSharing: peer != nil, shareViaException: false, completion: { _, _, _ in }), completed: nil, cancelled: nil), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) dismissAction() }), diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 76589daa99..f7bfd955df 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -2129,7 +2129,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { self.sendMessages(view: view, peer: targetPeer, messages: enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime) }) } else { - let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0?._asPeer(), contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in + let contactController = component.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: component.context), environment: ShareControllerAppEnvironment(sharedContext: component.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { [weak self, weak view] peer, contactData in guard let self, let view else { return } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index 267a19c295..50b7728237 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -680,7 +680,7 @@ extension ChatControllerImpl { strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime, postpone: postpone), postpone: postpone) }) } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { return } @@ -1847,7 +1847,7 @@ extension ChatControllerImpl { strongSelf.sendMessages([message], postpone: postpone) }) } else { - let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in + let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: ShareControllerAppAccountContext(context: strongSelf.context), environment: ShareControllerAppEnvironment(sharedContext: strongSelf.context.sharedContext), subject: .filter(peer: peerAndContactData.0.flatMap(EnginePeer.init), contactId: nil, contactData: contactData, completion: { peer, contactData in guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else { return } diff --git a/submodules/TelegramUI/Sources/OpenChatMessage.swift b/submodules/TelegramUI/Sources/OpenChatMessage.swift index 6284c66c28..addf21cee3 100644 --- a/submodules/TelegramUI/Sources/OpenChatMessage.swift +++ b/submodules/TelegramUI/Sources/OpenChatMessage.swift @@ -440,7 +440,7 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool { } else { contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: contact.firstName, lastName: contact.lastName, phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!!$_", value: contact.phoneNumber)]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "") } - let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer?._asPeer(), nil, contactData), completed: nil, cancelled: nil) + let controller = deviceContactInfoController(context: ShareControllerAppAccountContext(context: params.context), environment: ShareControllerAppEnvironment(sharedContext: params.context.sharedContext), updatedPresentationData: params.updatedPresentationData, subject: .vcard(peer, nil, contactData), completed: nil, cancelled: nil) params.navigationController?.pushViewController(controller) }) return true diff --git a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift index c157d3ef20..36ac6f790c 100644 --- a/submodules/TelegramUI/Sources/ThemeUpdateManager.swift +++ b/submodules/TelegramUI/Sources/ThemeUpdateManager.swift @@ -109,7 +109,7 @@ final class ThemeUpdateManagerImpl: ThemeUpdateManager { guard complete, let fullSizeData = fullSizeData else { return .complete() } - accountManager.mediaBox.storeResourceData(file.file.resource.id, data: fullSizeData, synchronous: true) + accountManager.resources.storeResourceData(id: EngineMediaResource.Id(file.file.resource.id), data: fullSizeData, synchronous: true) return .single((.cloud(PresentationCloudTheme(theme: theme, resolvedWallpaper: wallpaper, creatorAccountId: theme.isCreator ? account.id : nil)), presentationTheme)) } } else { diff --git a/submodules/WallpaperResources/Sources/WallpaperResources.swift b/submodules/WallpaperResources/Sources/WallpaperResources.swift index 5c33034f8f..438b4904d5 100644 --- a/submodules/WallpaperResources/Sources/WallpaperResources.swift +++ b/submodules/WallpaperResources/Sources/WallpaperResources.swift @@ -954,11 +954,11 @@ public func photoWallpaper(postbox: Postbox, photoLibraryResource: PhotoLibraryM } public func telegramThemeData(account: Account, accountManager: AccountManager, reference: MediaResourceReference, synchronousLoad: Bool = false) -> Signal { - let maybeFetched = accountManager.mediaBox.resourceData(reference.resource, option: .complete(waitUntilFetchStatus: false), attemptSynchronously: synchronousLoad) + let maybeFetched = accountManager.resources.data(resource: EngineMediaResource(reference.resource), attemptSynchronously: synchronousLoad) return maybeFetched |> take(1) |> mapToSignal { maybeData in - if maybeData.complete { + if maybeData.isComplete { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single(loadedData) } else { @@ -970,7 +970,7 @@ public func telegramThemeData(account: Account, accountManager: AccountManager take(1) |> mapToSignal { maybeData -> Signal<(PresentationTheme?, Data?), NoError> in - if maybeData.complete && isSupportedTheme { + if maybeData.isComplete && isSupportedTheme { let loadedData: Data? = try? Data(contentsOf: URL(fileURLWithPath: maybeData.path), options: []) return .single((loadedData.flatMap { makePresentationTheme(data: $0) }, nil)) } else { @@ -1211,7 +1211,7 @@ public func themeImage(account: Account, accountManager: AccountManager mapToSignal { data in - if data.complete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { + if data.isComplete, let imageData = try? Data(contentsOf: URL(fileURLWithPath: data.path)) { return .single((theme, .pattern(data: imageData, colors: file.settings.colors, intensity: intensity), thumbnailData)) } else { return .complete() @@ -1520,7 +1520,7 @@ public func themeIconImage(account: Account, accountManager: AccountManager