Narrow consumer-module migration of raw Message types to EngineMessage in
the StorageUsageScreen component. Two files touched; the module keeps
import Postbox because of two out-of-scope site clusters (preferences-
view observation + a Media-carrying Icon enum case) — both flagged in
CLAUDE.md and the wave doc as future-wave targets.
StorageUsageScreen.swift:
SelectionState.togglePeer(availableMessages:)
[EngineMessage.Id: Message] -> [EngineMessage.Id: EngineMessage]
AggregatedData.messages
[MessageId: Message] -> [EngineMessage.Id: EngineMessage]
AggregatedData.clearIncludeMessages / .clearExcludeMessages
[Message] -> [EngineMessage]
AggregatedData.init messages param — same swap
RenderResult.messages
[MessageId: Message] -> [EngineMessage.Id: EngineMessage]
openMessage(message: Message) -> openMessage(message: EngineMessage)
(unwrap to raw at OpenChatMessageParams
/ chatMediaListPreviewControllerData
call sites via ._asMessage())
StorageFileListPanelComponent.swift:
Item.message: Message -> EngineMessage
(internal .id / .timestamp / .media
usage compiles unchanged against the
EngineMessage class).
Wave-7 facade-boundary bridging dropped:
- renderStorageUsageStatsMessages call site: the .mapValues(EngineMessage.init)
on existingMessages and .mapValues { $0._asMessage() } on the result vanish;
AggregatedData.messages and RenderResult.messages are now engine-typed on
both sides of the facade.
- clearStorage call sites (2): the .map(EngineMessage.init) wraps around
includeMessages / excludeMessages vanish; locals become [EngineMessage].
- Inside AggregatedData.updateSelected... accumulation loop, four
item.message._asMessage() calls (where item.message was EngineMessage
and the target was [Message]) drop back to plain item.message.
- StorageMediaGridPanelComponent.Item(message: EngineMessage(message), ...)
at the RenderResult-build loop loses the EngineMessage(...) wrap since
`message` is already EngineMessage.
Out of scope (module keeps import Postbox):
- StorageUsageScreen.swift:1047-1062 / 3131-3185 — AccountSpecificCache-
StorageSettings observation via postbox.combinedView + PreferencesView
and a postbox.transaction block doing transaction.getPeer / getPeerCached-
Data as? CachedGroupData/CachedChannelData for peer-category classification.
- StorageFileListPanelComponent.swift:105 — Icon.media(Media, ...) enum case.
Constructed only as .media(TelegramMediaFile, ...) or .media(TelegramMedia-
Image, ...); trivial future wave to split into two cases.
Build verified green: 59s incremental, 27 actions, Telegram.ipa produced.
Plan: docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.7 KiB
Postbox → TelegramEngine Wave 8 Implementation Plan
For agentic workers: This plan was executed in a single session; steps below are a post-hoc record of the work landed, not a to-do list.
Goal: StorageUsageScreen consumer-module migration — drop all raw Message domain types from the screen's internal storage and public peer-panel item types, and eliminate the wave-7 facade-boundary bridging. Scope is narrower than a full de-Postbox of the module: direct postbox.combinedView / postbox.transaction sites for AccountSpecificCacheStorageSettings observation are left for a future wave.
Architecture: Two files modified. StorageFileListPanelComponent.Item.message and StorageUsageScreen's AggregatedData + RenderResult + SelectionState internal types are migrated from raw Message/[Message]/[MessageId: Message] to EngineMessage/[EngineMessage]/[EngineMessage.Id: EngineMessage]. The two external APIs that still take raw Message (OpenChatMessageParams.message, chatMediaListPreviewControllerData(message:)) are called with engineMessage._asMessage() at the call site.
Tech Stack: Swift / Bazel. No unit tests by repo policy — verification is a full project build.
Build command:
source ~/.zshrc 2>/dev/null; PATH=/opt/homebrew/opt/ruby/bin:`gem environment gemdir`/bin:$PATH 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
Scope
In scope:
StorageFileListPanelComponent.Item.message: Message→EngineMessage(the item type co-located with the panel component).StorageUsageScreen.Component.SelectionState.togglePeer(id:availableMessages: [EngineMessage.Id: Message])→[EngineMessage.Id: EngineMessage].StorageUsageScreen.Component.AggregatedData.messages: [MessageId: Message]→[EngineMessage.Id: EngineMessage].AggregatedData.clearIncludeMessages: [Message]/.clearExcludeMessages: [Message]→[EngineMessage](plus the corresponding local vars inAggregatedData.updateSelected...).AggregatedData.init(..., messages: [MessageId: Message])→[EngineMessage.Id: EngineMessage].StorageUsageScreen.Component.RenderResult.messages: [MessageId: Message]→[EngineMessage.Id: EngineMessage].openMessage(message: Message)→openMessage(message: EngineMessage).- Drop the now-redundant wave-7 facade-boundary bridging (
.mapValues(EngineMessage.init)onexistingMessages,.mapValues { $0._asMessage() }on the facade's engineMessages output,.map(EngineMessage.init)on the twoclearStoragecall sites,._asMessage()onitem.messageinside theAggregatedData.updateSelected...loop, andEngineMessage(message)inside theresult.imageItems.append(...)site).
Out of scope — left for a future wave:
- Direct postbox usage for
AccountSpecificCacheStorageSettingsobservation:StorageUsageScreen.swift:1047-1062and3131-3185. Blocksimport Postboxremoval. Requires engine equivalents forPostboxViewKey.preferences/PreferencesViewobservation and fortransaction.getPeer/transaction.getPeerCachedData— likely anEngineData-subscription based rewrite plus peer-category classification via already-existing engine APIs. StorageFileListPanelComponent'sIcon.media(Media, TelegramMediaImageRepresentation)enum case. Holds eitherTelegramMediaFileorTelegramMediaImage(always one of these two TelegramCore types perimageIconValue = .media(file, ...)and.media(image, ...)construction sites). Could be split into.mediaFile(TelegramMediaFile, ...)/.mediaImage(TelegramMediaImage, ...)to eliminate the rawMediaprotocol dependency; out of scope as it's unrelated to Message-type migration.
Tasks
Task 1: Migrate StorageFileListPanelComponent.Item.message
Files:
- Modify:
submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift—Item.messagetype andinit(message:)param.
No other changes inside the file. Internal usage (item.message.id, item.message.timestamp, item.message.media) already works on EngineMessage — EngineMessage.media returns [Media] (raw), so the as? TelegramMediaFile / as? TelegramMediaImage downcasts inside the for media in item.message.media loop still compile.
Task 2: Migrate StorageUsageScreen internal storage types
Files:
- Modify:
submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift
Change:
SelectionState.togglePeer(availableMessages: [EngineMessage.Id: Message])→[EngineMessage.Id: EngineMessage]. Body only usesmessageId.peerIdso no body change required.AggregatedData.messages/.clearIncludeMessages/.clearExcludeMessagestype declarations and the init param.- The selected-messages accumulation loop inside
AggregatedData(the block running from the photo/video/file/music category branches): dropitem.message._asMessage()in the two photo/video branches (imageItemsholds EngineMessage items, so._asMessage()was the EngineMessage→Message unwrap to fit the old[Message]local);item.messagein the file/music branches now passes through since Item.message is EngineMessage. RenderResult.messagestype.
Task 3: Drop wave-7 facade-boundary bridging
At StorageUsageScreen.swift:2397 the renderStorageUsageStatsMessages call previously wrapped input via (self.aggregatedData?.messages ?? [:]).mapValues(EngineMessage.init) and unwrapped output via .mapValues { $0._asMessage() }. With AggregatedData.messages and RenderResult.messages now EngineMessage-typed, both bridges vanish: the call just passes self.aggregatedData?.messages ?? [:] directly and assigns the result to result.messages unchanged.
At the two clearStorage call sites in StorageUsageScreen.swift (inside clearSelected(...)): aggregatedData.clearIncludeMessages.map(EngineMessage.init) → aggregatedData.clearIncludeMessages (same for excludeMessages), plus the local includeMessages: [Message] / excludeMessages: [Message] vars become [EngineMessage].
At the RenderResult-building loop (post-renderStorageUsageStatsMessages), StorageMediaGridPanelComponent.Item(message: EngineMessage(message), ...) → message: message since message is already EngineMessage.
Task 4: Migrate openMessage + external-API unwraps
openMessage(message: Message) → openMessage(message: EngineMessage). Two external APIs receive raw Message: pass message._asMessage() to OpenChatMessageParams(message:) inside openMessage, and to chatMediaListPreviewControllerData(message:) inside messageGaleryContextAction. Also drop the one-line let foundGalleryMessage: Message? = message + guard let galleryMessage = foundGalleryMessage dance inside openMessage — it's a no-op wrap preserved from an older version.
Task 5: Full project build
Expected clean (cached — 30 seconds on an incremental build; ~60s from a cold start since wave 7).
Task 6: Commit
Single wave-8 atomic commit.
Outcome (2026-04-20)
Single atomic commit landing the migration. Build verified green (59s incremental, 27 actions). Net -5 lines (simplification, as expected — most changes are type swaps and a handful of redundant wraps/unwraps removed).
Two files modified:
| File | Δ |
|---|---|
submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageUsageScreen.swift |
+33 / -44 |
submodules/TelegramUI/Components/StorageUsageScreen/Sources/StorageFileListPanelComponent.swift |
+3 / -3 |
Module did NOT become Postbox-free. Both files retain import Postbox for the out-of-scope sites listed above. Drop-candidacy inventory in StorageUsageScreen.swift:
- 1047–1062: preferences-view observation of
AccountSpecificCacheStorageSettingsviapostbox.combinedView+PreferencesView. - 3131–3185: second preferences-view observation +
postbox.transaction { transaction in ... transaction.getPeer / transaction.getPeerCachedData as? CachedGroupData / CachedChannelData ... }for classifying peer-storage-timeout exceptions.
And in StorageFileListPanelComponent.swift:
- 105:
Icon.media(Media, TelegramMediaImageRepresentation)enum case.
Future wave targets either the preferences-view observation sites (substantial — EngineData-subscription rewrite + peer-category classification via engine APIs) or the Icon.media split (trivial — 3 sites to update).
Plan / record: docs/superpowers/plans/2026-04-20-postbox-to-telegramengine-wave-8.md.