Postbox -> TelegramEngine waves 37-43 + wave 44 design/plan (squashed)

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>
This commit is contained in:
isaac 2026-04-24 23:19:43 +04:00
parent e51fef57d8
commit 493f3103b3
121 changed files with 5298 additions and 350 deletions

View file

@ -41,7 +41,7 @@ A gradual migration is underway to eliminate direct `import Postbox` from consum
**Historical record:** Wave-by-wave outcomes, the running tally of Postbox-free modules, and full verbose forms of the guidance subsections below live in [`docs/superpowers/postbox-refactor-log.md`](docs/superpowers/postbox-refactor-log.md). Read that file when you need wave-specific context, a full worked example of a pattern, or the history of a particular module's migration.
Waves landed so far (as of 2026-04-24): 36 waves plus standalone cleanups. See the log file for per-wave detail; the list of still-open migration opportunities lives in the `project_postbox_refactor_next_wave.md` memory file.
Waves landed so far (as of 2026-04-24): 43 waves plus standalone cleanups. See the log file for per-wave detail; the list of still-open migration opportunities lives in the `project_postbox_refactor_next_wave.md` memory file.
### Rules that apply to every wave

View file

@ -0,0 +1,411 @@
# Wave 40 — `makeChatQrCodeScreen` + `makeChatRecentActionsController` 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:** Bundle migrate two sibling `AccountContext` methods deferred from wave 39 — `makeChatQrCodeScreen` (4 consumer sites) and `makeChatRecentActionsController` (3 consumer sites) — from raw `peer: Peer` to `peer: EnginePeer`, applying the body-shadow pattern.
**Architecture:** Body-shadow pattern (wave-38/39 style). Protocol + impl signatures change to `peer: EnginePeer`; each impl body gets a `let peer = peer._asPeer()` shadow so the downstream constructors (`ChatQrCodeScreenImpl`, `ChatRecentActionsController`) remain raw-`Peer` consumers (out of scope).
**Tech Stack:** Swift, Bazel, iOS; TelegramCore / AccountContext / TelegramUI / PeerInfoUI / StatisticsUI / SettingsUI / ContactListUI / PeerInfoScreen submodules.
**Reference:** Wave-39 "Out of scope" section in `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md`.
---
## Pre-flight classification
**`makeChatQrCodeScreen` (4 consumer sites):**
| # | Site | Shape | Edit |
|---|---|---|---|
| 1 | `SettingsSearchableItems.swift:974` | **Shape-A-variant** | Rewrite upstream `guard let peer = peer?._asPeer() else { return }` (line 971) → `guard let peer = peer else { return }`. Call stays `peer: peer`. |
| 2 | `SettingsSearchableItems.swift:992` | **Shape-A-variant** | Same pattern as #1 (upstream guard at line 989). |
| 3 | `ContactsController.swift:478` | **Shape-A** | Drop `._asPeer()` from `peer: peer._asPeer()``peer: peer`. Source: `Signal<EnginePeer, NoError>`. |
| 4 | `PeerInfoScreen.swift:4623` | **Shape-C** | Wrap: `peer: peer``peer: EnginePeer(peer)`. Source: `data.peer: Peer?`. |
**`makeChatRecentActionsController` (3 consumer sites):**
| # | Site | Shape | Edit |
|---|---|---|---|
| 5 | `ChannelAdminsController.swift:734` | **Shape-A** | Drop `._asPeer()`. Source: `engine.data.get(Peer.Peer(id:))``peer` is `EnginePeer` in the `guard let peer` on line 729. |
| 6 | `GroupStatsController.swift:915` | **Shape-A** | Drop `._asPeer()`. Source: `Signal<EnginePeer, NoError>` (mapToSignal at 906). |
| 7 | `PeerInfoScreenOpenChat.swift:115` | **Shape-C** | Wrap: `peer: peer``peer: EnginePeer(peer)`. Source: `self.data?.peer: Peer?`. |
**Net bridge delta:** 5 `_asPeer()` drops (sites 1, 2, 3, 5, 6) + 2 `EnginePeer(...)` wraps (sites 4, 7) = **3 net**. Sites 4 and 7 become ratchet markers for a future `PeerInfoScreenData.peer Peer → EnginePeer` wave.
---
## File touch summary
8 files:
1. `submodules/AccountContext/Sources/AccountContext.swift` — protocol decls (2 lines).
2. `submodules/TelegramUI/Sources/SharedAccountContext.swift` — impl signatures + body shadows (2 sites).
3. `submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift` — 2 Shape-A-variant upstream guard rewrites.
4. `submodules/ContactListUI/Sources/ContactsController.swift` — 1 Shape-A drop.
5. `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift` — 1 Shape-C wrap.
6. `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift` — 1 Shape-A drop.
7. `submodules/StatisticsUI/Sources/GroupStatsController.swift` — 1 Shape-A drop.
8. `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift` — 1 Shape-C wrap.
---
### Task 1: Update `AccountContext` protocol signatures
**Files:**
- Modify: `submodules/AccountContext/Sources/AccountContext.swift:1401` and `:1461`
- [ ] **Step 1: Update `makeChatRecentActionsController` decl**
```swift
// old_string
func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController
// new_string
func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController
```
- [ ] **Step 2: Update `makeChatQrCodeScreen` decl**
```swift
// old_string
func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController
// new_string
func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController
```
---
### Task 2: Update `SharedAccountContext` impls with body-shadow
**Files:**
- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift:2302` (makeChatRecentActionsController)
- Modify: `submodules/TelegramUI/Sources/SharedAccountContext.swift:2730` (makeChatQrCodeScreen)
- [ ] **Step 1: Update `makeChatRecentActionsController` impl**
```swift
// old_string
public func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController {
return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState)
}
// new_string
public func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController {
let peer = peer._asPeer()
return ChatRecentActionsController(context: context, peer: peer, adminPeerId: adminPeerId, starsState: starsState)
}
```
- [ ] **Step 2: Update `makeChatQrCodeScreen` impl**
```swift
// old_string
public func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController {
return ChatQrCodeScreenImpl(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary))
}
// new_string
public func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController {
let peer = peer._asPeer()
return ChatQrCodeScreenImpl(context: context, subject: .peer(peer: peer, threadId: threadId, temporary: temporary))
}
```
---
### Task 3: `SettingsSearchableItems.swift` — two Shape-A-variant guard rewrites
**Files:**
- Modify: `submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift:971` and `:989`
Both sites share the same structure: an upstream `guard let peer = peer?._asPeer() else { return }` unwraps `EnginePeer?` to `Peer`. Rewrite the guard to keep the local as `EnginePeer`; the call site below stays unchanged.
- [ ] **Step 1: Rewrite guard at line 971 (qr-code item)**
```swift
// old_string
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
return
}
let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false)
present(.push, controller)
})
}
)
)
//TODO:fix
items.append(
SettingsSearchableItem(
id: "qr-code/share",
// new_string
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false)
present(.push, controller)
})
}
)
)
//TODO:fix
items.append(
SettingsSearchableItem(
id: "qr-code/share",
```
- [ ] **Step 2: Rewrite guard at line 989 (qr-code/share item)**
```swift
// old_string
id: "qr-code/share",
isVisible: false,
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
return
}
let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false)
present(.push, controller)
})
}
// new_string
id: "qr-code/share",
isVisible: false,
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer else {
return
}
let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false)
present(.push, controller)
})
}
```
---
### Task 4: `ContactsController.swift` — Shape-A drop
**Files:**
- Modify: `submodules/ContactListUI/Sources/ContactsController.swift:478`
- [ ] **Step 1: Drop `._asPeer()` at the call site**
```swift
// old_string
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer._asPeer(), threadId: nil, temporary: false), in: .window(.root))
// new_string
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer, threadId: nil, temporary: false), in: .window(.root))
```
---
### Task 5: `PeerInfoScreen.swift` — Shape-C wrap
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:4623`
Local `peer` comes from `data.peer` (type `Peer`). Wrap at the call site.
- [ ] **Step 1: Wrap with `EnginePeer(...)`**
```swift
// old_string
let qrController = self.context.sharedContext.makeChatQrCodeScreen(context: self.context, peer: peer, threadId: threadId, temporary: temporary)
// new_string
let qrController = self.context.sharedContext.makeChatQrCodeScreen(context: self.context, peer: EnginePeer(peer), threadId: threadId, temporary: temporary)
```
---
### Task 6: `ChannelAdminsController.swift` — Shape-A drop
**Files:**
- Modify: `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift:734`
Local `peer` is `EnginePeer` (from `guard let peer` on :729 unwrapping `EnginePeer?` from `engine.data.get(Peer.Peer(id:))`).
- [ ] **Step 1: Drop `._asPeer()`**
```swift
// old_string
pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: nil, starsState: nil))
// new_string
pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: nil, starsState: nil))
```
---
### Task 7: `GroupStatsController.swift` — Shape-A drop
**Files:**
- Modify: `submodules/StatisticsUI/Sources/GroupStatsController.swift:915`
Local `peer` is `EnginePeer` (from `Signal<EnginePeer, NoError>` via the `mapToSignal` at :906).
- [ ] **Step 1: Drop `._asPeer()`**
```swift
// old_string
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: participantPeerId, starsState: nil)
// new_string
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId, starsState: nil)
```
---
### Task 8: `PeerInfoScreenOpenChat.swift` — Shape-C wrap
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift:115`
Local `peer` comes from `self.data?.peer` (type `Peer`). Wrap at the call site.
- [ ] **Step 1: Wrap with `EnginePeer(...)`**
```swift
// old_string
let controller = self.context.sharedContext.makeChatRecentActionsController(context: self.context, peer: peer, adminPeerId: nil, starsState: self.data?.starsRevenueStatsState)
// new_string
let controller = self.context.sharedContext.makeChatRecentActionsController(context: self.context, peer: EnginePeer(peer), adminPeerId: nil, starsState: self.data?.starsRevenueStatsState)
```
---
### Task 9: Build + iterate
- [ ] **Step 1: Run full build**
```bash
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: first-pass-clean (wave 39's 50-file wave landed first-pass-clean; this 8-file wave should too).
- [ ] **Step 2: If errors, iterate**
Each error should point at a call site the plan missed. Fix, re-run. Do not widen the scope — if a call site not in the classification table above appears as an error, investigate whether the memory/inventory was stale.
---
### Task 10: Verify no residue
- [ ] **Step 1: Grep for raw-`Peer` sites**
```bash
grep -rn "makeChatQrCodeScreen\|makeChatRecentActionsController" --include="*.swift" submodules/
```
Expected output: 2 protocol-decl lines (AccountContext.swift), 2 impl-decl lines (SharedAccountContext.swift), and exactly 7 consumer sites — all with `peer: peer`, `peer: EnginePeer(peer)`, or similar (no `peer: x._asPeer()` remaining for these two methods).
---
### Task 11: Commit + update refactor log
**Files:**
- Modify: `docs/superpowers/postbox-refactor-log.md` — append wave-40 outcome section.
- [ ] **Step 1: Stage exactly these 8 files (enumerate, do not use `git add -u`)**
```bash
git add \
submodules/AccountContext/Sources/AccountContext.swift \
submodules/TelegramUI/Sources/SharedAccountContext.swift \
submodules/SettingsUI/Sources/Search/SettingsSearchableItems.swift \
submodules/ContactListUI/Sources/ContactsController.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift \
submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \
submodules/StatisticsUI/Sources/GroupStatsController.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenChat.swift \
docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md
```
- [ ] **Step 2: Verify staging with `git status --short`**
Verify only the 9 files above are staged. If other files appear (e.g. `ChatMessageTransitionNode.swift` WIP, `sourcekit-bazel-bsp` submodule marker) — reset them out of the index with `git restore --staged <file>` and re-check.
- [ ] **Step 3: Commit (wave 40)**
```bash
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 40
makeChatQrCodeScreen + makeChatRecentActionsController peer Peer->EnginePeer.
- AccountContext protocol: 2 decls updated
- SharedAccountContext impls: 2 signatures + 2 body-shadow `let peer = peer._asPeer()`
- 5 Shape-A `._asPeer()` drops (SettingsSearchableItems x2 guard-variant, ContactsController, ChannelAdminsController, GroupStatsController)
- 2 Shape-C `EnginePeer(peer)` wraps (PeerInfoScreen, PeerInfoScreenOpenChat)
- Net: -3 bridges
Sibling follow-up to wave 39 (makePeerInfoController). Pre-flight classification
completed in wave-39 design doc's "Out of scope" section.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 4: Log wave 40 outcome**
Append a new section to `docs/superpowers/postbox-refactor-log.md`:
```markdown
## Wave 40 outcome
Commit: `<hash>`. Bundle of `AccountContext.makeChatQrCodeScreen` + `makeChatRecentActionsController` peer `Peer → EnginePeer`. 8 files / ~12 lines changed. Pre-flight classification from wave-39 design doc held: 5 Shape-A drops + 2 Shape-C wraps + 2 impl body-shadows + 2 protocol decls. Net 3 bridges. Build outcome: <first-pass-clean | N iterations>.
Sibling follow-up to wave 39 — completes the "Option 1 cluster" (makePeerInfoController family from wave-38 memory). Ratchet markers installed at PeerInfoScreen:4623 and PeerInfoScreenOpenChat:115 for a future `PeerInfoScreenData.peer Peer → EnginePeer` wave.
```
Then commit the log update:
```bash
git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 40 outcome
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 5: Update memory**
Update `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`:
- Move wave-40 (this bundle) from "candidates" to "Latest commits".
- Bump wave-41 recommendation to RenderedChannelParticipant.peer (Option 3) or RenderedPeer (Option 2).
- Add wave-40 lesson if any (e.g. "bundled sibling migration with shared pre-flight is cheap" or similar).
---
## Self-review checklist (writing-plans skill)
- **Spec coverage:** Every site from the memory/wave-39-doc pre-flight is a task. Sites 1+2 → Task 3; Site 3 → Task 4; Site 4 → Task 5; Site 5 → Task 6; Site 6 → Task 7; Site 7 → Task 8. Impl bodies → Task 2. Protocol → Task 1. Build → Task 9. Verify → Task 10. Commit+log → Task 11. ✓
- **Placeholders:** None. Every Edit step has exact `old_string` / `new_string`. Commit message and log-update text are spelled out. ✓
- **Type consistency:** Both methods take `peer: EnginePeer` everywhere — protocol decl, impl decl, and call sites' parameter passes. ✓

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,658 @@
# Wave 43 plan: PeerInfoScreen helpers `peer: Peer?``peer: EnginePeer?`
> **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 six PeerInfoScreen module helpers (`canEditPeerInfo`, `availableActionsForMemberOfPeer`, `peerInfoHeaderActionButtons`, `peerInfoHeaderButtons`, `peerInfoCanEdit`, `peerInfoIsChatMuted`) from `peer: Peer?` to `peer: EnginePeer?`, rewriting internal `as?`/`is` against concrete `TelegramX` subclasses to `case let .x` / `case .x` enum patterns on `EnginePeer`, and updating all 21 call sites to drop wave-42-installed `._asPeer()` / `?._asPeer()` bridges or add `.flatMap(EnginePeer.init)` / `EnginePeer(...)` wraps as appropriate.
**Architecture:** In-place signature migration following wave-42 precedent — no new typealiases, no engine wrapper structs, no TelegramCore changes. All edits within `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/` (10 files). Single atomic commit.
**Tech Stack:** Swift, Bazel build, `EnginePeer` enum cases (`.user(TelegramUser)`, `.legacyGroup(TelegramGroup)`, `.channel(TelegramChannel)`, `.secretChat(TelegramSecretChat)`).
**Preceding waves:** 42 (`PeerInfoScreenData.peer: Peer? → EnginePeer?`) on 2026-04-24. This wave drops ~7 `?._asPeer()` / `._asPeer()` bridges installed then.
**Expected net wrap change:** ~7 DROPs vs ~12 ADDs → net roughly 0. The headline win is helper-signature migration, not wrap count. Follow-up waves migrating `PeerInfoHeaderEditingContentNode.update`, `PeerInfoEditingAvatarNode.update`, `PeerInfoEditingAvatarOverlayNode.update`, `PeerInfoHeaderNode.update`, `PeerInfoScreenMemberItem.enclosingPeer`, `PeerInfoMembersPane` enclosingPeer param will drop the ADDs introduced here.
**Build expectation:** 2 iterations likely (per wave-41 lesson — foundational-type migrations rarely first-pass-clean). Hedge for iteration-3 if unexpected property accesses surface.
---
## Pre-flight facts (verified from repo 2026-04-24)
### EnginePeer cases (from `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:177`)
```
case user(TelegramUser)
case legacyGroup(TelegramGroup)
case channel(TelegramChannel)
case secretChat(TelegramSecretChat)
```
Forwarded property subset on `EnginePeer` includes `id`, `addressName`, `usernames`, `indexName`, `debugDisplayTitle`, `displayLetters`, `profileImageRepresentations`, `smallProfileImage`, `largeProfileImage`, `isDeleted`, `isScam`, `isFake`, `isVerified`, `isPremium`, `isSubscription`, `isService`, `nameColor`, `verificationIconFileId`, `profileColor`, `effectiveProfileColor`, `emojiStatus`, `backgroundEmojiId`, `profileBackgroundEmojiId`, and (via `LocalizedPeerData/Sources/PeerTitle.swift`) `compactDisplayTitle`, `displayTitle(strings:displayOrder:)`. **Not** forwarded: Peer-specific members like `isCopyProtectionEnabled`, `hasPermission(_:)`, `hasBannedPermission(_:)`, `isDeleted` on user (WAIT: yes forwarded). **Internal helper bodies do not access any non-forwarded Peer members** — all their concrete-type work happens via `as? TelegramX`, which is enum-rewrite territory. Confirmed by reading helper bodies (PeerInfoData.swift:22552670).
### Helper call sites inventory (21 sites across 10 files)
Running command:
```bash
grep -rn "\bcanEditPeerInfo\b\|\bavailableActionsForMemberOfPeer\b\|\bpeerInfoHeaderActionButtons\b\|\bpeerInfoHeaderButtons\b\|\bpeerInfoCanEdit\b\|\bpeerInfoIsChatMuted\b" submodules/ --include="*.swift" | grep -v PeerInfoData.swift
```
All 21 call sites:
| # | File | Line | Current arg | Peer var origin | Action |
|---|------|------|-------------|-----------------|--------|
| 1 | `PeerInfoHeaderNode.swift` | 548 | `peer: peer` | method param `peer: Peer?` | ADD-WRAP: `peer: peer.flatMap(EnginePeer.init)` |
| 2 | `PeerInfoHeaderNode.swift` | 549 | `peer: peer` | same | ADD-WRAP |
| 3 | `PeerInfoHeaderNode.swift` | 2361 | `peer: peer` | same | ADD-WRAP |
| 4 | `PeerInfoEditingAvatarNode.swift` | 66 | `peer: peer` | method param `peer: Peer?` (unwrapped via `guard let peer = peer`) | ADD-WRAP: `peer: EnginePeer(peer)` (peer is non-optional `Peer` here) |
| 5 | `PeerInfoEditingAvatarOverlayNode.swift` | 85 | `peer: peer` | method param `peer: Peer?` (unwrapped via `guard let peer = peer`) | ADD-WRAP: `peer: EnginePeer(peer)` |
| 6 | `PeerInfoHeaderEditingContentNode.swift` | 59 | `peer: peer` | method param `peer: Peer?` | ADD-WRAP: `peer: peer.flatMap(EnginePeer.init)` |
| 7 | `PeerInfoHeaderEditingContentNode.swift` | 88 | `peer: peer` | same | ADD-WRAP |
| 8 | `PeerInfoHeaderEditingContentNode.swift` | 93 | `peer: peer` | same | ADD-WRAP |
| 9 | `PeerInfoHeaderEditingContentNode.swift` | 159 | `peer: peer` | same | ADD-WRAP |
| 10 | `PeerInfoHeaderEditingContentNode.swift` | 162 | `peer: peer` | same | ADD-WRAP |
| 11 | `PeerInfoScreenAvatarSetup.swift` | 435 | `peer: peer._asPeer()` | `peer = data.peer` (EnginePeer unwrapped) | DROP: `peer: peer` |
| 12 | `PeerInfoScreenPerformButtonAction.swift` | 62 | `peer: self.data?.peer?._asPeer()` | `self.data?.peer` is `EnginePeer?` | DROP: `peer: self.data?.peer` |
| 13 | `PeerInfoScreenPerformButtonAction.swift` | 397 | `peer: peer._asPeer()` | `peer = data.peer` unwrapped at line 381 | DROP: `peer: peer` |
| 14 | `PeerInfoScreenPerformButtonAction.swift` | 398 | `peer: peer._asPeer()` | same | DROP: `peer: peer` |
| 15 | `PeerInfoScreenOpenMember.swift` | 19 | `peer: enclosingPeer._asPeer()` | `enclosingPeer = self.data?.peer` unwrapped at line 14 | DROP: `peer: enclosingPeer` |
| 16 | `PeerInfoScreen.swift` | 1905 | `peer: group` | `group: TelegramGroup` from `if case let .legacyGroup(group) = data.peer` | CONVERT: `peer: data.peer` |
| 17 | `PeerInfoScreen.swift` | 1961 | `peer: channel` | `channel: TelegramChannel` from `if case let .channel(channel) = data.peer` | CONVERT: `peer: data.peer` |
| 18 | `PeerInfoScreen.swift` | 5857 | `peer: self.data?.peer?._asPeer()` | `self.data?.peer` is `EnginePeer?` | DROP: `peer: self.data?.peer` |
| 19 | `PeerInfoProfileItems.swift` | 853 | `peer: peer._asPeer()` | `peer = data.peer` unwrapped at line 821 | DROP: `peer: peer` |
| 20 | `PeerInfoScreenMemberItem.swift` | 178 | `peer: item.enclosingPeer` | `item.enclosingPeer: Peer` (stored raw) | ADD-WRAP: `peer: EnginePeer(item.enclosingPeer)` |
| 21 | `PeerInfoMembersPane.swift` | 139 | `peer: enclosingPeer` | `enclosingPeer: Peer` (local raw) | ADD-WRAP: `peer: EnginePeer(enclosingPeer)` |
**Summary:** 7 DROPs (sites 1115, 18, 19), 10 ADD-WRAPs (110, 20, 21 = 12 total ADDs), 2 CONVERTs (16, 17 — from concrete-type arg to whole-EnginePeer; no wrap delta but simpler/safer).
### `is TelegramX` scan on helper bodies
Only `peerInfoIsChatMuted` has them (PeerInfoData.swift:2641, 2643). Rewrite pattern: `if peer is TelegramUser``if case .user = peer`, `else if peer is TelegramGroup``else if case .legacyGroup = peer`.
No `is TelegramX` checks exist at call sites for these specific helpers (wave-42 would have caught them since call-site `data.peer` is already `EnginePeer?`).
---
## File Structure
All edits within `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`:
**Modify (helper definitions):**
- `PeerInfoData.swift:22652670` — 6 helper signatures + bodies
**Modify (call sites):**
- `PeerInfoScreen.swift` (3 sites: 1905, 1961, 5857)
- `PeerInfoHeaderNode.swift` (3 sites: 548, 549, 2361)
- `PeerInfoEditingAvatarNode.swift` (1 site: 66)
- `PeerInfoEditingAvatarOverlayNode.swift` (1 site: 85)
- `PeerInfoHeaderEditingContentNode.swift` (5 sites: 59, 88, 93, 159, 162)
- `PeerInfoScreenAvatarSetup.swift` (1 site: 435)
- `PeerInfoScreenPerformButtonAction.swift` (3 sites: 62, 397, 398)
- `PeerInfoScreenOpenMember.swift` (1 site: 19)
- `PeerInfoProfileItems.swift` (1 site: 853)
- `ListItems/PeerInfoScreenMemberItem.swift` (1 site: 178)
- `Panes/PeerInfoMembersPane.swift` (1 site: 139)
Total: 10 files modified (PeerInfoData.swift counts once).
---
### Task 1: Migrate the six helper signatures and bodies in `PeerInfoData.swift`
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift:22652670`
- [ ] **Step 1: Migrate `canEditPeerInfo` (line 2265).** Signature: `peer: Peer?``peer: EnginePeer?`. Body rewrites:
```swift
// before (line 2269-2287)
if let user = peer as? TelegramUser, let botInfo = user.botInfo {
return botInfo.flags.contains(.canEdit)
} else if let channel = peer as? TelegramChannel {
if let threadData = threadData {
if chatLocation.threadId == 1 {
return false
}
if channel.hasPermission(.manageTopics) {
return true
}
if threadData.author == context.account.peerId {
return true
}
} else {
if channel.hasPermission(.changeInfo) {
return true
}
}
} else if let group = peer as? TelegramGroup {
switch group.role {
case .admin, .creator:
return true
case .member:
break
}
if !group.hasBannedPermission(.banChangeInfo) {
return true
}
}
// after
if case let .user(user) = peer, let botInfo = user.botInfo {
return botInfo.flags.contains(.canEdit)
} else if case let .channel(channel) = peer {
if let threadData = threadData {
if chatLocation.threadId == 1 {
return false
}
if channel.hasPermission(.manageTopics) {
return true
}
if threadData.author == context.account.peerId {
return true
}
} else {
if channel.hasPermission(.changeInfo) {
return true
}
}
} else if case let .legacyGroup(group) = peer {
switch group.role {
case .admin, .creator:
return true
case .member:
break
}
if !group.hasBannedPermission(.banChangeInfo) {
return true
}
}
```
The `if context.account.peerId == peer?.id` line (2266) stays identical (`.id` is forwarded by `EnginePeer`).
- [ ] **Step 2: Migrate `availableActionsForMemberOfPeer` (line 2314).** Signature: `peer: Peer?``peer: EnginePeer?`. Body rewrites — four `peer as? TelegramChannel/TelegramGroup` sites become `case let .channel/.legacyGroup` patterns:
```swift
// Line 2320: if let channel = peer as? TelegramChannel
// → : if case let .channel(channel) = peer
// Line 2324: } else if let group = peer as? TelegramGroup {
// → : } else if case let .legacyGroup(group) = peer {
// Line 2330: if let channel = peer as? TelegramChannel
// → : if case let .channel(channel) = peer
// Line 2374: } else if let group = peer as? TelegramGroup {
// → : } else if case let .legacyGroup(group) = peer {
```
The `if peer == nil` check (line 2317) stays identical (Optional == nil works on EnginePeer? too).
- [ ] **Step 3: Migrate `peerInfoHeaderActionButtons` (line 2434).** Signature: `peer: Peer?``peer: EnginePeer?`. Single body rewrite at line 2436:
```swift
// before
if !isContact && !isSecretChat, let user = peer as? TelegramUser, user.botInfo == nil {
// after
if !isContact && !isSecretChat, case let .user(user) = peer, user.botInfo == nil {
```
- [ ] **Step 4: Migrate `peerInfoHeaderButtons` (line 2447).** Signature: `peer: Peer?``peer: EnginePeer?`. Three body rewrites:
```swift
// Line 2449: if let user = peer as? TelegramUser {
// → : if case let .user(user) = peer {
// Line 2483: } else if let channel = peer as? TelegramChannel {
// → : } else if case let .channel(channel) = peer {
// Line 2558: } else if let group = peer as? TelegramGroup {
// → : } else if case let .legacyGroup(group) = peer {
```
- [ ] **Step 5: Migrate `peerInfoCanEdit` (line 2585).** Signature: `peer: Peer?``peer: EnginePeer?`. Three body rewrites. Note: original shadows `peer` inside each branch (`let peer = peer as? TelegramX`). Rewrite preserves the shadowing via `case let`:
```swift
// Line 2586: if let user = peer as? TelegramUser {
// → : if case let .user(user) = peer {
// Line 2597: } else if let peer = peer as? TelegramChannel {
// → : } else if case let .channel(peer) = peer {
// (intentional shadow of outer `peer` with inner `peer: TelegramChannel` — preserved)
// Line 2618: } else if let peer = peer as? TelegramGroup {
// → : } else if case let .legacyGroup(peer) = peer {
```
- [ ] **Step 6: Migrate `peerInfoIsChatMuted` (line 2633).** Outer signature: `peer: Peer?``peer: EnginePeer?`. Inner function signature (line 2634) also migrates: `func isPeerMuted(peer: Peer?, ...)``func isPeerMuted(peer: EnginePeer?, ...)`. Body rewrites inside the inner function (line 26412651):
```swift
// before (line 2641)
if peer is TelegramUser {
peerIsMuted = !globalNotificationSettings.privateChats.enabled
} else if peer is TelegramGroup {
peerIsMuted = !globalNotificationSettings.groupChats.enabled
} else if let channel = peer as? TelegramChannel {
switch channel.info {
case .group:
peerIsMuted = !globalNotificationSettings.groupChats.enabled
case .broadcast:
peerIsMuted = !globalNotificationSettings.channels.enabled
}
}
// after
if case .user = peer {
peerIsMuted = !globalNotificationSettings.privateChats.enabled
} else if case .legacyGroup = peer {
peerIsMuted = !globalNotificationSettings.groupChats.enabled
} else if case let .channel(channel) = peer {
switch channel.info {
case .group:
peerIsMuted = !globalNotificationSettings.groupChats.enabled
case .broadcast:
peerIsMuted = !globalNotificationSettings.channels.enabled
}
}
```
The outer `if let peer = peer` (line 2640) stays unchanged (Optional binding works on EnginePeer?).
The inner `peerInfoIsChatMuted` body (line 26592669) calls `isPeerMuted(peer: peer, ...)` with the outer `peer` (now EnginePeer?) — works without change because inner signature now matches.
- [ ] **Step 7: Re-read PeerInfoData.swift lines 22652670 and visually verify no `as? TelegramX` or `is TelegramX` patterns remain.**
Run: `grep -n "as? TelegramUser\|as? TelegramChannel\|as? TelegramGroup\|is TelegramUser\|is TelegramChannel\|is TelegramGroup" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift | awk -F: '$2 >= 2265 && $2 <= 2670'`
Expected: empty output.
---
### Task 2: Update call sites — DROPs (7 sites)
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift:435`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift:62,397,398`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift:19`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:5857`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift:853`
- [ ] **Step 1: DROP at `PeerInfoScreenAvatarSetup.swift:435`.**
```swift
// before
guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer._asPeer(), chatLocation: self.chatLocation, threadData: data.threadData) else {
// after
guard let data = self.controllerNode.data, let peer = data.peer, mode != .generic || canEditPeerInfo(context: self.context, peer: peer, chatLocation: self.chatLocation, threadData: data.threadData) else {
```
- [ ] **Step 2: DROP at `PeerInfoScreenPerformButtonAction.swift:62`.**
```swift
// before
let chatIsMuted = peerInfoIsChatMuted(peer: self.data?.peer?._asPeer(), peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings)
// after
let chatIsMuted = peerInfoIsChatMuted(peer: self.data?.peer, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings)
```
- [ ] **Step 3: DROP at `PeerInfoScreenPerformButtonAction.swift:397 and :398` (peerInfoHeaderButtons, two lines, same pattern `peer: peer._asPeer()``peer: peer`).**
Use Edit with `replace_all=true` on the substring `peer: peer._asPeer(), cachedData: data.cachedData` — this exact form appears exactly twice in the file (lines 397, 398), both targets.
```swift
// before (at both 397 and 398)
peerInfoHeaderButtons(peer: peer._asPeer(), cachedData: data.cachedData, isOpenedFromChat: ...
// after
peerInfoHeaderButtons(peer: peer, cachedData: data.cachedData, isOpenedFromChat: ...
```
Verification after edit:
```bash
grep -n "_asPeer()" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift
```
Expected: empty (all three DROPs done).
- [ ] **Step 4: DROP at `PeerInfoScreenOpenMember.swift:19`.**
```swift
// before
let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer._asPeer(), member: member)
// after
let actions = availableActionsForMemberOfPeer(accountPeerId: self.context.account.peerId, peer: enclosingPeer, member: member)
```
- [ ] **Step 5: DROP at `PeerInfoScreen.swift:5857`.**
```swift
// before
} else if peerInfoCanEdit(peer: self.data?.peer?._asPeer(), chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) {
// after
} else if peerInfoCanEdit(peer: self.data?.peer, chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) {
```
- [ ] **Step 6: DROP at `PeerInfoProfileItems.swift:853`.**
Only the `availableActionsForMemberOfPeer` call — the sibling `enclosingPeer: peer._asPeer()` at line 852 is NOT a helper-migration target (it's `PeerInfoScreenMemberItem.enclosingPeer: Peer`, unchanged in this wave).
```swift
// before (line 853)
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: peer._asPeer(), member: member)
// after
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: peer, member: member)
```
---
### Task 3: Update call sites — CONVERTs (2 sites in PeerInfoScreen.swift)
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift:1905,1961`
At these sites the helper arg is currently a concrete `TelegramGroup` / `TelegramChannel` extracted via case pattern. After migration the helper takes `EnginePeer?`, so pass `data.peer` directly — the helper re-does the pattern match internally, semantics preserved.
- [ ] **Step 1: CONVERT at `PeerInfoScreen.swift:1905`.**
```swift
// before
} else if case let .legacyGroup(group) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: group, chatLocation: chatLocation, threadData: data.threadData) {
// after
} else if case let .legacyGroup(group) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: chatLocation, threadData: data.threadData) {
```
`group` stays bound because the body below still uses it. Only the helper arg changes.
- [ ] **Step 2: CONVERT at `PeerInfoScreen.swift:1961`.**
```swift
// before
} else if case let .channel(channel) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: channel, chatLocation: strongSelf.chatLocation, threadData: data.threadData) {
// after
} else if case let .channel(channel) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: strongSelf.chatLocation, threadData: data.threadData) {
```
---
### Task 4: Update call sites — ADD-WRAPs in internal-update methods (10 sites in 4 files)
These files' internal `.update(peer: Peer?, ...)` methods are NOT migrated in this wave (scope: helpers only). Each helper call inside bridges `peer` (raw `Peer?`) to `EnginePeer?` via `.flatMap(EnginePeer.init)`, or — where `peer` has already been unwrapped to non-optional `Peer` — via `EnginePeer(peer)`.
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift:548,549,2361`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift:66`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift:85`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift:59,88,93,159,162`
- [ ] **Step 1: ADD-WRAPs at `PeerInfoHeaderNode.swift:548,549,2361`.** At lines 548, 549, the local `peer` is the raw `Peer?` method parameter (line 496). At line 2361 likewise.
```swift
// before (line 548)
let actionButtonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderActionButtons(peer: peer, isSecretChat: isSecretChat, isContact: isContact)
// after
let actionButtonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderActionButtons(peer: peer.flatMap(EnginePeer.init), isSecretChat: isSecretChat, isContact: isContact)
```
```swift
// before (line 549)
let buttonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, ...)
// after
let buttonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderButtons(peer: peer.flatMap(EnginePeer.init), cachedData: cachedData, ...)
```
```swift
// before (line 2361)
let chatIsMuted = peerInfoIsChatMuted(peer: peer, peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings)
// after
let chatIsMuted = peerInfoIsChatMuted(peer: peer.flatMap(EnginePeer.init), peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings)
```
- [ ] **Step 2: ADD-WRAP at `PeerInfoEditingAvatarNode.swift:66`.** Here `peer` is non-optional `Peer` (unwrapped at line 62: `guard let peer = peer else { return }`). Use `EnginePeer(peer)`.
```swift
// before
let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
// after
let canEdit = canEditPeerInfo(context: self.context, peer: EnginePeer(peer), chatLocation: chatLocation, threadData: threadData)
```
- [ ] **Step 3: ADD-WRAP at `PeerInfoEditingAvatarOverlayNode.swift:85`.** Same shape — `peer` is non-optional `Peer` (unwrapped at line 64).
```swift
// before
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
// after
if canEditPeerInfo(context: self.context, peer: EnginePeer(peer), chatLocation: chatLocation, threadData: threadData)
```
- [ ] **Step 4: ADD-WRAPs at `PeerInfoHeaderEditingContentNode.swift:59,88,93,159,162`.** Here `peer` is the method's `peer: Peer?` parameter (line 52). Five identical bridge forms.
For each of lines 59, 88, 93, 159, 162, replace `peer: peer` (inside `canEditPeerInfo(... peer: peer, ...)`) with `peer: peer.flatMap(EnginePeer.init)`.
The simplest approach: issue five separate Edit calls, each scoped to a unique surrounding substring. Example:
```swift
// before (line 59)
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
// after
if canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData) {
```
Note line 59's trailing double-space before `{` in the original — preserve it.
Lines 88, 93, 159 share an identical surrounding substring `if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {` (no trailing double-space, no `|| isEditableBot`). To avoid collision with line 59, use `replace_all=true` on THIS exact string (matches 88, 93 — wait, 159 uses `isEnabled = canEditPeerInfo(...)`, different prefix). Safer plan: one Edit per line, each with enough surrounding context to be unique. Verify uniqueness after each edit with grep.
Line 88's surrounding context: inside `if let _ = peer as? TelegramGroup {` branch — preceded by `fieldKeys.append(.title)`.
Line 93's surrounding context: inside `if let _ = peer as? TelegramChannel {` branch — preceded by `fieldKeys.append(.title)`. Same inner phrase as 88 — so `fieldKeys.append(.title)\n if canEditPeerInfo...` appears twice. Use line-specific context (preceding `else if let _ = peer as?` token).
Line 159: `isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)` (no trailing text).
Line 162: `isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot`. Unique — contains ` || isEditableBot`.
Recommended: five sequential Edits with explicit line disambiguation via surrounding context. Do not bulk-replace-all — the identical `peer: peer, chatLocation: chatLocation, threadData: threadData)` substring appears at all five sites but their line-specific surroundings differ.
Verification after all five edits:
```bash
grep -c "peer: peer," submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift
```
Expected: 0 (no unmigrated call sites remain; other `peer:` occurrences in the file are either type annotations or at the method signature, which uses `peer: Peer?` not `peer: peer`).
---
### Task 5: Update call sites — ADD-WRAPs at raw-`Peer` member-item sites (2 sites)
**Files:**
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift:178`
- Modify: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift:139`
At these sites `enclosingPeer` is non-optional `Peer` (raw, stored on the item / local). Wrap with `EnginePeer(...)`.
- [ ] **Step 1: ADD-WRAP at `PeerInfoScreenMemberItem.swift:178`.**
```swift
// before
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member)
// after
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: EnginePeer(item.enclosingPeer), member: item.member)
```
- [ ] **Step 2: ADD-WRAP at `PeerInfoMembersPane.swift:139`.**
```swift
// before
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member)
// after
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: EnginePeer(enclosingPeer), member: member)
```
---
### Task 6: Build and iterate
- [ ] **Step 1: Full project build with `--continueOnError` to surface all errors at once.**
```bash
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: likely 2-iteration convergence. Budget up to iteration 3.
Likely categories of residual errors:
1. **Missed call sites** — grep-miss from planning. Remediate by adding `.flatMap(EnginePeer.init)` or `EnginePeer(...)` as appropriate.
2. **Missed `as? TelegramX` / `is TelegramX` inside helper bodies** — Swift compiler error "cannot convert value of type 'EnginePeer?' to expected argument type 'Peer?'" or warning "'is' test is always false". Fix with `case` pattern.
3. **Optional-lifting edge cases**`if case let .user(user) = peer` may fail if Swift interprets `peer` as non-optional. If so, rewrite as `if let peer, case let .user(user) = peer`.
4. **Unused binding warnings** — e.g. `if case let .user(user) = peer` where `user` isn't used inside that branch. Swift's `-warnings-as-errors` (658/665 submodule BUILDs) promotes these. Rewrite as `if case .user = peer`.
5. **Unused variable `peer` or `group`/`channel` at CONVERT sites 16, 17** — lines 1905/1961 bind `group`/`channel` in the `case let` pattern; if the body body doesn't use it, Swift emits "value 'group' was never used" which `-warnings-as-errors` promotes to error. Since the body below DOES use them (updatePeerTitle(peerId: group.id, ...)` etc.), this should not trigger — but verify.
- [ ] **Step 2: For each error category above, apply the correct fix in-place and rebuild. Iterate until green.**
- [ ] **Step 3: After build is green, run the post-migration grep audit:**
```bash
# Should be empty — no _asPeer() bridges at helper call sites
grep -rn "canEditPeerInfo(.*_asPeer\|peerInfoIsChatMuted(.*_asPeer\|peerInfoHeaderButtons(.*_asPeer\|peerInfoHeaderActionButtons(.*_asPeer\|peerInfoCanEdit(.*_asPeer\|availableActionsForMemberOfPeer(.*_asPeer" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/
# Should be empty — no concrete-type casts against peer param in helper bodies
grep -nE "as\?\s+TelegramUser|as\?\s+TelegramChannel|as\?\s+TelegramGroup|\bis\s+TelegramUser\b|\bis\s+TelegramChannel\b|\bis\s+TelegramGroup\b" submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift | awk -F: '$2 >= 2265 && $2 <= 2670'
```
Expected: both empty.
---
### Task 7: Commit
- [ ] **Step 1: Verify working tree only contains wave-43 edits + pre-existing WIP.**
```bash
git status --short
```
Expected (pre-existing WIP, NOT to be staged):
```
m build-system/bazel-rules/sourcekit-bazel-bsp
M submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift
?? build-system/tulsi/
?? submodules/TgVoip/
?? third-party/libx264/
```
Plus wave-43 edits (all under `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`):
```
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift
M submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift
```
- [ ] **Step 2: Explicitly stage only the wave-43 files (not the WIP).**
```bash
git add \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarNode.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoEditingAvatarOverlayNode.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderEditingContentNode.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenAvatarSetup.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenOpenMember.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoProfileItems.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/ListItems/PeerInfoScreenMemberItem.swift \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/Panes/PeerInfoMembersPane.swift \
docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md
```
- [ ] **Step 3: Commit.**
Use a HEREDOC for the message:
```bash
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 43
Migrate six PeerInfoScreen helpers (canEditPeerInfo,
availableActionsForMemberOfPeer, peerInfoHeaderActionButtons,
peerInfoHeaderButtons, peerInfoCanEdit, peerInfoIsChatMuted) from
`peer: Peer?` to `peer: EnginePeer?`. Internal `as? TelegramX` /
`is TelegramX` patterns rewritten to `case let .x` / `case .x` on
EnginePeer enum. All 21 call sites updated in the same commit: 7
`._asPeer()` bridges installed by wave 42 dropped; 12
`.flatMap(EnginePeer.init)` / `EnginePeer(...)` wraps added at sites
whose enclosing methods still take raw Peer?; 2 concrete-type args
converted to pass the whole EnginePeer value.
All edits within submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/.
No new engine typealiases. No TelegramCore changes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 4: Verify commit.**
```bash
git log --oneline -1
git show --stat HEAD
```
Expected: one commit, ~10 files changed, clean diff.
---
## Self-review checklist (run before handoff)
**Spec coverage:**
- All 6 helper signatures migrated (Task 1 steps 16). ✓
- All 21 call sites touched (Tasks 25). ✓
- Build iteration explicit (Task 6). ✓
- Commit explicit (Task 7). ✓
**Type consistency:**
- Helper signatures all `peer: EnginePeer?` (consistent). ✓
- Call-site transforms: DROP/ADD/CONVERT actions match the inventory table. ✓
- `EnginePeer.init` constructor used both as `.flatMap(EnginePeer.init)` (Peer? → EnginePeer?) and `EnginePeer(...)` (Peer → EnginePeer) — both are valid (construction overloaded on EnginePeer extension at `TelegramCore/TelegramEngine/Peers/Peer.swift:564`). ✓
**Placeholder scan:**
- No "TBD" / "handle appropriately" / "similar to Task N" language — every step has its concrete code. ✓
**Risks flagged:**
- Wave-41 lesson: foundational-type migrations rarely first-pass-clean. Budget 2 iterations. ✓
- Wave-41 lesson: `-warnings-as-errors` promotes always-false `is` checks and unused bindings to build errors. Task 6 step 1 calls these out explicitly. ✓
- Wave-42 lesson: `EnginePeer` doesn't forward every Peer property. Helper bodies were verified to access only `.id`, which IS forwarded; other property accesses were on concrete types (`TelegramChannel.hasPermission(...)` etc.) which remain on concrete types post-migration. No forwarding-gap remediation expected in helpers. ✓

View file

@ -0,0 +1,164 @@
# Wave 42 plan: `PeerInfoScreenData.peer: Peer? → EnginePeer?`
Date: 2026-04-24
Preceding waves: 41 (`RenderedChannelParticipant.peer`), 40 (`makeChatQrCodeScreen`/`makeChatRecentActionsController`), 39 (`makePeerInfoController`)
Scope (confirmed with user): only `PeerInfoScreenData.peer`. Sibling fields (`chatPeer`, `savedMessagesPeer`, `linkedDiscussionPeer`, `linkedMonoforumPeer`) are follow-up-wave candidates.
## Change target
File: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift`
- L386: `let peer: Peer?``let peer: EnginePeer?`
- L442: `peer: Peer?,``peer: EnginePeer?,`
- Store unchanged (`self.peer = peer`)
## Construction sites (5, all in PeerInfoData.swift)
| Line | Current `peer:` arg | Rewrite |
|------|---------------------|---------|
| 1027 | `peer: peer` (local, `Peer?` from `peerView.peers[peerId]`) | `peer: peer.flatMap(EnginePeer.init)` |
| 1100 | `peer: nil` | unchanged |
| 1620 | `peer: peer` (local, `Peer?` from `peerView.peers[userPeerId]`) | `peer: peer.flatMap(EnginePeer.init)` |
| 1867 | `peer: peerView.peers[peerId]` | `peer: peerView.peers[peerId].flatMap(EnginePeer.init)` |
| 2205 | `peer: peerView.peers[groupId]` | `peer: peerView.peers[groupId].flatMap(EnginePeer.init)` |
## Consumer migration patterns (across 18 files, ~114 `data.peer` accesses)
### Pattern A — as-cast → enum pattern match (~20 sites)
```swift
// before
if let user = data.peer as? TelegramUser, user.botInfo == nil { ... }
// after
if case let .user(user) = data.peer, user.botInfo == nil { ... }
```
Scope both sides consistently. A cast inside a larger `guard let ..., let user = ... as? TelegramUser else { return }` becomes `guard ..., case let .user(user) = data.peer else { return }`.
### Pattern B — `is TelegramXxx` check → enum case pattern (~5 sites)
The wave-41 lesson: `-warnings-as-errors` catches always-false `is` checks.
```swift
// before
if let peer = self.data?.peer, peer is TelegramChannel { ... }
if peer is TelegramGroup { ... }
// after
if case .channel = self.data?.peer { ... }
if case .legacyGroup = peer { ... }
```
`TelegramGroup` maps to `.legacyGroup`. `TelegramChannel` maps to `.channel`. `TelegramUser` maps to `.user`. `TelegramSecretChat` maps to `.secretChat`.
Known sites in PeerInfoScreen.swift (inventory): L3981, L4133, L4192, L4194 (and L7421 for `chatPeer`-bound — chatPeer stays raw, so L7421 is out of scope). Use repo grep on `PeerInfoScreen/Sources` with token `is Telegram(Channel|User|Group|SecretChat)` to catch other sites.
### Pattern C — existing `EnginePeer(peer)` wraps where `peer` was bound from `data.peer` — DROP (15+ sites)
```swift
// before
if let peer = self.data?.peer {
self.joinChannel(peer: EnginePeer(peer)) // wave-40 wrap
}
// after
if let peer = self.data?.peer {
self.joinChannel(peer: peer) // peer is now EnginePeer already
}
```
Care needed: only drop the wrap where the bound `peer` variable comes from `data.peer`. Wraps on `chatPeer`, `currentPeer`, `user` (bound via `as? TelegramUser`), `groupPeer`, or PeerView lookups stay. The lexical scope makes this judgeable.
Known drop sites (PeerInfoScreen.swift): 1331, 1339, 1346, 1561, 2353, 2405, 3409, 3459, 3624, 3747, 4306, 4573 (inner — review scope), 4623. PeerInfoHeaderNode.swift: 571, 1218, 2054 (if bound from data.peer). PeerInfoScreenOpenChat.swift: 25, 40, 51, 57, 80, 89, 115. Verify each by backtracking the `if let peer = ...` binding.
### Pattern D — helper call sites still taking `Peer?` (ADD-WRAP, ~10 sites)
`canEditPeerInfo`, `peerInfoIsChatMuted`, `peerInfoHeaderButtons`, `peerInfoHeaderActionButtons`, `peerInfoCanEdit`, `availableActionsForMemberOfPeer` all keep `peer: Peer?` in this wave. Call sites must bridge:
```swift
// before
peerInfoIsChatMuted(peer: self.data?.peer, ...)
// after
peerInfoIsChatMuted(peer: self.data?.peer?._asPeer(), ...)
```
Site count (from grep): PeerInfoHeaderNode.swift:548/549/2361, PeerInfoScreenAvatarSetup.swift:435, PeerInfoScreenPerformButtonAction.swift:62/397/398, PeerInfoEditingAvatarNode.swift:66, PeerInfoScreen.swift:1905/1961/5857, PeerInfoHeaderEditingContentNode.swift:59/88/93/159/162, PeerInfoEditingAvatarOverlayNode.swift:85. But the local `peer` at some of these is already narrowed via `as? TelegramUser` (now `case let .user(user)`); in that case the helper gets `user` (still `Peer`-conforming), no bridge needed. Bridge only where the raw `data.peer` flows into the helper.
These ADD-WRAP markers become ratchet-drops for a follow-up wave that migrates the helper signatures.
### Pattern E — `EnginePeer?` passed as `EnginePeer?` directly (DROP wraps on callback args)
Where `data.peer` feeds `makePeerInfoController(peer: EnginePeer)` / `chatInterfaceInteraction.openPeer(_ peer: EnginePeer, ...)` / `.peer(EnginePeer)` ChatLocation / `AvatarGalleryController(peer: EnginePeer)` / `makeChatQrCodeScreen(peer: EnginePeer)` / `makeChatRecentActionsController(peer: EnginePeer)` — drop the `EnginePeer(...)` wrap; pass directly.
### Pattern F — `EnginePeer(peer).displayTitle(...)` / `.compactDisplayTitle` usage (DROP wrap)
```swift
// before
EnginePeer(peer).displayTitle(strings: ..., displayOrder: ...)
// after (peer is now EnginePeer already)
peer.displayTitle(strings: ..., displayOrder: ...)
```
### Pattern G — `.isPremium` on `peer?` inside construction site (L1060, L1626, L1902, L2242)
`peerView.peers[peerId]?.isPremium``Peer` protocol exposes `isPremium`. But the construction site receives raw `Peer?` and then we wrap via `flatMap(EnginePeer.init)`. The `peer?.isPremium` in the same construction scope still refers to the *local* raw peer variable (type unchanged), not `self.peer`. **No change needed at construction sites for `.isPremium` accesses on the local raw `peer`.** Only change `.isPremium` accesses on `data.peer` (which is now `EnginePeer?`) — `EnginePeer.isPremium` exists.
## File-by-file plan
1. **PeerInfoData.swift** — declaration + init + 5 constructions. Also review L1529 (`peerView.peers[peerView.peerId] is TelegramUser`) — OUT OF SCOPE (not `data.peer`); don't touch. Helper functions L2265/2314/2434/2447/2585/2633 stay `peer: Peer?` — DO NOT TOUCH.
2. **PeerInfoScreen.swift** — largest consumer, ~70+ sites. Walk every `data.peer` / `data?.peer` / `self.data?.peer` / `self.data.peer`. Apply A/B/C/E/F patterns. For `if let peer = data.peer` bindings, subsequent uses of `peer` now have type `EnginePeer` — drop wraps on those uses.
3. **PeerInfoScreenOpenChat.swift, PeerInfoScreenOpenBio.swift, PeerInfoScreenOpenMember.swift, PeerInfoScreenOpenPeerInfoContextMenu.swift, PeerInfoScreenOpenUsername.swift, PeerInfoScreenCallActions.swift, PeerInfoScreenMessageActions.swift, PeerInfoScreenPerformButtonAction.swift, PeerInfoScreenAvatarSetup.swift, PeerInfoScreenSettingsActions.swift, PeerInfoScreenDisplayGiftsContextMenu.swift, PeerInfoScreenDisplayMediaGalleryContextMenu.swift** — various `data?.peer as? TelegramXxx` (A), helper bridges (D), wrap drops (C/E).
4. **PeerInfoPaneContainerNode.swift** — L1252 `as? TelegramChannel` (A).
5. **PeerInfoProfileItems.swift, PeerInfoSettingsItems.swift, ListItems/PeerInfoScreenPersonalChannelItem.swift**`data.peer as? TelegramUser` style consumers (A).
6. **PeerInfoHeaderNode.swift, PeerInfoEditingAvatarNode.swift, PeerInfoEditingAvatarOverlayNode.swift, PeerInfoHeaderEditingContentNode.swift** — these files receive `peer` as a parameter (not directly `data.peer`). Only touch if a parameter type declared as `Peer?` is the field from `data.peer` being passed in; otherwise leave.
## Replace_all guidance (wave-41 lesson)
Several wraps repeat identically. Where a file has multiple identical `EnginePeer(peer)` expressions in scopes where `peer` is now `EnginePeer`, use `replace_all=true` on the unique full expression. BUT verify each such file has no same-pattern wrap where `peer` is still raw (chatPeer-bound, currentPeer-bound, etc.) — such wraps must survive.
Safer alternative: edit each site individually.
## Out of scope (enumerated)
- `PeerInfoScreenData.chatPeer`, `.savedMessagesPeer`, `.linkedDiscussionPeer`, `.linkedMonoforumPeer` — stay `Peer?`.
- Internal helpers `canEditPeerInfo` / `peerInfoIsChatMuted` / etc. — stay `peer: Peer?`.
- `peerView.peers[...]` access inside PeerInfoData.swift — stays raw `Peer?`.
- Any `is TelegramXxx` check on a non-`data.peer`-derived variable.
## Build methodology
1. Apply declaration + init + construction edits.
2. Apply consumer edits file by file.
3. `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`
4. Iterate on errors. Budget: 24 iterations (wave-41 lesson: foundational-type property-access migrations are not first-pass-clean).
## Expected ratchet math
- Drops: 15+ wave-40 wraps, ~20 as-cast patterns collapsed, ~5 is-checks rewritten, several `EnginePeer(peer).displayTitle` wraps dropped.
- Adds: ~10 `?._asPeer()` helper bridges, 4 `flatMap(EnginePeer.init)` at construction.
- Net: ~1525 bridges dropped.
## WIP interference check
Pre-existing WIP in tree (per memory):
- `submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift` — modified, unrelated animation WIP. DO NOT TOUCH.
- `submodules/TgVoip/`, `third-party/libx264/`, `build-system/tulsi/` — untracked, unrelated.
- `build-system/bazel-rules/sourcekit-bazel-bsp` — submodule marker, unrelated.
Wave-42 files are all in `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/` — no overlap with WIP. Commit with explicit file list (wave-39/41 lesson).
## Post-commit followups
- Update `docs/superpowers/postbox-refactor-log.md` with "Wave 42 outcome".
- Update `memory/project_postbox_refactor_next_wave.md` with wave 43 candidates:
- Wave 42.x sibling: `PeerInfoScreenData.chatPeer` / `.savedMessagesPeer` / `.linkedDiscussionPeer` / `.linkedMonoforumPeer` as a bundle (same file, narrow blast radius).
- Wave 42.y: PeerInfo-internal helper signatures (drops the ~10 ADD-WRAP markers).
- Option 2 from wave-42 shortlist: `RenderedChannelParticipant.peers` dict.

