Squashes 20 commits — the implementation and outcome commits of
waves 37 through 43 plus wave 44's spec and implementation-plan
docs — into a single commit. Per-wave lessons remain recorded in
docs/superpowers/postbox-refactor-log.md. The unrelated "Add swift
svg" commit is preserved separately outside this squash.
Wave 37 — peerTokenTitle: peer Peer → EnginePeer (1 file)
Wave 38 — canSendMessagesToPeer: peer Peer → EnginePeer (12 files)
Wave 39 — AccountContext.makePeerInfoController: peer Peer → EnginePeer (52 files)
Wave 40 — makeChatQrCodeScreen + makeChatRecentActionsController bundle (8 files)
Wave 41 — RenderedChannelParticipant.peer: Peer → EnginePeer (28 files)
Wave 42 — PeerInfoScreenData.peer: Peer? → EnginePeer? (17 files)
Wave 43 — PeerInfoScreen 6 helpers: peer Peer? → EnginePeer? (12 files)
Wave 44 — RenderedChannelParticipant.peers design doc + implementation plan
(impl and outcome land in subsequent commits, not part of squash)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
33 KiB
Wave 41 — RenderedChannelParticipant.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 TelegramCore.RenderedChannelParticipant.peer from Postbox Peer to TelegramCore EnginePeer. Drop ~37 bridges (net ~−14 after adds) and eliminate 2 Shape-C ratchet wraps installed by wave 39.
Architecture: Single atomic commit. One TelegramCore struct field change + 16 TelegramCore internal construction sites wrapped with EnginePeer(peer) + 17 consumer files updated: ZERO sites untouched (~160), ~32 DROP sites unwrapped, 9 CAST sites rewritten to pattern-match, 3 ADD-ASPEER sites append ._asPeer(), 7 ADD-WRAP consumer constructors wrap raw Peer with EnginePeer.
Tech Stack: Swift, Bazel (Make.py wrapper), TelegramCore, Postbox → TelegramEngine refactor conventions per CLAUDE.md.
Build command:
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
File Structure
Created: none.
Modified (27 files):
TelegramCore (10 files):
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift— struct field type + init param + Equatable implsubmodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift— 1 constructor wrapsubmodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift— 1 constructor wrapsubmodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift— 7 constructor wrapssubmodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift— 1 constructor wrapsubmodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift— 1 constructor wrapsubmodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift— 2 constructor wrapssubmodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift— 1 constructor wrapsubmodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift— 1 constructor wrapsubmodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift— 1 constructor wrap
PeerInfoUI (6 files):
submodules/PeerInfoUI/Sources/ChannelAdminsController.swiftsubmodules/PeerInfoUI/Sources/ChannelBlacklistController.swiftsubmodules/PeerInfoUI/Sources/ChannelMembersController.swiftsubmodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swiftsubmodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swiftsubmodules/PeerInfoUI/Sources/ChannelPermissionsController.swift
Other consumers (11 files):
submodules/SearchPeerMembers/Sources/SearchPeerMembers.swiftsubmodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swiftsubmodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swiftsubmodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swiftsubmodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swiftsubmodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swiftsubmodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swiftsubmodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swiftsubmodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swiftsubmodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift(noparticipant.peeredits needed — all ZERO; file touched only if build surfaces type issues)submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift(no edits expected — onlyitem.peer.idreference is ZERO)
Task 1: Migrate the struct definition
File: submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift
- Step 1.1: Edit struct field, init param, and Equatable impl
Replace the entire struct body:
public struct RenderedChannelParticipant: Equatable {
public let participant: ChannelParticipant
public let peer: EnginePeer
public let peers: [PeerId: Peer]
public let presences: [PeerId: PeerPresence]
public init(participant: ChannelParticipant, peer: EnginePeer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) {
self.participant = participant
self.peer = peer
self.peers = peers
self.presences = presences
}
public static func ==(lhs: RenderedChannelParticipant, rhs: RenderedChannelParticipant) -> Bool {
return lhs.participant == rhs.participant && lhs.peer == rhs.peer
}
}
Note: the file already imports both Postbox (for Peer/PeerId/PeerPresence) and TelegramCore internal symbols (EnginePeer visible from within the same module). No import changes needed.
Task 2: Wrap TelegramCore-internal constructor sites
Each site receives a raw Peer and must now wrap it with EnginePeer(peer). All edits are identical in shape.
- Step 2.1:
submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift:65
Before:
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences))
After:
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: presences))
- Step 2.2:
submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift:255
Before:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences))
After:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences))
- Step 2.3:
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift— 7 constructor wraps
Line 271:
action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: peer))
// becomes:
action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer)))
Line 279 (two constructors on one line):
action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
// becomes:
action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer)))
Line 287 (two constructors on one line):
action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
// becomes:
action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer)))
Line 483 (two constructors on one line):
action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
// becomes:
action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer)))
- Step 2.4:
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift:140
Before:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences), isMember)
After:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences), isMember)
- Step 2.5:
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift:115
Before:
items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences))
After:
items.append(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: renderedPresences))
- Step 2.6:
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift:180
Before:
return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: accountUser, peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))]
After:
return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: EnginePeer(accountUser), peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences))]
- Step 2.7:
submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift:82
Before:
return RenderedChannelParticipant(participant: updatedParticipant, peer: peer, peers: peers, presences: presences)
After:
return RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences)
- Step 2.8:
submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift:262
Before:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: adminPeer, peers: peers, presences: presences))
After:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(adminPeer), peers: peers, presences: presences))
- Step 2.9:
submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift:95
Before:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))
After:
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences))
Task 3: Consumer — PeerInfoUI/ChannelAdminsController.swift
File: submodules/PeerInfoUI/Sources/ChannelAdminsController.swift
- Step 3.1: Line 326 — DROP
EnginePeer(participant.peer)wrap.
Before:
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: participant.presences[participant.peer.id].flatMap { EnginePeer.Presence($0) }, text: peerText.isEmpty ? .presence : .text(peerText, .secondary), label: label, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: action, setPeerIdWithRevealedOptions: { previousId, id in
After: replace peer: EnginePeer(participant.peer) → peer: participant.peer (leave the rest of the line intact).
- Step 3.2: Line 921 — DROP
._asPeer()in constructor.
Before:
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer._asPeer(), presences: presences))
After:
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer, presences: presences))
(peer here is already EnginePeer — confirmed by surrounding code where creatorPeer: EnginePeer? is assigned from this same loop variable.)
- Step 3.3: Line 926 — DROP
._asPeer()in constructor.
Before:
result.append(RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: .internal_groupSpecific), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: rank, subscriptionUntilDate: nil), peer: peer._asPeer(), peers: peers.mapValues({ $0._asPeer() }), presences: presences))
After: change peer: peer._asPeer() → peer: peer. Leave peers.mapValues({ $0._asPeer() }) intact — peers field is unchanged.
Task 4: Consumer — PeerInfoUI/ChannelBlacklistController.swift
File: submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift
- Step 4.1: Line 170 (or 381 — the site installed by wave 39; the file has one site
EnginePeer(participant.peer))
Before:
peer: EnginePeer(participant.peer)
After:
peer: participant.peer
Note: the file may have a single such site; use:
grep -n 'EnginePeer(participant\.peer)' submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift
and DROP every match.
Task 5: Consumer — PeerInfoUI/ChannelMembersController.swift
File: submodules/PeerInfoUI/Sources/ChannelMembersController.swift
- Step 5.1: Line 305 — CAST rewrite.
Before:
if let user = participant.peer as? TelegramUser, let _ = user.botInfo {
After:
if case let .user(user) = participant.peer, let _ = user.botInfo {
- Step 5.2: Line 334 — DROP wrap.
Before:
peer: EnginePeer(participant.peer)
After:
peer: participant.peer
- Step 5.3: Line 707 — DROP wrap (the wave-39-installed Shape-C wrap).
Before:
peer: EnginePeer(participant.peer)
After:
peer: participant.peer
Task 6: Consumer — PeerInfoUI/ChannelMembersSearchContainerNode.swift
File: submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift
This file has the most sites (4 CAST, 3 DROP pairs, 3 ADD-WRAP constructor sites).
- Step 6.1: Line 212 — DROP two wraps on one line.
Before:
peer: .peer(peer: EnginePeer(participant.peer), chatPeer: EnginePeer(participant.peer)),
After:
peer: .peer(peer: participant.peer, chatPeer: participant.peer),
- Step 6.2: Line 223 — DROP wrap.
Before:
interaction.peerSelected(EnginePeer(participant.peer), participant)
After:
interaction.peerSelected(participant.peer, participant)
- Step 6.3: Line 752 — CAST rewrite.
Before:
if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil {
After:
if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil {
-
Step 6.4: Line 884 — CAST rewrite. Same pattern as 6.3.
-
Step 6.5: Line 987 — ADD-WRAP constructor.
Before:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)
After:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer))
(peer here is raw Peer from peerView.peers[participant.peerId] — confirmed by surrounding iteration code.)
- Step 6.6: Line 994 — ADD-WRAP constructor.
Change peer: peer to peer: EnginePeer(peer). Full site for reference:
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: .legacyGroup(group))), promotedBy: creatorPeer?.id ?? context.account.peerId, canBeEditedByAccountPeer: creatorPeer?.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }))
Change only peer: peer, → peer: EnginePeer(peer),.
- Step 6.7: Line 998 — ADD-WRAP constructor.
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }))
Change only peer: peer, → peer: EnginePeer(peer),.
-
Step 6.8: Line 1052 — CAST rewrite. Same pattern as 6.3.
-
Step 6.9: Line 1136 — CAST rewrite. Same pattern as 6.3.
Task 7: Consumer — PeerInfoUI/ChannelMembersSearchControllerNode.swift
File: submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift
- Step 7.1: Line 148 — DROP wrap.
Before:
peer: EnginePeer(participant.peer)
After:
peer: participant.peer
(The line has the wrap appearing twice — search the file for EnginePeer(participant.peer) and drop each occurrence. Use Edit with replace_all if unambiguous.)
- Step 7.2: Line 404 — ADD-WRAP constructor.
Before:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences)
After:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer), presences: peerView.peerPresences)
- Step 7.3: Line 409 — ADD-WRAP constructor.
Change peer: peer, → peer: EnginePeer(peer), in the full line:
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: ChannelParticipantAdminInfo(rights: TelegramChatAdminRights(rights: TelegramChatAdminRightsFlags.peerSpecific(peer: EnginePeer(mainPeer))), promotedBy: creator.id, canBeEditedByAccountPeer: creator.id == context.account.peerId), banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences)
-
Step 7.4: Line 413 — ADD-WRAP constructor. Same
peer: peer,→peer: EnginePeer(peer),. -
Step 7.5: Line 516 — CAST rewrite.
Before:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
After:
if case let .user(user) = participant.peer, user.botInfo != nil {
- Step 7.6: Line 558 — CAST rewrite. Same pattern as 7.5.
Task 8: Consumer — PeerInfoUI/ChannelPermissionsController.swift
File: submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift
- Step 8.1: Lines 480 and 483 — DROP wraps.
Both lines contain EnginePeer(participant.peer). Change each to participant.peer.
If the two occurrences are unambiguous, use Edit with replace_all=true on EnginePeer(participant.peer) → participant.peer.
Task 9: Consumer — SearchPeerMembers/SearchPeerMembers.swift
File: submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift
- Step 9.1: Lines 30, 36, 61, 76 — DROP wraps.
All four sites are EnginePeer(participant.peer). Use Edit with replace_all=true:
- old:
EnginePeer(participant.peer) - new:
participant.peer
Verify with grep -n 'EnginePeer(participant\.peer)' submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift → should return empty after edit.
Task 10: Consumer — ChatRecentActionsController.swift
File: submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift
- Step 10.1: Line 359 — DROP wrap.
Before:
EnginePeer(participant.peer)
After:
participant.peer
Task 11: Consumer — ChatRecentActionsFilterController.swift
File: submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift
- Step 11.1: Line 217 — DROP wrap.
Change EnginePeer(participant.peer) → participant.peer on line 217.
- Step 11.2: Line 445 — ADD-WRAP constructor rewrite.
Before:
if let peer = peer, case let .user(user) = peer {
return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: user)
}
After:
if let peer = peer, case let .user(user) = peer {
return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: .user(user))
}
(.user(user) is the enum case EnginePeer.user(TelegramUser). Alternative: peer: EnginePeer(user) or peer: peer — but peer: peer reuses the already-unwrapped EnginePeer and is the cleanest. Use peer: peer.)
Preferred after:
if let peer = peer, case let .user(user) = peer {
return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer)
}
Task 12: Consumer — ChatRecentActionsHistoryTransition.swift
File: submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift
This is the highest-volume consumer file (12 EnginePeer(new.peer) sites + 2 ADD-ASPEER sites).
- Step 12.1: DROP all
EnginePeer(new.peer)wraps.
Use Edit with replace_all=true:
- old:
EnginePeer(new.peer) - new:
new.peer
After: grep EnginePeer(new\.peer) should return empty.
- Step 12.2: Line 675 — ADD-ASPEER.
Before:
peers[participant.peer.id] = participant.peer
After:
peers[participant.peer.id] = participant.peer._asPeer()
(Target dict is SimpleDictionary<PeerId, Peer>; the value side needs raw Peer.)
- Step 12.3: Line 2275 — ADD-ASPEER.
Before:
peers[new.peer.id] = new.peer
After:
peers[new.peer.id] = new.peer._asPeer()
Task 13: Consumer — PeerInfoMembers.swift
File: submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift
- Step 13.1: Line 33 — ADD-ASPEER.
Before:
var peer: Peer {
switch self {
case let .channelMember(participant, _):
return participant.peer
After:
var peer: Peer {
switch self {
case let .channelMember(participant, _):
return participant.peer._asPeer()
No other edits in this file. The participant.peer.id accesses at lines 22, 44 are ZERO; item.peer.id at line 171 is ZERO.
Task 14: Consumer — ShareWithPeersScreenState.swift
File: submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift
- Step 14.1: Line 558 — DROP wrap.
Before:
peers.append(EnginePeer(participant.peer))
After:
peers.append(participant.peer)
- Step 14.2: Line 566 — CAST rewrite.
Before:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
After:
if case let .user(user) = participant.peer, user.botInfo != nil {
- Step 14.3: Line 576 — DROP wrap.
Before:
peers.append(EnginePeer(participant.peer))
After:
peers.append(participant.peer)
Task 15: Consumer — AdminUserActionsSheet.swift
File: submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift
This file has ~6 EnginePeer(peer.peer) / EnginePeer(component.peers[0].peer) wraps and many ZERO sites.
- Step 15.1: Use Edit with
replace_all=true: - old:
EnginePeer(peer.peer) - new:
peer.peer
This covers lines 284, 522, 523.
- Step 15.2: Edit the
EnginePeer(component.peers[0].peer)sites at lines 404, 416, 417.
Use Edit with replace_all=true:
-
old:
EnginePeer(component.peers[0].peer) -
new:
component.peers[0].peer -
Step 15.3: Verify no other
EnginePeer(wraps around.peeraccesses remain onRenderedChannelParticipant. Run:
grep -n 'EnginePeer(.*\.peer)' submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift
Confirm remaining matches are on non-RCP types (e.g., some other context-derived peer).
Task 16: Consumer — StoryContentLiveChatComponent.swift
File: submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift
- Step 16.1: Line 370 — DROP
._asPeer()in constructor.
Before:
peer: author._asPeer()
After:
peer: author
(author is EnginePeer — confirmed by the surrounding code that uses author.id and by the chatPeer signal's return type.)
Task 17: Consumer — ChatControllerAdminBanUsers.swift
File: submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift
- Step 17.1: Line 226 — ADD-WRAP constructor.
Before:
let peer = author
renderedParticipants.append(RenderedChannelParticipant(
participant: participant,
peer: peer
))
After:
let peer = author
renderedParticipants.append(RenderedChannelParticipant(
participant: participant,
peer: EnginePeer(peer)
))
(Confirmed author is raw Peer via presentMultiBanMessageOptions(... authors: [Peer], ...) signature on line 45.)
- Step 17.2: Line 372 — DROP
._asPeer()in constructor.
Before:
peer: authorPeer._asPeer()
After:
peer: authorPeer
(Confirmed authorPeer is EnginePeer? at line 327 via engine.data.get(Peer.Peer(id:)) signal; already guard-unwrapped.)
- Step 17.3: Line 757 — DROP
._asPeer()in constructor.
Same edit pattern as 17.2: peer: authorPeer._asPeer() → peer: authorPeer.
Task 18: Full build verification
- Step 18.1: Run the full build with
--continueOnError.
source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion --cacheDir ~/telegram-bazel-cache build --configurationPath build-system/appstore-configuration.json --gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git --gitCodesigningType development --gitCodesigningUseCurrent --buildNumber=1 --configuration=debug_sim_arm64 --continueOnError
Expected: build success. First-pass-clean is the goal (wave-39 pattern applies — classification is exact, migration is mechanical, no inference-bearing return types).
If the build fails, expect errors only in files in this plan. Any error outside the plan's file list is either:
- a pre-existing unrelated WIP (e.g.,
ChatMessageTransitionNode.swift) — not a wave-41 issue - a genuine miss in pre-flight classification — record which file, update the plan, and re-run
For each error in wave-41 files:
- Read the error
- Classify: is it a shape we mis-identified (ZERO that's not actually transparent) or a new shape (dict subscript, function arg to a
Peer-typed param, etc.)? - Apply the appropriate fix (
._asPeer()if raw Peer needed; unwrap the wrap if EnginePeer needed) - Re-run the build
Budget: 1–3 build iterations.
- Step 18.2: Post-build grep verification.
Run these greps and confirm they return only the expected residual matches:
grep -rn 'EnginePeer(participant\.peer)' submodules/ --include='*.swift' | grep -v submodules/TelegramCore/ | grep -v submodules/Postbox/
Expected: empty.
grep -rn 'EnginePeer(new\.peer)' submodules/ --include='*.swift' | grep -v submodules/TelegramCore/
Expected: empty.
grep -rn 'participant\.peer as\? TelegramUser' submodules/ --include='*.swift'
Expected: empty.
grep -n 'public let peer:' submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift
Expected: public let peer: EnginePeer.
Task 19: Commit
- Step 19.1: Stage only wave-41 files (explicitly enumerate — wave-39 lesson).
git status --short
Inspect the output. Only wave-41 files should appear as modified. If pre-existing WIP (e.g., submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift) is also modified, do NOT include it in the commit.
git add \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \
submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \
submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \
submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \
submodules/PeerInfoUI/Sources/ChannelMembersController.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \
submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift \
submodules/SearchPeerMembers/Sources/SearchPeerMembers.swift \
submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift \
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsController.swift \
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsFilterController.swift \
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoMembers.swift \
submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreenState.swift \
submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentLiveChatComponent.swift \
submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift \
docs/superpowers/specs/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration-design.md \
docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md
(Add any additional files the build iterations surfaced.)
Run git status --short and confirm only staged wave-41 files are green, and any unrelated WIP is still marked as unstaged.
- Step 19.2: Commit.
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 41
Migrate RenderedChannelParticipant.peer from Postbox `Peer` to
TelegramCore `EnginePeer`. 27 files touched: 10 TelegramCore
(1 struct + 9 files with constructor wraps) + 17 consumer files.
Drops the 2 Shape-C wraps installed by wave 39 (ChannelMembersController
and ChannelBlacklistController) plus ~37 additional EnginePeer(...) /
._asPeer() bridges across the consumer surface. Net ~-14 bridges
after the 16 TelegramCore-internal EnginePeer(peer) wraps and the 7
consumer ADD-WRAP constructor sites. RCP.peers and RCP.presences
dictionaries remain Postbox-typed (deferred).
EOF
)"
- Step 19.3: Confirm commit landed and working tree is clean except for pre-existing WIP.
git status --short
git log -1 --oneline
Task 20: Log the wave outcome
- Step 20.1: Append wave 41 entry to
docs/superpowers/postbox-refactor-log.md.
Format (matching prior wave entries):
## Wave 41 outcome — RenderedChannelParticipant.peer: Peer → EnginePeer (2026-04-24)
Landed as commit `<hash>`. 27 files / ~45 site edits / net ~-14 bridges.
**Shape distribution:**
- TelegramCore: 16 constructor sites wrapped with `EnginePeer(peer)` across 9 files + struct field migrated in ChannelParticipants.swift
- Consumers: ~32 DROP (EnginePeer/._asPeer unwraps), 9 CAST (as? TelegramUser → if case let .user), 3 ADD-ASPEER, 7 ADD-WRAP constructor sites
**First-pass-clean:** <yes|no, iterations count>. Extends wave-39 lesson: first-pass-clean
is achievable when classification is exact and all patterns are mechanical.
**Ratchet economics:** drops 2 wave-39 Shape-C wraps
(ChannelMembersController:707, ChannelBlacklistController:381) and installs 7 ADD-WRAP
consumer constructor sites as ratchet markers for a future
`RenderedChannelParticipant.peers: [PeerId: Peer] → [EnginePeer.Id: EnginePeer]` wave.
**Spec:** `docs/superpowers/specs/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration-design.md`.
**Plan:** `docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md`.
-
Step 20.2: Update the
project_postbox_refactor_next_wave.mdmemory file with the wave 41 outcome and the wave 42 candidate (likelyPeerInfoScreenData.peer → EnginePeer). -
Step 20.3: Commit docs updates.
git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 41 outcome
EOF
)"