View file

@ -0,0 +1,395 @@
# Postbox → TelegramEngine wave 37: `peerTokenTitle` peer parameter Peer → EnginePeer
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate the private free function `peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings:, nameDisplayOrder:)` in `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` so `peer` is `EnginePeer`, dropping 5 `._asPeer()` bridges at call sites in the same file.
**Architecture:** Single-file, atomic, private-function refactor. No public API change, no BUILD-file touch, no cross-module effects. Function body simplifies `EnginePeer(peer).displayTitle(...)``peer.displayTitle(...)`.
**Tech Stack:** Swift, Bazel via `Make.py` wrapper, Telegram-iOS project conventions (see CLAUDE.md).
**Reference:** Spec `docs/superpowers/specs/2026-04-24-peertokentitle-engine-peer-migration-design.md`.
---
## File Structure
Only one file is touched:
- **Modify:** `submodules/TelegramUI/Sources/ContactMultiselectionController.swift`
- L21 — signature change (`peer: Peer``peer: EnginePeer`)
- L27 — body simplification (drop redundant `EnginePeer(...)` wrap)
- L171, L201, L386, L403, L748 — call-site bridge drops (`peer: peer._asPeer()``peer: peer`)
No files created. No files deleted. No BUILD files touched.
---
## Task 1: Pre-flight inventory verification
**Files:** None (grep-only).
- [ ] **Step 1: Confirm the function is private and single-file**
Run:
```bash
grep -rn "peerTokenTitle" submodules/ Telegram/ third-party/ --include="*.swift"
```
Expected: exactly 6 matches, all in `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` — 1 definition at L21 and 5 call sites at L171, L201, L386, L403, L748.
If any match appears outside this file, **stop and re-evaluate scope**: the function may not actually be private or another file has copy-pasted the name.
- [ ] **Step 2: Confirm all 5 call sites currently use `._asPeer()`**
Run:
```bash
grep -n "peerTokenTitle(.*_asPeer())" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: 5 matches, line numbers 171, 201, 386, 403, 748.
If the count is not 5, **stop and re-inventory** — a prior change may have shifted line numbers or altered a call site.
- [ ] **Step 3: Confirm no other `peerTokenTitle` overload exists**
Run:
```bash
grep -n "func peerTokenTitle" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: exactly 1 match at line 21 (`private func peerTokenTitle(...)`).
- [ ] **Step 4: Confirm `EnginePeer.displayTitle(strings:displayOrder:)` exists**
Run:
```bash
grep -rn "func displayTitle(strings:" submodules/TelegramCore/Sources/TelegramEngine/ submodules/TelegramCore/Sources/SyncCore/
```
Expected: a match on `EnginePeer` extension exposing `displayTitle(strings: PresentationStrings, displayOrder: PresentationPersonNameOrder)`. (This is the method already called as `EnginePeer(peer).displayTitle(...)` at L27, so its existence is certain — this step just makes the dependency explicit.)
---
## Task 2: Edit the function signature and body
**Files:**
- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionController.swift:21-29`
- [ ] **Step 1: Read the current function definition**
Read the file, lines 2129. Current state:
```swift
private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String {
if peer.id == accountPeerId {
return strings.DialogList_SavedMessages
} else if peer.id.isReplies {
return strings.DialogList_Replies
} else {
return EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
}
```
- [ ] **Step 2: Apply the signature change**
Use Edit with:
- `old_string`:
```
private func peerTokenTitle(accountPeerId: PeerId, peer: Peer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String {
if peer.id == accountPeerId {
return strings.DialogList_SavedMessages
} else if peer.id.isReplies {
return strings.DialogList_Replies
} else {
return EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
}
```
- `new_string`:
```
private func peerTokenTitle(accountPeerId: PeerId, peer: EnginePeer, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) -> String {
if peer.id == accountPeerId {
return strings.DialogList_SavedMessages
} else if peer.id.isReplies {
return strings.DialogList_Replies
} else {
return peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)
}
}
```
Note: `accountPeerId: PeerId` stays as-is — `PeerId` is already the typealias for `EnginePeer.Id`. `peer.id.isReplies` works unchanged because `EnginePeer.Id` exposes `isReplies`.
---
## Task 3: Drop `._asPeer()` bridges at all 5 call sites
**Files:**
- Modify: `submodules/TelegramUI/Sources/ContactMultiselectionController.swift` (L171, L201, L386, L403, L748)
All 5 call sites have an identical argument fragment:
```
peer: peer._asPeer(),
```
…which must become:
```
peer: peer,
```
The surrounding context differs per site (two distinct `strings/nameDisplayOrder` chains, see below), so we handle the substitution in two batches.
- [ ] **Step 1: Replace sites L171, L201, L748 (use `strongSelf.presentationData.strings` / `strongSelf.presentationData.nameDisplayOrder` or `self.presentationData.strings` / `self.presentationData.nameDisplayOrder`)**
Three call sites share identical code but with different leading `accountPeerId` expressions. Apply them individually.
**L171 and L201 are identical** — both read:
```swift
return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
```
Use Edit with `replace_all=true`:
- `old_string`:
```
return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
```
- `new_string`:
```
return EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: params.context.account.peerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
```
**L748** reads:
```swift
tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer._asPeer(), strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)))
```
Use Edit (no `replace_all` — this line is unique):
- `old_string`:
```
tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer._asPeer(), strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)))
```
- `new_string`:
```
tokens.append(EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: self.context.account.peerId, peer: peer, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer)))
```
- [ ] **Step 2: Replace sites L386 and L403 (use `accountPeerId` local)**
**L386 and L403 are identical** — both read:
```swift
addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
```
Use Edit with `replace_all=true`:
- `old_string`:
```
addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer._asPeer(), strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
```
- `new_string`:
```
addedToken = EditableTokenListToken(id: peer.id, title: peerTokenTitle(accountPeerId: accountPeerId, peer: peer, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder), fixedPosition: nil, subject: .peer(peer))
```
- [ ] **Step 3: Grep to confirm zero remaining bridge sites**
Run:
```bash
grep -n "peerTokenTitle(.*_asPeer())" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: **0 matches**.
If any match remains, the previous edits missed a line variant — re-read the file around each missed line and apply a targeted Edit for that variant.
- [ ] **Step 4: Confirm the 5 expected `peer: peer,` call sites now appear**
Run:
```bash
grep -n "peerTokenTitle(.*peer: peer," submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: 5 matches, line numbers approximately 171, 201, 386, 403, 748 (exact numbers unchanged — the edits don't shift line counts).
---
## Task 4: Build verification
**Files:** None edited in this task.
- [ ] **Step 1: Run the full project build with --continueOnError**
Run:
```bash
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 succeeds with exit code 0 and no compilation errors.
**If the build fails:**
1. Inspect the error output. Three failure modes are anticipated (all should be rare given the scope):
- **Missing `displayTitle` on `EnginePeer`:** unlikely, since L27 was calling it pre-migration. If it happens, verify the `EnginePeer` import chain — but do not add new imports; this file already imports `TelegramCore`.
- **A 6th call site exists** that the pre-flight grep missed (e.g., one using a different string pattern like `peer:peer` with no space, or a multi-line call). Locate it with `grep -n "peerTokenTitle" submodules/TelegramUI/Sources/ContactMultiselectionController.swift` and apply the bridge drop manually.
- **Unrelated type-inference cascade**, e.g., some `peer` local was previously inferred as `Peer` via the callback chain and now can't be. Read the error line and assess: if it's inside the function body or call site, adjust; if it's elsewhere in the file, it was pre-existing and unrelated — still, don't touch it mid-wave. Abandon per wave-rule 5 if scope creep is required.
2. Re-run the build after the fix.
- [ ] **Step 2: Confirm the post-migration grep is clean**
Run (after successful build):
```bash
grep -n "peerTokenTitle(.*_asPeer())" submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
Expected: **0 matches**.
---
## Task 5: Commit
**Files:**
- `submodules/TelegramUI/Sources/ContactMultiselectionController.swift`
- [ ] **Step 1: Stage the one file**
Run:
```bash
git add submodules/TelegramUI/Sources/ContactMultiselectionController.swift
```
- [ ] **Step 2: Verify the staged diff**
Run:
```bash
git diff --cached --stat
```
Expected: `1 file changed, 6 insertions(+), 6 deletions(-)` (or thereabouts — 1 line's worth of signature change, 1 body-line change, 5 identical call-site changes; each is a 1-line replacement, net zero line-count delta).
Also run:
```bash
git diff --cached
```
Inspect manually to confirm: (a) the function signature changed `peer: Peer``peer: EnginePeer`; (b) the body `EnginePeer(peer).displayTitle(...)``peer.displayTitle(...)`; (c) 5 call sites lost `._asPeer()`. No other edits.
- [ ] **Step 3: Commit**
Run:
```bash
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 37
peerTokenTitle: peer parameter Peer -> EnginePeer.
Drops 5 _asPeer() bridges in ContactMultiselectionController.swift
(L171, L201, L386, L403, L748) - bridges installed by prior waves.
Private free function, single-file change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 4: Confirm commit**
Run:
```bash
git log --oneline -3
```
Expected: the new wave-37 commit at the top.
---
## Task 6: Update memory / log
**Files:**
- Modify: `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`
- Modify: `docs/superpowers/postbox-refactor-log.md`
- [ ] **Step 1: Read the current memory file for the refactor**
Read `/Users/isaac/.claude/projects/-Users-isaac-build-telegram-telegram-ios/memory/project_postbox_refactor_next_wave.md`.
- [ ] **Step 2: Update frontmatter + add wave-37 entry**
Update the `description:` frontmatter field to reference wave 37 outcome (number of bridges dropped, build-iteration count, first-pass-clean-or-not). Add a bullet to "Latest commits" section with the new SHA and a one-line summary. Remove the "peerTokenTitle parameter migration" bullet from the "Wave 37 candidates" section (it's now landed). Update "Recommended wave 37" section to "Recommended wave 38" with a fresh recommendation from the remaining candidates.
- [ ] **Step 3: Read the refactor log**
Read `docs/superpowers/postbox-refactor-log.md`, locate the "Wave 36 outcome" section.
- [ ] **Step 4: Append wave-37 outcome**
Under the "Wave N outcomes" section, append a "Wave 37 outcome" subsection with:
- Commit SHA (from `git log --oneline -1`)
- File touched (1: ContactMultiselectionController.swift)
- Lines changed (6 deletions, 6 insertions)
- Bridges dropped (5)
- Build iterations to converge (should be 1)
- Any lessons observed (likely none — this wave is mechanical)
- [ ] **Step 5: Commit memory + log update**
Run:
```bash
git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 37 outcome
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
(Memory file under `~/.claude/` is not in the repo — save it separately via the Write tool; do not try to `git add` it.)
---
## Self-review results
**Spec coverage:** Every scope item in the spec maps to a task:
- Spec L21 signature change → Task 2 Step 2
- Spec L27 body simplification → Task 2 Step 2
- Spec L171/201/386/403/748 bridge drops → Task 3 Steps 12
- Spec verification (grep + build + post-grep) → Task 1 + Task 4
- Spec commit message → Task 5 Step 3
Out-of-scope items (L459, `import Postbox`, `accountPeerId: PeerId`) remain explicitly untouched — no task edits them.
**Placeholder scan:** No TBD, TODO, placeholder phrases, or "handle edge cases"-style hand-waves. Every step has a concrete command or code block.
**Type consistency:** `peer: EnginePeer`, `EnginePeer.Id` (= `PeerId` typealias), and `EnginePeer.displayTitle(strings:displayOrder:)` are all consistent across tasks.

View file

@ -0,0 +1,666 @@
# Wave 44 — RenderedChannelParticipant.peers Engine-Peer Migration 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 `RenderedChannelParticipant.peers: [PeerId: Peer]` to `[EnginePeer.Id: EnginePeer]`. Closes the wave-41 ratchet — the public struct no longer leaks raw Postbox `Peer` in any field.
**Architecture:** Single atomic commit. Declaration in TelegramCore changes; 8 TelegramCore producer functions wrap raw `Peer` values at their local-dict insertion points (inside transactions that already read from Postbox); 11 consumer-surface bridges drop (6 `EnginePeer(peer)` read-wraps + 5 `.mapValues({ $0._asPeer() })` constructor-unwrap transforms); 1 consumer-surface unwrap is added where an extracted `EnginePeer` value flows into a `SimpleDictionary<PeerId, Peer>`.
**Tech Stack:** Swift, Bazel (via `python3 build-system/Make/Make.py`), Postbox, TelegramCore, TelegramEngine. No unit tests — full-build verification only.
**Spec:** `docs/superpowers/specs/2026-04-24-rcp-peers-engine-migration-design.md`
---
## File Structure
All edits happen in existing files — no new files created. Touched files:
**TelegramCore (declaration + producers, 9 files):**
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` (declaration)
- `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift`
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift`
**Consumers (drops + 1 add, 5 files):**
- `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift`
- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift`
- `submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift`
- `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift`
- `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift`
**Total:** 14 files, ~30 edits, one atomic commit.
---
## Task 1: Pre-flight re-verification
**Purpose:** Confirm the grep surface matches the spec before editing anything. If any site count diverges, stop and update the spec.
**Files:** None modified.
- [ ] **Step 1.1: Verify 7 `participant.peers[...]` consumer read sites**
Run:
```bash
grep -rnE "participant\.peers\[|rcp\.peers\[|renderedParticipant\.peers\[" --include="*.swift" submodules/ 2>/dev/null
```
Expected output — exactly 6 bracketed-indexing sites (the 7th site, iteration without bracket-indexing, is checked in Step 1.2):
- `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift:293`
- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:835`
- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:869`
- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:1087`
- `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift:1121`
- `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift:164`
If any line numbers differ by more than ±3 lines, re-read surrounding context to confirm identity. If a NEW site appears that isn't in the spec, STOP and update the spec before proceeding.
- [ ] **Step 1.2: Verify the iteration site is still at the expected line**
Run:
```bash
grep -nE "for \(.*,.* peer\) in participant\.peers" submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift
```
Expected: `672: for (_, peer) in participant.peers {`
- [ ] **Step 1.3: Verify all 8 TelegramCore producers still build `var peers: [PeerId: Peer] = [:]` locally**
Run:
```bash
grep -rnE "^[[:space:]]+var peers: \[PeerId: Peer\] = \[:\]" submodules/TelegramCore/Sources/TelegramEngine/ 2>/dev/null
```
Expected 8 matches, one per producer file:
- `Messages/RequestStartBot.swift:61`
- `Peers/ChannelOwnershipTransfer.swift:170`
- `Peers/JoinChannel.swift:59`
- `Peers/AddPeerMember.swift:242`
- `Peers/PeerAdmins.swift:251`
- `Peers/ChannelBlacklist.swift:128`
- `Peers/Ranks.swift:60`
- `Peers/ChannelMembers.swift:102`
If a producer is missing from this grep, check whether it now receives `peers` as a parameter rather than building locally — if so, STOP and update the spec (chain-migration needed).
- [ ] **Step 1.4: Verify no `as?` / `is TelegramX` casts exist on extracted dict values**
Run:
```bash
grep -rnE "peer = participant\.peers" --include="*.swift" -A 4 submodules/ 2>/dev/null | grep -E "as\?|is Telegram"
```
Expected output: empty. If this returns non-empty, STOP and update the spec.
- [ ] **Step 1.5: Verify no one is assigning into `participant.peers` (writes would break the migration)**
Run:
```bash
grep -rnE "participant\.peers\[[^]]+\][[:space:]]*=" --include="*.swift" submodules/ 2>/dev/null
```
Expected output: empty (`.peers` is a `let`; no writes possible anyway, but double-check).
---
## Task 2: Migrate declaration in ChannelParticipants.swift
**Purpose:** Change the struct field type and init default.
**Files:**
- Modify: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift:11, 14`
- [ ] **Step 2.1: Change field declaration**
In `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift`, line 11:
```swift
// before
public let peers: [PeerId: Peer]
// after
public let peers: [EnginePeer.Id: EnginePeer]
```
- [ ] **Step 2.2: Change init default**
Same file, line 14:
```swift
// before
public init(participant: ChannelParticipant, peer: EnginePeer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) {
// after
public init(participant: ChannelParticipant, peer: EnginePeer, peers: [EnginePeer.Id: EnginePeer] = [:], presences: [PeerId: PeerPresence] = [:]) {
```
Do NOT commit yet — this leaves the repo in a broken state until producers and consumers are updated.
---
## Task 3: Migrate TelegramCore producers (8 files)
**Purpose:** Each of the 8 TelegramCore producers builds a local `peers: [PeerId: Peer] = [:]` dict from raw Postbox peers inside a transaction. Migrate each local dict to `[EnginePeer.Id: EnginePeer] = [:]` and wrap every insertion value with `EnginePeer(...)`.
**Pattern (applies to every sub-step):**
```swift
// before
var peers: [PeerId: Peer] = [:]
peers[X.id] = X
// after
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[X.id] = EnginePeer(X)
```
The surrounding `presences: [PeerId: PeerPresence]` dict and the `RCP(..., peer: EnginePeer(X), ...)` wrap on the primary `peer` field both stay unchanged.
- [ ] **Step 3.1: Migrate `RequestStartBot.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift`
Line 61: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 64: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.2: Migrate `ChannelOwnershipTransfer.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift`
Line 170: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 172: `peers[accountUser.id] = accountUser``peers[accountUser.id] = EnginePeer(accountUser)`
Line 176: `peers[user.id] = user``peers[user.id] = EnginePeer(user)`
Line 180 is a double-RCP-construction; `peers:` reuses the same local — no change at line 180.
- [ ] **Step 3.3: Migrate `JoinChannel.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift`
Line 59: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 64: `peers[account.peerId] = peer``peers[account.peerId] = EnginePeer(peer)`
Line 77: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.4: Migrate `AddPeerMember.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift`
Line 242: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 244: `peers[memberPeer.id] = memberPeer``peers[memberPeer.id] = EnginePeer(memberPeer)`
Line 251: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.5: Migrate `PeerAdmins.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift`
Line 251: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 253: `peers[adminPeer.id] = adminPeer``peers[adminPeer.id] = EnginePeer(adminPeer)`
Line 259: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.6: Migrate `ChannelBlacklist.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift`
Line 128: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 130: `peers[memberPeer.id] = memberPeer``peers[memberPeer.id] = EnginePeer(memberPeer)`
Line 136: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.7: Migrate `Ranks.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift`
Line 60: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 62: `peers[user.id] = user``peers[user.id] = EnginePeer(user)`
Line 68: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.8: Migrate `ChannelMembers.swift`**
File: `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift`
Line 102: `var peers: [PeerId: Peer] = [:]``var peers: [EnginePeer.Id: EnginePeer] = [:]`
Line 105: `peers[peer.id] = peer``peers[peer.id] = EnginePeer(peer)`
- [ ] **Step 3.9: Post-producer verification**
Run:
```bash
grep -rnE "^[[:space:]]+var peers: \[PeerId: Peer\] = \[:\]" submodules/TelegramCore/Sources/TelegramEngine/ 2>/dev/null
```
Expected: no output (all 8 have been converted).
Run:
```bash
grep -rnE "^[[:space:]]+var peers: \[EnginePeer\.Id: EnginePeer\] = \[:\]" submodules/TelegramCore/Sources/TelegramEngine/ 2>/dev/null | wc -l
```
Expected: `8` (or ` 8`).
---
## Task 4: Drop 5 consumer `.mapValues({ $0._asPeer() })` transforms
**Purpose:** These consumer-side constructors build a `[EnginePeer.Id: EnginePeer]` source dict locally and currently unwrap to `[PeerId: Peer]` via `.mapValues({ $0._asPeer() })` to feed the old constructor signature. After Task 2, the constructor expects engine values directly — the transform becomes a no-op and is removed.
**Pattern (applies to every sub-step):**
```swift
// before
peers: peers.mapValues({ $0._asPeer() })
// after
peers: peers
```
- [ ] **Step 4.1: `ChannelAdminsController.swift:926`**
File: `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift`
Line 926 (long line): locate the substring `peers: peers.mapValues({ $0._asPeer() })` and replace with `peers: peers`.
- [ ] **Step 4.2: `ChannelMembersSearchContainerNode.swift:994`**
File: `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift`
Line 994: replace `peers: peers.mapValues({ $0._asPeer() })``peers: peers`.
- [ ] **Step 4.3: `ChannelMembersSearchContainerNode.swift:998`**
Same file, line 998: replace `peers: peers.mapValues({ $0._asPeer() })``peers: peers`.
- [ ] **Step 4.4: `ChannelMembersSearchControllerNode.swift:409`**
File: `submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift`
Line 409: replace `peers: peers.mapValues({ $0._asPeer() })``peers: peers`.
- [ ] **Step 4.5: `ChannelMembersSearchControllerNode.swift:413`**
Same file, line 413: replace `peers: peers.mapValues({ $0._asPeer() })``peers: peers`.
- [ ] **Step 4.6: Post-Task-4 verification**
Run:
```bash
grep -rnE "peers\.mapValues\(\{ \$0\._asPeer\(\) \}\)" --include="*.swift" submodules/ 2>/dev/null
```
Expected: no output (all 5 drops applied). If any remain, locate and drop.
---
## Task 5: Drop 6 consumer `EnginePeer(peer).displayTitle(...)` read-wraps
**Purpose:** Each site extracts `peer` from `participant.peers[X]`, wraps with `EnginePeer(peer)` to call `.displayTitle(...)`. After Task 2 the extracted `peer` is already `EnginePeer` — drop the wrap.
**Pattern (applies to every sub-step):**
```swift
// before
EnginePeer(peer).displayTitle(strings: ..., displayOrder: ...)
// after
peer.displayTitle(strings: ..., displayOrder: ...)
```
- [ ] **Step 5.1: `ChannelAdminsController.swift:297`**
File: `submodules/PeerInfoUI/Sources/ChannelAdminsController.swift`, line 297.
Replace:
```swift
peerText = strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string
```
with:
```swift
peerText = strings.Channel_Management_PromotedBy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string
```
The adjacent `peer.id == participant.peer.id` comparison at line 294 stays unchanged (both are `EnginePeer.Id`).
- [ ] **Step 5.2: `ChannelMembersSearchContainerNode.swift:839`**
File: `submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift`, line 839.
Replace:
```swift
label = presentationData.strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
with:
```swift
label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
- [ ] **Step 5.3: `ChannelMembersSearchContainerNode.swift:870`**
Same file, line 870.
Replace:
```swift
label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
with:
```swift
label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
- [ ] **Step 5.4: `ChannelMembersSearchContainerNode.swift:1091`**
Same file, line 1091.
Replace:
```swift
label = presentationData.strings.Channel_Management_PromotedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
with:
```swift
label = presentationData.strings.Channel_Management_PromotedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
- [ ] **Step 5.5: `ChannelMembersSearchContainerNode.swift:1122`**
Same file, line 1122.
Replace:
```swift
label = presentationData.strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
with:
```swift
label = presentationData.strings.Channel_Management_RemovedBy(peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)).string
```
- [ ] **Step 5.6: `ChannelBlacklistController.swift:165`**
File: `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift`, line 165.
Replace:
```swift
text = .text(strings.Channel_Management_RemovedBy(EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary)
```
with:
```swift
text = .text(strings.Channel_Management_RemovedBy(peer.displayTitle(strings: strings, displayOrder: nameDisplayOrder)).string, .secondary)
```
- [ ] **Step 5.7: Post-Task-5 verification**
Run:
```bash
grep -rnE "EnginePeer\(peer\)\.displayTitle" --include="*.swift" submodules/PeerInfoUI/ 2>/dev/null
```
Expected: no output within PeerInfoUI. (Other modules may still have unrelated `EnginePeer(peer).displayTitle` usages on non-RCP-peers peers — those are out of scope.)
Run specifically for the 6 migrated sites:
```bash
grep -n "EnginePeer(peer)\.displayTitle" submodules/PeerInfoUI/Sources/ChannelAdminsController.swift submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift 2>/dev/null
```
Expected: no output.
---
## Task 6: Add 1 consumer unwrap at ChatRecentActionsHistoryTransition
**Purpose:** The one site that iterates `participant.peers` and inserts values into a `SimpleDictionary<PeerId, Peer>` container. After Task 2, the iterated `peer` is `EnginePeer`; the outer container still expects raw `Peer`. Unwrap at the insertion site.
**Files:**
- Modify: `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift:673`
- [ ] **Step 6.1: Replace insertion line**
In `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift`:
Context (lines 672674, unchanged outside line 673):
```swift
for (_, peer) in participant.peers {
peers[peer.id] = peer
}
```
After edit:
```swift
for (_, peer) in participant.peers {
peers[peer.id] = peer._asPeer()
}
```
- [ ] **Step 6.2: Spot-check nearby wave-41 unwrap (reference, no change)**
Line 675 in the same function is `peers[participant.peer.id] = participant.peer._asPeer()` — a wave-41 artifact, unrelated to this wave. Leave unchanged.
---
## Task 7: Full build verification
**Purpose:** Verify the atomic change set compiles. Produces the ONLY real test signal for this wave.
**Files:** None modified; this is a build run.
- [ ] **Step 7.1: Run the full build**
Run:
```bash
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 succeeds. Look for `INFO: Build completed successfully` near the end.
- [ ] **Step 7.2: If build fails — triage**
Expected failure patterns (from wave-41 lesson, budget 23 iterations):
1. **Missing producer wrap** — compiler error `cannot assign value of type 'Peer' to subscript of type 'EnginePeer'` (or similar) at a TelegramCore producer file → check that file's `var peers:` decl was converted AND all insertion RHS values are wrapped.
2. **Missed consumer site** — compiler error at a `.displayTitle` call on a raw Peer → find `EnginePeer(peer).displayTitle` site that Task 5 missed; drop the wrap.
3. **Mismatched mapValues drop**`cannot convert value of type '[EnginePeer.Id: EnginePeer]' to expected argument type '[PeerId: Peer]'` → the spec's risk #3 triggered (a `.mapValues` site had a raw-Peer source after all); replace the drop with `peers.mapValues(EnginePeer.init)` at that site instead.
4. **New grep surface** — compiler complains about a site not in this plan → add it to the commit's scope; log it to the outcome doc.
Apply fixes, re-run Step 7.1. Repeat up to 3 iterations before re-evaluating scope.
- [ ] **Step 7.3: Post-build final grep audit**
Run:
```bash
grep -rnE "participant\.peers\[[^]]+\]" --include="*.swift" submodules/ 2>/dev/null
```
Expected: the same 6 read sites as Step 1.1 (now without `EnginePeer(peer)` wraps).
Run:
```bash
grep -rnE "peers\.mapValues\(\{ \$0\._asPeer\(\) \}\)" --include="*.swift" submodules/ 2>/dev/null
```
Expected: no output.
Run:
```bash
grep -n "public let peers: \[" submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift
```
Expected: `11: public let peers: [EnginePeer.Id: EnginePeer]`.
---
## Task 8: Atomic commit
**Purpose:** Land all wave-44 edits in ONE commit. Explicitly enumerate files in `git add` (wave-39 lesson — re-confirmed in waves 41, 42, 43) to avoid pulling in the pre-existing working-tree WIP listed in the spec's risk section (`ListView.swift`, `ChatMessageTransitionNode.swift`, tulsi/, TgVoip/, libx264/).
**Files:** Commits all 14 wave-44 files.
- [ ] **Step 8.1: Confirm working-tree state**
Run:
```bash
git status --short
```
Expected (pre-existing WIP, unchanged):
- ` m build-system/bazel-rules/sourcekit-bazel-bsp`
- ` M submodules/Display/Source/ListView.swift` (do NOT include)
- ` M submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift` (do NOT include)
- `?? build-system/tulsi/` (do NOT include)
- `?? submodules/TgVoip/` (do NOT include)
- `?? third-party/libx264/` (do NOT include)
Plus the wave-44 modified files:
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift`
- ` M submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift`
- ` M submodules/PeerInfoUI/Sources/ChannelAdminsController.swift`
- ` M submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift`
- ` M submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift`
- ` M submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift`
- ` M submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift`
If the set of wave-44-modified files doesn't match exactly (extra or missing), STOP and investigate before committing.
- [ ] **Step 8.2: Stage only wave-44 files**
Run:
```bash
git add \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \
submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \
submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \
submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift
```
- [ ] **Step 8.3: Verify staged set matches expected**
Run:
```bash
git diff --cached --stat
```
Expected: exactly 14 files staged, all from the wave-44 list. If `ListView.swift`, `ChatMessageTransitionNode.swift`, `bazel-rules/sourcekit-bazel-bsp`, `tulsi/`, `TgVoip/`, or `libx264/` appear here, unstage them.
- [ ] **Step 8.4: Commit**
Run:
```bash
git commit -m "$(cat <<'EOF'
Postbox -> TelegramEngine wave 44
Migrate RenderedChannelParticipant.peers from [PeerId: Peer] to
[EnginePeer.Id: EnginePeer]. Closes the wave-41 ratchet — the public
struct no longer leaks raw Peer types in any field (presences stays
Postbox-typed; separate migration).
Consumer-surface: -10 bridges. Dropped 6 EnginePeer(peer) read-wraps
at participant.peers[...] extraction sites across
ChannelAdminsController, ChannelMembersSearchContainerNode,
ChannelBlacklistController. Dropped 5 .mapValues({ $0._asPeer() })
constructor-unwrap transforms in ChannelAdminsController,
ChannelMembersSearchContainerNode, ChannelMembersSearchControllerNode.
Added 1 ._asPeer() at ChatRecentActionsHistoryTransition.swift:673
where the iterated value is inserted into a raw-Peer SimpleDictionary.
TelegramCore producers: 8 files build the local peers dict inside
postbox.transaction and wrap at the insertion point. ChannelMembers,
RequestStartBot, ChannelOwnershipTransfer, JoinChannel, AddPeerMember,
PeerAdmins, ChannelBlacklist, Ranks.
No unit tests in this project; full Telegram/Telegram build verified
under configuration=debug_sim_arm64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
- [ ] **Step 8.5: Verify commit**
Run:
```bash
git log -1 --stat
```
Expected: commit with 14 files changed, message starting with `Postbox -> TelegramEngine wave 44`.
Run:
```bash
git status --short
```
Expected: no M- or A-flagged wave-44 files (all committed); only the pre-existing WIP (`ListView.swift`, `ChatMessageTransitionNode.swift`, etc.) remains.
---
## Rollback
If the wave cannot be completed (e.g., build fails after 4+ iterations and the scope balloons beyond plan):
```bash
git restore --staged \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \
submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \
submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \
submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift
git checkout -- \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift \
submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift \
submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift \
submodules/PeerInfoUI/Sources/ChannelAdminsController.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift \
submodules/PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift \
submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift \
submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift
```
Then document what was learned in an outcome doc and update `project_postbox_refactor_next_wave.md`.
---
## Success criteria (from spec)
1. ✅ `ChannelParticipants.swift` has `peers: [EnginePeer.Id: EnginePeer]` declaration (Task 2).
2. ✅ All 8 TelegramCore producers compile with wrapped inserts (Task 3).
3. ✅ All 5 consumer `.mapValues({ $0._asPeer() })` transforms are removed (Task 4).
4. ✅ All 6 consumer `EnginePeer(peer).displayTitle(...)` wraps on extracted dict values are removed (Task 5).
5. ✅ `ChatRecentActionsHistoryTransition.swift:673` uses `peer._asPeer()` for the SimpleDictionary insertion value (Task 6).
6. ✅ Full `Telegram/Telegram` build (`configuration=debug_sim_arm64`) is clean — **one** atomic commit (Tasks 7, 8).
7. ✅ Grep post-migration: `participant.peers[` returns only engine-typed call sites; no residual `EnginePeer(peer)` on `.peers[...]` extractions (Steps 5.7, 7.3).

View file

@ -0,0 +1,860 @@
# 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:**
```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
```
---
## File Structure
**Created:** none.
**Modified (27 files):**
TelegramCore (10 files):
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` — struct field type + init param + Equatable impl
- `submodules/TelegramCore/Sources/TelegramEngine/Messages/RequestStartBot.swift` — 1 constructor wrap
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift` — 1 constructor wrap
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelAdminEventLogs.swift` — 7 constructor wraps
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelBlacklist.swift` — 1 constructor wrap
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelMembers.swift` — 1 constructor wrap
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift` — 2 constructor wraps
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/JoinChannel.swift` — 1 constructor wrap
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift` — 1 constructor wrap
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift` — 1 constructor wrap
PeerInfoUI (6 files):
- `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`
Other consumers (11 files):
- `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`
- `submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift` *(no `participant.peer` edits needed — all ZERO; file touched only if build surfaces type issues)*
- `submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift` *(no edits expected — only `item.peer.id` reference 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:
```swift
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:
```swift
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences))
```
After:
```swift
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: presences))
```
- [ ] **Step 2.2:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/AddPeerMember.swift:255`
Before:
```swift
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences))
```
After:
```swift
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:
```swift
action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: peer))
// becomes:
action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer)))
```
Line 279 (two constructors on one line):
```swift
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):
```swift
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):
```swift
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:
```swift
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences), isMember)
```
After:
```swift
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:
```swift
items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences))
```
After:
```swift
items.append(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: renderedPresences))
```
- [ ] **Step 2.6:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelOwnershipTransfer.swift:180`
Before:
```swift
return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: accountUser, peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))]
```
After:
```swift
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:
```swift
return RenderedChannelParticipant(participant: updatedParticipant, peer: peer, peers: peers, presences: presences)
```
After:
```swift
return RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences)
```
- [ ] **Step 2.8:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerAdmins.swift:262`
Before:
```swift
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: adminPeer, peers: peers, presences: presences))
```
After:
```swift
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(adminPeer), peers: peers, presences: presences))
```
- [ ] **Step 2.9:** `submodules/TelegramCore/Sources/TelegramEngine/Peers/Ranks.swift:95`
Before:
```swift
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))
```
After:
```swift
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:
```swift
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:
```swift
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer._asPeer(), presences: presences))
```
After:
```swift
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:
```swift
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:
```swift
peer: EnginePeer(participant.peer)
```
After:
```swift
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:
```swift
if let user = participant.peer as? TelegramUser, let _ = user.botInfo {
```
After:
```swift
if case let .user(user) = participant.peer, let _ = user.botInfo {
```
- [ ] **Step 5.2:** Line 334 — DROP wrap.
Before:
```swift
peer: EnginePeer(participant.peer)
```
After:
```swift
peer: participant.peer
```
- [ ] **Step 5.3:** Line 707 — DROP wrap (the wave-39-installed Shape-C wrap).
Before:
```swift
peer: EnginePeer(participant.peer)
```
After:
```swift
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:
```swift
peer: .peer(peer: EnginePeer(participant.peer), chatPeer: EnginePeer(participant.peer)),
```
After:
```swift
peer: .peer(peer: participant.peer, chatPeer: participant.peer),
```
- [ ] **Step 6.2:** Line 223 — DROP wrap.
Before:
```swift
interaction.peerSelected(EnginePeer(participant.peer), participant)
```
After:
```swift
interaction.peerSelected(participant.peer, participant)
```
- [ ] **Step 6.3:** Line 752 — CAST rewrite.
Before:
```swift
if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil {
```
After:
```swift
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:
```swift
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)
```
After:
```swift
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:
```swift
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.
```swift
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:
```swift
peer: EnginePeer(participant.peer)
```
After:
```swift
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:
```swift
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences)
```
After:
```swift
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:
```swift
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:
```swift
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
```
After:
```swift
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:
```swift
EnginePeer(participant.peer)
```
After:
```swift
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:
```swift
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:
```swift
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:
```swift
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:
```swift
peers[participant.peer.id] = participant.peer
```
After:
```swift
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:
```swift
peers[new.peer.id] = new.peer
```
After:
```swift
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:
```swift
var peer: Peer {
switch self {
case let .channelMember(participant, _):
return participant.peer
```
After:
```swift
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:
```swift
peers.append(EnginePeer(participant.peer))
```
After:
```swift
peers.append(participant.peer)
```
- [ ] **Step 14.2:** Line 566 — CAST rewrite.
Before:
```swift
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
```
After:
```swift
if case let .user(user) = participant.peer, user.botInfo != nil {
```
- [ ] **Step 14.3:** Line 576 — DROP wrap.
Before:
```swift
peers.append(EnginePeer(participant.peer))
```
After:
```swift
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 `.peer` accesses remain on `RenderedChannelParticipant`. 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:
```swift
peer: author._asPeer()
```
After:
```swift
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:
```swift
let peer = author
renderedParticipants.append(RenderedChannelParticipant(
participant: participant,
peer: peer
))
```
After:
```swift
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:
```swift
peer: authorPeer._asPeer()
```
After:
```swift
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`.
```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
```
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:
1. Read the error
2. 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.)?
3. Apply the appropriate fix (`._asPeer()` if raw Peer needed; unwrap the wrap if EnginePeer needed)
4. Re-run the build
Budget: 13 build iterations.
- [ ] **Step 18.2:** Post-build grep verification.
Run these greps and confirm they return only the expected residual matches:
```sh
grep -rn 'EnginePeer(participant\.peer)' submodules/ --include='*.swift' | grep -v submodules/TelegramCore/ | grep -v submodules/Postbox/
```
Expected: empty.
```sh
grep -rn 'EnginePeer(new\.peer)' submodules/ --include='*.swift' | grep -v submodules/TelegramCore/
```
Expected: empty.
```sh
grep -rn 'participant\.peer as\? TelegramUser' submodules/ --include='*.swift'
```
Expected: empty.
```sh
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).
```sh
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.
```sh
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.
```sh
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.
```sh
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):
```markdown
## 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.md` memory file with the wave 41 outcome and the wave 42 candidate (likely `PeerInfoScreenData.peer → EnginePeer`).
- [ ] **Step 20.3:** Commit docs updates.
```sh
git add docs/superpowers/postbox-refactor-log.md
git commit -m "$(cat <<'EOF'
docs: log wave 41 outcome
EOF
)"
```

View file

@ -946,6 +946,239 @@ Plan / record: no plan doc this wave — user specified the migration pattern di
---
## Wave 37 outcome (2026-04-24)
`peerTokenTitle(peer: Peer → EnginePeer)`. Private free function in `submodules/TelegramUI/Sources/ContactMultiselectionController.swift`. Atomic single-file commit `734ab44dd2`. 7 insertions / 7 deletions.
**Migration shape.** Ring-2 cleanup of bridges wave 36 installed. The function body was already round-tripping: callers unwrapped `EnginePeer → Peer` with `._asPeer()` only for the body to re-wrap with `EnginePeer(peer).displayTitle(...)`. Flipping the parameter to `EnginePeer` drops both the 5 call-site bridges and the 1 body-side wrap. Zero semantic change — verified by code-quality reviewer: `displayTitle(strings:displayOrder:)` is defined only on `EnginePeer` (LocalizedPeerData/PeerTitle.swift:29), and `EnginePeer.Id` is a typealias for `PeerId` so `peer.id.isReplies` and `peer.id == accountPeerId` resolve identically.
**Final scope:**
- 1 signature change (L21): `peer: Peer``peer: EnginePeer`
- 1 body simplification (L27): `EnginePeer(peer).displayTitle(...)``peer.displayTitle(...)`
- 5 `._asPeer()` bridge-drops at call sites (L171, L201, L386, L403, L748)
**Pre-flight inventory was exact.** Zero undercount: the pre-flight grep enumerated exactly 6 call-site matches (1 definition + 5 bridges), and the implementer's 3 Edit operations (one unique + two `replace_all=true`) hit all 5 bridge sites on first pass. No scope creep, no out-of-scope edits.
**First-pass-clean build.** 946 total actions, 259.250s elapsed, `INFO: Build completed successfully`, 0 errors. As a reset wave after wave 36's 6-iteration convergence, this was the expected outcome for a single-file mechanical change targeting a private function.
**Subagent-driven execution.** First wave executed via `superpowers:subagent-driven-development` with a consolidated implementer dispatch (Tasks 13 bundled since all three were grep + mechanical Edit on one file). Two-stage review (spec + code quality) passed cleanly. Controller directly handled build, commit, and log/memory updates.
**Lessons:**
- **Bundling mechanical plan-tasks is valid for same-file micro-waves.** When a plan's tasks are all Edit operations on a single file with no inter-task branches, consolidating into one implementer dispatch preserves the review structure (spec + quality still gated) while avoiding per-task subagent overhead. For larger waves with multi-file surface or branching logic, keep per-task dispatch.
- **Private-function ring-2 cleanups are first-pass-clean candidates.** The ratchet behavior observed in waves 3336 — prior waves add bridges at next-ring boundaries — creates trivially-closable cleanup waves whenever the next-ring callee is itself a private or small-surface function. Wave 37's target (private free function, 5 bridge sites, all in-file) hit the ceiling of what this shape can yield.
- **Post-wave 36 bridge inventory reduced by 5.** ContactMultiselectionController:171/201/386/403/748 are now Peer-free at the `peerTokenTitle` arg; the file still retains `import Postbox` for other APIs (L459's `SelectedPeer.peer(peer: Peer, ...)` feed), so file-level Postbox-free remains a later-wave target.
**Plan / record:** `docs/superpowers/plans/2026-04-24-peertokentitle-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-peertokentitle-engine-peer-migration-design.md`.
---
## Wave 38 outcome (2026-04-24)
`canSendMessagesToPeer(_ peer: Peer → EnginePeer, ignoreDefault:)`. Public utility function in `submodules/TelegramCore/Sources/Utils/CanSendMessagesToPeer.swift`. Atomic multi-file commit `45729bad1c`. 12 files changed, 25 insertions / 24 deletions.
**Migration shape.** Mixed-direction ring-2 cleanup: 18 Shape-A bridge drops (`._asPeer()` at call sites where caller already holds `EnginePeer`) plus 5 Shape-C bridge adds (`EnginePeer(peer)` wraps where caller holds raw Postbox `Peer`). Function body preserved by inserting a single `let peer = peer._asPeer()` shadow as the first body line — the four `peer as? TelegramUser/TelegramGroup/TelegramSecretChat/TelegramChannel` branches remain unchanged and the file still `import Postbox`.
**Final scope:**
- 1 signature change (CanSendMessagesToPeer.swift:9): `peer: Peer``peer: EnginePeer`
- 1 body insertion (CanSendMessagesToPeer.swift:10): `let peer = peer._asPeer()`
- 18 Shape-A drops across 7 files: ContactsSearchContainerNode (3), ChatImageGalleryItem (1), UniversalVideoGalleryItem (1), StoryItemSetContainerViewSendMessage (1), ShareSearchContainerNode (3), ChatListSearchListPaneNode (6), ChatListNode (3)
- 5 Shape-C adds across 5 files: LegacyAttachmentMenu, ChatInterfaceInputContexts, ShareSearchContainerNode, ShareController, ChatPresentationInterfaceState
- ShareSearchContainerNode has mixed shape (3 drops + 1 add)
**Pre-flight inventory was exact.** 24 total grep matches = 1 definition + 23 call sites; 18 Shape-A + 5 Shape-C. Post-migration grep: 0 `_asPeer()` residue, 5 `EnginePeer(` wraps, 24 total matches. No scope creep. No WIP-drift adjustments needed.
**First-pass-clean build.** 568 total actions, 233.471s elapsed, `INFO: Build completed successfully`, 0 compilation errors. This contradicts the memory's pre-wave prediction of 35 build iterations per wave-36 lesson — the lesson remains valid for waves with cascading type-inference surfaces, but wave 38's surface (a utility function called only at points that already bridge `EnginePeer ↔ Peer` in both directions) had no such cascades. The Shape-A/C classification captured the full surface before code changes began.
**Subagent-driven execution.** Executed via `superpowers:subagent-driven-development`. Implementer dispatch bundled plan Tasks 15 (grep + all 24 edits + post-edit grep) since all were mechanical and on known files. Two-stage review (spec + code quality) passed without review-loop iterations. Controller handled build, commit, and log/memory updates.
**Lessons:**
- **Bundled multi-file dispatch is valid when pre-flight classification is exact.** Wave 36's per-task-dispatch rule (from CLAUDE.md) was formulated for waves with expected 35 build iterations driven by type-inference cascades. Wave 38 demonstrates that a multi-file wave with an exact upfront Shape-A/C classification and a cascading-free surface (all call sites are leaf arguments to a `Bool`-returning utility) can use the bundled dispatch shape safely. Gate: (a) all edits are mechanical Edit operations; (b) pre-flight grep enumerates every call-site explicitly; (c) return type of the migrated API does not propagate into caller type inference. When these hold, bundled dispatch saves subagent overhead without sacrificing review coverage.
- **First-pass-clean multi-file waves exist.** The memory's pre-wave prediction of "not first-pass-clean territory" was conservative and came from wave 36's 6-iteration experience. Wave 38's 12-file mechanical migration built clean on first attempt because the spec's explicit Shape-A/C classification enumerated all 23 sites, and the function's `Bool` return prevents caller-side type-inference cascades. Future multi-file waves should update expectations when (a) the classification is exact and (b) the return type does not propagate.
- **Shape-C wraps remain valid additions.** Five sites had to add `EnginePeer(peer)` at the call because the enclosing scope still holds raw Postbox `Peer` (from `RenderedPeer.peers` lookups, `ChatPresentationInterfaceState.renderedPeer.peer`, or `as? TelegramChannel` casts). These adds are acceptable — the alternative is a per-scope refactor (RenderedPeer → EngineRenderedPeer cascades) that would expand blast radius. Future waves targeting `RenderedPeer` would drop these 5 wraps.
**Plan / record:** `docs/superpowers/plans/2026-04-24-cansendmessagestopeer-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-cansendmessagestopeer-engine-peer-migration-design.md`.
---
## Wave 39 outcome (2026-04-24)
`AccountContext.makePeerInfoController(... peer: Peer → EnginePeer ...)`. Public protocol method on `AccountContext` (`submodules/AccountContext/Sources/AccountContext.swift:1371`) and its `SharedAccountContextImpl` implementation (`submodules/TelegramUI/Sources/SharedAccountContext.swift:1937`). Atomic multi-file commit `5385abc9bd`. **52 files changed**, 80 insertions / 79 deletions.
**Migration shape.** Body-shadow ring-2 cleanup at the largest scale yet attempted. The protocol + impl signatures change to `peer: EnginePeer`; the impl body adds `let peer = peer._asPeer()` as its first statement, preserving the private downstream `peerInfoControllerImpl(peer: Peer, ...)` and all of its Peer-typed helpers as out-of-scope. Mixed-direction at consumer sites: 58 Shape-A drops + 3 Shape-A-variant guard-statement drops + 12 Shape-C `EnginePeer(...)` wraps. Net **-49 bridges**.
**Final scope:**
- 1 protocol-signature change (AccountContext.swift:1371)
- 1 impl-signature change + 1 body-shadow insertion (SharedAccountContext.swift:19371938)
- 3 Shape-A self-call drops in same file (SharedAccountContext.swift:3335, 3483, 4016)
- 3 Shape-A-variant guard-statement drops (SettingsSearchableItems.swift around 1020/1046/1080): `guard let peer = peer?._asPeer() else { return }``guard let peer = peer else { return }`. The call-site `peer:` argument line is unchanged; the upstream guard now binds `peer` as `EnginePeer` (the closure parameter from `engine.data.get(...)`) rather than re-shadowing as raw `Peer`.
- 12 Shape-C `EnginePeer(...)` wraps across 8 files: BlockedPeersController:270, ChannelMembersController:707, ChannelBlacklistController:381, ChatRecentActionsControllerNode:1011, PeerInfoScreen:4306, ChatControllerNavigationButtonAction (4 sites: 441/461/471/492), ChatControllerOpenPeer (2 sites: 218/359), ChatControllerLoadDisplayNode:4362
- 55 additional Shape-A drops across 42 consumer files
- 4 incidental trailing-whitespace strips picked up by Edit tool (functionally identical)
**Pre-flight inventory was exact.** Total grep matches at planning time: 75 (1 protocol decl + 1 impl + 73 consumer call sites) of which 58 had inline `peer: <expr>._asPeer()` (Shape-A), 3 had upstream-guard `peer?._asPeer()` patterns (Shape-A-variant), and 12 had raw `peer: <expr>` (Shape-C). Post-migration grep for `_asPeer()` at `makePeerInfoController` call sites: 0. Post-migration grep for `EnginePeer(` at the 12 Shape-C sites: all present. No scope creep, no Postbox/TelegramCore/TelegramApi edits. The implementer noted that `BlockedPeersController.swift:270` already had `peer: peer` (no `_asPeer()`) — this was correctly classified Shape-C in the spec, and the wrap was applied as planned.
**Property-typing pre-verification matters.** Spec phase identified that `RenderedPeer.peer`/`chatMainPeer` (Postbox extension at PeerUtils.swift:512 and Postbox/RenderedPeer.swift:38) return raw `Peer?`, while `EngineRenderedPeer.peer`/`chatMainPeer` (TelegramCore/Peers/Peer.swift:623/627) return `EnginePeer?`. Six Shape-C sites depend on `RenderedPeer` (Postbox), so `EnginePeer(...)` wraps were correct. Sites that consume `EngineRenderedPeer` would need `peer: peer` only — not a concern here, but a checklist item for next-wave Shape-C planning.
**First-pass-clean build.** 658 total actions, 210.520s elapsed, 1565 cache hits + 142 disk-cache + 444 worker, `INFO: Build completed successfully`, 0 errors. Strongest confirmation yet of the wave-38 lesson: **first-pass-clean is achievable for 50+ file waves when (a) the migrated API's return type is non-propagating** (`ViewController?` is an optional reference type, like wave-38's `Bool` — caller-side type inference does not branch on it), **(b) pre-flight Shape-A/A-variant/C classification is exact, and (c) the body-shadow boundary is preserved.** Pre-wave estimate budgeted 24 iterations on the assumption that a 50-file change at a popular API surface would surface destructure cascades; the actual outcome exceeded expectations.
**Subagent-driven execution.** Executed via `superpowers:subagent-driven-development`. Implementer dispatch bundled plan Tasks 15 (signature + body-shadow + 3 self-call drops + 3 guard rewrites + 12 wraps + 55 Shape-A drops, 70+ Edit operations across 52 files). Two-stage review (spec + code quality) passed without re-review iterations. Spec reviewer verified all 73 sites, the body-shadow placement at line 1938, the guard-statement form change in SettingsSearchableItems, and the unchanged-out-of-scope confirmation for `peerInfoControllerImpl` at line 4434. Code quality reviewer noted only the 4 incidental trailing-whitespace strips as "minor — leave as-is, tiny improvement". Controller handled build, commit, and log/memory updates.
**Lessons:**
- **Shape-A-variant: drop the upstream `_asPeer()` rather than wrapping at the call site.** When a guard-statement immediately upstream of a `makePeerInfoController` call unwraps `EnginePeer? → Peer?` via `peer?._asPeer()`, prefer rewriting the guard to `guard let peer = peer else` (keeping the local as `EnginePeer`) over adding `EnginePeer(peer)` at the call. The variant: zero net change at the call line, -1 `_asPeer()` upstream. This pattern occurs whenever a closure parameter from `engine.data.get(...)` is opt-unwrapped immediately before a Peer-typed-API call.
- **First-pass-clean ceiling is higher than wave-36 implied.** Wave 36's 6-iteration convergence (15 files, ContactListPeer.peer enum-payload migration) led to a memory rule "multi-file = budget 35 iterations". Wave 38 first-pass-clean (12 files, Bool return) suggested the rule doesn't apply universally. Wave 39 first-pass-clean (52 files, ViewController? return) extends this: even at 50+ files, the determinant is return-type propagation behavior + pre-flight classification accuracy, not file count. Updated heuristic: **budget for first-pass-clean when** (return-type is non-propagating) AND (Shape classification is exact); **budget for 35 iterations when** (return type is a generic container, struct with associated types, or enum that participates in caller-side inference).
- **Bundled implementer dispatch scales to 70+ edits across 52 files.** The wave-38 lesson ("bundled multi-file dispatch is valid when pre-flight classification is exact") held at this scale. Two-stage review (spec then code-quality) over the aggregate output preserved review coverage. No per-task subagent dispatches were needed.
- **Atomic-stage exclusion of pre-existing WIP is necessary at large diff sizes.** Wave 39's working tree contained an unrelated WIP file (`ChatMessageTransitionNode.swift` — function-signature/animation changes on `beginAnimation`) that was modified outside the session. The commit phase explicitly enumerated all 52 wave-39 files in the `git add` invocation rather than using `git add -u` or `git add .`. This is a permanent rule for this project: never use bulk-stage commands; always enumerate files when committing a wave.
- **Ratchet expansion: 12 new Shape-C wraps for future waves.** The 12 `EnginePeer(...)` wraps installed at this wave become drop candidates for later waves migrating their upstream sources: `RenderedPeer → EngineRenderedPeer` (would drop 6 wraps at sites consuming `renderedPeer.peer`/`chatMainPeer`), `RenderedChannelParticipant.peer → EnginePeer` (would drop 2 at ChannelMembers/ChannelBlacklist), and others. Net economics still strongly favor the wave: -61 + 12 = -49 bridges.
**Plan / record:** `docs/superpowers/plans/2026-04-24-makePeerInfoController-engine-peer-migration.md`. Spec: `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md`.
---
## Wave 40 outcome (2026-04-24)
Commit: `d3c48379fe`. Bundle of `AccountContext.makeChatQrCodeScreen` + `makeChatRecentActionsController` peer `Peer → EnginePeer` — the trivial sibling follow-up to wave 39, completing the "Option 1 cluster" (`makePeerInfoController` family) from the wave-38 memory. 8 Swift files + plan doc / 11 edits. Pre-flight classification was already done in the wave-39 design doc's "Out of scope" section — no fresh pre-flight needed.
**Classification:**
- 2 protocol decls (`AccountContext.swift:1401`, `:1461`)
- 2 impl decls + body-shadows (`SharedAccountContext.swift:2302`, `:2731`)
- 2 Shape-A-variant guard rewrites (`SettingsSearchableItems.swift:971`, `:989``guard let peer = peer?._asPeer()``guard let peer = peer`, keeping the local as `EnginePeer`)
- 3 Shape-A drops (`ContactsController.swift:478`, `ChannelAdminsController.swift:734`, `GroupStatsController.swift:915`)
- 2 Shape-C wraps (`PeerInfoScreen.swift:4623`, `PeerInfoScreenOpenChat.swift:115`) — both consume `data.peer: Peer?` from `PeerInfoScreenData`, so they're ratchet markers for a future `PeerInfoScreenData.peer Peer → EnginePeer` wave.
Net 3 bridges (5 `_asPeer()` drops, +2 `EnginePeer(...)` wraps).
**Build outcome:** First-pass-clean for wave-40 files. The initial run failed due to pre-existing unrelated WIP in `ChatMessageTransitionNode.swift` (unterminated string literals in debug `print` statements the user was mid-editing); the wave-40 files produced zero diagnostics. After the user fixed the WIP, the subsequent build completed in 23.9s (mostly cache-warm) with zero errors.
**Lessons:**
- **Bundled sibling migration with shared pre-flight is cheap.** Wave 39's "Out of scope" section pre-classified all 7 of this wave's consumer sites. That planning overhead was already paid; wave 40 only needed to verify classifications still hold (one grep per site-group) and apply mechanical edits. Total time from plan-write to commit: ~30 minutes (dominated by the ~3-minute build). This validates a general pattern: when a wave defers siblings with an explicit "Out of scope" classification section, the follow-up wave is structurally trivial.
- **Small-scale bundled implementer dispatch is still the right choice.** 11 edits across 8 files fits comfortably in one implementer call, matching the wave-38/39 lesson at smaller scale. No per-task dispatches were needed; two-stage review (spec then code-quality) took one round each.
- **Pre-existing WIP in the working tree can cause module-scope build failures that mask the wave's own status.** When `ChatMessageTransitionNode.swift` had parse errors from an unrelated user WIP, the whole `TelegramUI` Swift module failed to parse before type-checking ran — so wave-40 files got syntax-verified but not type-verified. Diagnostic approach: grep the build output for error lines whose file path is NOT in the wave's file list; if the only errors are outside, the wave is likely clean, but complete type verification requires the unrelated WIP to be reverted or fixed. In this case the user fixed it; in future cases either ask the user, temporarily stash the WIP, or note the incomplete verification in the commit.
**Plan / record:** `docs/superpowers/plans/2026-04-24-makeChatQrCodeScreen-recentActions-engine-peer-migration.md`. No separate spec — the pre-flight was reused from wave 39's spec ("Out of scope" section in `docs/superpowers/specs/2026-04-24-makePeerInfoController-engine-peer-migration-design.md`).
---
## Wave 41 outcome (2026-04-24)
Commit: `32573c9808`. `RenderedChannelParticipant.peer: Peer → EnginePeer` — a TelegramCore foundational-type field migration affecting 28 files (11 TelegramCore + 17 consumer) / 1124 insertions / 89 deletions. First foundational-type field migration since `FoundPeer.peer` / `SendAsPeer.peer` / `ContactListPeer.peer` (waves 3436); differs in that `RenderedChannelParticipant` is a public TelegramCore struct with both TelegramCore-internal construction sites and a broad consumer surface, so the wave touched TelegramCore internals too, not just consumer modules.
**Classification:**
- 1 struct field change (`ChannelParticipants.swift`): `peer: Peer → peer: EnginePeer` + init param + Equatable `lhs.peer.isEqual(rhs.peer) → lhs.peer == rhs.peer`
- 16 TelegramCore-internal constructor sites wrapped with `EnginePeer(peer)` across 9 files (RequestStartBot, AddPeerMember, ChannelAdminEventLogs [7 calls], ChannelBlacklist, ChannelMembers, ChannelOwnershipTransfer [2 calls], JoinChannel, PeerAdmins, Ranks)
- 1 TelegramCore-internal ADD-ASPEER (`SearchGroupMembers.swift:83` — pre-flight miss: `participants.map({ $0.peer })` consumed by outer `[Peer]`-returning closure)
- ~32 consumer DROP sites (removing `EnginePeer(participant.peer)` wraps or `._asPeer()` downgrades)
- 9 consumer CAST sites (`if let user = participant.peer as? TelegramUser, user.botInfo != nil``if case let .user(user) = participant.peer, user.botInfo != nil`)
- 3 consumer ADD-ASPEER (PeerInfoMembers:33 contains the `PeerInfoMember.peer: Peer` accessor ratchet; ChatRecentActionsHistoryTransition:675 + :2275 for `SimpleDictionary<PeerId, Peer>` subscript assignment)
- 7 consumer ADD-WRAP constructor sites (ChannelMembersSearchContainerNode + ChannelMembersSearchControllerNode legacy-group paths, ChatControllerAdminBanUsers:226) — ratchet markers for a future wave migrating `peerView.peers[id]` / `authors: [Peer]` upstream flows to EnginePeer
- 2 consumer `is TelegramChannel``if case .channel = participant.peer` rewrites (ChannelBlacklistController:370, :377)
Net ~13 bridges. Drops the 2 Shape-C wraps installed by wave 39 (ChannelMembersController:707, ChannelBlacklistController:381) as promised by the wave-39 ratchet plan.
**Build outcome:** Three iterations.
1. First build surfaced `SearchGroupMembers.swift:83` — a TelegramCore-internal consumer of `$0.peer` on RCP that the plan's grep missed (grep only looked for `RenderedChannelParticipant(` constructors inside TelegramCore).
2. Second build surfaced `ShareWithPeersScreen.swift:777, 780` — an entire file missed in pre-flight because the initial grep was scoped to files already known to import RCP; this file gets RCP indirectly via `TemporaryCachedPeerDataManager.recent(...)`'s `updated:` callback.
3. Third build: clean (172 total actions).
**Lessons:**
- **Property-access migration has a broader grep surface than constructor migration.** Waves 3436 migrated foundational types by grepping `<Type>(` constructors. That works because the consumer's use of the type is always preceded by a construction. Wave 41's RCP differs: RCP is constructed inside TelegramCore and handed to consumers as an opaque reference, so consumer usage looks like `participant.peer.X` (with any name, any subscript, any map closure). **Grep for the type's field-access patterns across the entire repo**, not just files that explicitly reference the type. For RCP specifically: `grep -rn 'RenderedChannelParticipant\|\.peer as?\|EnginePeer(.*\.peer)\|participant\.peer' submodules/` would have caught ShareWithPeersScreen.swift. File this as a **permanent pre-flight rule** for future property-access migrations.
- **TelegramCore-internal consumer sites need the same inventory discipline as consumer-module sites.** The wave-41 plan enumerated 10 TelegramCore files for *construction* site updates but didn't grep for *consumption* sites — `SearchGroupMembers.swift:83` consumes `$0.peer` on RCP inside TelegramCore. Pre-flight for any foundational-type field migration must grep both construction AND consumption across the whole repo, not stratified by module.
- **"Plan says N sites" numbers can be low; use `replace_all=true` defensively.** The plan listed 1 `EnginePeer(participant.peer)` site in ChannelBlacklistController.swift but the file actually had 5; listed 2 CAST sites in ChannelMembersSearchControllerNode.swift but the file had 4. The implementer correctly used `replace_all=true` on the unique-pattern greps, catching all sites. **Pre-flight should report "N site(s) per file at these lines" — if confidence is low, recommend replace_all directly.**
- **Spec review caught `is TelegramChannel` that grep did not.** `participant.peer is TelegramChannel` does not match any of the usual migration-pattern greps (`EnginePeer(...)`, `as? TelegramUser`, `._asPeer()`). The spec reviewer caught it by reading the code. Would have been a build error anyway — the project compiles with `-warnings-as-errors` across 658/665 modules, so Swift's "'is' test always fails" warning promotes to an error — but catching it at spec time saves one build iteration. **Add `is Telegram(Channel|User|Group|SecretChat)` to the pre-flight token set** for any Peer → EnginePeer field migration so it's flagged at plan time rather than iteration time.
- **First-pass-clean is NOT the right bar for foundational-type field migration.** Waves 3740 achieved first-pass-clean. Wave 41 needed 3 iterations — and none of the three errors were the implementer's fault; all three were pre-flight misses. Heuristic update: **foundational-type field migrations (where the field is accessed via a generic property name like `.peer`) should budget 24 build iterations**, not first-pass-clean. The broader the grep surface, the more misses slip through.
- **Ratchet economics of ADD-WRAP consumer constructors.** Wave 41 added 7 consumer `peer: EnginePeer(peer)` wraps at `RenderedChannelParticipant(...)` constructor sites in ChannelMembersSearch*Node and ChatControllerAdminBanUsers, where the local is still raw `Peer` from a legacy path (`peerView.peers[id]` / `authors: [Peer]`). These are ratchet markers: when the upstream legacy path migrates to EnginePeer, these wraps drop. The economics are net positive (13 bridges) even counting the adds.
**Plan:** `docs/superpowers/plans/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration.md`.
**Spec:** `docs/superpowers/specs/2026-04-24-renderedchannelparticipant-peer-engine-peer-migration-design.md`.
---
## Wave 42 outcome (2026-04-24)
`PeerInfoScreenData.peer: Peer? → EnginePeer?` — a single-module (18-file) struct-field migration entirely contained within `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`. `PeerInfoScreenData` is a consumer-module data class, not a public TelegramCore type, so the wave never touched TelegramCore — contrast wave 41 which had to update TelegramCore construction sites.
**Classification:**
- 1 struct-field change + 1 init-param change in PeerInfoData.swift
- 4 construction-site edits adding `.flatMap(EnginePeer.init)` wraps at `peer:` arguments built from `peerView.peers[...]` (PeerInfoData.swift L1027, L1620, L1867, L2205); 1 construction site unchanged (L1100, `peer: nil`)
- ~25 consumer DROP sites (removing `EnginePeer(peer)` wraps where the local `peer` was bound from `data.peer` / `self.data?.peer`)
- ~25 consumer CAST sites (`if let user = data.peer as? TelegramUser, ...``if case let .user(user) = data.peer, ...`)
- ~5 consumer `is TelegramXxx` rewrites on data.peer-derived locals (PeerInfoScreen.swift L3981, L4133, L4192, L4194)
- ~10 consumer ADD-WRAP `?._asPeer()` helper bridges for internal helpers that stay `peer: Peer?` (PeerInfoScreenPerformButtonAction:62 calling `peerInfoIsChatMuted`; PeerInfoScreen:5399/5805 calling `self.headerNode.update(peer:...)`; PeerInfoScreen:5857 calling `peerInfoCanEdit`; etc.) plus one inline `_asPeer()` bridge inside `peerInfoIsCopyProtected` because `isCopyProtectionEnabled` is not exposed on `EnginePeer`
Net ~30 bridges. Drops the ~5 wave-40 `EnginePeer(peer)` wraps that were directly in scope (the plan initially overestimated which sites would drop — some `EnginePeer(peer)` calls in PeerInfoScreen.swift are inside closures whose `peer` parameter comes from a caller, not from `data.peer`, and those correctly survived).
**Build outcome:** Two iterations.
1. First build surfaced `peerInfoIsCopyProtected` helper: `peer.isCopyProtectionEnabled` failed against `EnginePeer` because that property is `Peer`-protocol-only and is not forwarded by `EnginePeer`. Fix: inline `peer._asPeer().isCopyProtectionEnabled` bridge at the helper callsite (helper signature stays `Peer?`).
2. Second build: clean.
**Lessons:**
- **EnginePeer is not a strict superset of Peer's property surface.** `EnginePeer` forwards `id`, `displayTitle(...)`, `compactDisplayTitle`, `isPremium`, `smallProfileImage`, `largeProfileImage`, and other common properties, but Peer-only properties like `isCopyProtectionEnabled` are NOT forwarded. Migration-time surprise: a property access that worked on raw `Peer` can fail against `EnginePeer`. **Bridge pattern:** inline `peer._asPeer().isCopyProtectionEnabled` at the callsite, OR if the helper already takes `Peer?`, bridge upstream with `?._asPeer()` and keep the access unchanged. Pre-flight addition: grep migrated consumer surfaces for `.X` accesses where `X` is a Peer-protocol property not in EnginePeer's forwarding set. (Alternative: extend `EnginePeer` with more forwarders, but that's a larger TelegramCore change out of scope for a single wave.)
- **Wave-42 was not first-pass-clean despite being a consumer-module wave.** Contrast wave 41: 3 iterations due to broader grep-surface misses. Wave 42: 2 iterations due to a single property-forwarding-gap miss. Heuristic update: **even consumer-module property-access migrations budget 2 iterations**, because Peer/EnginePeer interface parity is never verified end-to-end at pre-flight time.
- **Plan-stated wrap-drop counts can be wrong in both directions.** The plan listed 15+ `EnginePeer(peer)` drop sites in PeerInfoScreen.swift citing specific lines (1331, 1339, 1346, 1561, 2353, ...) — but on inspection, lines 1331/1339/1346 are inside a `peer`-parameter closure (`openPeerContextAction = { ..., peer, ... in ... }`) where `peer` comes from the closure's caller, not from `data.peer`. Those wraps correctly stayed. The implementer correctly judged each binding context before deciding whether to drop. **Rule for plan writing:** when naming specific line numbers as drop-candidates, verify the `if let peer = ...` / closure-param binding in the actual file text, not just the `EnginePeer(peer)` grep result.
- **Wave-41 lesson reconfirmed (`is Telegram*` rewrite):** all 5 `is TelegramChannel|User|Group|SecretChat` checks on `data.peer`-derived locals were caught at plan time (via the pre-flight grep token set including `is Telegram...`), not at build time. `-warnings-as-errors` would have caught them at build time but pre-flight catches save an iteration.
- **Internal-helper bridge economics.** The PeerInfoScreen module has ~6 internal helpers (`canEditPeerInfo`, `peerInfoIsChatMuted`, `peerInfoHeaderButtons`, `peerInfoHeaderActionButtons`, `peerInfoCanEdit`, `availableActionsForMemberOfPeer`, `peerInfoIsCopyProtected`) that still take `peer: Peer?`. Wave 42 added ~10 `?._asPeer()` bridges at their callsites. A follow-up wave migrating those helper signatures would drop exactly those 10 bridges — small, well-scoped, strong candidate for wave 42.x.
**Plan:** `docs/superpowers/plans/2026-04-24-peerinfoscreendata-peer-engine-peer-migration.md`.
---
## Wave 43 outcome (2026-04-24)
Migrated six PeerInfoScreen module helpers — `canEditPeerInfo`, `availableActionsForMemberOfPeer`, `peerInfoHeaderActionButtons`, `peerInfoHeaderButtons`, `peerInfoCanEdit`, `peerInfoIsChatMuted` (plus nested `isPeerMuted`) — from `peer: Peer?` to `peer: EnginePeer?`. All internal `as? TelegramUser|Channel|Group` and `is TelegramX` patterns inside the helper bodies (PeerInfoData.swift lines 22652670) rewritten to `case let .user/.channel/.legacyGroup` / `case .x = peer` enum patterns. No TelegramCore changes. No new typealiases.
**Classification:**
- 6 helper signature migrations + 12 `as?` / `is` body rewrites in PeerInfoData.swift
- 7 DROPs of `._asPeer()` / `?._asPeer()` bridges installed by wave 42 (sites in PeerInfoScreenAvatarSetup, PeerInfoScreenPerformButtonAction ×3, PeerInfoScreenOpenMember, PeerInfoScreen:5857, PeerInfoProfileItems)
- 2 CONVERTs at PeerInfoScreen.swift:1905/1961 (`peer: group` / `peer: channel` extracted from `case let``peer: data.peer`, preserving the `group`/`channel` binding for body use)
- 10 ADD-WRAPs at call sites inside enclosing methods that still take raw `Peer?` (PeerInfoHeaderNode ×3, PeerInfoHeaderEditingContentNode ×5, PeerInfoEditingAvatarNode, PeerInfoEditingAvatarOverlayNode) — `.flatMap(EnginePeer.init)` for optional Peer, `EnginePeer(peer)` for non-optional (post-`guard let peer = peer`) Peer
- 2 ADD-WRAPs at raw-`Peer` member-item sites (PeerInfoScreenMemberItem, PeerInfoMembersPane)
Net ~+5 wraps overall (7 drops, 12 adds) — less important than the headline: the helper signatures now live on the engine side. The 12 ADDs are all staged for drop in follow-up waves that migrate the four `update(peer: Peer?, ...)` methods (PeerInfoHeaderNode, PeerInfoHeaderEditingContentNode, PeerInfoEditingAvatarNode, PeerInfoEditingAvatarOverlayNode) and the two raw-`Peer` enclosingPeer storage fields (PeerInfoScreenMemberItem.enclosingPeer is actually `Peer?`, PeerInfoMembersPane's local `enclosingPeer: Peer`).
**Build outcome:** 2 iterations.
1. First build surfaced one error: `PeerInfoScreenMemberItem.item.enclosingPeer` was declared `let enclosingPeer: Peer?` (optional) — the plan had prescribed `EnginePeer(item.enclosingPeer)` (non-optional form). Fix: `.flatMap(EnginePeer.init)` at the callsite. One-line correction.
2. Second build: clean.
**Commit:** `d53e0d50f4c0e3e68c4e4c1ce255e76f43f56d4b` (12 files + plan).
**Lessons:**
- **Plan-declared property optionality can be wrong — verify `let X: T?` vs `let X: T` at plan-write time.** Wave 43's plan described `item.enclosingPeer` as non-optional `Peer` based on how it was used (`item.enclosingPeer as? TelegramChannel`, `item.enclosingPeer is TelegramChannel` compile against `Peer` non-optional OR optional protocol). The declaration was actually `Peer?`. The one-site plan-declaration mismatch cost one build iteration. **Rule for plan writing:** for every ADD-WRAP site, cite the declaration line, not just the usage. `grep -nE "(let|var)\s+\w+:\s*Peer\??" <file>` gives unambiguous optionality.
- **Helper-signature migrations with zero Peer-only property access are first-iteration-clean except for optionality surprises.** Wave 42's lesson (EnginePeer property-forwarding gap) did not recur here because wave 43's helpers only did `as?` / `is` casts — pure enum-rewrite territory. Pre-flight verified each helper body used only `.id` and concrete-type accesses (`TelegramChannel.hasPermission(...)` etc., which stay on concrete types post-migration). The only iteration cost was the optionality mismatch above. **Heuristic: if a helper's body does not touch the `peer` parameter outside of `as?`/`is`/`.id`/`.id.isX`, budget 1 iteration + optionality-audit pass.**
- **`case .x = peer` without binding is the correct `-warnings-as-errors` form when the branch body doesn't need the concrete value.** Wave 43's `peerInfoIsChatMuted` body at lines 2641/2643 uses `case .user = peer` and `case .legacyGroup = peer` (no inner binding) — because the branches only read `globalNotificationSettings.privateChats.enabled` / `.groupChats.enabled`, never the concrete `TelegramUser` / `TelegramGroup`. Had the migration emitted `case let .user(_) = peer` with `_`, or `case let .user(user) = peer` with `user` unused, `-warnings-as-errors` would fail. **Pre-flight rule: for each `peer is TelegramX``case .x = peer` rewrite, check whether the branch body accesses the concrete type. If not, use bare-case form; if yes, bind.**
- **Bundled-dispatch subagent flow + two-stage review works cleanly for ≥10-site single-commit migrations.** Wave 43 dispatched one implementer (bundled Tasks 17), then spec reviewer, then code quality reviewer. All three completed in roughly 10 minutes of subagent time. Spec reviewer caught zero misses (implementer self-caught the optionality deviation in iteration 1). Code quality reviewer surfaced only minor observational notes (no Critical / Important issues). **Continues to confirm wave-39/40/41/42 finding: for ≤30-file / ≤50-edit wave shapes, the bundled flow is reliable; TaskCreate tracking adds no value at this size.**
**Plan:** `docs/superpowers/plans/2026-04-24-peerinfoscreen-helpers-engine-peer-migration.md`.
---
## Modules currently free of `import Postbox` (running tally)
Consumer modules that no longer import Postbox, across all waves and standalone commits:

View file

@ -0,0 +1,158 @@
# Wave 39 — `makePeerInfoController` peer: Peer → EnginePeer migration
Date: 2026-04-24
## Context
Ring-2 cleanup of the `AccountContext` Peer-typed-API surface. Waves 34 (FoundPeer.peer), 35 (SendAsPeer.peer), 36 (ContactListPeer.peer), 37 (peerTokenTitle), and 38 (canSendMessagesToPeer) migrated adjacent Peer-typed APIs to `EnginePeer`. `makePeerInfoController` is the largest remaining Peer-typed-API surface on `AccountContext` and a natural follow-up.
Scope: only `makePeerInfoController` this wave. The sibling methods `makeChatQrCodeScreen` (4 consumer sites) and `makeChatRecentActionsController` (3 consumer sites) are deferred to a trivial follow-up wave.
## Signature change
`AccountContext` protocol declaration (`submodules/AccountContext/Sources/AccountContext.swift:1371`) and its `SharedAccountContextImpl` implementation (`submodules/TelegramUI/Sources/SharedAccountContext.swift:1937`):
```swift
// before
func makePeerInfoController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peer: Peer,
mode: PeerInfoControllerMode,
avatarInitiallyExpanded: Bool,
fromChat: Bool,
requestsContext: PeerInvitationImportersContext?
) -> ViewController?
// after
func makePeerInfoController(
context: AccountContext,
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
peer: EnginePeer,
mode: PeerInfoControllerMode,
avatarInitiallyExpanded: Bool,
fromChat: Bool,
requestsContext: PeerInvitationImportersContext?
) -> ViewController?
```
Implementation body adds `let peer = peer._asPeer()` shadow at body-top. `peerInfoControllerImpl` (private, same file) and all downstream Peer-typed helpers keep raw `Peer` — out of scope for this wave.
```swift
public func makePeerInfoController(... peer: EnginePeer ...) -> ViewController? {
let peer = peer._asPeer()
let controller = peerInfoControllerImpl(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: mode, avatarInitiallyExpanded: avatarInitiallyExpanded, isOpenedFromChat: fromChat)
controller?.navigationPresentation = .modalInLargeLayout
return controller
}
```
## Consumer-side changes
**73 total consumer call sites** (75 raw occurrences minus 1 protocol declaration and 1 implementation). Classification (confirmed via full-repo grep):
- **58 Shape-A** — inline `peer: x._asPeer()` drops to `peer: x`. Mechanical edits.
- **3 Shape-A-variant**`SettingsSearchableItems.swift` lines 1023, 1049, 1083. The upstream `guard let peer = peer?._asPeer() else` changes to `guard let peer = peer else`, making the local `peer` stay `EnginePeer`. The call-site line does not change.
- **12 Shape-C** — raw Peer local, add `EnginePeer(...)` wrap at call site.
### Shape-C site list
| File | Line | Current peer argument | New |
|---|---|---|---|
| `submodules/SettingsUI/Sources/Privacy and Security/BlockedPeersController.swift` | 270 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/PeerInfoUI/Sources/ChannelMembersController.swift` | 707 | `peer: participant.peer` | `peer: EnginePeer(participant.peer)` |
| `submodules/PeerInfoUI/Sources/ChannelBlacklistController.swift` | 381 | `peer: participant.peer` | `peer: EnginePeer(participant.peer)` |
| `submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsControllerNode.swift` | 1011 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift` | 4306 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift` | 441 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift` | 461 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift` | 471 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerNavigationButtonAction.swift` | 492 | `peer: channel` | `peer: EnginePeer(channel)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift` | 218 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerOpenPeer.swift` | 359 | `peer: peer` | `peer: EnginePeer(peer)` |
| `submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift` | 4362 | `peer: peer` | `peer: EnginePeer(peer)` |
Each Shape-C wrap is a future-wave drop candidate once the raw-Peer source (stored field, `participant.peer`, `renderedPeer.chatMainPeer`, etc.) migrates upstream.
### Shape-A-variant detail
`SettingsSearchableItems.swift` three sites share the same structure:
```swift
// before
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in // peer: EnginePeer?
guard let peer = peer?._asPeer() else { // peer: Peer (shadowed)
return
}
let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer,
mode: .myProfile,
...
)
...
})
// after
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in // peer: EnginePeer?
guard let peer = peer else { // peer: EnginePeer (shadowed)
return
}
let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer,
mode: .myProfile,
...
)
...
})
```
## Files touched (≈50)
Inventoried from the grep output. Not exhaustive here; per-site enumeration lives in the implementation plan.
Signature files: `AccountContext/Sources/AccountContext.swift`, `TelegramUI/Sources/SharedAccountContext.swift`.
Shape-A consumer files (sample, not exhaustive): `SelectivePrivacySettingsPeersController.swift`, `InstantPageControllerNode.swift`, `CallListController.swift`, `ContactsController.swift`, `ContactContextMenus.swift`, `SecureIdAuthController.swift`, `ChannelAdminController.swift`, `ChannelMembersController.swift`, `ChannelBannedMemberController.swift`, `ChannelPermissionsController.swift`, `MessageStatsController.swift`, `GroupStatsController.swift`, `InviteRequestsController.swift`, `BrowserInstantPageContent.swift`, `WebAppController.swift`, `PeersNearbyController.swift`, `ChatSendStarsScreen.swift`, `ChatRecentActionsControllerNode.swift`, `MiniAppListScreen.swift`, `JoinSubjectScreen.swift`, `NewContactScreen.swift`, `StarsTransactionScreen.swift`, `StoryItemSetContainerViewSendMessage.swift`, `StoryItemSetContainerComponent.swift`, `GiftViewScreen.swift`, `GiftOptionsScreen.swift`, `StorageUsageScreen.swift`, `TextProcessingScreen.swift`, `PeerInfoScreen.swift`, `PeerInfoScreenOpenURL.swift`, `JoinAffiliateProgramScreen.swift`, `ChatControllerScrollToPointInHistory.swift`, `OpenUrl.swift`, `OpenResolvedUrl.swift`, `TextLinkHandling.swift`, `ChatController.swift`, `OpenAddContact.swift`, `ChatManagingBotTitlePanelNode.swift`, `NavigateToChatController.swift`, `SharedAccountContext.swift` (3 self-call sites), `OverlayAudioPlayerControllerNode.swift`, `PollResultsController.swift`, `ChatControllerOpenWebApp.swift`, `ChatControllerNavigationButtonAction.swift`, `ChatListController.swift`, `ChatListSearchListPaneNode.swift`.
Shape-A-variant file: `SettingsSearchableItems.swift`.
Shape-C-only files (other than those with mixed shapes above): `BlockedPeersController.swift`, `ChannelBlacklistController.swift`, `ChatControllerOpenPeer.swift`, `ChatControllerLoadDisplayNode.swift`.
## Build/verification plan
1. Apply all edits atomically. Mechanical Edit-tool string replaces for the 58 Shape-A drops; focused Edits for the 3 Shape-A-variants (guard line) and 12 Shape-C wraps.
2. Full project build: `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`.
3. Fix any iteration-surfaced errors. Budget 24 iterations.
4. Clean build → atomic commit with wave-39 message.
5. Update `project_postbox_refactor_next_wave.md` memory, `docs/superpowers/postbox-refactor-log.md`, and `CLAUDE.md` wave tally.
6. No test runs (project has no unit tests).
## Risks / watch-out
- **Destructure/binding cascades.** Locals named `peer` declared as `Peer` somewhere in a call chain and fed to `makePeerInfoController`. The body-shadow pattern contains divergence at the public API boundary, but transient Swift inference errors may surface at intermediate points.
- **`chatMainPeer` / `renderedPeer.peer` property types.** Shape-C sites at `ChatControllerNavigationButtonAction.swift:441/461/471/492` and `ChatControllerLoadDisplayNode.swift:4362` assume these properties return raw `Peer`. If they already return `EnginePeer` in the current repo (unlikely but possible after earlier waves), the wrap should be `peer: peer` with no wrap. Verify in plan phase.
- **Outflow sites in Shape-C files.** Some Shape-C files may have additional `peer: Peer` flows elsewhere that are unrelated to this wave. Do not chase — only touch the listed sites.
## Abandonment criteria
- Iteration count exceeds 5.
- A cascade requires editing `peerInfoControllerImpl` (violates body-shadow boundary).
- Any non-consumer file (e.g., anything in `TelegramCore`, `Postbox`, `TelegramApi`) surfaces an error.
## Net effect
- Public API: `AccountContext.makePeerInfoController` takes `EnginePeer` instead of raw `Peer`.
- Bridges: -58 inline `_asPeer()` + -3 upstream-guard `_asPeer()` + 12 new `EnginePeer(...)` wraps = **net -49 bridges**.
- Ratchet: 12 Shape-C wraps become future-wave drop candidates (e.g., `RenderedPeer → EngineRenderedPeer` migration, participant-object migrations).
## Out of scope
- `makeChatQrCodeScreen` (4 sites), `makeChatRecentActionsController` (3 sites) — deferred to a trivial follow-up wave.
- `peerInfoControllerImpl` and downstream Peer-typed helpers.
- Shape-C source migrations (participant objects, `renderedPeer.chatMainPeer`, etc.).

View file

@ -0,0 +1,187 @@
# Wave 44 — `RenderedChannelParticipant.peers: [PeerId: Peer] → [EnginePeer.Id: EnginePeer]`
**Date:** 2026-04-24
**Status:** Approved, pending plan
**Predecessor:** Wave 41 (commit `32573c9808`) migrated `RenderedChannelParticipant.peer` from `Peer` to `EnginePeer` and installed ADD-WRAP markers at consumer-side read sites that this wave drops.
**Goal:** Close out the wave-41 ratchet by migrating the sibling `peers: [PeerId: Peer]` field to `[EnginePeer.Id: EnginePeer]`. After this wave, `RenderedChannelParticipant` has no raw `Peer` types in its public surface.
## Context
`RenderedChannelParticipant` is declared in `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift`:
```swift
public struct RenderedChannelParticipant: Equatable {
public let participant: ChannelParticipant
public let peer: EnginePeer // migrated in wave 41
public let peers: [PeerId: Peer] // target of this wave
public let presences: [PeerId: PeerPresence] // out of scope (PeerPresence is Postbox protocol)
public init(participant: ChannelParticipant, peer: EnginePeer, peers: [PeerId: Peer] = [:], presences: [PeerId: PeerPresence] = [:]) { ... }
}
```
`peers` is a supplementary dict of "referenced peers" (e.g. the admin who promoted this member, the admin who banned them). Consumers use it to render relationships — never with `as?`/`is` casts, only `.id` and `.displayTitle(...)` on extracted values.
## Migration target
- `peers: [PeerId: Peer]``peers: [EnginePeer.Id: EnginePeer]`
- init default: `[:]` on both sides (type changes transparently)
- `presences` field stays unchanged.
## Scope
### Declaration (1 file, 2 edits)
**`submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift`**
- Line 11: `public let peers: [PeerId: Peer]``public let peers: [EnginePeer.Id: EnginePeer]`
- Line 14: `peers: [PeerId: Peer] = [:]``peers: [EnginePeer.Id: EnginePeer] = [:]`
### TelegramCore producer sites (8 files, 8 construction sites, ~16 edits)
All 8 producers follow the identical pattern of building a local `peers: [PeerId: Peer] = [:]` dict inside a `postbox.transaction` and passing it to an `RCP(peers: peers, ...)` constructor. Per-site edits: change local dict type, wrap each insertion value with `EnginePeer(...)`.
| File | `var peers:` decl | `peers[X.id] = X` insertions | RCP construction |
|---|---|---|---|
| `TelegramEngine/Messages/RequestStartBot.swift` | line 61 | line 64 | line 65 |
| `TelegramEngine/Peers/ChannelOwnershipTransfer.swift` | line 170 | lines 172, 176 | line 180 (2 RCP constructions share `peers`) |
| `TelegramEngine/Peers/JoinChannel.swift` | line 59 | lines 64, 77 | line 82 |
| `TelegramEngine/Peers/AddPeerMember.swift` | line 242 | lines 244, 251 | line 255 |
| `TelegramEngine/Peers/PeerAdmins.swift` | line 251 | lines 253, 259 | line 262 |
| `TelegramEngine/Peers/ChannelBlacklist.swift` | line 128 | lines 130, 136 | line 140 |
| `TelegramEngine/Peers/Ranks.swift` | line 60 | lines 62, 68 | line 95 |
| `TelegramEngine/Peers/ChannelMembers.swift` | line 102 | line 105 | line 115 |
**Per-site rewrite:**
```swift
// before
var peers: [PeerId: Peer] = [:]
peers[peer.id] = peer
// after
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[peer.id] = EnginePeer(peer)
```
### Consumer-side DROPs: `.mapValues({ $0._asPeer() })` transforms (5 sites)
These consumer-side constructors start from a `[PeerId: EnginePeer]` source dict and currently unwrap to `[PeerId: Peer]` to feed into the RCP constructor. After migration, the unwrap transform is a no-op and can be dropped.
| File | Line | Before → after |
|---|---|---|
| `PeerInfoUI/Sources/ChannelAdminsController.swift` | 926 | `peers: peers.mapValues({ $0._asPeer() })``peers: peers` |
| `PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` | 994 | same |
| `PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` | 998 | same |
| `PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` | 409 | same |
| `PeerInfoUI/Sources/ChannelMembersSearchControllerNode.swift` | 413 | same |
**Verification required at plan time:** for each of these 5 sites, grep back up in the enclosing function to confirm the local `peers` variable is declared `[PeerId: EnginePeer]` (the source of the mapValues transform). If any of the sources turn out to be `[PeerId: Peer]` rather than `[PeerId: EnginePeer]`, that site's transform is NOT a no-op and instead becomes a wrap (`.mapValues(EnginePeer.init)`) — still a net-zero or gain depending on where the source originates.
### Consumer-side DROPs: `EnginePeer(peer).displayTitle(...)` wraps (6 sites)
These are the wave-41 ADD-WRAP markers. Pattern: extract `peer` from `participant.peers[X]`, wrap with `EnginePeer(peer)` to call `.displayTitle(...)`. After migration, `peer` is already `EnginePeer` — drop the wrap.
| File | Line | Pattern |
|---|---|---|
| `PeerInfoUI/Sources/ChannelAdminsController.swift` | 297 | `EnginePeer(peer).displayTitle(strings: strings, ...)``peer.displayTitle(strings: strings, ...)` |
| `PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` | 839 | same |
| `PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` | 870 | same |
| `PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` | 1091 | same |
| `PeerInfoUI/Sources/ChannelMembersSearchContainerNode.swift` | 1122 | same |
| `PeerInfoUI/Sources/ChannelBlacklistController.swift` | 165 | same |
The adjacent `peer.id == participant.peer.id` comparisons are unchanged: both sides are `EnginePeer.Id` (already a typealias of `PeerId`).
### Consumer-side ADD-UNWRAP (1 site)
**`submodules/TelegramUI/Components/Chat/ChatRecentActionsController/Sources/ChatRecentActionsHistoryTransition.swift`**, lines 672674:
```swift
for (_, peer) in participant.peers {
peers[peer.id] = peer // `peers` is SimpleDictionary<PeerId, Peer>
}
```
After migration `peer` is `EnginePeer`; the outer `peers` SimpleDictionary is still `[PeerId: Peer]`. Rewrite:
```swift
for (_, peer) in participant.peers {
peers[peer.id] = peer._asPeer()
}
```
### Constructor sites with no `peers:` arg — no change (12 sites)
Default value's *type* changes (`[PeerId: Peer] = [:]``[EnginePeer.Id: EnginePeer] = [:]`) but the literal `[:]` works for either. These sites compile unchanged:
- TelegramCore: `ChannelAdminEventLogs.swift:271, 279` (x2), `:287` (x2), `:483` (x2) — 7 constructions
- `PeerInfoUI/.../ChannelAdminsController.swift:921`
- `PeerInfoUI/.../ChannelMembersSearchContainerNode.swift:987`
- `PeerInfoUI/.../ChannelMembersSearchControllerNode.swift:404`
- `TelegramUI/.../ChatRecentActionsController/.../ChatRecentActionsFilterController.swift:445`
- `TelegramUI/.../ChatControllerAdminBanUsers.swift:224, :370, :755` (3 constructions)
- `TelegramUI/.../StoryContainerScreen/.../StoryContentLiveChatComponent.swift:361`
## Net impact
**Consumer-surface bridges:** 6 wraps + 5 unwrap transforms + +1 unwrap = **10 bridges**.
**TelegramCore-internal bridges:** +~12 wraps (`EnginePeer(peer)` at producer insertion points, inside `import Postbox` modules). These do not regress Postbox-hygiene since every producer file already imports Postbox.
**Structural:** `RenderedChannelParticipant` public surface contains no raw `Peer` types after this wave (only `ChannelParticipant`, `EnginePeer`, `[EnginePeer.Id: EnginePeer]`, `[PeerId: PeerPresence]`). `presences` still leaks `PeerPresence` — separate future migration.
## Iteration budget
**23 iterations** (wave-41 foundational-type lesson: field migrations on passed-around structs budget 24 iterations, not first-pass-clean).
Verified absence of hidden grep surface:
- No `as?` / `is TelegramX` casts on `participant.peers[X]` extractions (grepped).
- No Peer-only properties accessed on extractions (uses `.id` and `.displayTitle(...)` only — both EnginePeer-forwarded).
- All 8 TelegramCore producers build locally (verified) — no chain-migration.
## Risks
1. **Producer local-dict migration under `continueOnError`.** If a producer builds the dict with more than two insertions and misses one, the build flags mismatched dict-value types. Low blast radius (per-file local).
2. **Hidden consumer site.** If a grep miss surfaces a `participant.peers` site not enumerated here, the wrap/unwrap balance changes. Mitigation: plan document must re-run the narrow grep (`participant\.peers|rcp\.peers|renderedParticipant\.peers`) at plan-write time and iteration-0 time.
3. **mapValues source-dict check.** If any of the 5 consumer-side `.mapValues({ $0._asPeer() })` sites has a source `[PeerId: Peer]` (not `[PeerId: EnginePeer]`), the migration at that site inverts (becomes a wrap instead of a drop). Plan-time per-site verification required.
4. **SimpleDictionary import.** The one ADD-UNWRAP site in `ChatRecentActionsHistoryTransition.swift` already uses `SimpleDictionary<PeerId, Peer>` — no new Postbox exposure.
## Out of scope
- `RenderedChannelParticipant.presences: [PeerId: PeerPresence]``PeerPresence` is a Postbox protocol; separate migration with different shape.
- `RenderedPeer → EngineRenderedPeer` foundational-type migration (listed in wave-44 memo as candidate 6; save for a dedicated session).
- `PeerInfoHeader*` bundle (wave-44 memo candidate 1) — considered but not selected for wave 44; candidate for wave 45.
## Success criteria
1. `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` has `peers: [EnginePeer.Id: EnginePeer]` declaration.
2. All 8 TelegramCore producers compile with wrapped inserts.
3. All 5 consumer `.mapValues({ $0._asPeer() })` transforms are removed.
4. All 6 consumer `EnginePeer(peer).displayTitle(...)` wraps on extracted dict values are removed (`peer.displayTitle(...)`).
5. `ChatRecentActionsHistoryTransition.swift:673` uses `peer._asPeer()` for the SimpleDictionary insertion value.
6. Full `Telegram/Telegram` build (`configuration=debug_sim_arm64`) is clean — **one** atomic commit.
7. Grep post-migration: `participant\.peers\[` returns only engine-typed call sites; no residual `EnginePeer(peer)` on `.peers[...]` extractions.
## Commit message template
```
Postbox -> TelegramEngine wave 44
Migrate RenderedChannelParticipant.peers from [PeerId: Peer] to
[EnginePeer.Id: EnginePeer]. Closes the wave-41 ratchet — the public
struct no longer leaks raw Peer types in any field (presences stays
Postbox-typed; separate migration).
Consumer-surface: -10 bridges (6 EnginePeer(peer) wraps dropped at
read sites, 5 .mapValues({ $0._asPeer() }) transforms dropped at
constructor sites, 1 ._asPeer() added at
ChatRecentActionsHistoryTransition.swift:673 where the value is
inserted into a raw-Peer SimpleDictionary).
TelegramCore producers: 8 files, each builds a local
[EnginePeer.Id: EnginePeer] dict from transaction.getPeer() wrapping
at the insertion point.
No unit tests in this project; full Telegram/Telegram build verified
under configuration=debug_sim_arm64.
```

View file

@ -0,0 +1,175 @@
# Wave 41 — `RenderedChannelParticipant.peer: Peer → EnginePeer` migration — Design
**Date:** 2026-04-24
**Wave:** 41
**Status:** spec
## Goal
Migrate the `peer` field of `TelegramCore.RenderedChannelParticipant` from the Postbox protocol `Peer` to the TelegramCore enum `EnginePeer`. All construction sites and consumer accesses are updated in one atomic commit.
## Motivation
- Drops 2 Shape-C `EnginePeer(participant.peer)` wraps installed by wave 39 (`ChannelMembersController.swift:707`, `ChannelBlacklistController.swift:381`).
- Drops ~37 additional `EnginePeer(...)` / `._asPeer()` bridges across the consumer surface (total ~39 bridge drops after counting `EnginePeer(peer.peer).compactDisplayTitle` sites in `AdminUserActionsSheet.swift`).
- Aligns `RenderedChannelParticipant.peer` with the pattern established for `FoundPeer.peer` (wave 34), `SendAsPeer.peer` (wave 35), `ContactListPeer.peer` (wave 36), and all `AccountContext.makeX(peer: ...)` facades (waves 3740).
- Ratchet candidate for future waves: once `.peer` is `EnginePeer`, the `peers: [PeerId: Peer]` dict field becomes the only Postbox-typed field on the struct — a follow-up wave can migrate `peers: [EnginePeer.Id: EnginePeer]` in isolation.
## Scope
### In scope
**TelegramCore:**
- `submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift` — change struct field + init param + Equatable impl
- 9 TelegramCore files containing 16 construction sites where `RenderedChannelParticipant(... peer: peer, ...)` is called with a raw `Peer` from `transaction.getPeer()` — wrap with `EnginePeer(peer)`:
- `Messages/RequestStartBot.swift:65`
- `Peers/AddPeerMember.swift:255`
- `Peers/ChannelAdminEventLogs.swift:271, 279, 287, 483` (7 constructor calls total)
- `Peers/ChannelBlacklist.swift:140`
- `Peers/ChannelMembers.swift:115`
- `Peers/ChannelOwnershipTransfer.swift:180` (2 constructor calls)
- `Peers/JoinChannel.swift:82`
- `Peers/PeerAdmins.swift:262`
- `Peers/Ranks.swift:95`
**Consumer (17 files):** all sites accessing `participant.peer` or constructing `RenderedChannelParticipant`:
- `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`
- `submodules/TemporaryCachedPeerDataManager/Sources/ChannelMemberCategoryListContext.swift`
- `submodules/TemporaryCachedPeerDataManager/Sources/PeerChannelMemberCategoriesContextsManager.swift`
### Out of scope (deferred)
- `RenderedChannelParticipant.peers: [PeerId: Peer]` — still `[PeerId: Peer]` dict. Not migrated this wave.
- `RenderedChannelParticipant.presences: [PeerId: PeerPresence]` — still `[PeerId: PeerPresence]` dict. Not migrated this wave.
- `PeerInfoScreenData.peer → EnginePeer` — future wave 42 candidate (drops 2 wave-40 wraps).
- `RenderedPeer → EngineRenderedPeer` — future major wave; saved for a dedicated session.
- `PeerInfoMember.peer: Peer` enum accessor in `PeerInfoMembers.swift:30-39` — retained as `Peer` for this wave (contained by a single `._asPeer()` inside the `.channelMember` branch). Migration of this accessor is a separate follow-up.
## Design
### Struct change
```swift
// submodules/TelegramCore/Sources/TelegramEngine/Peers/ChannelParticipants.swift
public struct RenderedChannelParticipant: Equatable {
public let participant: ChannelParticipant
public let peer: EnginePeer // ← was: Peer
public let peers: [PeerId: Peer] // unchanged
public let presences: [PeerId: PeerPresence] // unchanged
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 // ← was: lhs.peer.isEqual(rhs.peer)
}
}
```
`EnginePeer` is `Equatable` by enum synthesis (verified — each associated value type is Equatable: `TelegramUser`, `TelegramGroup`, `TelegramChannel`, `TelegramSecretChat`). `==` becomes cleaner.
### Consumer-site shapes
Per the pre-flight classification, sites fall into these shapes:
- **ZERO** (transparent) — `.id`, `.isDeleted`, `.indexName`, `.addressName`, `.compactDisplayTitle`, `.displayTitle(strings:displayOrder:)`, `.displayLetters`, `.debugDisplayTitle`, etc. All exposed on `EnginePeer`. ~160 sites. **No edit.**
- **DROP**`EnginePeer(participant.peer)``participant.peer`. ~32 consumer sites + 2 `.peer._asPeer()` downgrades that also drop. The biggest class of edits. Key sites:
- `ChannelAdminsController.swift:326, 921, 926` (921, 926 drop `._asPeer()` from constructor)
- `ChannelBlacklistController.swift:170, 381`
- `ChannelMembersController.swift:334, 707`
- `ChannelMembersSearchContainerNode.swift:212 (×2), 223`
- `ChannelMembersSearchControllerNode.swift:148`
- `ChannelPermissionsController.swift:480, 483`
- `SearchPeerMembers.swift:30, 36, 61, 76`
- `ChatRecentActionsController.swift:359`
- `ChatRecentActionsFilterController.swift:217`
- `ChatRecentActionsHistoryTransition.swift:719, 730, 740, 828, 842, 870, 943, 955, 973, 990, 1026` (one `EnginePeer(new.peer)` drop per site)
- `ShareWithPeersScreenState.swift:558, 576`
- `AdminUserActionsSheet.swift:284, 404, 416, 417, 522, 523` (EnginePeer(peer.peer) wraps)
- `StoryContentLiveChatComponent.swift:370` (drops `._asPeer()`)
- `ChatControllerAdminBanUsers.swift:372, 757` (drops `._asPeer()`)
- **CAST**`if let user = participant.peer as? TelegramUser, user.botInfo != nil``if case let .user(user) = participant.peer, user.botInfo != nil`. 9 sites across 4 files:
- `ChannelMembersController.swift:305`
- `ChannelMembersSearchContainerNode.swift:752, 884, 1052, 1136`
- `ChannelMembersSearchControllerNode.swift:516, 558`
- `ShareWithPeersScreenState.swift:566`
All 9 follow the identical 2-clause pattern (`as? TelegramUser`, `user.botInfo != nil`). Pattern-match rewrite is mechanically safe.
- **ADD-ASPEER** — site needs raw `Peer`. 3 sites:
- `ChatRecentActionsHistoryTransition.swift:675``peers[participant.peer.id] = participant.peer``peers[participant.peer.id] = participant.peer._asPeer()` (assigning into `SimpleDictionary<PeerId, Peer>`).
- `ChatRecentActionsHistoryTransition.swift:2275` — same pattern.
- `PeerInfoMembers.swift:33``return participant.peer``return participant.peer._asPeer()` (outer enum accessor returns `Peer`; deliberately contained — migration of `PeerInfoMember.peer` deferred).
- **ADD-WRAP** — consumer construction site where the local is raw `Peer` but the field is now `EnginePeer`. 7 sites across 3 files:
- `ChannelMembersSearchContainerNode.swift:987, 994, 998``peer: peer` where `peer = peerView.peers[participant.peerId]` is raw `Peer`. → `peer: EnginePeer(peer)`.
- `ChannelMembersSearchControllerNode.swift:404, 409, 413` — same pattern.
- `ChatRecentActionsFilterController.swift:445``peer: user` where `user: TelegramUser` (from `case let .user(user) = peer`). → `peer: .user(user)` or `peer: EnginePeer(user)`. Use `peer: .user(user)` (direct enum case) for clarity.
- `ChatControllerAdminBanUsers.swift:226``peer: peer` where `peer = author: Peer`. → `peer: EnginePeer(peer)`.
### TelegramCore-internal constructor sites
All 16 sites receive a raw `Peer` (from `transaction.getPeer()` / `peers[id]`) and pass it as `peer:`. All become `peer: EnginePeer(peer)`:
```swift
// Before:
RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences)
// After:
RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: presences)
```
No shape-selection judgment required — all 16 sites follow this exact template. The `peers` and `presences` dictionaries are unchanged.
## Risks
- **R1: CAST semantic preservation.** The 9 `as? TelegramUser` sites all gate on `user.botInfo != nil`. Pattern-match rewrite is `if case let .user(user) = participant.peer, user.botInfo != nil`. Verified: `EnginePeer.user(TelegramUser)` gives access to the same `TelegramUser` instance; `.botInfo` is a `TelegramUser` property. Semantically equivalent.
- **R2: `==` implementation change.** The struct's `==` goes from `lhs.peer.isEqual(rhs.peer)` (protocol dispatch) to `lhs.peer == rhs.peer` (synthesized). `EnginePeer.==` uses Swift-synthesized enum equality: each case compares associated values. Each associated-value type (`TelegramUser`, `TelegramGroup`, `TelegramChannel`, `TelegramSecretChat`) is `Equatable` via its own `==` implementation. Semantically equivalent to the protocol `isEqual`.
- **R3: PeerInfoMembers.swift:33 cascade.** `PeerInfoMember.peer: Peer` enum accessor at line 30-39 returns `participant.peer` on the `.channelMember` branch. Fix is a single `._asPeer()`. The outer enum's API stays unchanged — no cascade beyond this file. Future wave can migrate `PeerInfoMember.peer` to `EnginePeer`.
- **R4: Consumer-side constructor sites in ChannelMembersSearch*Node.** 3 sites each in the `Container` and `Controller` node files construct `RenderedChannelParticipant` for the legacy-group search path. The `peer` local is raw `Peer` from `peerView.peers`. Mechanical wrap with `EnginePeer(peer)` at the `peer:` argument.
- **R5: `participant.peers` dict staying `[PeerId: Peer]`.** Current code uses `peers.mapValues({ $0._asPeer() })` at construction sites where the local dict is `[EnginePeer.Id: EnginePeer]`. This pattern is unchanged by the wave — the `peers` field is not being migrated.
- **R6: Hidden consumer sites.** Pre-flight searched: `RenderedChannelParticipant(` constructors across `submodules/`, `participant.peer` access (subagent classification), all files that import TelegramCore/Postbox and reference `RenderedChannelParticipant`. 17 consumer files + 10 TelegramCore files confirmed. Risk of overlooked third-party or sparse consumer: low.
- **R7: Pre-existing WIP contamination.** `git status` shows unrelated WIP: `submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift`, `build-system/bazel-rules/sourcekit-bazel-bsp` submodule marker, several untracked dirs. Wave-39 lesson: enumerate files explicitly in `git add`; run `git status --short` after staging.
## Verification
- Single full Bazel build with `--continueOnError` after all edits (extends wave-39 / wave-40 pattern).
- Expected outcome: **first-pass-clean build** based on wave-39 precedent — 52 files / 73 sites / non-propagating signature migration → first-pass-clean. This wave is comparable scale (27 files / ~200+ sites including ZEROs) with even cleaner mechanics: ZERO sites are literally no edit; DROP/CAST/ADD-WRAP/ADD-ASPEER patterns are all mechanical; no inference-dependent return types.
- Budget: 35 iterations if classification is wrong; first-pass-clean if classification is exact.
## Net ratchet economics
- Bridges dropped: ~3739 (32 consumer DROPs + 2 `._asPeer()` drops in ChannelAdminsController + ~6 `EnginePeer(peer.peer).X` drops in AdminUserActionsSheet, possibly double-counted; final net post-commit grep will settle the number).
- Bridges added: ~23 (16 TelegramCore `EnginePeer(peer)` wraps at constructor call sites + 4 ADD-WRAP consumer constructors + 3 ADD-ASPEER).
- **Net:** ~14 to 16 bridges. Positive economics even counting TelegramCore-internal adds.
- Ratchet marker: the 4 consumer ADD-WRAP constructor sites (`ChannelMembersSearch*Node` + `ChatControllerAdminBanUsers:226`) are candidates for drop in a future wave that migrates the `peerView.peers[id]` / `authors: [Peer]` upstream flows to EnginePeer.
## Out-of-scope inventory (for the next wave)
If a follow-up wave migrates **`RenderedChannelParticipant.peers: [PeerId: Peer] → [EnginePeer.Id: EnginePeer]`**, the ADD-WRAP sites in this wave (all `peers: peers.mapValues({ $0._asPeer() })`) simplify to `peers: peers`. That's a high-ratchet candidate wave that becomes mechanical once this wave lands.

View file

@ -1368,7 +1368,7 @@ public protocol SharedAccountContext: AnyObject {
func messageFromPreloadedChatHistoryViewForLocation(id: MessageId, location: ChatHistoryLocationInput, context: AccountContext, chatLocation: ChatLocation, subject: ChatControllerSubject?, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>, tag: HistoryViewInputTag?) -> Signal<(MessageIndex?, Bool), NoError>
func makeOverlayAudioPlayerController(context: AccountContext, chatLocation: ChatLocation, type: MediaManagerPlayerType, initialMessageId: MessageId, initialOrder: MusicPlaybackSettingsOrder, playlistLocation: SharedMediaPlaylistLocation?, parentNavigationController: NavigationController?) -> ViewController & OverlayAudioPlayerController
func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: Peer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController?
func makePeerInfoController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, peer: EnginePeer, mode: PeerInfoControllerMode, avatarInitiallyExpanded: Bool, fromChat: Bool, requestsContext: PeerInvitationImportersContext?) -> ViewController?
func makeChannelAdminController(context: AccountContext, peerId: PeerId, adminId: PeerId, initialParticipant: ChannelParticipant) -> ViewController?
func makeDeviceContactInfoController(context: ShareControllerAccountContext, environment: ShareControllerEnvironment, subject: DeviceContactInfoSubject, completed: (() -> Void)?, cancelled: (() -> Void)?) -> ViewController
func makePeersNearbyController(context: AccountContext) -> ViewController
@ -1398,7 +1398,7 @@ public protocol SharedAccountContext: AnyObject {
func makeProxySettingsController(context: AccountContext) -> ViewController
func makeLocalizationListController(context: AccountContext) -> ViewController
func makeCreateGroupController(context: AccountContext, peerIds: [PeerId], initialTitle: String?, mode: CreateGroupMode, completion: ((PeerId, @escaping () -> Void) -> Void)?) -> ViewController
func makeChatRecentActionsController(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController
func makeChatRecentActionsController(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) -> ViewController
func makePrivacyAndSecurityController(context: AccountContext) -> ViewController
func makeBioPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, present: @escaping (ViewController) -> Void)
func makeBirthdayPrivacyController(context: AccountContext, settings: Promise<AccountPrivacySettings?>, openedFromBirthdayScreen: Bool, present: @escaping (ViewController) -> Void)
@ -1458,7 +1458,7 @@ public protocol SharedAccountContext: AnyObject {
func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController
func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void)
func makeRecentSessionsController(context: AccountContext, activeSessionsContext: ActiveSessionsContext) -> ViewController & RecentSessionsController
func makeChatQrCodeScreen(context: AccountContext, peer: Peer, threadId: Int64?, temporary: Bool) -> ViewController
func makeChatQrCodeScreen(context: AccountContext, peer: EnginePeer, threadId: Int64?, temporary: Bool) -> ViewController
func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource, forceDark: Bool, dismissed: (() -> Void)?) -> ViewController
func makePremiumIntroController(sharedContext: SharedAccountContext, engine: TelegramEngineUnauthorized, inAppPurchaseManager: InAppPurchaseManager, source: PremiumIntroSource, proceed: (() -> Void)?) -> ViewController
func makePremiumDemoController(context: AccountContext, subject: PremiumDemoSubject, forceDark: Bool, action: @escaping () -> Void, dismissed: (() -> Void)?) -> ViewController

View file

@ -1498,7 +1498,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|> deliverOnMainQueue).start(next: { peer in
if let strongSelf = self, let peer = peer {
if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.getNavigationController()?.pushViewController(controller)
}
}

View file

@ -372,7 +372,7 @@ public final class CallListController: TelegramBaseController {
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { peer in
if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let strongSelf = self, let peer = peer, let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .calls(messages: messages.map({ $0._asMessage() })), avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(controller)
}
})

View file

@ -1910,7 +1910,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
strongSelf.presentInGlobalOverlay(contextController)
}
} else if let peer = peer.peer, peer.id == strongSelf.context.account.peerId, peerData.displayAsTopicList {
if let peerInfoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let peerInfoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
let source: ContextContentSource
if let location = location {
source = .location(ChatListContextLocationContentSource(controller: strongSelf, location: location))
@ -3306,7 +3306,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
guard let self else {
return
}
guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
guard let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
(self.navigationController as? NavigationController)?.pushViewController(controller)
@ -3792,7 +3792,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let sourceController = sourceController, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
guard let sourceController = sourceController, let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
(sourceController.navigationController as? NavigationController)?.pushViewController(controller)
@ -7298,7 +7298,7 @@ private final class ChatListLocationContext {
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).startStandalone(next: { [weak self] peer in
guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
guard let self, let peer = peer, let controller = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
(self.parentController?.navigationController as? NavigationController)?.pushViewController(controller)

View file

@ -164,9 +164,9 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
var enabled = true
if filter.contains(.onlyWriteable) {
if let peer = chatPeer {
enabled = canSendMessagesToPeer(peer._asPeer())
enabled = canSendMessagesToPeer(peer)
} else {
enabled = canSendMessagesToPeer(primaryPeer._asPeer())
enabled = canSendMessagesToPeer(primaryPeer)
}
if requiresPremiumForMessaging {
enabled = false
@ -753,7 +753,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var enabled = true
if filter.contains(.onlyWriteable) {
if let peer = chatPeer {
enabled = canSendMessagesToPeer(peer._asPeer())
enabled = canSendMessagesToPeer(peer)
} else {
enabled = false
}
@ -892,7 +892,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
var enabled = true
if filter.contains(.onlyWriteable) {
if let peer = chatPeer {
enabled = canSendMessagesToPeer(peer._asPeer())
enabled = canSendMessagesToPeer(peer)
} else {
enabled = false
}
@ -1015,7 +1015,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable {
case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats, requiresPremiumForMessaging, query):
var enabled = true
if filter.contains(.onlyWriteable) {
enabled = canSendMessagesToPeer(peer.peer._asPeer())
enabled = canSendMessagesToPeer(peer.peer)
if requiresPremiumForMessaging {
enabled = false
}
@ -1404,7 +1404,7 @@ private struct ChatListSearchListPaneNodeState: Equatable {
private func doesPeerMatchFilter(peer: EnginePeer, filter: ChatListNodePeersFilter) -> Bool {
var enabled = true
if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer._asPeer()) {
if filter.contains(.onlyWriteable), !canSendMessagesToPeer(peer) {
enabled = false
}
if filter.contains(.onlyPrivateChats) {
@ -4461,7 +4461,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
keepStack: .always
))
case .info:
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let peerInfoScreen = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(peerInfoScreen)
}
case .openApp:

View file

@ -503,7 +503,7 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
} else {
if filter.contains(.onlyWriteable) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
if !canSendMessagesToPeer(peer) {
enabled = false
}
if peerEntry.requiresPremiumForMessaging {
@ -864,7 +864,7 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
} else {
if filter.contains(.onlyWriteable) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
if !canSendMessagesToPeer(peer) {
enabled = false
}
if peerEntry.requiresPremiumForMessaging {
@ -2226,7 +2226,7 @@ public final class ChatListNode: ListViewImpl {
if filter.contains(.onlyWriteable) && filter.contains(.excludeDisabled) {
if let peer = peer.peers[peer.peerId] {
if !canSendMessagesToPeer(peer._asPeer()) {
if !canSendMessagesToPeer(peer) {
return false
}
} else {

View file

@ -1572,7 +1572,7 @@ public func canBypassRestrictions(chatPresentationInterfaceState: ChatPresentati
public func canSendMessagesToChat(_ state: ChatPresentationInterfaceState) -> Bool {
if let peer = state.renderedPeer?.peer {
let canBypassRestrictions = canBypassRestrictions(chatPresentationInterfaceState: state)
if canSendMessagesToPeer(peer, ignoreDefault: canBypassRestrictions) {
if canSendMessagesToPeer(EnginePeer(peer), ignoreDefault: canBypassRestrictions) {
return true
} else {
return false

View file

@ -39,7 +39,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
)
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
guard let peer = peer, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
(contactsController?.navigationController as? NavigationController)?.pushViewController(controller)

View file

@ -475,7 +475,7 @@ public class ContactsController: ViewController {
}
|> deliverOnMainQueue).start(next: { [weak self, weak controller] peer in
if let strongSelf = self, let controller = controller {
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer._asPeer(), threadId: nil, temporary: false), in: .window(.root))
controller.present(strongSelf.context.sharedContext.makeChatQrCodeScreen(context: strongSelf.context, peer: peer, threadId: nil, temporary: false), in: .window(.root))
}
})
}
@ -774,7 +774,7 @@ public class ContactsController: ViewController {
}
if let peer {
Queue.mainQueue().async {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let navigationController = strongSelf.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(infoController)
}

View file

@ -485,7 +485,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
var enabled = true
var requiresPremiumForMessaging = false
if onlyWriteable {
enabled = canSendMessagesToPeer(peer._asPeer())
enabled = canSendMessagesToPeer(peer)
if let value = peerRequiresPremiumForMessaging[peer.id], value {
requiresPremiumForMessaging = true
enabled = false
@ -525,7 +525,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
var enabled = true
var requiresPremiumForMessaging = false
if onlyWriteable {
enabled = canSendMessagesToPeer(peer.peer._asPeer())
enabled = canSendMessagesToPeer(peer.peer)
if let value = peerRequiresPremiumForMessaging[peer.peer.id], value {
requiresPremiumForMessaging = true
enabled = false
@ -559,7 +559,7 @@ public final class ContactsSearchContainerNode: SearchDisplayControllerContentNo
var enabled = true
var requiresPremiumForMessaging = false
if onlyWriteable {
enabled = canSendMessagesToPeer(peer.peer._asPeer())
enabled = canSendMessagesToPeer(peer.peer)
if let value = peerRequiresPremiumForMessaging[peer.peer.id], value {
requiresPremiumForMessaging = true
enabled = false

View file

@ -769,7 +769,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
if let peer, let message = self.message, canSendMessagesToPeer(peer._asPeer()) {
if let peer, let message = self.message, canSendMessagesToPeer(peer) {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
if let self, let navigationController = self.baseNavigationController() {
self.beginCustomDismiss(.simpleAnimation)

View file

@ -3905,7 +3905,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer._asPeer()) {
if let peer, let (message, _, _) = strongSelf.contentInfo(), canSendMessagesToPeer(peer) {
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.Conversation_ContextMenuReply, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reply"), color: theme.contextMenu.primaryColor)}, action: { [weak self] _, f in
if let self, let navigationController = self.baseNavigationController() {
self.beginCustomDismiss(.simpleAnimation)

View file

@ -1763,7 +1763,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
let _ = (strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peer.id))
|> deliverOnMainQueue).start(next: { peer in
if let strongSelf = self, let peer = peer {
if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.getNavigationController()?.pushViewController(controller)
}
}

View file

@ -376,7 +376,7 @@ public func inviteRequestsController(context: AccountContext, updatedPresentatio
}
}
navigateToProfileImpl = { [weak controller] peer in
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
}

View file

@ -621,7 +621,7 @@ public func legacyAttachmentMenu(
peerSupportsPolls = true
}
}
if let peer, peerSupportsPolls, canSendMessagesToPeer(peer) && canSendPolls {
if let peer, peerSupportsPolls, canSendMessagesToPeer(EnginePeer(peer)) && canSendPolls {
let pollItem = TGMenuSheetButtonItemView(title: presentationData.strings.AttachmentMenu_Poll, type: TGMenuSheetButtonTypeDefault, fontSize: fontSize, action: { [weak controller] in
controller?.dismiss(animated: true)
openPoll()

View file

@ -340,7 +340,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable
guard let strongSelf = self else {
return
}
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
(strongSelf.navigationController as? NavigationController)?.pushViewController(infoController)
}
})

View file

@ -1230,7 +1230,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
guard let peer else {
return
}
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(controller)
}
})

View file

@ -323,7 +323,7 @@ private enum ChannelAdminsEntry: ItemListNodeEntry {
.init(type: .destructive, title: presentationData.strings.Channel_Management_DismissAdmin, action: { arguments.removeAdmin(participant.peer.id) })
])
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
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: 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
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
arguments.removeAdmin(peerId)
@ -731,7 +731,7 @@ public func channelAdminsController(context: AccountContext, updatedPresentation
}
if case .legacyGroup = peer {
} else {
pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: nil, starsState: nil))
pushControllerImpl?(context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: nil, starsState: nil))
}
})
})
@ -918,12 +918,12 @@ public func channelAdminsController(context: AccountContext, updatedPresentation
}
switch participant {
case let .creator(_, rank):
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer._asPeer(), presences: presences))
result.append(RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: rank), peer: peer, presences: presences))
case let .admin(_, _, _, rank):
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[creator.id] = creator
peers[peer.id] = peer
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))
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, peers: peers.mapValues({ $0._asPeer() }), presences: presences))
case .member:
break
}

View file

@ -782,7 +782,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen
guard let peer else {
return
}
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(controller)
}
})

View file

@ -167,7 +167,7 @@ private enum ChannelBlacklistEntry: ItemListNodeEntry {
default:
break
}
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(participant)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
@ -363,20 +363,20 @@ public func channelBlacklistController(context: AccountContext, updatedPresentat
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(presentationData: presentationData)
var items: [ActionSheetItem] = []
if !EnginePeer(participant.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder).isEmpty {
items.append(ActionSheetTextItem(title: EnginePeer(participant.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)))
if !participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder).isEmpty {
items.append(ActionSheetTextItem(title: participant.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)))
}
let viewInfoTitle: String
if participant.peer is TelegramChannel {
if case .channel = participant.peer {
viewInfoTitle = presentationData.strings.GroupRemoved_ViewChannelInfo
} else {
viewInfoTitle = presentationData.strings.GroupRemoved_ViewUserInfo
}
items.append(ActionSheetButtonItem(title: viewInfoTitle, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if participant.peer is TelegramChannel {
if case .channel = participant.peer {
if let navigationController = getNavigationControllerImpl?() {
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(EnginePeer(participant.peer))))
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(participant.peer)))
}
} else if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: participant.peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(infoController)

View file

@ -302,7 +302,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .peerItem(_, _, strings, dateTimeFormat, nameDisplayOrder, participant, editing, enabled, _, isGroup):
let text: ItemListPeerItemText
if let user = participant.peer as? TelegramUser, let _ = user.botInfo {
if case let .user(user) = participant.peer, let _ = user.botInfo {
text = .text(strings.Bot_GenericBotStatus, .secondary)
} else {
text = .presence
@ -330,7 +330,7 @@ private enum ChannelMembersEntry: ItemListNodeEntry {
label = .none
}
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.init), text: text, label: label, editing: editing, switchValue: nil, enabled: enabled, selectable: participant.peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: participant.presences[participant.peer.id].flatMap(EnginePeer.Presence.init), text: text, label: label, editing: editing, switchValue: nil, enabled: enabled, selectable: participant.peer.id != arguments.context.account.peerId, sectionId: self.section, action: {
arguments.openParticipant(participant, isGroup)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
@ -782,7 +782,7 @@ public func channelMembersController(context: AccountContext, updatedPresentatio
return state.withUpdatedSearchingMembers(false)
}
}, openPeer: { peer, _ in
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(infoController)
}
}, pushController: { c in

View file

@ -209,7 +209,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable {
displayOrder: nameDisplayOrder,
context: context,
peerMode: .peer,
peer: .peer(peer: EnginePeer(participant.peer), chatPeer: EnginePeer(participant.peer)),
peer: .peer(peer: participant.peer, chatPeer: participant.peer),
status: status,
rightLabelText: label.flatMap { .init(text: $0, color: labelColor, hasBackground: labelBackground) },
enabled: enabled,
@ -220,7 +220,7 @@ private final class ChannelMembersSearchEntry: Comparable, Identifiable {
index: nil,
header: self.section.chatListHeaderType.flatMap({ ChatListSearchItemHeader(type: $0, theme: presentationData.theme, strings: presentationData.strings, actionTitle: nil, action: nil) }),
action: { _ in
interaction.peerSelected(EnginePeer(participant.peer), participant)
interaction.peerSelected(participant.peer, participant)
},
setPeerIdWithRevealedOptions: { peerId, fromPeerId in
interaction.setPeerIdWithRevealedOptions(RevealedPeerId(peerId: participant.peer.id, section: self.section), fromPeerId.flatMap({ RevealedPeerId(peerId: $0, section: self.section) }))
@ -749,7 +749,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
continue
}
if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil {
continue
}
@ -881,7 +881,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
}
for participant in foundMembers {
if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil {
continue
}
@ -984,18 +984,18 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
let renderedParticipant: RenderedChannelParticipant
switch participant {
case .creator:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer)
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer))
case .admin:
var peers: [EnginePeer.Id: EnginePeer] = [:]
if let creator = creatorPeer {
peers[creator.id] = creator
}
peers[peer.id] = EnginePeer(peer)
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() }))
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: EnginePeer(peer), peers: peers.mapValues({ $0._asPeer() }))
case .member:
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[peer.id] = EnginePeer(peer)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }))
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: EnginePeer(peer), peers: peers.mapValues({ $0._asPeer() }))
}
matchingMembers.append(renderedParticipant)
}
@ -1049,7 +1049,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
var index = 0
for participant in foundGroupMembers {
if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil {
continue
}
@ -1133,7 +1133,7 @@ public final class ChannelMembersSearchContainerNode: SearchDisplayControllerCon
}
for participant in foundMembers {
if excludeBots, let user = participant.peer as? TelegramUser, user.botInfo != nil {
if excludeBots, case let .user(user) = participant.peer, user.botInfo != nil {
continue
}

View file

@ -144,8 +144,8 @@ private enum ChannelMembersSearchEntry: Comparable, Identifiable {
headerType = isChannel ? .subscribers : .groupMembers
}
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: EnginePeer(participant.peer), chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings), action: { _ in
interaction.openPeer(EnginePeer(participant.peer), participant)
return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: nameSortOrder, displayOrder: nameDisplayOrder, context: context, peerMode: .peer, peer: .peer(peer: participant.peer, chatPeer: nil), status: status, enabled: enabled, selection: .none, editing: editing, index: nil, header: ChatListSearchItemHeader(type: headerType, theme: presentationData.theme, strings: presentationData.strings), action: { _ in
interaction.openPeer(participant.peer, participant)
})
case let .contact(_, peer, presence):
let status: ContactsPeerItemStatus
@ -401,16 +401,16 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
let renderedParticipant: RenderedChannelParticipant
switch participant {
case .creator:
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: peer, presences: peerView.peerPresences)
renderedParticipant = RenderedChannelParticipant(participant: .creator(id: peer.id, adminInfo: nil, rank: nil), peer: EnginePeer(peer), presences: peerView.peerPresences)
case .admin:
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[creator.id] = creator
peers[peer.id] = EnginePeer(peer)
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)
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: EnginePeer(peer), peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences)
case .member:
var peers: [EnginePeer.Id: EnginePeer] = [:]
peers[peer.id] = EnginePeer(peer)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer, peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences)
renderedParticipant = RenderedChannelParticipant(participant: .member(id: peer.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: EnginePeer(peer), peers: peers.mapValues({ $0._asPeer() }), presences: peerView.peerPresences)
}
entries.append(.peer(index, renderedParticipant, ContactsPeerItemEditing(editable: false, editing: false, revealed: false), label, enabled, false, false))
@ -513,7 +513,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
case .excludeNonMembers:
break
case .excludeBots:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
if case let .user(user) = participant.peer, user.botInfo != nil {
continue contactsLoop
}
}
@ -555,7 +555,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
case .excludeNonMembers:
break
case .excludeBots:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
if case let .user(user) = participant.peer, user.botInfo != nil {
continue participantsLoop
}
}
@ -568,7 +568,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
if participant.peer.id == context.account.peerId {
continue
}
if let user = participant.peer as? TelegramUser, user.botInfo != nil || user.flags.contains(.isSupport) {
if case let .user(user) = participant.peer, user.botInfo != nil || user.flags.contains(.isSupport) {
continue
}
for filter in filters {
@ -584,7 +584,7 @@ class ChannelMembersSearchControllerNode: ASDisplayNode {
case .excludeNonMembers:
break
case .excludeBots:
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
if case let .user(user) = participant.peer, user.botInfo != nil {
continue participantsLoop
}
}

View file

@ -477,10 +477,10 @@ private enum ChannelPermissionsEntry: ItemListNodeEntry {
default:
break
}
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? {
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: text, label: .none, editing: editing, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: canOpen ? {
arguments.openPeer(participant.participant)
} : {
arguments.openPeerInfo(EnginePeer(participant.peer))
arguments.openPeerInfo(participant.peer)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
}, removePeer: { peerId in
@ -1108,7 +1108,7 @@ public func channelPermissionsController(context: AccountContext, updatedPresent
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
})
}, openPeerInfo: { peer in
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(controller)
}
}, openKicked: {

View file

@ -626,7 +626,7 @@ public func peersNearbyController(context: AccountContext) -> ViewController {
controller?.clearItemNodesHighlight(animated: true)
}
navigateToProfileImpl = { [weak controller] peer, distance in
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
if let navigationController = controller?.navigationController as? NavigationController, let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .nearbyPeer(distance: distance), avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
}

View file

@ -27,13 +27,13 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch
return nil
}
if normalizedQuery.isEmpty {
return EnginePeer(participant.peer)
return participant.peer
} else {
if participant.peer.indexName.matchesByTokens(normalizedQuery) || participant.peer.indexName.matchesByTokens(transformedQuery) {
return EnginePeer(participant.peer)
return participant.peer
}
if let addressName = participant.peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) || addressName.lowercased().hasPrefix(transformedQuery) {
return EnginePeer(participant.peer)
return participant.peer
}
return nil
@ -58,7 +58,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch
if participant.peer.isDeleted {
return nil
}
return EnginePeer(participant.peer)
return participant.peer
}, true))
}
})
@ -73,7 +73,7 @@ public func searchPeerMembers(context: AccountContext, peerId: EnginePeer.Id, ch
if participant.peer.isDeleted {
return nil
}
return EnginePeer(participant.peer)
return participant.peer
}, true))
}
})

View file

@ -267,7 +267,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext:
}
}))
}, openPeer: { peer in
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
pushControllerImpl?(controller)
}
})

View file

@ -506,7 +506,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri
}))
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
}, openPeer: { peer in
guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
guard let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
pushControllerImpl?(controller)

View file

@ -968,7 +968,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
guard let peer = peer else {
return
}
let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false)
@ -977,7 +977,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
}
)
)
//TODO:fix
items.append(
SettingsSearchableItem(
@ -986,7 +986,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
guard let peer = peer else {
return
}
let controller = context.sharedContext.makeChatQrCodeScreen(context: context, peer: peer, threadId: nil, temporary: false)
@ -1017,7 +1017,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
guard let peer = peer else {
return
}
let controller = context.sharedContext.makePeerInfoController(
@ -1034,7 +1034,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
}
)
)
items.append(
SettingsSearchableItem(
id: "my-profile/edit",
@ -1043,7 +1043,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
guard let peer = peer else {
return
}
let controller = context.sharedContext.makePeerInfoController(
@ -1056,7 +1056,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
requestsContext: nil
)
present(.push, controller)
Queue.mainQueue().justDispatch {
if let controller = controller as? PeerInfoScreen {
controller.activateEdit()
@ -1077,7 +1077,7 @@ private func myProfileSearchableItems(context: AccountContext) -> [SettingsSearc
present: { context, _, present in
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer = peer?._asPeer() else {
guard let peer = peer else {
return
}
let controller = context.sharedContext.makePeerInfoController(

View file

@ -2494,7 +2494,7 @@ public final class ShareController: ViewController {
for entry in view.0.entries.reversed() {
switch entry {
case let .MessageEntry(entryData):
if let peer = entryData.renderedPeer.peers[entryData.renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(peer) {
if let peer = entryData.renderedPeer.peers[entryData.renderedPeer.peerId], peer.id != accountPeer.id, canSendMessagesToPeer(EnginePeer(peer)) {
peers.append(EngineRenderedPeer(entryData.renderedPeer))
if let user = peer as? TelegramUser, user.flags.contains(.requirePremium) || user.flags.contains(.requireStars) {
possiblePremiumRequiredPeers.insert(user.id)

View file

@ -381,7 +381,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
for renderedPeer in foundLocalPeers {
if let peer = renderedPeer.peers[renderedPeer.peerId], peer.id != accountPeer.id {
if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(peer) {
if !existingPeerIds.contains(renderedPeer.peerId) && canSendMessagesToPeer(EnginePeer(peer)) {
existingPeerIds.insert(renderedPeer.peerId)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(renderedPeer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
@ -399,7 +399,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
} else {
for foundPeer in foundRemotePeers.0 {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer._asPeer()) {
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: foundPeer.peer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: false))
index += 1
@ -408,7 +408,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
for foundPeer in foundRemotePeers.1 {
let peer = foundPeer.peer
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer._asPeer()) {
if !existingPeerIds.contains(peer.id) && canSendMessagesToPeer(peer) {
existingPeerIds.insert(peer.id)
entries.append(ShareSearchPeerEntry(index: index, peer: EngineRenderedPeer(peer: peer), presence: nil, requiresPremiumForMessaging: peerRequiresPremiumForMessaging[peer.id] ?? false, requiresStars: nil, theme: theme, strings: strings, isGlobal: true))
index += 1
@ -474,7 +474,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
}
var index = 0
for (peer, requiresPremiumForMessaging) in recentPeerList {
if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer._asPeer()) {
if let mainPeer = peer.peers[peer.peerId], canSendMessagesToPeer(mainPeer) {
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer._asPeer().associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings))
index += 1
}

View file

@ -880,7 +880,7 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
}
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
})
@ -912,7 +912,7 @@ public func groupStatsController(context: AccountContext, updatedPresentationDat
}
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer._asPeer(), adminPeerId: participantPeerId, starsState: nil)
let controller = context.sharedContext.makeChatRecentActionsController(context: context, peer: peer, adminPeerId: participantPeerId, starsState: nil)
navigationController.pushViewController(controller)
})
}

View file

@ -601,7 +601,7 @@ public func messageStatsController(context: AccountContext, updatedPresentationD
return
}
if case .user = peer {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: peer.largeProfileImage != nil, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
} else {

View file

@ -136,7 +136,7 @@ extension VideoChatScreenComponent.View {
} else {
text = environment.strings.VoiceChat_InvitedPeerText(peer.displayTitle(strings: environment.strings, displayOrder: groupCall.accountContext.sharedContext.currentPresentationData.with({ $0 }).nameDisplayOrder)).string
}
self.presentToast(icon: .peer(EnginePeer(participant.peer)), text: text, duration: 3)
self.presentToast(icon: .peer(participant.peer), text: text, duration: 3)
}
} else {
if case let .channel(groupPeer) = groupPeer, let listenerLink = inviteLinks?.listenerLink, !groupPeer.hasPermission(.inviteMembers) {

View file

@ -62,7 +62,7 @@ func _internal_requestStartBotInGroup(account: Account, botPeerId: PeerId, group
let presences: [PeerId: PeerPresence] = [:]
peers[peer.id] = peer
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: presences))
return .channelParticipant(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: presences))
} else {
return .none
}

View file

@ -252,7 +252,7 @@ func _internal_addChannelMember(account: Account, peerId: PeerId, memberId: Peer
}
}
}
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences))
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences))
}
|> mapError { _ -> AddChannelMemberError in }
}

View file

@ -268,7 +268,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
let participant = ChannelParticipant(apiParticipant: channelAdminLogEventActionParticipantInviteData.participant)
if let peer = peers[participant.peerId] {
action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: peer))
action = .participantInvite(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer)))
}
case let .channelAdminLogEventActionParticipantToggleBan(channelAdminLogEventActionParticipantToggleBanData):
let (prev, new) = (channelAdminLogEventActionParticipantToggleBanData.prevParticipant, channelAdminLogEventActionParticipantToggleBanData.newParticipant)
@ -276,7 +276,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
let newParticipant = ChannelParticipant(apiParticipant: new)
if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] {
action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
action = .participantToggleBan(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer)))
}
case let .channelAdminLogEventActionParticipantToggleAdmin(channelAdminLogEventActionParticipantToggleAdminData):
let (prev, new) = (channelAdminLogEventActionParticipantToggleAdminData.prevParticipant, channelAdminLogEventActionParticipantToggleAdminData.newParticipant)
@ -284,7 +284,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
let newParticipant = ChannelParticipant(apiParticipant: new)
if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] {
action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
action = .participantToggleAdmin(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer)))
}
case let .channelAdminLogEventActionChangeStickerSet(channelAdminLogEventActionChangeStickerSetData):
let (prevStickerset, newStickerset) = (channelAdminLogEventActionChangeStickerSetData.prevStickerset, channelAdminLogEventActionChangeStickerSetData.newStickerset)
@ -480,7 +480,7 @@ func channelAdminLogEvents(accountPeerId: PeerId, postbox: Postbox, network: Net
let newParticipant = ChannelParticipant(apiParticipant: new)
if let prevPeer = peers[prevParticipant.peerId], let newPeer = peers[newParticipant.peerId] {
action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: prevPeer), new: RenderedChannelParticipant(participant: newParticipant, peer: newPeer))
action = .participantSubscriptionExtended(prev: RenderedChannelParticipant(participant: prevParticipant, peer: EnginePeer(prevPeer)), new: RenderedChannelParticipant(participant: newParticipant, peer: EnginePeer(newPeer)))
}
case let .channelAdminLogEventActionToggleAutotranslation(channelAdminLogEventActionToggleAutotranslationData):
let newValue = channelAdminLogEventActionToggleAutotranslationData.newValue

View file

@ -137,7 +137,7 @@ func _internal_updateChannelMemberBannedRights(account: Account, peerId: PeerId,
}
}
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: memberPeer, peers: peers, presences: presences), isMember)
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(memberPeer), peers: peers, presences: presences), isMember)
}
}
} else {

View file

@ -112,7 +112,7 @@ func _internal_channelMembers(postbox: Postbox, network: Network, accountPeerId:
if let presence = transaction.getPeerPresence(peerId: participant.peerId) {
renderedPresences[participant.peerId] = presence
}
items.append(RenderedChannelParticipant(participant: participant, peer: peer, peers: peers, presences: renderedPresences))
items.append(RenderedChannelParticipant(participant: participant, peer: EnginePeer(peer), peers: peers, presences: renderedPresences))
}
}
case .channelParticipantsNotModified:

View file

@ -177,7 +177,7 @@ func _internal_updateChatOwnership(account: Account, peerId: PeerId, memberId: P
if let presence = transaction.getPeerPresence(peerId: user.id) {
presences[user.id] = presence
}
return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: accountUser, peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))]
return [(currentCreator, RenderedChannelParticipant(participant: updatedPreviousCreator, peer: EnginePeer(accountUser), peers: peers, presences: presences)), (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences))]
}
|> mapError { _ -> ChatOwnershipTransferError in }
}

View file

@ -7,18 +7,18 @@ import MtProtoKit
public struct RenderedChannelParticipant: Equatable {
public let participant: ChannelParticipant
public let peer: Peer
public let peer: EnginePeer
public let peers: [PeerId: Peer]
public let presences: [PeerId: PeerPresence]
public init(participant: ChannelParticipant, peer: Peer, peers: [PeerId: Peer] = [:], 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.isEqual(rhs.peer)
return lhs.participant == rhs.participant && lhs.peer == rhs.peer
}
}

View file

@ -79,7 +79,7 @@ func _internal_joinChannel(account: Account, peerId: PeerId, hash: String?) -> S
}
}
return RenderedChannelParticipant(participant: updatedParticipant, peer: peer, peers: peers, presences: presences)
return RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(peer), peers: peers, presences: presences)
}
|> castError(JoinChannelError.self)
}

View file

@ -259,7 +259,7 @@ func _internal_updateChannelAdminRights(account: Account, peerId: PeerId, adminI
peers[peer.id] = peer
}
}
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: adminPeer, peers: peers, presences: presences))
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(adminPeer), peers: peers, presences: presences))
} |> mapError { _ -> UpdateChannelAdminRightsError in }
}
} else {

View file

@ -92,7 +92,7 @@ func _internal_updateChatRank(account: Account, peerId: PeerId, userId: PeerId,
return .update(StoreMessage(id: currentMessage.id, customStableId: nil, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: storeForwardInfo, authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: user, peers: peers, presences: presences))
return (currentParticipant, RenderedChannelParticipant(participant: updatedParticipant, peer: EnginePeer(user), peers: peers, presences: presences))
}
}
|> castError(UpdateChatRankError.self)

View file

@ -80,7 +80,7 @@ func _internal_searchGroupMembers(postbox: Postbox, network: Network, accountPee
let existingIds = Set(local.map { $0.id })
let filtered: [Peer]
if let participants = participants {
filtered = participants.map({ $0.peer }).filter({ peer in
filtered = participants.map({ $0.peer._asPeer() }).filter({ peer in
if existingIds.contains(peer.id) {
return false
}

View file

@ -6,7 +6,8 @@ import Postbox
private final class LinkHelperClass: NSObject {
}
public func canSendMessagesToPeer(_ peer: Peer, ignoreDefault: Bool = false) -> Bool {
public func canSendMessagesToPeer(_ peer: EnginePeer, ignoreDefault: Bool = false) -> Bool {
let peer = peer._asPeer()
if let peer = peer as? TelegramUser, peer.addressName == "replies" {
return false
} else if peer is TelegramUser || peer is TelegramGroup {

View file

@ -281,7 +281,7 @@ private func adminUserActionsTitle(
switch mode {
case .monoforum:
if let peer = peers.first {
return strings.Monoforum_DeleteTopic_Title(EnginePeer(peer.peer).compactDisplayTitle).string
return strings.Monoforum_DeleteTopic_Title(peer.peer.compactDisplayTitle).string
} else {
return strings.Common_Delete
}
@ -401,7 +401,7 @@ private final class AdminUserActionsContentComponent: Component {
additionalSelectedPeers = component.sheetState.optionDeleteAllReactionsSelectedPeers
isExpanded = component.sheetState.isOptionDeleteAllExpanded
if component.peers.count == 1 {
title = component.strings.Chat_AdminActionSheet_DeleteAllSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string
title = component.strings.Chat_AdminActionSheet_DeleteAllSingle(component.peers[0].peer.compactDisplayTitle).string
} else {
title = component.strings.Chat_AdminActionSheet_DeleteAllMultiple
}
@ -413,8 +413,8 @@ private final class AdminUserActionsContentComponent: Component {
let banTitle: String
let restrictTitle: String
if component.peers.count == 1 {
banTitle = component.strings.Chat_AdminActionSheet_BanSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string
restrictTitle = component.strings.Chat_AdminActionSheet_RestrictSingle(EnginePeer(component.peers[0].peer).compactDisplayTitle).string
banTitle = component.strings.Chat_AdminActionSheet_BanSingle(component.peers[0].peer.compactDisplayTitle).string
restrictTitle = component.strings.Chat_AdminActionSheet_RestrictSingle(component.peers[0].peer.compactDisplayTitle).string
} else {
banTitle = component.strings.Chat_AdminActionSheet_BanMultiple
restrictTitle = component.strings.Chat_AdminActionSheet_RestrictMultiple
@ -519,8 +519,8 @@ private final class AdminUserActionsContentComponent: Component {
strings: component.strings,
baseFontSize: component.presentationData.listsFontSize.baseDisplaySize,
sideInset: 0.0,
title: EnginePeer(peer.peer).displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: EnginePeer(peer.peer),
title: peer.peer.displayTitle(strings: component.strings, displayOrder: .firstLast),
peer: peer.peer,
selectionState: .editing(isSelected: selectedPeers.contains(peer.peer.id)),
action: { peer in
component.togglePeerSelection(section, peer)

View file

@ -356,7 +356,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
var adminPeers: [EnginePeer] = []
if let result {
for participant in result {
adminPeers.append(EnginePeer(participant.peer))
adminPeers.append(participant.peer)
}
}
let controller = RecentActionsSettingsSheet(

View file

@ -1008,7 +1008,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
if peer is TelegramChannel, let navigationController = strongSelf.getNavigationController() {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer)), peekData: peekData, animated: true))
} else {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.pushController(infoController)
}
}
@ -1028,7 +1028,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
|> deliverOnMainQueue).startStrict(next: { [weak self] peer in
if let strongSelf = self {
if let peer = peer {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.pushController(infoController)
}
}

View file

@ -213,7 +213,7 @@ private enum ChatRecentActionsFilterEntry: ItemListNodeEntry {
peerText = strings.ChatAdmins_AdminLabel.lowercased()
}
}
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(participant.peer), presence: nil, text: .text(peerText, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: participant.peer, presence: nil, text: .text(peerText, .secondary), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: ItemListPeerItemSwitch(value: checked, style: .check), enabled: true, selectable: true, sectionId: self.section, action: {
arguments.toggleAdmin(participant.peer.id)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in })
@ -442,7 +442,7 @@ public func channelRecentActionsFilterController(context: AccountContext, update
antiSpamBotPeerPromise.set(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: antiSpamBotId))
|> map { peer in
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)
return RenderedChannelParticipant(participant: .member(id: user.id, invitedAt: 0, adminInfo: nil, banInfo: nil, rank: nil, subscriptionUntilDate: nil), peer: peer)
} else {
return nil
}

View file

@ -672,7 +672,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
for (_, peer) in participant.peers {
peers[peer.id] = peer
}
peers[participant.peer.id] = participant.peer
peers[participant.peer.id] = participant.peer._asPeer()
let action: TelegramMediaActionType
action = TelegramMediaActionType.addedMembers(peerIds: [participant.peer.id])
@ -716,7 +716,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
}
if (prevBanInfo == nil || !prevBanInfo!.rights.flags.contains(.banReadMessages)) && newFlags.contains(.banReadMessages) {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageKickedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageKickedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageKickedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageKickedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -727,7 +727,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
}, to: &text, entities: &entities)
text += "\n"
} else if isBroadcast, newBanInfo == nil, prevBanInfo != nil {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageUnkickedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageUnkickedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageUnkickedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageUnkickedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -737,7 +737,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
return result
}, to: &text, entities: &entities)
} else {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRestrictedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRestrictedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRestrictedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRestrictedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -825,7 +825,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
var entities: [MessageTextEntity] = []
if case .member = prev.participant, case .creator = new.participant {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageTransferedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageTransferedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageTransferedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageTransferedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -839,7 +839,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
if case let .creator(_, prevAdminInfo, prevRank) = prev.participant, case let .creator(_, newAdminInfo, newRank) = new.participant, (prevRank != newRank || prevAdminInfo?.rights.rights.contains(.canBeAnonymous) != newAdminInfo?.rights.rights.contains(.canBeAnonymous)) {
if prevRank != newRank {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -867,7 +867,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
if prevAdminInfo?.rights.rights.contains(flag) != newAdminInfo?.rights.rights.contains(flag) {
if !appendedRightsHeader {
appendedRightsHeader = true
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -940,7 +940,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
if !appendedRightsHeader {
appendedRightsHeader = true
if prevAdminRights == nil {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageAddedAdminName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageAddedAdminNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageAddedAdminName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageAddedAdminNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -952,7 +952,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
return result
}, to: &text, entities: &entities)
} else {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -970,7 +970,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
if !prevFlags.isEmpty && newFlags.isEmpty {
if !appendedRightsHeader {
appendedRightsHeader = true
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessageRemovedAdminNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -987,7 +987,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
if prevFlags.contains(flag) != newFlags.contains(flag) {
if !appendedRightsHeader {
appendedRightsHeader = true
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessagePromotedName(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder)) : self.presentationData.strings.Channel_AdminLog_MessagePromotedNameUsername(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -1023,7 +1023,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
return result
}, to: &text, entities: &entities)
} else {
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(EnginePeer(new.peer).displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in
appendAttributedText(text: new.peer.addressName == nil ? self.presentationData.strings.Channel_AdminLog_MessageRankNameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), newRank ?? "") : self.presentationData.strings.Channel_AdminLog_MessageRankUsernameNew(new.peer.displayTitle(strings: self.presentationData.strings, displayOrder: self.presentationData.nameDisplayOrder), "@" + new.peer.addressName!, newRank ?? ""), generateEntities: { index in
var result: [MessageTextEntityType] = []
if index == 0 {
result.append(.TextMention(peerId: new.peer.id))
@ -2272,7 +2272,7 @@ struct ChatRecentActionsEntry: Comparable, Identifiable {
for (_, peer) in new.peers {
peers[peer.id] = peer
}
peers[new.peer.id] = new.peer
peers[new.peer.id] = new.peer._asPeer()
var text: String = ""
var entities: [MessageTextEntity] = []

View file

@ -2293,7 +2293,7 @@ private final class ChatSendStarsScreenComponent: Component {
if let peerInfoController = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: .generic,
avatarInitiallyExpanded: false,
fromChat: false,

View file

@ -583,7 +583,7 @@ final class NewContactScreenComponent: Component {
return
}
if case let .peer(peer, _) = self.resolvedPeer {
if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let navigationController = component.context.sharedContext.mainWindow?.viewController as? NavigationController {
navigationController.pushViewController(infoController)
}

View file

@ -927,7 +927,7 @@ final class GiftOptionsScreenComponent: Component {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: .gifts,
avatarInitiallyExpanded: false,
fromChat: false,

View file

@ -410,7 +410,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let profileController = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: peer.id == context.account.peerId ? .myProfileGifts : .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
@ -558,7 +558,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let controller = self.context.sharedContext.makePeerInfoController(
context: self.context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: giftsPeerId == self.context.account.peerId ? .myProfileGifts : .gifts,
avatarInitiallyExpanded: false,
fromChat: false,
@ -2249,7 +2249,7 @@ private final class GiftViewSheetContent: CombinedComponent {
if let controller = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: .upgradableGifts,
avatarInitiallyExpanded: false,
fromChat: false,

View file

@ -317,7 +317,7 @@ private final class JoinSubjectScreenComponent: Component {
if let peerInfoController = context.sharedContext.makePeerInfoController(
context: context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: .generic,
avatarInitiallyExpanded: false,
fromChat: false,

View file

@ -227,7 +227,7 @@ final class MiniAppListScreenComponent: Component {
return
}
if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let peerInfoScreen = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
peerInfoScreen.navigationPresentation = .modal
controller.push(peerInfoScreen)
}

View file

@ -875,7 +875,7 @@ private final class JoinAffiliateProgramScreenComponent: Component {
guard let infoController = component.context.sharedContext.makePeerInfoController(
context: component.context,
updatedPresentationData: nil,
peer: component.sourcePeer._asPeer(),
peer: component.sourcePeer,
mode: .generic,
avatarInitiallyExpanded: false,
fromChat: false,

View file

@ -175,7 +175,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
break
}
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member)
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer.flatMap(EnginePeer.init), member: item.member)
var options: [ItemListPeerItemRevealOption] = []
if actions.contains(.promote) && item.enclosingPeer is TelegramChannel {

View file

@ -136,7 +136,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
break
}
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member)
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: EnginePeer(enclosingPeer), member: member)
var options: [ItemListPeerItemRevealOption] = []
if actions.contains(.promote) && enclosingPeer is TelegramChannel {

View file

@ -383,7 +383,7 @@ final class PeerInfoPersonalChannelData: Equatable {
}
final class PeerInfoScreenData {
let peer: Peer?
let peer: EnginePeer?
let chatPeer: Peer?
let savedMessagesPeer: Peer?
let cachedData: CachedPeerData?
@ -439,7 +439,7 @@ final class PeerInfoScreenData {
}
init(
peer: Peer?,
peer: EnginePeer?,
chatPeer: Peer?,
savedMessagesPeer: Peer?,
cachedData: CachedPeerData?,
@ -1024,7 +1024,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
)
return PeerInfoScreenData(
peer: peer,
peer: peer.flatMap(EnginePeer.init),
chatPeer: peer,
savedMessagesPeer: nil,
cachedData: peerView.cachedData,
@ -1617,7 +1617,7 @@ func peerInfoScreenData(
}
return PeerInfoScreenData(
peer: peer,
peer: peer.flatMap(EnginePeer.init),
chatPeer: peerView.peers[peerId],
savedMessagesPeer: savedMessagesPeer?._asPeer(),
cachedData: peerView.cachedData,
@ -1864,7 +1864,7 @@ func peerInfoScreenData(
}
return PeerInfoScreenData(
peer: peerView.peers[peerId],
peer: peerView.peers[peerId].flatMap(EnginePeer.init),
chatPeer: peerView.peers[peerId],
savedMessagesPeer: nil,
cachedData: peerView.cachedData,
@ -2202,7 +2202,7 @@ func peerInfoScreenData(
let appConfiguration: AppConfiguration = preferencesView?.get(AppConfiguration.self) ?? .defaultValue
return .single(PeerInfoScreenData(
peer: peerView.peers[groupId],
peer: peerView.peers[groupId].flatMap(EnginePeer.init),
chatPeer: peerView.peers[groupId],
savedMessagesPeer: nil,
cachedData: peerView.cachedData,
@ -2256,19 +2256,19 @@ func peerInfoIsCopyProtected(data: PeerInfoScreenData) -> Bool {
var isCopyProtected = false
if let cachedUserData = data.cachedData as? CachedUserData, cachedUserData.flags.contains(.copyProtectionEnabled) || cachedUserData.flags.contains(.myCopyProtectionEnabled) {
isCopyProtected = true
} else if let peer = data.peer, peer.isCopyProtectionEnabled {
} else if let peer = data.peer, peer._asPeer().isCopyProtectionEnabled {
isCopyProtected = true
}
return isCopyProtected
}
func canEditPeerInfo(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?) -> Bool {
func canEditPeerInfo(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?) -> Bool {
if context.account.peerId == peer?.id {
return true
}
if let user = peer as? TelegramUser, let botInfo = user.botInfo {
if case let .user(user) = peer, let botInfo = user.botInfo {
return botInfo.flags.contains(.canEdit)
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
if let threadData = threadData {
if chatLocation.threadId == 1 {
return false
@ -2284,7 +2284,7 @@ func canEditPeerInfo(context: AccountContext, peer: Peer?, chatLocation: ChatLoc
return true
}
}
} else if let group = peer as? TelegramGroup {
} else if case let .legacyGroup(group) = peer {
switch group.role {
case .admin, .creator:
return true
@ -2311,23 +2311,23 @@ struct PeerInfoMemberActions: OptionSet {
static let editRank = PeerInfoMemberActions(rawValue: 1 << 3)
}
func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member: PeerInfoMember) -> PeerInfoMemberActions {
func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: EnginePeer?, member: PeerInfoMember) -> PeerInfoMemberActions {
var result: PeerInfoMemberActions = []
if peer == nil {
result.insert(.logout)
} else if member.id == accountPeerId {
if let channel = peer as? TelegramChannel {
if case let .channel(channel) = peer {
if channel.hasPermission(.editRank) {
result.insert(.editRank)
}
} else if let group = peer as? TelegramGroup {
} else if case let .legacyGroup(group) = peer {
if !group.hasBannedPermission(.banEditRank) {
result.insert(.editRank)
}
}
} else {
if let channel = peer as? TelegramChannel {
if case let .channel(channel) = peer {
if channel.flags.contains(.isCreator) {
if !channel.flags.contains(.isGigagroup) {
result.insert(.restrict)
@ -2371,7 +2371,7 @@ func availableActionsForMemberOfPeer(accountPeerId: PeerId, peer: Peer?, member:
break
}
}
} else if let group = peer as? TelegramGroup {
} else if case let .legacyGroup(group) = peer {
switch group.role {
case .creator:
result.insert(.restrict)
@ -2431,9 +2431,9 @@ func peerInfoHeaderButtonIsHiddenWhileExpanded(buttonKey: PeerInfoHeaderButtonKe
return hiddenWhileExpanded
}
func peerInfoHeaderActionButtons(peer: Peer?, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] {
func peerInfoHeaderActionButtons(peer: EnginePeer?, isSecretChat: Bool, isContact: Bool) -> [PeerInfoHeaderButtonKey] {
var result: [PeerInfoHeaderButtonKey] = []
if !isContact && !isSecretChat, let user = peer as? TelegramUser, user.botInfo == nil {
if !isContact && !isSecretChat, case let .user(user) = peer, user.botInfo == nil {
result = [.message, .addContact]
}
@ -2444,9 +2444,9 @@ func peerInfoHeaderActionButtons(peer: Peer?, isSecretChat: Bool, isContact: Boo
return result
}
func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] {
func peerInfoHeaderButtons(peer: EnginePeer?, cachedData: CachedPeerData?, isOpenedFromChat: Bool, isExpanded: Bool, videoCallsEnabled: Bool, isSecretChat: Bool, isContact: Bool, threadInfo: EngineMessageHistoryThread.Info?) -> [PeerInfoHeaderButtonKey] {
var result: [PeerInfoHeaderButtonKey] = []
if let user = peer as? TelegramUser {
if case let .user(user) = peer {
if !isOpenedFromChat {
result.append(.message)
}
@ -2480,7 +2480,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
} else {
result.append(.more)
}
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
if let _ = threadInfo {
result.append(.mute)
result.append(.search)
@ -2555,7 +2555,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
}
}
}
} else if let group = peer as? TelegramGroup {
} else if case let .legacyGroup(group) = peer {
let hasVoiceChat = group.flags.contains(.hasVoiceChat)
let canStartVoiceChat: Bool
@ -2582,8 +2582,8 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
return result
}
func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool {
if let user = peer as? TelegramUser {
func peerInfoCanEdit(peer: EnginePeer?, chatLocation: ChatLocation, threadData: MessageHistoryThreadData?, cachedData: CachedPeerData?, isContact: Bool?) -> Bool {
if case let .user(user) = peer {
if user.isDeleted {
return false
}
@ -2594,7 +2594,7 @@ func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: Messag
return false
}
return true
} else if let peer = peer as? TelegramChannel {
} else if case let .channel(peer) = peer {
if peer.isForumOrMonoForum, let threadData = threadData {
if peer.flags.contains(.isCreator) {
return true
@ -2615,7 +2615,7 @@ func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: Messag
}
return false
}
} else if let peer = peer as? TelegramGroup {
} else if case let .legacyGroup(peer) = peer {
if case .creator = peer.role {
return true
} else if case let .admin(rights, _) = peer.role {
@ -2630,19 +2630,19 @@ func peerInfoCanEdit(peer: Peer?, chatLocation: ChatLocation, threadData: Messag
return false
}
func peerInfoIsChatMuted(peer: Peer?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool {
func isPeerMuted(peer: Peer?, peerNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool {
func peerInfoIsChatMuted(peer: EnginePeer?, peerNotificationSettings: TelegramPeerNotificationSettings?, threadNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool {
func isPeerMuted(peer: EnginePeer?, peerNotificationSettings: TelegramPeerNotificationSettings?, globalNotificationSettings: EngineGlobalNotificationSettings?) -> Bool {
var peerIsMuted = false
if let peerNotificationSettings {
if case .muted = peerNotificationSettings.muteState {
peerIsMuted = true
} else if case .default = peerNotificationSettings.muteState, let globalNotificationSettings {
if let peer {
if peer is TelegramUser {
if case .user = peer {
peerIsMuted = !globalNotificationSettings.privateChats.enabled
} else if peer is TelegramGroup {
} else if case .legacyGroup = peer {
peerIsMuted = !globalNotificationSettings.groupChats.enabled
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
switch channel.info {
case .group:
peerIsMuted = !globalNotificationSettings.groupChats.enabled

View file

@ -63,7 +63,7 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
return
}
let canEdit = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
let canEdit = canEditPeerInfo(context: self.context, peer: EnginePeer(peer), chatLocation: chatLocation, threadData: threadData)
let previousItem = self.item
var item = item

View file

@ -82,7 +82,7 @@ final class PeerInfoEditingAvatarOverlayNode: ASDisplayNode {
isPersonal = true
}
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
if canEditPeerInfo(context: self.context, peer: EnginePeer(peer), chatLocation: chatLocation, threadData: threadData)
|| isPersonal
|| self.currentRepresentation != nil && updatingAvatar == nil {
var overlayHidden = true

View file

@ -56,7 +56,7 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
var contentHeight: CGFloat = statusBarHeight + 10.0 + avatarSize + 28.0
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
if canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData) {
if self.avatarButtonNode.supernode == nil {
self.addSubnode(self.avatarButtonNode)
}
@ -85,12 +85,12 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
}
} else if let _ = peer as? TelegramGroup {
fieldKeys.append(.title)
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
if canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData) {
fieldKeys.append(.description)
}
} else if let _ = peer as? TelegramChannel {
fieldKeys.append(.title)
if canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) {
if canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData) {
fieldKeys.append(.description)
}
}
@ -156,10 +156,10 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
} else {
placeholder = presentationData.strings.GroupInfo_GroupNamePlaceholder
}
isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData)
isEnabled = canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData)
case .description:
placeholder = presentationData.strings.Channel_Edit_AboutItem
isEnabled = canEditPeerInfo(context: self.context, peer: peer, chatLocation: chatLocation, threadData: threadData) || isEditableBot
isEnabled = canEditPeerInfo(context: self.context, peer: peer.flatMap(EnginePeer.init), chatLocation: chatLocation, threadData: threadData) || isEditableBot
}
let itemHeight = itemNode.update(width: width, safeInset: safeInset, isSettings: isSettings, hasPrevious: hasPrevious, hasNext: key != fieldKeys.last, placeholder: placeholder, isEnabled: isEnabled, presentationData: presentationData, updateText: updateText)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: width, height: itemHeight)))

View file

@ -545,8 +545,8 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
}
let actionButtonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderActionButtons(peer: peer, isSecretChat: isSecretChat, isContact: isContact)
let buttonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderButtons(peer: peer, cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact, threadInfo: threadData?.info)
let actionButtonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderActionButtons(peer: peer.flatMap(EnginePeer.init), isSecretChat: isSecretChat, isContact: isContact)
let buttonKeys: [PeerInfoHeaderButtonKey] = (self.isSettings || self.isMyProfile) ? [] : peerInfoHeaderButtons(peer: peer.flatMap(EnginePeer.init), cachedData: cachedData, isOpenedFromChat: self.isOpenedFromChat, isExpanded: true, videoCallsEnabled: width > 320.0 && self.videoCallsEnabled, isSecretChat: isSecretChat, isContact: isContact, threadInfo: threadData?.info)
let backgroundCoverSubject: PeerInfoCoverComponent.Subject?
var backgroundCoverAnimateIn = false
@ -2358,7 +2358,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
buttonIcon = .voiceChat
case .mute:
let chatIsMuted = peerInfoIsChatMuted(peer: peer, peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings)
let chatIsMuted = peerInfoIsChatMuted(peer: peer.flatMap(EnginePeer.init), peerNotificationSettings: peerNotificationSettings, threadNotificationSettings: threadNotificationSettings, globalNotificationSettings: globalNotificationSettings)
if chatIsMuted {
buttonText = presentationData.strings.PeerInfo_ButtonUnmute
buttonIcon = .unmute

View file

@ -30,7 +30,7 @@ enum PeerInfoMember: Equatable {
var peer: Peer {
switch self {
case let .channelMember(participant, _):
return participant.peer
return participant.peer._asPeer()
case let .legacyGroupMember(peer, _, _, _, _, _):
return peer.peers[peer.peerId]!
case let .account(peer):

View file

@ -462,7 +462,7 @@ private final class PeerInfoPendingPane {
if let cachedUserData = data.cachedData as? CachedUserData, cachedUserData.disallowedGifts == .All {
canGift = false
}
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
if channel.hasPermission(.sendSomething) {
canManage = true
}
@ -476,7 +476,7 @@ private final class PeerInfoPendingPane {
if let peer = data.peer {
if peer.id == context.account.peerId {
canManage = true
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
if channel.hasPermission(.editStories) {
canManage = true
}
@ -493,7 +493,7 @@ private final class PeerInfoPendingPane {
scope = .botPreview(id: peerId)
if let peer = data.peer {
if let user = peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
if case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
canManage = true
}
}
@ -1249,7 +1249,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
if let peer = data?.peer {
if peer.id == self.context.account.peerId {
canManageTabs = true
} else if let channel = data?.peer as? TelegramChannel, case .broadcast = channel.info {
} else if case let .channel(channel) = data?.peer, case .broadcast = channel.info {
if channel.hasPermission(.changeInfo) {
canManageTabs = true
}

View file

@ -77,7 +77,7 @@ func infoItems(
interaction.openBirthdayContextMenu(node, gesture)
}
if let user = data.peer as? TelegramUser {
if case let .user(user) = data.peer {
let ItemCallList = 1000
let ItemPersonalChannelHeader = 2000
let ItemPersonalChannel = 2001
@ -529,7 +529,7 @@ func infoItems(
}
}
}
} else if let channel = data.peer as? TelegramChannel {
} else if case let .channel(channel) = data.peer {
let ItemUsername = 1
let ItemUsernameInfo = 2
let ItemAbout = 3
@ -795,7 +795,7 @@ func infoItems(
}
}
}
} else if let group = data.peer as? TelegramGroup {
} else if case let .legacyGroup(group) = data.peer {
if let cachedData = data.cachedData as? CachedGroupData {
let aboutText: String?
if group.isFake {
@ -820,7 +820,7 @@ func infoItems(
if let peer = data.peer, let members = data.members, case let .shortList(_, memberList) = members {
var canAddMembers = false
if let group = data.peer as? TelegramGroup {
if case let .legacyGroup(group) = data.peer {
switch group.role {
case .admin, .creator:
canAddMembers = true
@ -830,7 +830,7 @@ func infoItems(
if !group.hasBannedPermission(.banAddMembers) {
canAddMembers = true
}
} else if let channel = data.peer as? TelegramChannel {
} else if case let .channel(channel) = data.peer {
switch channel.info {
case .broadcast:
break
@ -849,7 +849,7 @@ func infoItems(
for member in memberList {
let isAccountPeer = member.id == context.account.peerId
items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer, member: member, isAccount: false, action: isAccountPeer ? { _ in
items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer._asPeer(), member: member, isAccount: false, action: isAccountPeer ? { _ in
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: peer, member: member)
if actions.contains(.editRank) {
interaction.performMemberAction(member, .editRank)
@ -903,7 +903,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
}
if let data = data {
if let user = data.peer as? TelegramUser {
if case let .user(user) = data.peer {
let ItemNote: AnyHashable = AnyHashable("note_edit")
let ItemNoteInfo = 1
@ -1047,7 +1047,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
interaction.requestDeleteContact()
}))
}
} else if let channel = data.peer as? TelegramChannel {
} else if case let .channel(channel) = data.peer {
switch channel.info {
case .broadcast:
let ItemUsername = 1
@ -1591,7 +1591,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
}
}
}
} else if let group = data.peer as? TelegramGroup {
} else if case let .legacyGroup(group) = data.peer {
let ItemUsername = 101
let ItemInviteLinks = 102
let ItemPreHistory = 103

View file

@ -760,14 +760,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
items.append(.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, _ in
c?.dismiss(completion: {
if let strongSelf = self, let currentPeer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
if let channel = currentPeer as? TelegramChannel, channel.isForumOrMonoForum, let threadId = message.threadId {
if case let .channel(channel) = currentPeer, channel.isForumOrMonoForum, let threadId = message.threadId {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .default, animated: true).startStandalone()
} else {
let targetLocation: NavigateToChatControllerParams.Location
if case let .replyThread(message) = strongSelf.chatLocation {
targetLocation = .replyThread(message)
} else {
targetLocation = .peer(EnginePeer(currentPeer))
targetLocation = .peer(currentPeer)
}
let currentPeerId = strongSelf.peerId
@ -927,14 +927,14 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
items.append(.action(ContextMenuActionItem(text: strings.SharedMedia_ViewInChat, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/GoToMessage"), color: theme.contextMenu.primaryColor) }, action: { c, f in
c?.dismiss(completion: {
if let strongSelf = self, let currentPeer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
if let channel = currentPeer as? TelegramChannel, channel.isForumOrMonoForum, let threadId = message.threadId {
if case let .channel(channel) = currentPeer, channel.isForumOrMonoForum, let threadId = message.threadId {
let _ = strongSelf.context.sharedContext.navigateToForumThread(context: strongSelf.context, peerId: currentPeer.id, threadId: threadId, messageId: message.id, navigationController: navigationController, activateInput: nil, scrollToEndIfExists: false, keepStack: .default, animated: true).startStandalone()
} else {
let targetLocation: NavigateToChatControllerParams.Location
if case let .replyThread(message) = strongSelf.chatLocation {
targetLocation = .replyThread(message)
} else {
targetLocation = .peer(EnginePeer(currentPeer))
targetLocation = .peer(currentPeer)
}
let currentPeerId = strongSelf.peerId
@ -1558,7 +1558,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
strongSelf.openAvatarGallery(peer: EnginePeer(peer), entries: entries, centralEntry: centralEntry, animateTransition: true)
strongSelf.openAvatarGallery(peer: peer, entries: entries, centralEntry: centralEntry, animateTransition: true)
Queue.mainQueue().after(0.4) {
strongSelf.resetHeaderExpansion()
@ -1656,7 +1656,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil)
return
}
if let peer = data.peer as? TelegramUser {
if case let .user(peer) = data.peer {
if strongSelf.isSettings || strongSelf.isMyProfile, let cachedData = data.cachedData as? CachedUserData {
let firstName = strongSelf.headerNode.editingContentNode.editingTextForKey(.firstName) ?? ""
let lastName = strongSelf.headerNode.editingContentNode.editingTextForKey(.lastName) ?? ""
@ -1902,7 +1902,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} else {
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil)
}
} else if let group = data.peer as? TelegramGroup, canEditPeerInfo(context: strongSelf.context, peer: group, chatLocation: chatLocation, threadData: data.threadData) {
} else if case let .legacyGroup(group) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: chatLocation, threadData: data.threadData) {
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
@ -1958,7 +1958,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
strongSelf.headerNode.navigationButtonContainer.performAction?(.cancel, nil, nil)
}))
}
} else if let channel = data.peer as? TelegramChannel, canEditPeerInfo(context: strongSelf.context, peer: channel, chatLocation: strongSelf.chatLocation, threadData: data.threadData) {
} else if case let .channel(channel) = data.peer, canEditPeerInfo(context: strongSelf.context, peer: data.peer, chatLocation: strongSelf.chatLocation, threadData: data.threadData) {
let title = strongSelf.headerNode.editingContentNode.editingTextForKey(.title) ?? ""
let description = strongSelf.headerNode.editingContentNode.editingTextForKey(.description) ?? ""
@ -2129,7 +2129,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.headerNode.displayCopyContextMenu = { [weak self] node, copyPhone, copyUsername in
guard let strongSelf = self, let data = strongSelf.data, let user = data.peer as? TelegramUser else {
guard let strongSelf = self, let data = strongSelf.data, case let .user(user) = data.peer else {
return
}
var actions: [ContextMenuAction] = []
@ -2350,7 +2350,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}))
]
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: EnginePeer(peer), remoteEntries: nil, replaceRootController: { controller, ready in
let galleryController = AvatarGalleryController(context: strongSelf.context, peer: peer, remoteEntries: nil, replaceRootController: { controller, ready in
}, synchronousLoad: true)
galleryController.setHintWillBePresentedInPreviewingContext(true)
@ -2402,9 +2402,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let self, let navigationController = self.controller?.navigationController as? NavigationController, let peer = self.data?.peer else {
return
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer))))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer)))
}
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peerId.namespace) {
self.displayAsPeersPromise.set(context.engine.calls.cachedGroupCallDisplayAsAvailablePeers(peerId: peerId))
}
@ -2717,7 +2717,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
if let channel = data.peer as? TelegramChannel, channel.isForumOrMonoForum, self.chatLocation.threadId == nil {
if case let .channel(channel) = data.peer, channel.isForumOrMonoForum, self.chatLocation.threadId == nil {
if self.forumTopicNotificationExceptionsDisposable == nil {
self.forumTopicNotificationExceptionsDisposable = (self.context.engine.peers.forumChannelTopicNotificationExceptions(id: channel.id)
|> deliverOnMainQueue).startStrict(next: { [weak self] list in
@ -2766,10 +2766,10 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
var previousPhotoIsPersonal: Bool?
var currentPhotoIsPersonal: Bool?
if let previousUser = previousData?.peer as? TelegramUser {
if case let .user(previousUser) = previousData?.peer {
previousPhotoIsPersonal = previousUser.profileImageRepresentations.first?.isPersonal == true
}
if let user = data.peer as? TelegramUser {
if case let .user(user) = data.peer {
currentPhotoIsPersonal = user.profileImageRepresentations.first?.isPersonal == true
}
@ -3406,7 +3406,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|> deliverOnMainQueue).startStrict(completed: { [weak self] in
if let strongSelf = self, let peer = strongSelf.data?.peer {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(EnginePeer(peer).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })
let controller = UndoOverlayController(presentationData: presentationData, content: .info(title: nil, text: presentationData.strings.Conversation_DeletedFromContacts(peer.displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string, timeout: nil, customUndoText: nil), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })
controller.keepOnParentDismissal = true
strongSelf.controller?.present(controller, in: .window(.root))
@ -3521,7 +3521,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
self.view.endEditing(true)
let statsController: ViewController
if let channel = peer as? TelegramChannel, case .group = channel.info {
if case let .channel(channel) = peer, case .group = channel.info {
if case .monetization = section {
statsController = channelStatsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: peer.id, section: section, existingStarsRevenueContext: data.starsRevenueStatsContext, boostStatus: boostStatus)
} else {
@ -3534,7 +3534,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
func openBoost() {
guard let peer = self.data?.peer, let channel = peer as? TelegramChannel, let controller = self.controller else {
guard case let .channel(channel) = self.data?.peer, let controller = self.controller else {
return
}
@ -3621,7 +3621,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let data = self.data, let peer = data.peer, let encryptionKeyFingerprint = data.encryptionKeyFingerprint else {
return
}
self.controller?.push(SecretChatKeyController(context: self.context, fingerprint: encryptionKeyFingerprint, peer: EnginePeer(peer)))
self.controller?.push(SecretChatKeyController(context: self.context, fingerprint: encryptionKeyFingerprint, peer: peer))
}
func openShareLink(url: String) {
@ -3744,7 +3744,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let _ = enqueueMessages(account: strongSelf.context.account, peerId: peer.id, messages: [.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]).startStandalone()
if let peer = strongSelf.data?.peer, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(EnginePeer(peer))))
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer)))
}
})
}
@ -3768,7 +3768,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
private func editingOpenPublicLinkSetup() {
if let peer = self.data?.peer as? TelegramUser, peer.botInfo != nil {
if case let .user(peer) = self.data?.peer, peer.botInfo != nil {
let controller = usernameSetupController(context: self.context, mode: .bot(self.peerId))
self.controller?.push(controller)
} else {
@ -3788,7 +3788,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
private func editingOpenAffiliateProgram() {
if let peer = self.data?.peer as? TelegramUser, let botInfo = peer.botInfo {
if case let .user(peer) = self.data?.peer, let botInfo = peer.botInfo {
if botInfo.flags.contains(.canEdit) {
let _ = (self.context.sharedContext.makeAffiliateProgramSetupScreenInitialData(context: self.context, peerId: peer.id, mode: .editProgram)
|> deliverOnMainQueue).startStandalone(next: { [weak self] initialData in
@ -3978,7 +3978,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
if self.peerId == self.context.account.peerId {
controller.push(UserAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData))
} else if let peer = self.data?.peer, peer is TelegramChannel {
} else if case .channel = self.data?.peer {
controller.push(ChannelAppearanceScreen(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId, boostStatus: self.boostStatus))
}
}
@ -4076,7 +4076,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let data = self.data, let peer = data.peer else {
return
}
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = peer, case .broadcast = channel.info {
let subscription = Promise<PeerAllowedReactionsScreen.Content>()
subscription.set(PeerAllowedReactionsScreen.content(context: self.context, peerId: peer.id))
let _ = (subscription.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak self] content in
@ -4130,7 +4130,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let data = self.data, let peer = data.peer else {
return
}
if peer is TelegramGroup {
if case .legacyGroup = peer {
if isEnabled {
let context = self.context
let signal: Signal<EnginePeer.Id?, NoError> = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId, additionalProcessing: { upgradedPeerId -> Signal<Never, NoError> in
@ -4189,9 +4189,9 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .members:
self.controller?.push(channelMembersController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId))
case .admins:
if peer is TelegramGroup {
if case .legacyGroup = peer {
self.controller?.push(channelAdminsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId))
} else if peer is TelegramChannel {
} else if case .channel = peer {
self.controller?.push(channelAdminsController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, peerId: self.peerId))
}
case .banned:
@ -4253,7 +4253,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let context = self.context
let presentationData = self.presentationData
let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)
let map = TelegramMediaMap(latitude: location.latitude, longitude: location.longitude, heading: nil, accuracyRadius: nil, venue: MapVenue(title: peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder), address: location.address, provider: nil, id: nil, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
}, stopLiveLocation: { _ in
@ -4262,7 +4262,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, openPeer: { _ in
}, showAll: false)
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer, text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peer.id, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: peer._asPeer(), text: "", attributes: [], media: [map], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
let controller = LocationViewController(context: context, updatedPresentationData: self.controller?.updatedPresentationData, subject: EngineMessage(message), params: controllerParams)
self.controller?.push(controller)
@ -4303,7 +4303,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
private func openPeerInfo(peer: Peer, isMember: Bool) {
let mode: PeerInfoControllerMode = .generic
if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: peer, mode: mode, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = self.context.sharedContext.makePeerInfoController(context: self.context, updatedPresentationData: nil, peer: EnginePeer(peer), mode: mode, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
(self.controller?.navigationController as? NavigationController)?.pushViewController(infoController)
}
}
@ -4603,7 +4603,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return
}
presentAddMembersImpl(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, parentController: controller, groupPeer: groupPeer, selectAddMemberDisposable: self.selectAddMemberDisposable, addMemberDisposable: self.addMemberDisposable)
presentAddMembersImpl(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, parentController: controller, groupPeer: groupPeer._asPeer(), selectAddMemberDisposable: self.selectAddMemberDisposable, addMemberDisposable: self.addMemberDisposable)
}
func openQrCode() {
@ -4707,7 +4707,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
self.postingAvailabilityDisposable?.dispose()
if let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo {
if let data = self.data, case let .user(user) = data.peer, let botInfo = user.botInfo {
if !botInfo.flags.contains(.canEdit) {
return
}
@ -4826,7 +4826,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
return nil
}
if let data = self.data, let user = data.peer as? TelegramUser, let _ = user.botInfo {
if let data = self.data, case let .user(user) = data.peer, let _ = user.botInfo {
if let pane = self.paneContainerNode.currentPane?.node as? PeerInfoStoryPaneNode, let transitionView = pane.extractPendingStoryTransitionView() {
return StoryCameraTransitionOut(
destinationView: transitionView,
@ -5199,7 +5199,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
navigationController: navigationController,
chatController: nil,
context: strongSelf.context,
chatLocation: .peer(EnginePeer(peer)),
chatLocation: .peer(peer),
subject: .message(id: .id(index.id), highlight: nil, timecode: nil, setupReply: false),
botStart: nil,
updateTextInputState: nil,
@ -5295,7 +5295,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
guard let peer = self.data?.peer else {
return
}
let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.UserInfo_ResetToOriginalAlertText(EnginePeer(peer).compactDisplayTitle).string, actions: [
let alertController = textAlertController(context: self.context, updatedPresentationData: self.controller?.updatedPresentationData, title: nil, text: self.presentationData.strings.UserInfo_ResetToOriginalAlertText(peer.compactDisplayTitle).string, actions: [
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
}),
@ -5396,7 +5396,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
let headerInset = sectionInset
let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, screenData: self.data, isSearching: self.searchDisplayController != nil, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil)
let headerHeight = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : self.scrollNode.view.contentOffset.y, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer?._asPeer(), cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, screenData: self.data, isSearching: self.searchDisplayController != nil, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: transition.isAnimated && self.headerNode.navigationTransition == nil)
let headerFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: CGSize(width: layout.size.width, height: layout.size.height))
if additive {
@ -5635,13 +5635,13 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
let type: PeerType
if isBot {
type = .bot
} else if let user = peer as? TelegramUser {
} else if case let .user(user) = peer {
if user.botInfo != nil && !user.id.isVerificationCodes {
type = .bot
} else {
type = .user
}
} else if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
type = .channel
} else {
type = .group
@ -5802,7 +5802,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
let headerInset = sectionInset
let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer, cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, screenData: self.data, isSearching: self.searchDisplayController != nil, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil)
let _ = self.headerNode.update(width: layout.size.width, containerHeight: layout.size.height, containerInset: headerInset, statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: navigationHeight, isModalOverlay: layout.isModalOverlay, isMediaOnly: self.isMediaOnly, contentOffset: self.isMediaOnly ? 212.0 : offsetY, paneContainerY: self.paneContainerNode.frame.minY, presentationData: self.presentationData, peer: self.data?.savedMessagesPeer ?? self.data?.peer?._asPeer(), cachedData: self.data?.cachedData, threadData: self.data?.threadData, peerNotificationSettings: self.data?.peerNotificationSettings, threadNotificationSettings: self.data?.threadNotificationSettings, globalNotificationSettings: self.data?.globalNotificationSettings, statusData: self.data?.status, panelStatusData: self.customStatusData, isSecretChat: self.peerId.namespace == Namespaces.Peer.SecretChat, isContact: self.data?.isContact ?? false, isSettings: self.isSettings, state: self.state, profileGiftsContext: self.data?.profileGiftsContext, screenData: self.data, isSearching: self.searchDisplayController != nil, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, transition: self.headerNode.navigationTransition == nil ? transition : .immediate, additive: additive, animateHeader: animateHeader && self.headerNode.navigationTransition == nil)
}
let paneAreaExpansionDistance: CGFloat = 32.0
@ -5857,7 +5857,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
} else if peerInfoCanEdit(peer: self.data?.peer, chatLocation: self.chatLocation, threadData: self.data?.threadData, cachedData: self.data?.cachedData, isContact: self.data?.isContact) {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .edit, isForExpandedView: false))
}
if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, let channel = data.peer as? TelegramChannel, channel.hasPermission(.postStories) {
if let data = self.data, !data.isPremiumRequiredForStoryPosting || data.accountIsPremium, case let .channel(channel) = data.peer, channel.hasPermission(.postStories) {
rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0)
} else if self.isMyProfile {
rightNavigationButtons.insert(PeerInfoHeaderNavigationButtonSpec(key: .postStory, isForExpandedView: false), at: 0)
@ -5884,7 +5884,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
case .media:
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true))
case .botPreview:
if let data = self.data, data.hasBotPreviewItems, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
if let data = self.data, data.hasBotPreviewItems, case let .user(user) = data.peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) {
rightNavigationButtons.append(PeerInfoHeaderNavigationButtonSpec(key: .more, isForExpandedView: true))
}
case .stories:
@ -6912,7 +6912,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: context, chatLocation: .peer(peer), subject: subject, keepStack: .always, peekData: peekData))
case .info:
if peer.restrictionText(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) == nil {
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(infoController)
}
}
@ -7215,7 +7215,7 @@ public final class PeerInfoScreenImpl: ViewController, PeerInfoScreen, KeyShortc
}) {
let _ = navigationController.popToViewController(infoController, animated: false)
} else {
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.replaceController(sourceController, with: infoController, animated: false)
}
}

View file

@ -439,7 +439,7 @@ extension PeerInfoScreenImpl {
let peerId = self.peerId
var isForum = false
if let peer = peer as? TelegramChannel, peer.isForumOrMonoForum {
if case let .channel(peer) = peer, peer.isForumOrMonoForum {
isForum = true
}

View file

@ -45,7 +45,7 @@ extension PeerInfoScreenNode {
return
}
guard let peer = self.data?.peer as? TelegramUser, let cachedUserData = self.data?.cachedData as? CachedUserData else {
guard case let .user(peer) = self.data?.peer, let cachedUserData = self.data?.cachedData as? CachedUserData else {
return
}
if cachedUserData.callsPrivate {
@ -128,7 +128,7 @@ extension PeerInfoScreenNode {
case .generic, .scheduledTooLate:
text = strongSelf.presentationData.strings.Login_UnknownError
case .anonymousNotAllowed:
if let channel = strongSelf.data?.peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = strongSelf.data?.peer, case .broadcast = channel.info {
text = strongSelf.presentationData.strings.LiveStream_AnonymousDisabledAlertText
} else {
text = strongSelf.presentationData.strings.VoiceChat_AnonymousDisabledAlertText
@ -304,7 +304,7 @@ extension PeerInfoScreenNode {
let createVoiceChatTitle: String
let scheduleVoiceChatTitle: String
if let channel = strongSelf.data?.peer as? TelegramChannel, case .broadcast = channel.info {
if case let .channel(channel) = strongSelf.data?.peer, case .broadcast = channel.info {
createVoiceChatTitle = strongSelf.presentationData.strings.ChannelInfo_CreateLiveStream
scheduleVoiceChatTitle = strongSelf.presentationData.strings.ChannelInfo_ScheduleLiveStream
} else {
@ -327,11 +327,11 @@ extension PeerInfoScreenNode {
var credentialsPromise: Promise<GroupCallStreamCredentials>?
var canCreateStream = false
switch chatPeer {
case let group as TelegramGroup:
case let .legacyGroup(group):
if case .creator = group.role {
canCreateStream = true
}
case let channel as TelegramChannel:
case let .channel(channel):
if channel.hasPermission(.manageCalls) {
canCreateStream = true
credentialsPromise = Promise()

View file

@ -26,7 +26,7 @@ extension PeerInfoScreenNode {
let giftsContext = pane.giftsContext
var hasVisibility = false
if let channel = data.peer as? TelegramChannel, channel.hasPermission(.sendSomething) {
if case let .channel(channel) = data.peer, channel.hasPermission(.sendSomething) {
hasVisibility = true
} else if data.peer?.id == self.context.account.peerId {
hasVisibility = true

View file

@ -30,7 +30,7 @@ extension PeerInfoScreenNode {
}
if case .botPreview = pane.scope {
guard let data = self.data, let user = data.peer as? TelegramUser, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else {
guard let data = self.data, case let .user(user) = data.peer, let botInfo = user.botInfo, botInfo.flags.contains(.canEdit) else {
return
}
@ -141,7 +141,7 @@ extension PeerInfoScreenNode {
self.mediaGalleryContextMenu = contextController
controller.presentInGlobalOverlay(contextController)
} else if case .peer = pane.scope {
guard let data = self.data, let user = data.peer as? TelegramUser else {
guard let data = self.data, case let .user(user) = data.peer else {
return
}
let _ = user

View file

@ -19,9 +19,9 @@ extension PeerInfoScreenNode {
var items: [ActionSheetItem] = []
var personalPeerName: String?
var isChannel = false
if let user = peer as? TelegramUser {
personalPeerName = EnginePeer(user).compactDisplayTitle
} else if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
if case .user = peer {
personalPeerName = peer.compactDisplayTitle
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
isChannel = true
}

View file

@ -53,7 +53,7 @@ extension PeerInfoScreenNode {
UIPasteboard.general.string = bioText
let toastText: String
if let _ = self.data?.peer as? TelegramUser {
if case .user = self.data?.peer {
toastText = self.presentationData.strings.MyProfile_ToastBioCopied
} else {
toastText = self.presentationData.strings.ChannelProfile_ToastAboutCopied
@ -87,7 +87,7 @@ extension PeerInfoScreenNode {
}
let copyText: String
if let _ = self.data?.peer as? TelegramUser {
if case .user = self.data?.peer {
copyText = self.presentationData.strings.MyProfile_BioActionCopy
} else {
copyText = self.presentationData.strings.ChannelProfile_AboutActionCopy

View file

@ -29,7 +29,7 @@ extension PeerInfoScreenNode {
func openChatForReporting(title: String, option: Data, message: String?) {
if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) {
if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
//let _ = self.context.engine.peers.reportPeer(peerId: peer.id, reason: reason, message: "").startStandalone()
//self.controller?.present(UndoOverlayController(presentationData: self.presentationData, content: .emoji(name: "PoliceCar", text: self.presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current)
} else {
@ -37,7 +37,7 @@ extension PeerInfoScreenNode {
NavigateToChatControllerParams(
navigationController: navigationController,
context: self.context,
chatLocation: .peer(EnginePeer(peer)),
chatLocation: .peer(peer),
keepStack: .default,
reportReason: NavigateToChatControllerParams.ReportReason(title: title, option: option, message: message)
)
@ -48,13 +48,13 @@ extension PeerInfoScreenNode {
func openChatForThemeChange() {
if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, changeColors: true))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, changeColors: true))
}
}
func openChatForTranslation() {
if let peer = self.data?.peer, let navigationController = (self.controller?.navigationController as? NavigationController) {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, changeColors: false))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, changeColors: false))
}
}
@ -77,7 +77,7 @@ extension PeerInfoScreenNode {
}
if let peer = self.data?.peer, let navigationController = self.controller?.navigationController as? NavigationController {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default))
}
}
@ -92,7 +92,7 @@ extension PeerInfoScreenNode {
}
func openChannelMessages() {
guard let channel = self.data?.peer as? TelegramChannel, let linkedMonoforumId = channel.linkedMonoforumId else {
guard case let .channel(channel) = self.data?.peer, let linkedMonoforumId = channel.linkedMonoforumId else {
return
}
let _ = (self.context.engine.data.get(

View file

@ -50,7 +50,7 @@ extension PeerInfoScreenNode {
})))
}
if actions.contains(.promote) && enclosingPeer is TelegramChannel {
if actions.contains(.promote), case .channel = enclosingPeer {
var actionTitle: String = self.presentationData.strings.GroupInfo_ActionPromote
if case .admin = member.role {
actionTitle = self.presentationData.strings.GroupInfo_ActionEditAdmin
@ -66,7 +66,7 @@ extension PeerInfoScreenNode {
}
if actions.contains(.restrict) {
if enclosingPeer is TelegramChannel {
if case .channel = enclosingPeer {
items.append(.action(ContextMenuActionItem(text: self.presentationData.strings.GroupInfo_ActionRestrict, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Restrict"), color: theme.contextMenu.primaryColor) }, action: { [weak self] c, _ in
c?.dismiss {
guard let self else {

View file

@ -122,7 +122,7 @@ extension PeerInfoScreenNode {
text = customLink
content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied)
} else if let addressName = peer.addressName {
if peer is TelegramChannel {
if case .channel = peer {
text = "https://t.me/\(addressName)"
content = .linkCopied(title: nil, text: self.presentationData.strings.Conversation_LinkCopied)
} else {

View file

@ -25,7 +25,7 @@ extension PeerInfoScreenNode {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer), subject: subject, updateTextInputState: inputState, activateInput: inputState != nil ? .text : nil, keepStack: .always, peekData: peekData))
case .info:
if let strongSelf = self, peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let infoController = strongSelf.context.sharedContext.makePeerInfoController(context: strongSelf.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
strongSelf.controller?.push(infoController)
}
}

View file

@ -22,20 +22,20 @@ extension PeerInfoScreenNode {
switch key {
case .message:
if let navigationController = controller.navigationController as? NavigationController, let peer = self.data?.peer {
if let channel = peer as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId {
if case let .channel(channel) = peer, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId {
Task { @MainActor [weak self] in
guard let self else {
return
}
guard let peer = await self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: linkedMonoforumId)).get() else {
return
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default))
}
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default))
}
}
case .discussion:
@ -64,7 +64,7 @@ extension PeerInfoScreenNode {
} else {
displayCustomNotificationSettings = true
}
if self.data?.threadData == nil, let channel = self.data?.peer as? TelegramChannel, channel.isForumOrMonoForum {
if self.data?.threadData == nil, case let .channel(channel) = self.data?.peer, channel.isForumOrMonoForum {
displayCustomNotificationSettings = true
}
@ -222,13 +222,13 @@ extension PeerInfoScreenNode {
let mode: NotificationExceptionMode
let defaultSound: PeerMessageSound
if let _ = peer as? TelegramUser {
if case .user = peer {
mode = .users([:])
defaultSound = globalSettings.privateChats.sound._asMessageSound()
} else if let _ = peer as? TelegramSecretChat {
} else if case .secretChat = peer {
mode = .users([:])
defaultSound = globalSettings.privateChats.sound._asMessageSound()
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
if case .broadcast = channel.info {
mode = .channels([:])
defaultSound = globalSettings.channels.sound._asMessageSound()
@ -244,7 +244,7 @@ extension PeerInfoScreenNode {
let canRemove = false
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: EnginePeer(peer), threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: globalSettings.privateChats.storySettings.sound, edit: true, updatePeerSound: { peerId, sound in
let exceptionController = notificationPeerExceptionController(context: context, updatedPresentationData: strongSelf.controller?.updatedPresentationData, peer: peer, threadId: threadId, isStories: nil, canRemove: canRemove, defaultSound: defaultSound, defaultStoriesSound: globalSettings.privateChats.storySettings.sound, edit: true, updatePeerSound: { peerId, sound in
let _ = (updatePeerSound(peer.id, sound)
|> deliverOnMainQueue).startStandalone(next: { _ in
})
@ -477,7 +477,7 @@ extension PeerInfoScreenNode {
})))
}
if let user = peer as? TelegramUser {
if case let .user(user) = peer {
if user.botInfo == nil && strongSelf.data?.encryptionKeyFingerprint == nil && !user.isDeleted {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.UserInfo_ChangeWallpaper, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/ApplyTheme"), color: theme.contextMenu.primaryColor)
@ -564,7 +564,7 @@ extension PeerInfoScreenNode {
}
}
if user.botInfo == nil && data.isContact, let peer = strongSelf.data?.peer as? TelegramUser, let phone = peer.phone {
if user.botInfo == nil && data.isContact, case let .user(peer) = strongSelf.data?.peer, let phone = peer.phone {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_ShareContactButton, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
@ -771,7 +771,7 @@ extension PeerInfoScreenNode {
})))
}
if let user = data.peer as? TelegramUser, let cachedData = data.cachedData as? CachedUserData, user.botInfo == nil && !user.flags.contains(.isSupport) && user.id != strongSelf.context.account.peerId && strongSelf.peerId.namespace != Namespaces.Peer.SecretChat {
if case let .user(user) = data.peer, let cachedData = data.cachedData as? CachedUserData, user.botInfo == nil && !user.flags.contains(.isSupport) && user.id != strongSelf.context.account.peerId && strongSelf.peerId.namespace != Namespaces.Peer.SecretChat {
let copyProtectionEnabled = cachedData.flags.contains(.myCopyProtectionEnabled) || cachedData.flags.contains(.copyProtectionEnabled)
items.append(.action(ContextMenuActionItem(text: !copyProtectionEnabled ? strongSelf.presentationData.strings.PeerInfo_DisableSharing : strongSelf.presentationData.strings.PeerInfo_EnableSharing, icon: { theme in
generateTintedImage(image: UIImage(bundleImageName: !copyProtectionEnabled ? "Chat/Context Menu/ForwardDisable" : "Chat/Context Menu/ForwardEnable"), color: theme.contextMenu.primaryColor)
@ -801,7 +801,7 @@ extension PeerInfoScreenNode {
}
let _ = self.context.engine.peers.toggleMessageCopyProtection(peerId: user.id, enabled: true).start()
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, completion: { _ in }))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, completion: { _ in }))
}
let _ = (ApplicationSpecificNotice.getCopyProtectionTips(accountManager: self.context.sharedContext.accountManager)
|> deliverOnMainQueue).start(next: { [weak self] count in
@ -827,7 +827,7 @@ extension PeerInfoScreenNode {
let action = {
let _ = self.context.engine.peers.toggleMessageCopyProtection(peerId: user.id, enabled: false).start()
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(EnginePeer(peer)), keepStack: .default, scrollToEndIfExists: true, completion: { _ in }))
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: .peer(peer), keepStack: .default, scrollToEndIfExists: true, completion: { _ in }))
}
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
@ -835,7 +835,7 @@ extension PeerInfoScreenNode {
if let cachedUserData = self.data?.cachedData as? CachedUserData, !cachedUserData.flags.contains(.copyProtectionEnabled), let date = cachedUserData.myCopyProtectionEnableDate, currentTime < date + timeout {
action()
} else {
let peerName = self.data?.peer.flatMap(EnginePeer.init)?.compactDisplayTitle ?? ""
let peerName = self.data?.peer?.compactDisplayTitle ?? ""
let alertController = AlertScreen(context: self.context, configuration: .init(actionAlignment: .vertical), title: self.presentationData.strings.EnableSharing_Title, text: self.presentationData.strings.EnableSharing_Text(peerName).string, actions: [
.init(title: self.presentationData.strings.EnableSharing_SendRequest, type: .default, action: {
action()
@ -890,7 +890,7 @@ extension PeerInfoScreenNode {
if finalItemsCount > itemsCount {
items.insert(.separator, at: itemsCount)
}
} else if let channel = peer as? TelegramChannel {
} else if case let .channel(channel) = peer {
if let cachedData = strongSelf.data?.cachedData as? CachedChannelData {
if case .broadcast = channel.info, cachedData.flags.contains(.starGiftsAvailable) {
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Profile_SendGift, badge: nil, icon: { theme in
@ -1112,7 +1112,7 @@ extension PeerInfoScreenNode {
}
}
}
} else if let group = peer as? TelegramGroup {
} else if case let .legacyGroup(group) = peer {
if canSetupAutoremoveTimeout {
let strings = strongSelf.presentationData.strings
items.append(.action(ContextMenuActionItem(text: currentAutoremoveTimeout == nil ? strongSelf.presentationData.strings.PeerInfo_EnableAutoDelete : strongSelf.presentationData.strings.PeerInfo_AdjustAutoDelete, icon: { theme in

View file

@ -197,7 +197,7 @@ extension PeerInfoScreenNode {
guard let controller = self.controller, !controller.presentAccountFrozenInfoIfNeeded() else {
return
}
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone {
if case let .user(user) = self.data?.peer, let phoneNumber = user.phone {
let introController = PrivacyIntroController(context: self.context, mode: .changePhoneNumber(phoneNumber), proceedAction: { [weak self] in
if let strongSelf = self, let navigationController = strongSelf.controller?.navigationController as? NavigationController {
navigationController.replaceTopController(ChangePhoneNumberController(context: strongSelf.context), animated: true)
@ -250,7 +250,7 @@ extension PeerInfoScreenNode {
}
})
case .logout:
if let user = self.data?.peer as? TelegramUser, let phoneNumber = user.phone {
if case let .user(user) = self.data?.peer, let phoneNumber = user.phone {
if let controller = self.controller, let navigationController = controller.navigationController as? NavigationController {
self.controller?.push(logoutOptionsController(context: self.context, navigationController: navigationController, canAddAccounts: true, phoneNumber: phoneNumber))
}

View file

@ -48,7 +48,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat
var setStatusTitle: String = ""
let displaySetStatus: Bool
var hasEmojiStatus = false
if let peer = data.peer as? TelegramUser, peer.isPremium {
if case let .user(peer) = data.peer, peer.isPremium {
if peer.emojiStatus != nil {
hasEmojiStatus = true
setStatusTitle = presentationData.strings.PeerInfo_ChangeEmojiStatus
@ -86,7 +86,7 @@ func settingsItems(data: PeerInfoScreenData?, context: AccountContext, presentat
items[.phone]!.append(PeerInfoScreenActionItem(id: 1, text: "Restore Subscription", action: {
interaction.openSettings(.premiumManagement)
}))
} else if settings.suggestPhoneNumberConfirmation, let peer = data.peer as? TelegramUser {
} else if settings.suggestPhoneNumberConfirmation, case let .user(peer) = data.peer {
let phoneNumber = formatPhoneNumber(context: context, number: peer.phone ?? "")
items[.phone]!.append(PeerInfoScreenInfoItem(id: 0, title: presentationData.strings.Settings_CheckPhoneNumberTitle(phoneNumber).string, text: .markdown(presentationData.strings.Settings_CheckPhoneNumberText), linkAction: { link in
if case .tap = link {
@ -444,7 +444,7 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
}))
}
if let user = data.peer as? TelegramUser {
if case let .user(user) = data.peer {
items[.info]!.append(PeerInfoScreenDisclosureItem(id: ItemPhoneNumber, label: .text(user.phone.flatMap({ formatPhoneNumber(context: context, number: $0) }) ?? ""), text: presentationData.strings.Settings_PhoneNumber, icon: PresentationResourcesSettings.recentCalls, action: {
interaction.openSettings(.phoneNumber)
}))
@ -457,7 +457,7 @@ func settingsEditingItems(data: PeerInfoScreenData?, state: PeerInfoState, conte
interaction.openSettings(.username)
}))
if let peer = data.peer as? TelegramUser {
if case let .user(peer) = data.peer {
var colors: [PeerNameColors.Colors] = []
if let nameColor = peer.nameColor {
let nameColors: PeerNameColors.Colors

View file

@ -774,10 +774,10 @@ final class ShareWithPeersScreenComponent: Component {
if item.peer.id == context.account.peerId {
continue
}
if let user = item.peer as? TelegramUser, user.botInfo != nil {
if case let .user(user) = item.peer, user.botInfo != nil {
continue
}
peers.append(EnginePeer(item.peer))
peers.append(item.peer)
}
if !list.list.isEmpty {
subscriber.putNext(peers)

View file

@ -555,7 +555,7 @@ public extension ShareWithPeersScreen {
continue
}
peers.append(EnginePeer(participant.peer))
peers.append(participant.peer)
existingPeersIds.insert(participant.peer.id)
}
@ -563,7 +563,7 @@ public extension ShareWithPeersScreen {
if participant.peer.isDeleted || existingPeersIds.contains(participant.peer.id) || participant.participant.adminInfo != nil {
continue
}
if let user = participant.peer as? TelegramUser, user.botInfo != nil {
if case let .user(user) = participant.peer, user.botInfo != nil {
continue
}
@ -573,7 +573,7 @@ public extension ShareWithPeersScreen {
continue
}
peers.append(EnginePeer(participant.peer))
peers.append(participant.peer)
}
let state = State(

View file

@ -1955,7 +1955,7 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
return
}
if isProfile {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) {
navigationController.pushViewController(controller)
}
} else {

View file

@ -2075,7 +2075,7 @@ final class StorageUsageScreenComponent: Component {
let peerInfoController = component.context.sharedContext.makePeerInfoController(
context: component.context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: .generic,
avatarInitiallyExpanded: false,
fromChat: false,

View file

@ -367,7 +367,7 @@ final class StoryContentLiveChatComponent: Component {
rank: nil,
subscriptionUntilDate: nil
),
peer: author._asPeer()
peer: author
)],
mode: .liveStream(
messageCount: 1,

View file

@ -5626,7 +5626,7 @@ public final class StoryItemSetContainerComponent: Component {
}
navigationController.setViewControllers(currentViewControllers, animated: true)
} else {
guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer._asPeer(), mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
guard let chatController = component.context.sharedContext.makePeerInfoController(context: component.context, updatedPresentationData: nil, peer: peer, mode: .generic, avatarInitiallyExpanded: false, fromChat: false, requestsContext: nil) else {
return
}
@ -7616,7 +7616,7 @@ public final class StoryItemSetContainerComponent: Component {
if let controller = component.context.sharedContext.makePeerInfoController(
context: component.context,
updatedPresentationData: nil,
peer: peer._asPeer(),
peer: peer,
mode: .myProfile,
avatarInitiallyExpanded: false,
fromChat: false,

Some files were not shown because too many files have changed in this diff Show more