Postbox -> TelegramEngine waves 46-93 (squashed)

Squash of 63 commits spanning waves 46-93 (plus interspersed docs commits)
of the gradual Postbox->TelegramEngine consumer-side migration.

Scope: 139 files changed, 2123 insertions(+), 452 deletions(-).

## Themes by wave-block

**Waves 46-58 — Peer field migrations + facade additions**
Foundational EnginePeer convenience init additions (PeerReference, RenderedPeer,
SelectivePrivacyPeer). Multiple `peer: Peer` field migrations across PeerInfo,
ChatList, and SettingsUI components.

**Waves 59-73 — peer field cascade + EnginePeer wrap drops**
Series of single- to two-file peer-field migrations; consumer-side wrap
removal (`EnginePeer(peer)` -> direct EnginePeer use); `as? TelegramUser`
cast conversion to `case let .user(...)` enum match. Wave 64: RenderedPeer
convenience init. Wave 68: SelectivePrivacyPeer convenience init.

**Waves 74-83 — controller-Node bridge cleanup + small migrations**
Wave-71 shadow-pattern cleanup at controller->Node bridges. Migrations of
ChatRecentActionsController.peer (74), PeerInfoMember (75), MentionChatInputPanelItem
(76), PassportUI SecureIdAuthController (77), AccountWithInfo + ShareController
(78), peerInputActivitiesPromise (79), InactiveChannel (80), BlockedPeers (81),
openHashtag resolveSignal (82), NotificationExceptionsList (83).

**Waves 84-90 — TelegramEngine.Resources facade migrations**
Per-method Shape-A/B sweeps converting `<ctx>.account.postbox.mediaBox.X(...)`
to `<ctx>.engine.resources.X(...)`. Wave 90 was a single-commit big sweep:
40 fetchedMediaResource sites in 25 files migrated to engine.resources.fetch
facade in one atomic pass with first-pass-clean build.

Methods covered: storeResourceData, completedResourcePath, cancelInteractiveResourceFetch,
resourceRangesStatus, resourceStatus, fetch (fetchedMediaResource).

**Waves 91-92 — additional type migrations**
Wave 91: ItemListWebsiteItem.peer + RecentSessionsController enum-case payload
+ openWebSession callback Peer? -> EnginePeer?.
Wave 92: ChatListController StateHolder.EntryContext status type
MediaResourceStatus -> EngineMediaResource.FetchStatus.

**Wave 93 — speculative `import Postbox` drop sweep**
Drop import from 7 wave-touched files where it became unused; restore in 5
files where bare PeerId/Message/MediaId/StoryId references escaped the
pre-flight regex. Includes one MediaId(...) -> EngineMedia.Id(...) swap in
InAppPurchaseManager to unlock its import drop.

## Build state

Final state at squash: clean Telegram/Telegram build at debug_sim_arm64.

## Persistent-state notes

- Pre-existing WIP unchanged across the squashed range:
  - build-system/bazel-rules/sourcekit-bazel-bsp submodule marker
  - Untracked: build-system/tulsi/, submodules/TgVoip/, third-party/libx264/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
isaac 2026-04-25 20:48:15 +04:00
parent 6b7a23867c
commit d1aa0db537
139 changed files with 2124 additions and 453 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): 44 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): 45 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,116 @@
# Wave 54: ClearPeerHistory.init + openClearHistory `chatPeer: 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.
**Goal:** Migrate the `chatPeer:` parameter type on both `ClearPeerHistory.init` and `openClearHistory` from `Peer` to `EnginePeer`. Closes wave-53's deferred sibling.
**Wave shape:** Bundled method-signature migration (familiar from waves 41/44/47/50/53). Mechanical `as?`/`is` cluster on a single field, with EnginePeer.init boundary lifts at each call site.
**Tech Stack:** Swift, Bazel, Make.py.
---
## Pre-Flight Inventory (validated 2026-04-25)
**2 files modified, 16 edits.**
### File 1: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift` (PIS)
| Line | Current | After | Note |
|---|---|---|---|
| 3213 | `func openClearHistory(... peer: Peer, chatPeer: Peer) {` | `... chatPeer: EnginePeer)` | type-site |
| 3230 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap |
| 3232 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap |
| 3251 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap |
| 3269 | `EnginePeer(chatPeer).compactDisplayTitle` | `chatPeer.compactDisplayTitle` | drop wrap |
| 7416 | `init(... peer: Peer, chatPeer: Peer, cachedData: ...)` | `... chatPeer: EnginePeer, cachedData: ...` | type-site |
| 7421 | `} else if chatPeer is TelegramSecretChat {` | `} else if case .secretChat = chatPeer {` | conversion |
| 7425 | `} else if let group = chatPeer as? TelegramGroup {` | `} else if case let .legacyGroup(group) = chatPeer {` | conversion |
| 7436 | `} else if let channel = chatPeer as? TelegramChannel {` | `} else if case let .channel(channel) = chatPeer {` | conversion |
| 7464 | `if let user = chatPeer as? TelegramUser, user.botInfo != nil {` | `if case let .user(user) = chatPeer, user.botInfo != nil {` | conversion |
`peer:` parameter stays Peer-typed in both functions: `openClearHistory` doesn't reference `peer` in its body; `ClearPeerHistory.init` uses only `peer.id == context.account.peerId` (line 7417), which works on Peer (and would also work on EnginePeer, but migrating it would require 6 boundary lifts at PISPBA call sites for no internal benefit).
### File 2: `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreenPerformButtonAction.swift` (PISPBA)
| Line | Current | After | Note |
|---|---|---|---|
| 851 | `chatPeer: chatPeer._asPeer()` | `chatPeer: chatPeer` | drop wave-53 ADD |
| 857 | `chatPeer: user` | `chatPeer: EnginePeer(user)` | boundary lift (TelegramUser) |
| 1067 | `chatPeer: channel` | `chatPeer: EnginePeer(channel)` | boundary lift (TelegramChannel) |
| 1073 | `chatPeer: channel` | `chatPeer: EnginePeer(channel)` | boundary lift (TelegramChannel) |
| 1234 | `chatPeer: group` | `chatPeer: EnginePeer(group)` | boundary lift (TelegramGroup) |
| 1240 | `chatPeer: group` | `chatPeer: EnginePeer(group)` | boundary lift (TelegramGroup) |
### Net accounting
- **Drops:** 5 (4 `EnginePeer(chatPeer).compactDisplayTitle` + 1 `_asPeer()` bridge from wave 53).
- **Adds:** 5 boundary lifts (5 `EnginePeer(...)` wraps at PISPBA call sites).
- **Conversions:** 4 (`is`/`as?``case let`).
- **Type-site:** 2 (signature changes on PIS:3213 and PIS:7416).
Net internal-bridge progress: `5 drops 5 adds = 0 raw count`. But ratchet kills 4 internal display-call wraps (`EnginePeer(chatPeer).compactDisplayTitle` patterns) which is the hot path; only call-site boundary lifts remain. Closes wave-53's deferred ADD at PISPBA:851.
---
## Tasks
### Task 1: PIS signature edits + body wrap drops
- [ ] **Step 1: Edit `openClearHistory` signature at PIS:3213**
Replace `peer: Peer, chatPeer: Peer)` with `peer: Peer, chatPeer: EnginePeer)`.
- [ ] **Step 2: Drop 4 `EnginePeer(chatPeer).compactDisplayTitle` wraps**
`replace_all=true` of `EnginePeer(chatPeer).compactDisplayTitle``chatPeer.compactDisplayTitle`. (Only 4 occurrences in the file, all in `openClearHistory` body; verified by grep.)
- [ ] **Step 3: Edit `ClearPeerHistory.init` signature at PIS:7416**
Replace `peer: Peer, chatPeer: Peer, cachedData:` with `peer: Peer, chatPeer: EnginePeer, cachedData:`.
- [ ] **Step 4: Convert PIS:7421 `is TelegramSecretChat`**
Replace `} else if chatPeer is TelegramSecretChat {` with `} else if case .secretChat = chatPeer {`.
- [ ] **Step 5: Convert PIS:7425 `as? TelegramGroup`**
Replace `} else if let group = chatPeer as? TelegramGroup {` with `} else if case let .legacyGroup(group) = chatPeer {`.
- [ ] **Step 6: Convert PIS:7436 `as? TelegramChannel`**
Replace `} else if let channel = chatPeer as? TelegramChannel {` with `} else if case let .channel(channel) = chatPeer {`.
- [ ] **Step 7: Convert PIS:7464 `as? TelegramUser`**
Replace `if let user = chatPeer as? TelegramUser, user.botInfo != nil {` with `if case let .user(user) = chatPeer, user.botInfo != nil {`.
### Task 2: PISPBA call-site lifts + bridge drop
- [ ] **Step 1: Drop wave-53 `_asPeer()` bridge at PISPBA:851**
Replace `chatPeer: chatPeer._asPeer()` with `chatPeer: chatPeer`.
- [ ] **Step 2: Lift PISPBA:857 `chatPeer: user`**
Replace `peer: user, chatPeer: user)` with `peer: user, chatPeer: EnginePeer(user))`.
- [ ] **Step 3: Lift channel call sites (PISPBA:1067 + 1073)**
`replace_all=true` of `chatPeer: channel``chatPeer: EnginePeer(channel)`. Verify exactly 2 hits flipped.
- [ ] **Step 4: Lift group call sites (PISPBA:1234 + 1240)**
`replace_all=true` of `chatPeer: group``chatPeer: EnginePeer(group)`. Verify exactly 2 hits flipped.
### Task 3: Build verification
- [ ] **Step 1: Run full build with `--continueOnError`.**
Forecast 1 iteration. Risk: hidden `chatPeer` access on Peer-typed shape elsewhere (none expected — body audit complete).
### Task 4: Commit + log
- [ ] **Step 1: Commit wave with the two file paths explicitly.**
- [ ] **Step 2: Update `docs/superpowers/postbox-refactor-log.md` and the memory file.**
- [ ] **Step 3: Commit log.**

View file

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

View file

@ -0,0 +1,106 @@
# Wave 49 — `PeerInfoScreenData.linkedDiscussionPeer` + `.linkedMonoforumPeer` `Peer? → EnginePeer?` (bundle)
**Date:** 2026-04-25
**Predecessor:** Wave 48 (commit `1e4c2eea33`) — savedMessagesPeer single-field migration.
**Shape:** Cross-file bundled struct-field migration (2 sibling fields, 2 files). Both fields are module-internal; no external consumer references them on `PeerInfoScreenData`. Bundled because both fields:
- Share a sibling declaration site in `PeerInfoData.swift`.
- Have parallel local-source patterns (raw `Peer?` from `peerView.peers[id]` dict lookup; **not** an engine signal as in wave 48).
- Are both consumed in `PeerInfoProfileItems.swift` only.
- Migrating one without the other adds friction at the source-construction sites where they're computed together.
## Pre-flight inventory
`grep -rEn "(\w+\??)\.linkedDiscussionPeer\b|(\w+\??)\.linkedMonoforumPeer\b" submodules/ Telegram/`:
- `submodules/PeerInfoUI/Sources/ChannelDiscussionGroupSetupController.swift:600,651` — references are on a local `view` object (different type, NOT PeerInfoScreenData). Out of scope.
- All `data.linkedDiscussionPeer` / `data.linkedMonoforumPeer` accesses live in PIPI within the PeerInfoScreen module.
Within scope:
### `PeerInfoData.swift` (storage class + 2 init sites that compute the locals)
| Site | Code | Action |
|------|------|--------|
| :396 | `let linkedDiscussionPeer: Peer?` (field decl) | Type → `EnginePeer?` |
| :397 | `let linkedMonoforumPeer: Peer?` (field decl) | Type → `EnginePeer?` |
| :453 | `linkedDiscussionPeer: Peer?,` (init param) | Type → `EnginePeer?` |
| :454 | `linkedMonoforumPeer: Peer?,` (init param) | Type → `EnginePeer?` |
| :498 | `self.linkedDiscussionPeer = linkedDiscussionPeer` | No change |
| :499 | `self.linkedMonoforumPeer = linkedMonoforumPeer` | No change |
| :1038, :1111, :1631 | `linkedDiscussionPeer: nil,` (init kwargs) | No change |
| :1039, :1112, :1632 | `linkedMonoforumPeer: nil,` (init kwargs) | No change |
| :1836 | `var discussionPeer: Peer?` (local) | Type → `EnginePeer?` |
| :1838 | `discussionPeer = peer` (where `peer = peerView.peers[linkedDiscussionPeerId]`, raw `Peer`) | Wrap → `discussionPeer = EnginePeer(peer)` |
| :1841 | `var monoforumPeer: Peer?` (local) | Type → `EnginePeer?` |
| :1843 | `monoforumPeer = peerView.peers[linkedMonoforumId]` (dict lookup, `Peer?`) | Wrap → `monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)` |
| :2131 | `var discussionPeer: Peer?` (local, parallel to :1836) | Type → `EnginePeer?` |
| :2133 | `discussionPeer = peer` (parallel to :1838) | Wrap → `discussionPeer = EnginePeer(peer)` |
| :2136 | `var monoforumPeer: Peer?` (local, parallel to :1841) | Type → `EnginePeer?` |
| :2138 | `monoforumPeer = peerView.peers[linkedMonoforumId]` (parallel to :1843) | Wrap with `.flatMap(EnginePeer.init)` |
| :1878, :1879, :2216, :2217 | init kwargs `linkedDiscussionPeer: discussionPeer,` / `linkedMonoforumPeer: monoforumPeer,` | No change (locals migrate; pass through) |
That's **12 edits** in PID. Note the `var` declarations and assignments at :1836:1843 and :2131:2138 are *parallel pairs* (verified by grep). Use `replace_all=true` for the duplicate snippets.
### `PeerInfoProfileItems.swift` (3 edits)
| Site | Code | Action |
|------|------|--------|
| :1098 | `if let peer = data.linkedDiscussionPeer { ... }` | No change (binding works on `EnginePeer?`) |
| :1099 | `if let addressName = peer.addressName, !addressName.isEmpty {` | No change — `EnginePeer.addressName` forwarded (verified at `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:461`) |
| :1102 | `discussionGroupTitle = EnginePeer(peer).displayTitle(strings: ..., displayOrder: ...)` | **Drop wrap**`peer.displayTitle(...)` |
| :1197 | `if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel {` | **Pattern rewrite**`if case let .channel(monoforumPeer) = data.linkedMonoforumPeer {` |
| :1198 | `monoforumPeer.sendPaidMessageStars` | No change — `sendPaidMessageStars` is a `TelegramChannel` property (`SyncCore_TelegramChannel.swift:215`); `case .channel` binds to `TelegramChannel` |
| :1404 | `if let linkedDiscussionPeer = data.linkedDiscussionPeer {` | No change (binding works) |
| :1406 | `if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty {` | No change (forwarded) |
| :1409 | `peerTitle = EnginePeer(linkedDiscussionPeer).displayTitle(...)` | **Drop wrap**`linkedDiscussionPeer.displayTitle(...)` |
3 edits in PIPI.
## EnginePeer property forwarding audit
- `EnginePeer.addressName` — forwarded at `Peer.swift:461`. ✓
- `EnginePeer.displayTitle(strings:displayOrder:)` — defined as `EnginePeer` instance method (used elsewhere via `EnginePeer(...).displayTitle(...)` pattern; once we have an `EnginePeer`, it's directly callable). ✓
- `case .channel` binding payload is `TelegramChannel`. ✓
- `TelegramChannel.sendPaidMessageStars` — exists (`SyncCore_TelegramChannel.swift:215`). ✓
## Net bridge count
- **ADDs (4):** boundary lifts at PID:1838 (`EnginePeer(peer)`), PID:1843 (`.flatMap(EnginePeer.init)`), PID:2133, PID:2138. These lift the Postbox-typed `peerView.peers[...]` value to the engine type at the boundary — the correct semantic position for a Postbox→Engine refactor (mirrors wave 42 where `peer.flatMap(EnginePeer.init)` lift was added at PID:1620).
- **DROPs (2):** PIPI:1102 and :1409 lose `EnginePeer(...)` wraps around `displayTitle` calls.
- **Net text bridges:** +2. **But:** the ADDs are correct boundary lifts; the field-typed-as-`EnginePeer?` is the canonical state. The 2 displayTitle DROPs are the actual ratchet value.
- **Plus:** 1 cleaner pattern (PIPI:1197 `as?` cast → `case let .channel`), no text saving but better Swift idiom.
## Edit list
### `PeerInfoData.swift` (12 edits, but Edit text uses `replace_all=true` to bundle parallel pairs)
1. Line 396: `let linkedDiscussionPeer: Peer?``let linkedDiscussionPeer: EnginePeer?`
2. Line 397: `let linkedMonoforumPeer: Peer?``let linkedMonoforumPeer: EnginePeer?`
3. Line 453: `linkedDiscussionPeer: Peer?,``linkedDiscussionPeer: EnginePeer?,`
4. Line 454: `linkedMonoforumPeer: Peer?,``linkedMonoforumPeer: EnginePeer?,`
5. Lines 1836 + 2131 (`replace_all=true` over `var discussionPeer: Peer?`): → `var discussionPeer: EnginePeer?`
6. Lines 1838 + 2133 (`replace_all=true` over `discussionPeer = peer`): → `discussionPeer = EnginePeer(peer)`
7. Lines 1841 + 2136 (`replace_all=true` over `var monoforumPeer: Peer?`): → `var monoforumPeer: EnginePeer?`
8. Lines 1843 + 2138 (`replace_all=true` over `monoforumPeer = peerView.peers[linkedMonoforumId]`): → `monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)`
### `PeerInfoProfileItems.swift` (3 edits)
9. Line 1102: `discussionGroupTitle = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)``discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)`
10. Line 1197: `if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel {``if case let .channel(monoforumPeer) = data.linkedMonoforumPeer {`
11. Line 1409: `peerTitle = EnginePeer(linkedDiscussionPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)``peerTitle = linkedDiscussionPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)`
## Out of scope
- `PeerInfoScreenData.chatPeer` — large blast radius. Defer.
- `PeerInfoScreenMemberItem.enclosingPeer`. Defer.
## Build & verify
Standard Bazel command. Expected 1 iteration if forwarding audit holds; 2 if a `displayTitle` overload-resolution surprise surfaces.
## Commit
`Postbox -> TelegramEngine wave 49`. Body: bundle + edits summary + ADD/DROP accounting.
## Outcome capture
Append Wave 49 entry to `docs/superpowers/postbox-refactor-log.md`; update memory file.

View file

@ -0,0 +1,70 @@
# Wave 48 — `PeerInfoScreenData.savedMessagesPeer` `Peer? → EnginePeer?`
**Date:** 2026-04-25
**Predecessor:** Wave 47 (commit `d7b7536440`) — stored PHN.peer single-file private migration.
**Shape:** Cross-file struct-field migration. Storage class is internal to PeerInfoScreen module; no external consumer references PSD.savedMessagesPeer.
## Target
`submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoData.swift`, `PeerInfoScreenData.savedMessagesPeer: Peer?` at line 388.
## Pre-flight inventory
`grep -rEn "(\w+\??)\.savedMessagesPeer\b" submodules/ Telegram/` → matches only inside `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`. No external consumer. The same field name appears in unrelated places (TelegramEngineMessages.swift, ChatListUI, etc.) but those are different declarations on different types.
Within PeerInfoScreen module:
| Site | Code | Action |
|------|------|--------|
| `PeerInfoData.swift:388` | `let savedMessagesPeer: Peer?` (struct field decl) | Type change → `EnginePeer?` |
| `PeerInfoData.swift:444` | `savedMessagesPeer: Peer?,` (init param) | Type change → `EnginePeer?` |
| `PeerInfoData.swift:489` | `self.savedMessagesPeer = savedMessagesPeer` (assignment) | No change (passthrough) |
| `PeerInfoData.swift:1029` | `savedMessagesPeer: nil,` (init kwarg) | No change (`nil` works for either) |
| `PeerInfoData.swift:1102` | `savedMessagesPeer: nil,` | No change |
| `PeerInfoData.swift:13131317` | `let savedMessagesPeer: Signal<EnginePeer?, NoError>` (local) | No change — already `EnginePeer?` |
| `PeerInfoData.swift:1622` | `savedMessagesPeer: savedMessagesPeer?._asPeer(),` | **Drop bridge**`savedMessagesPeer: savedMessagesPeer,` |
| `PeerInfoData.swift:1869` | `savedMessagesPeer: nil,` | No change |
| `PeerInfoData.swift:2207` | `savedMessagesPeer: nil,` | No change |
| `PeerInfoScreen.swift:5399` | `peer: self.data?.savedMessagesPeer.flatMap(EnginePeer.init) ?? self.data?.peer,` | **Drop bridge**`peer: self.data?.savedMessagesPeer ?? self.data?.peer,` |
| `PeerInfoScreen.swift:5805` | same as :5399 | Same drop |
Total edits: 5 (3 in PID, 2 in PIS).
## EnginePeer / read-site audit
The local signal at `PeerInfoData.swift:1313` already produces `EnginePeer?` from `engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(...))`. The `?._asPeer()` at line 1622 was an artificial demotion. Migrating the field type to `EnginePeer?` removes both the demotion at the storage site and the `flatMap(EnginePeer.init)` re-promotions at the read sites — a clean ratchet.
PIS:5399 and :5805 use the field as input to `headerNode.update(... peer: ...)`, whose `peer` parameter has been `EnginePeer?` since wave 45. The `??` coalescing operand is `self.data?.peer` (already `EnginePeer?`). Result: drop the `.flatMap(EnginePeer.init)` and the expression compiles.
## Edit list
### PeerInfoData.swift (3 edits)
1. Line 388: `let savedMessagesPeer: Peer?``let savedMessagesPeer: EnginePeer?`
2. Line 444: `savedMessagesPeer: Peer?,``savedMessagesPeer: EnginePeer?,`
3. Line 1622: `savedMessagesPeer: savedMessagesPeer?._asPeer(),``savedMessagesPeer: savedMessagesPeer,`
### PeerInfoScreen.swift (2 edits, identical text)
4. Line 5399: `peer: self.data?.savedMessagesPeer.flatMap(EnginePeer.init) ?? self.data?.peer,``peer: self.data?.savedMessagesPeer ?? self.data?.peer,`
5. Line 5805: same
Use `replace_all=true` for the PIS edit since the matched text appears at both call sites verbatim.
## Out of scope
- `PeerInfoScreenData.chatPeer` — large blast radius (5 `as? TelegramX` checks downstream + ClearPeerHistory init parameter), defer.
- `PeerInfoScreenData.linkedDiscussionPeer`, `linkedMonoforumPeer` — both have `as? TelegramChannel` consumer sites in `PeerInfoProfileItems.swift`. Defer.
- `PeerInfoScreenMemberItem.enclosingPeer` — defer (separate target).
## Build & verify
Same Bazel command as wave 47. Expected 1-iteration first-pass-clean (single-pattern bridge removal, no enum-case rewrites, no Peer-only property access).
## Commit
`Postbox -> TelegramEngine wave 48`. Body lists the 5-edit summary and notes 3 internal bridges (1 PID + 2 PIS, identical PIS text appears twice).
## Outcome capture
Append a Wave 48 entry to `docs/superpowers/postbox-refactor-log.md` and update memory file `project_postbox_refactor_next_wave.md`.

View file

@ -0,0 +1,62 @@
# Wave 47 — `PeerInfoHeaderNode.peer` stored field `Peer? → EnginePeer?`
**Date:** 2026-04-25
**Predecessor:** Wave 46 (commit `5ca99da5a7`) — PeerInfo avatar chain.
**Shape:** Single-file stored-field type migration. No external API change (field is `private`).
## Target
`submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift`, stored field `private var peer: Peer?` at line 92.
## Pre-flight inventory
`grep -n "self\.peer\b" PeerInfoHeaderNode.swift` returns exactly 3 references:
| Line | Code | Site type | Action |
|------|------|-----------|--------|
| 426 | `if let peer = self.peer, peer.profileImageRepresentations.isEmpty && gallery {` | Read | None — `profileImageRepresentations` is forwarded by `EnginePeer` (see `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:485`). Compiles unchanged. |
| 521 | `self.peer = peer?._asPeer()` | Assignment | Drop the bridge → `self.peer = peer`. The `peer` parameter is already `EnginePeer?` after wave 45. |
| 20492054 | `guard let self, let peer = self.peer, ...` followed by `peer: EnginePeer(peer),` | Read | Drop the wrap at line 2054 → `peer: peer,`. |
External access check: `grep -rn "headerNode\.peer\b" submodules/ Telegram/` returns empty. The field is private; only same-file siblings touch it.
EnginePeer forwarding (re-confirmed at plan time):
- `profileImageRepresentations` — forwarded (Peer.swift:485). ✓
- `EnginePeer(peer)` (PHN:2054) — accepts `EnginePeer` directly when the local is already `EnginePeer`; drop the constructor.
Field-declaration change is the only "type" change needed. The 3 callers' adjustments are mechanical bridge drops.
## Edit list
1. Line 92: `private var peer: Peer?``private var peer: EnginePeer?`
2. Line 521: `self.peer = peer?._asPeer()``self.peer = peer`
3. Line 2054: `peer: EnginePeer(peer),``peer: peer,`
Total: 3 edits in 1 file.
## Out of scope
- `PeerInfoData.swift:355,487` — different classes' `self.peer` assignments (different types). Audit confirms these are `RenderedChannelParticipant.peer` and similar — already migrated in earlier waves or owned by other types.
- `PeerInfoAvatarTransformContainerNode.peer` (line 223) — already `EnginePeer?` after wave 46.
## Build & verify
```sh
source ~/.zshrc 2>/dev/null; \
python3 build-system/Make/Make.py --overrideXcodeVersion \
--cacheDir ~/telegram-bazel-cache build \
--configurationPath build-system/appstore-configuration.json \
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
--gitCodesigningType development --gitCodesigningUseCurrent \
--buildNumber=1 --configuration=debug_sim_arm64
```
Expected: 1-iteration first-pass-clean. Only PeerInfoScreen + TelegramUI recompile.
## Commit
`Postbox -> TelegramEngine wave 47`. Body lists the 3-edit summary and notes -3 internal bridges.
## Outcome capture
Append a Wave 47 entry to `docs/superpowers/postbox-refactor-log.md` and update memory file `project_postbox_refactor_next_wave.md`.

View file

@ -1216,6 +1216,661 @@ Net consumer-surface: **10 bridges**. TelegramCore-internal: +~12 wraps insid
---
## Wave 45 outcome (2026-04-24)
Migrated the four `update(..., peer: Peer?, ...)` methods across the PeerInfoHeader node hierarchy — `PeerInfoHeaderNode.update`, `PeerInfoHeaderEditingContentNode.update`, `PeerInfoEditingAvatarNode.update`, `PeerInfoEditingAvatarOverlayNode.update` — from raw `Peer?` to `EnginePeer?`. Consumer-surface ratchet that drops ~15 wave-43-era ADD-WRAP bridges. Single-module wave (all four files live in `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/`). No new typealiases. No engine wrapper structs. The stored `PeerInfoHeaderNode.peer: Peer?` field stays raw (intentional scope cap) and is bridged with `peer?._asPeer()` at assignment.
**Classification:**
- 4 signature changes: `peer: Peer?``peer: EnginePeer?` at PHN:496, PHECN:52, PEAN:61, PEAON:63.
- 15 DROP wraps across the four files:
- PHN body: 7 drops at :524 (listContainerNode.peer), :548 (peerInfoHeaderActionButtons), :549 (peerInfoHeaderButtons), :571 (backgroundCoverSubject = .peer), :1218 (displayTitle), :1964 (listContainerNode.update), :2361 (peerInfoIsChatMuted).
- PHECN body: 5 drops at :59, :88, :93, :159, :162 (all `canEditPeerInfo` wraps).
- PEAN body: 2 drops at :66 (canEditPeerInfo) and :88 (avatarNode.setPeer).
- PEAON body: 1 drop at :85 (canEditPeerInfo).
- 5 enum-case rewrites in PHN: `peer as? TelegramChannel` / `peer as? TelegramUser``case let .channel(x) = peer` / `case let .user(x) = peer` at :622, :1225, :1238, :1273, :2354.
- 7 enum-case rewrites in PHECN: `peer as? TelegramUser/Group/Channel` at :73, :77, :86 (no-bind), :91 (no-bind), :107 (inner rebind), :154.
- 1 enum-case rewrite in PEAN: `peer as? TelegramChannel` at :93.
- 1 enum-case rewrite in PEAON: `peer as? TelegramChannel` at :74.
- 4 `_asPeer()` bridges ADDED at Peer-only-property sites (wave-42 lesson pattern):
- PEAN:159 `PeerReference(peer._asPeer())` — PeerReference.init takes raw Peer.
- PEAN:166 `peer._asPeer().isCopyProtectionEnabled` — property not forwarded on EnginePeer.
- PHECN:115 `(peer?._asPeer() as? TelegramUser)?.lastName` — inline expression form kept; chose bridge over multi-line `case let` expansion.
- PHN:521 `self.peer = peer?._asPeer()` — stored raw Peer? field stays raw.
- 1 ADD wrap at PHN:363 (`itemsUpdated` closure) — closure reads stored raw `self.peer` and forwards to the migrated `PEAN.update`.
- 1 ADD bridge at PHN:1815 for `self.avatarListNode.update(..., peer: peer?._asPeer(), ...)``PeerInfoAvatarListNode.update(size:avatarSize:isExpanded:peer:...)` still takes raw `Peer?` (plan pre-flight missed this site). Candidate for wave 46.
- 2 external call sites in `PeerInfoScreen.swift` at :5399 and :5805 reshuffled: `peer: self.data?.savedMessagesPeer ?? self.data?.peer?._asPeer()``peer: self.data?.savedMessagesPeer.flatMap(EnginePeer.init) ?? self.data?.peer`. Net-zero at these sites (swap `?._asPeer()` bridge for `.flatMap(EnginePeer.init)` wrap).
Net consumer-surface: **10 bridges** (15 drops 4 internal `_asPeer()` bridges 1 internal closure ADD-WRAP at :363 1 :1815 bridge the plan missed; the two external call sites are net-zero). No new `import Postbox` in any consumer module; no Postbox-hygiene regression.
**Build outcome:** 1 iteration (first-pass-clean).
**Commit:** `6b7a23867c` (5 files, 41 insertions / 41 deletions).
**Lessons:**
- **Plan pre-flight grep for call sites must include internal sibling-method callers, not just the target method's direct callers.** The plan enumerated the three internal call sites of PEAN.update / PHECN.update / PEAON.update inside PeerInfoHeaderNode.update's body (:633, :1816, :1817) and the `itemsUpdated` closure (:363), plus the two external call sites (:5399, :5805). It MISSED the `self.avatarListNode.update(..., peer:...)` call at PHN:1815 — a different target node's update method (`PeerInfoAvatarListNode.update(size:avatarSize:isExpanded:peer:...)`) that happens to live on the line immediately before `self.editingContentNode.avatarNode.update(peer: peer, ...)` at :1816. Pre-flight grep token `\.update\(.+peer:` over the four files would have caught it. Fix for future pre-flight grep: for each signature migration, grep ALL `update` methods called with the migrated param and verify each target's signature — not just the siblings being migrated.
- **Wave-43 binding-rule continues to hold at scale.** All 13 `as? TelegramUser/Group/Channel` rewrites in this wave obeyed the bind-when-used / no-bind-when-unused rule (PHECN:86 and :91 used `case .legacyGroup = peer` / `case .channel = peer` no-bind form because the branch body only appends field keys; all 11 others bind because the branch uses the concrete value). Zero `-warnings-as-errors` unused-binding risks surfaced during review. **Rule confirmed as durable.**
- **EnginePeer forwarding audit is worth a pre-flight pass for multi-property-accessing methods.** PHN's `update` method body accesses ~15 EnginePeer-forwarded properties (id, isFake, isScam, isPremium, isVerified, debugDisplayTitle, displayTitle, addressName, effectiveProfileColor, emojiStatus, verificationIconFileId, profileImageRepresentations, etc.) plus 2 concrete-class properties requiring enum bindings (`.phone` on TelegramUser; `.info` on TelegramChannel). Without a pre-flight forwarding audit against `submodules/TelegramCore/Sources/TelegramEngine/Peers/Peer.swift:485-560` (the EnginePeer property-forwarding extension), iteration surprises are likely. This wave's audit at plan time caught `isCopyProtectionEnabled` as the only non-forwarded property in PHN's body — matching the single bridge in PHN. **Rule: for multi-access methods, read the EnginePeer property-forwarding extension in full at plan time; enumerate every `peer.X` site and mark forwarded vs not.**
- **Case-pattern-against-optional is idiomatic and compiles cleanly.** All rewrites use `if case let .user(user) = peer` (or sibling case forms) against `EnginePeer?` — Swift extracts through Optional implicitly. Precedent: `PeerInfoScreenSettingsActions.swift:200` `if case let .user(user) = self.data?.peer, let phoneNumber = user.phone`. Build did not complain at any rewrite site. **Conservative fallback `if let peer, case let .x(y) = peer` is NOT needed; drop from future plans' "fallback" sections.**
- **Bundling net-zero external-call-site migrations with net-negative internal drops is fine for ratchet-focused waves.** The two PHN.update call sites in PeerInfoScreen.swift swap one bridge form for another (`?._asPeer()``.flatMap(EnginePeer.init)`) — literally net-zero on wrap count. The wave accepted this because (a) the internal drops (~15) dominate, (b) migrating `savedMessagesPeer` field to remove the external wrap would expand scope to a different file's public struct field, and (c) the wave-goal was "ratchet wave-43 ADDs", which is accomplished purely via internal drops. **Heuristic: if external call sites are net-zero and the internal scope is net-negative, bundle them; don't let the external net-zero block the ratchet.**
**Plan:** `docs/superpowers/plans/2026-04-24-peerinfoheader-update-bundle-engine-peer.md`.
---
## Wave 46 outcome (2026-04-25)
Migrated the PeerInfoScreen-local avatar chain: `PeerInfoAvatarListNode.update(peer:)` together with `PeerInfoAvatarTransformContainerNode.update(peer:)` / `.updateStoryView(peer:)` and the private `Params.peer` stored field — raw `Peer?``EnginePeer?` across 4 files. Ratchet wave: drops the wave-45 ADD at `PeerInfoHeaderNode.swift:1815` plus a pre-existing external bridge at `PeerInfoScreen.swift:2574`, collapses one internal wave-45 wrap inside PIATCN's `setPeer` body, and adds 2 `_asPeer()` bridges for Peer-only surfaces (`PeerReference.init` and `.isCopyProtectionEnabled`). Net: 1 bridge. The PeerInfoScreen-local `PeerInfoAvatarListNode` class shadows a same-named submodule class — the wave targets the local (PeerInfoScreen/Sources) file only; the submodule is untouched. No new typealiases. No engine wrapper structs. No `import Postbox` change.
**Classification:**
- 4 signature changes: `peer: Peer?``peer: EnginePeer?` on `PeerInfoAvatarListNode.update(peer:)` and its `arguments` tuple, plus `PeerInfoAvatarTransformContainerNode.update(peer:)`, `.updateStoryView(peer:)`, and the private `Params.peer` stored field.
- 2 DROPs at call sites (ratchet-value):
- `PeerInfoHeaderNode.swift:1815``peer: peer?._asPeer()``peer: peer` (the wave-45 ADD flagged for ratchet).
- `PeerInfoScreen.swift:2574``peer: peer?._asPeer()``peer: peer` (pre-existing external bridge; direct caller of `updateStoryView`).
- 1 internal WRAP collapsed inside PIATCN's `setPeer(...)` body: `peer: EnginePeer(peer)``peer: peer` (type flowed once upstream migrated).
- 2 ADDs inside PIATCN body for Peer-only surface:
- PIATCN ~:404 `PeerReference(peer._asPeer())``PeerReference.init` takes raw `Peer`, not `EnginePeer`.
- PIATCN ~:406 `peer._asPeer().isCopyProtectionEnabled` — property defined on the `Peer` protocol (`PeerUtils.swift:236`), not forwarded on EnginePeer (same finding as wave 45 for PEAN:166).
- 2 `as? TelegramChannel``case let .channel(...) = peer` rewrites inside PIATCN (both `update` and `updateStoryView` bodies test `channel.isForumOrMonoForum`; the `let channel` binding is retained because the body uses the concrete value).
Net consumer-surface: **1 bridge** (2 drops + 1 wrap collapse 2 adds). External API: no change.
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.587s; only PeerInfoScreen + TelegramUI recompiled.
**Commit:** `5ca99da5a7` (4 files, 13 insertions / 13 deletions).
**Lessons:**
- **Shadowing-class-name pre-flight disambiguation.** `PeerInfoAvatarListNode` is defined in two places: `submodules/PeerInfoAvatarListNode/Sources/PeerInfoAvatarListNode.swift` (the submodule) AND `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoAvatarListNode.swift` (the PeerInfoScreen-local class that shadows it). Wave 46's pre-flight nearly targeted the wrong file when the plan inspected the signature by class name alone. Mitigation for future waves: when the target method's signature has distinctive parameter names (here `avatarSize:isForum:threadId:threadInfo:`), grep against those distinctive tokens alongside the class name to disambiguate — and when the class name appears in multiple files, resolve the module path before writing the plan.
- **Chain-audit extends via sibling internal methods (`updateStoryView` pattern).** The initial audit targeted only the `update(peer:)` chain. But `PeerInfoAvatarTransformContainerNode.update`'s tail calls `self.updateStoryView(peer:)` — which also takes raw `Peer?`. Expanding the wave to include `updateStoryView` was a single-file additional edit and additionally dropped the `PeerInfoScreen.swift:2574` external bridge (a direct caller of `updateStoryView`, not `update`). Pattern for future chain waves: grep the target implementation body for `self\.\w+\(.+peer:` to find internal sibling methods whose signature migration naturally bundles with the primary chain.
- **First-pass-clean extends to chain migrations when both forwarding audit and scope audit are done at plan time.** Wave 46 is the 4th consecutive wave (42/43/44/45/46 — wave 44 needed 2 iterations) hitting 0-to-1-iteration convergence. The 1-iteration-clean pattern is reproducible when (a) the `_asPeer()` bridge sites are identified at plan time by cross-referencing the body against the EnginePeer property-forwarding extension AND `PeerReference`'s init overloads, and (b) enum-case conversions are pre-written in the plan with bindless-vs-binding form pre-classified. Both inputs were present in the wave 46 plan.
- **Chain-bundling heuristic validated at 3 methods / 4 files.** Migrating a narrow call chain (`PeerInfoAvatarListNode.update``PIATCN.update``PIATCN.updateStoryView`) + all external call sites in a single commit is the right granularity: the internal `peer` variables flow transparently once each downstream signature migrates, and the external call-site greps (2 sites total) complete the ratchet. Net: clean atomic commit, no bridging churn remains at the boundary. For future chain waves, bundle the chain methods plus external callers in one commit; don't split by method.
**Plan:** `docs/superpowers/plans/2026-04-25-peerinfo-avatar-chain-engine-peer.md`.
---
## Wave 47 outcome (2026-04-25)
Migrated the stored `PeerInfoHeaderNode.peer` field from `Peer?` to `EnginePeer?`. Single-file wave; field is `private`, so no external API change. 4 edits / 1 file, all inside `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoHeaderNode.swift`.
**Edits:**
- Line 92: `private var peer: Peer?``private var peer: EnginePeer?`
- Line 363 (`itemsUpdated` closure): `peer: EnginePeer(peer)``peer: peer` (drops bridge; `peer` is captured from `strongSelf.peer`).
- Line 521 (assignment in `update`): `self.peer = peer?._asPeer()``self.peer = peer`.
- Line 2054 (ProfileLevelInfoScreen push closure): `peer: EnginePeer(peer),``peer: peer,`.
PHN:426 (`peer.profileImageRepresentations.isEmpty`) compiles unchanged because `profileImageRepresentations` is forwarded by `EnginePeer` (`Peer.swift:485`).
Net consumer-surface: **3 internal bridges**.
**Build outcome:** 2 iterations.
- Iteration 1 failed at PHN:363 — pre-flight grep used `self\.peer\b` and missed the `strongSelf.peer` capture inside the `itemsUpdated` closure body. The memory file's wave-47 candidate notes had explicitly flagged PHN:363; the omission was in the executor's grep, not the plan's identification.
- Iteration 2 clean. Full-project Bazel build 28.727s; only PeerInfoScreen + TelegramUI recompiled.
**Commit:** `d7b7536440` (4 insertions / 4 deletions in PHN.swift; plan file added).
**Lessons:**
- **Pre-flight grep for stored-field migrations must include closure-capture aliases (`strongSelf`, `self_`, etc.), not just `self.<field>`.** Wave 47's first-iteration failure was 100% avoidable: the same field is referenced via `strongSelf.peer` inside an `itemsUpdated` closure, and the bare `self\.peer\b` grep missed it. For future stored-field migrations, the canonical grep pattern is `(self|strongSelf|[a-zA-Z_]*[Ss]elf)\.<field>\b`. The convention `strongSelf` appears throughout the PeerInfo codebase whenever closures use `[weak self]`.
- **Memory-stored candidate notes can pre-list the bridge sites.** The memory file `project_postbox_refactor_next_wave.md` had explicitly named PHN:363 (`itemsUpdated` closure read), PHN:521 (stored assignment), and PHN:2054 (ProfileLevelInfoScreen push wrap) before the wave started. Reading those during plan-write would have caught the iteration-1 miss. Treat the wave-N+1 candidate notes in memory as a load-bearing input to the plan, not just narrative.
- **Single-file `private` stored-field migrations are the cleanest possible wave shape.** No external API surface, no cross-module recompilation, blast radius bounded to the same file's other methods. When a wave reaches this shape, it is a near-zero-risk drop. Future stored-field-migration waves should be explicitly classified during planning as "single-file private" / "cross-file private" / "public-surface" to set the iteration budget.
**Plan:** `docs/superpowers/plans/2026-04-25-phn-peer-stored-field-engine-peer.md`.
---
## Wave 48 outcome (2026-04-25)
Migrated `PeerInfoScreenData.savedMessagesPeer: Peer? → EnginePeer?`. Cross-file struct-field migration contained within the PeerInfoScreen module; the field has no external consumer (`grep -rEn "(\w+\??)\.savedMessagesPeer\b"` matches only inside the PeerInfoScreen sources). 5 edits across 2 files.
**Edits:**
- `PeerInfoData.swift:388` — field decl `Peer?``EnginePeer?`.
- `PeerInfoData.swift:444` — init param `Peer?``EnginePeer?`.
- `PeerInfoData.swift:1622` — drop `?._asPeer()` bridge. The source local `let savedMessagesPeer: Signal<EnginePeer?, NoError>` (PID:1313) already produces `EnginePeer?`; the bridge was an artificial demotion.
- `PeerInfoScreen.swift:5399` and `:5805` — drop `.flatMap(EnginePeer.init)` bridge in `headerNode.update(... peer: ...)` call. The `peer` parameter of `headerNode.update` has been `EnginePeer?` since wave 45, and the `??` coalescing operand `self.data?.peer` has been `EnginePeer?` since wave 42; once the field migrates, both ends of the expression are `EnginePeer?` and the `flatMap` bridge falls out.
The other 4 init kwarg sites (PID:1029, :1102, :1869, :2207) all pass `nil` and require no change.
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.858s; only PeerInfoScreen + TelegramUI recompiled.
**Commit:** `1e4c2eea33` (5 insertions / 5 deletions; plan file added).
**Lessons:**
- **Internal-storage demotion → external re-promotion** is a high-yield ratchet pattern. The field had a `?._asPeer()` demotion at the storage site, then `.flatMap(EnginePeer.init)` re-promotions at every read. Migrating the field type drops both ends in one wave — the underlying signal pipeline never needed the `Peer?` form at all. Pattern to look for in future PSD-class migrations: any field whose initialization expression names `_asPeer()` strongly indicates that the field's source data is already `EnginePeer?`. Grep candidate fields' init expressions for `_asPeer()` to surface these high-leverage migrations.
- **Containment audit must distinguish field declarations on different types.** The grep `grep -rln "savedMessagesPeer"` returned 6 modules outside PeerInfoScreen. All matched references were independent declarations on unrelated types (TelegramEngineMessages, ChatListUI nodes, ChatControllerContentData). The narrower regex `(\w+\??)\.savedMessagesPeer\b` filtered to actual field-access patterns and confirmed no external consumer. For future struct-field migrations, prefer the access-pattern regex over plain text grep; common field names will hit unrelated declarations and inflate apparent blast radius.
- **`replace_all=true` is correct for verbatim-duplicated call sites.** PIS:5399 and :5805 are byte-identical `headerNode.update(...)` calls with the same `peer:` argument. Single-Edit-with-replace_all replaced both atomically. No collisions because the `headerNode.update` argument list with `flatMap(EnginePeer.init)` is sufficiently long to be unique.
- **Wave-shape G' (sibling-of-wave-42 ratchet) revalidated.** Wave 42 migrated `PeerInfoScreenData.peer: Peer? → EnginePeer?`. Wave 48 follows the same pattern on a sibling field. Future waves on `chatPeer`, `linkedDiscussionPeer`, `linkedMonoforumPeer` will not all be this clean: `chatPeer` has 5 `as? TelegramX` checks downstream + cross-method propagation into ClearPeerHistory; `linkedMonoforumPeer` has an `as? TelegramChannel` check at PIPI:1197. Field-by-field selection should consider downstream consumer shape, not just declaration site.
**Plan:** `docs/superpowers/plans/2026-04-25-peerinfoscreendata-savedmessagespeer-engine-peer.md`.
---
## Wave 49 outcome (2026-04-25)
Bundled migration: `PeerInfoScreenData.linkedDiscussionPeer` + `.linkedMonoforumPeer`, both `Peer? → EnginePeer?`. Cross-file struct-field migration over 2 files. Bundled because both fields share parallel local-source patterns (raw `peerView.peers[id]` dict lookup) and the same single consumer file (`PeerInfoProfileItems.swift`). The bundle is justified: the source-of-truth init blocks compute `discussionPeer` and `monoforumPeer` as a sibling pair at PID:18361843 and again at PID:21312138; migrating one without the other would leave a half-Peer-half-EnginePeer init block.
**Edits:**
`PeerInfoData.swift` (12 edits via `replace_all=true` on the parallel pair):
- Lines 396397 — field decls Peer? → EnginePeer?.
- Lines 453454 — init params Peer? → EnginePeer?.
- Lines 1836+2131 — `var discussionPeer: EnginePeer?` (parallel pair).
- Lines 1838+2133 — `discussionPeer = EnginePeer(peer)` (lift raw Peer at boundary).
- Lines 1841+2136 — `var monoforumPeer: EnginePeer?` (parallel pair).
- Lines 1843+2138 — `monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)` (lift Peer? at boundary).
`PeerInfoProfileItems.swift` (3 edits):
- :1102 — `EnginePeer(peer).displayTitle(...)``peer.displayTitle(...)`.
- :1197 — `if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel``if case let .channel(monoforumPeer) = data.linkedMonoforumPeer`. The `case .channel` payload is `TelegramChannel`, so the downstream `monoforumPeer.sendPaidMessageStars` access at :1198 continues to compile.
- :1409 — `EnginePeer(linkedDiscussionPeer).displayTitle(...)``linkedDiscussionPeer.displayTitle(...)`.
**Net bridge accounting:**
- ADDs (4): boundary lifts at PID:1838, :1843, :2133, :2138. These lift the Postbox-typed `peerView.peers[...]` dict-lookup result to the engine type at the data-flow boundary — the canonical Postbox→Engine position. Mirrors wave 42's `peer.flatMap(EnginePeer.init)` lift at PID:1620.
- DROPs (2): displayTitle `EnginePeer(...)` wraps at PIPI:1102 and :1409.
- Plus 1 idiom cleanup (PIPI:1197 — `as?` cast → enum-case pattern); no text saving but better Swift idiom.
The +4/2 net text-bridge count is acceptable here because the ADDs are not "internal bridges" — they're the canonical Postbox→Engine boundary that any well-typed engine field requires. Wave-tracking should distinguish "boundary lifts" (correct, permanent) from "internal bridges" (incorrect, ratchet target).
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.479s; only PeerInfoScreen + TelegramUI recompiled. Pre-flight EnginePeer-property forwarding audit (`addressName` at Peer.swift:461; `displayTitle` is an EnginePeer instance method; `case .channel` binds `TelegramChannel`; `TelegramChannel.sendPaidMessageStars` exists at SyncCore_TelegramChannel.swift:215) all verified at plan time and proved correct.
**Commit:** `79698e4513` (15 edits across 2 files; plan added).
**Lessons:**
- **Bundle waves around source-of-truth coherence, not just consumer overlap.** Wave 49's bundling was justified primarily by the *source* side: both fields are computed in the same init block as a sibling pair (PID:18361843, :21312138). Migrating only one would leave the init block in a half-typed state and force an artificial bridge. Future bundling decisions should weight this "shared source-of-truth init" factor at least as heavily as "shared consumer file."
- **Boundary-lift bridges are not "internal bridges" — wave-tracking should distinguish.** Wave-49's +4 ADDs are at the Postbox↔Engine boundary (raw `peerView.peers[...]` Postbox-typed dict → engine-typed field). They are the *correct* place for the lift; they will not ratchet away in a future wave. Internal bridges (e.g. PID:1622's `_asPeer()` in wave 48, or `EnginePeer(...)` wraps at consumer-side display calls) are the actual ratchet targets. Future wave outcome reports should categorize ADDs as "boundary lift" vs "internal bridge" to avoid confusion in net-bridge accounting.
- **Parallel-pair `replace_all=true` works for multi-line blocks when the parallel pair is byte-identical.** PID:18361843 and PID:21312138 are byte-identical 9-line blocks. One Edit-with-replace_all applied the type+wrap rewrite to both. Pre-flight verification: `diff <(sed -n '1836,1843p' file) <(sed -n '2131,2138p' file)` returned empty before the edit. Pattern generalizes to any "compute the same locals in two adjacent init paths" code structure (a common shape in PeerInfoData.swift's signal pipelines).
- **Whitespace nuance with `replace_all` multi-line edits.** The blank separator between the two `var X: Peer?` blocks had trailing spaces (Swift code with whitespace-trimming editor settings). The new_string used a clean blank line; the resulting file has a plain `\n` separator instead of ` \n`. Cosmetic only — no compile impact — but worth noting for future plans: when the indentation-sensitive pre-text uses trailing whitespace, mirror it exactly in the new_string to avoid inadvertent normalization.
- **`case .channel` pattern at field-binding compiles cleanly against `EnginePeer?` (re-confirmed wave 45 lesson).** No `if let ... case let .channel = ...` two-step needed; single-step `if case let .channel(monoforumPeer) = data.linkedMonoforumPeer` works directly.
**Plan:** `docs/superpowers/plans/2026-04-25-peerinfoscreendata-linked-peers-engine-peer.md`.
---
## Wave 50 outcome (2026-04-25)
`enclosingPeer: Peer? → EnginePeer?` migration across the PeerInfo members chain. 19 edits across 3 files (`PeerInfoScreenMemberItem.swift`, `PeerInfoMembersPane.swift`, `PeerInfoProfileItems.swift`). Cross-file private struct-field migration with stored-form ratchet (wave-47 taxonomy: "cross-file private"). Closes the wave-48-pattern internal-demotion-and-external-re-promotion ratchet at PIMP:354363, where `engine.data.subscribe(...)` produced an `EnginePeer?` that was demoted to `Peer?` for storage and then re-promoted at every consumer.
**Edits:**
`PeerInfoScreenMemberItem.swift` (7 edits):
- :23 — stored field `let enclosingPeer: Peer? → EnginePeer?`.
- :34 — init param `Peer? → EnginePeer?`.
- :152, :154 — `as? TelegramChannel` / `as? TelegramGroup``case let .channel(channel)` / `case let .legacyGroup(group)`.
- :178 — `peer: item.enclosingPeer.flatMap(EnginePeer.init)``peer: item.enclosingPeer` (auto-promotes to `EnginePeer?`).
- :181, :187 — `is TelegramChannel``case .channel = ...` (wave-41 always-false-warning fix).
`PeerInfoMembersPane.swift` (11 edits):
- :92, :271, :442 — three func sigs `enclosingPeer: Peer → EnginePeer`.
- :113, :115 — `as? TelegramChannel` / `as? TelegramGroup``case let .channel(channel)` / `case let .legacyGroup(group)`.
- :139 — `peer: EnginePeer(enclosingPeer)``peer: enclosingPeer` (drop wrap, auto-promotes).
- :142, :148 — `is TelegramChannel``case .channel = ...`.
- :293 — stored field `private var enclosingPeer: Peer? → EnginePeer?`.
- :361 — `strongSelf.enclosingPeer = enclosingPeer._asPeer()``strongSelf.enclosingPeer = enclosingPeer`.
- :363 — `strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), ...)``..., enclosingPeer: enclosingPeer, ...`.
`PeerInfoProfileItems.swift` (1 edit):
- :852 — `enclosingPeer: peer._asPeer()``enclosingPeer: peer` (boundary lift; `peer` is already `EnginePeer` from the closure scope's `data.peer` post-wave-42).
**Net bridge accounting:**
- DROPs (5): 2× `_asPeer()` demotion (PIMP:361, :363), 1× `EnginePeer(...)` wrap (PIMP:139), 1× `flatMap(EnginePeer.init)` (PSMI:178), 1× boundary `_asPeer()` lift (PSPB:852).
- ADDs (0): no new bridges. Pattern conversions (`as?``case let`, `is``case`) are not bridges; they're idiom shifts mandated by the EnginePeer enum representation.
- Pass-through call sites at PIMP:275, :276, :437, :438, :451, :485 needed no edits — types flow transparently through stored field, local var, and func params after the signature changes.
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.290s; only PeerInfoScreen + TelegramUI recompiled. Continues the wave 42/45/46/48/49 first-pass-clean streak (6 of last 8 waves first-pass-clean; wave 44 was 2 iterations, wave 47 was 2 iterations).
**Commit:** `a1b77bcf74` (19 edits across 3 files).
**Lessons:**
- **The wave-48 internal-demotion ratchet pattern is reliably first-pass-clean.** When the source signal already produces the engine type and is being demoted at storage, migrating the storage to the engine type is a 1-iteration target if the consumer's only Peer-only access is through `as? TelegramX` / `is TelegramX` (mechanical conversions to `case let`/`case`). Wave 50 reproduces the wave-48 pattern verbatim and lands the same first-pass-clean outcome.
- **Pre-flight EnginePeer-property forwarding audit confirmed unnecessary for `case let`-bound concrete types.** The `case let .channel(channel)` / `case let .legacyGroup(group)` patterns bind a `TelegramChannel` / `TelegramGroup` directly — methods like `.hasPermission(.editRank)` and `.hasBannedPermission(.banEditRank)` are class methods on the concrete type and call cleanly without `_asPeer()`. The forwarding audit in earlier waves was load-bearing for properties accessed *off the bare EnginePeer* (e.g., `peer.displayTitle` requiring an EnginePeer method); when accessed off a case-bound concrete type, no audit is needed.
- **Bundling rationale: PSMI + PIMP + PSPB:852 share the same data-flow contour.** `PeerInfoProfileItems.swift:852` is the only producer of `PeerInfoScreenMemberItem` in the migration's scope (PSI:132 also produces but with `enclosingPeer: nil`, no edit needed). PIMP and PSMI form a tight panenode pair. The 1-edit PSPB inclusion was non-controversial because the PSPB:852 site is the canonical Postbox→Engine boundary above the migrated chain. Boundary-lift inclusion for a 1-edit site is correct.
- **Bundled implementer dispatch with a single subagent is the right shape for a 19-edit / 3-file mechanical wave.** Per-task subagent dispatch (1 task per file) would have triggered three implementer roundtrips and three review rounds for what is essentially one cohesive editing unit that doesn't pass build at any per-file checkpoint. The subagent-driven-development skill's "fresh subagent per task" convention adapts to "fresh subagent per cohesive editing unit" when individual tasks are not independently verifiable.
**Plan:** `docs/superpowers/plans/2026-04-25-peerinfo-enclosingpeer-engine-peer.md`.
---
## Wave 51 outcome (2026-04-25)
`GroupsInCommonListEntry.peer: Peer → EnginePeer` migration in `PeerInfoGroupsInCommonPaneNode.swift` (single-file private struct-field). 7 edits, 1 file, **first-pass-clean**. Wave-shape: narrow internal struct-field migration with deliberate boundary scoping — public init's `openPeerContextAction: (Bool, Peer, …)` field left unmigrated to avoid cascading into `PeerInfoPaneContainerNode` (parent), `PeerInfoRecommendedPeersPaneNode` (sibling pane sharing the closure type), and upstream callers in `PeerInfoScreen.swift`. Saved for a coordinated wave once a wider closure-type sweep is justified.
**Edits:**
`PeerInfoGroupsInCommonPaneNode.swift` (7 edits):
- :28 — stored field `var peer: Peer → var peer: EnginePeer`.
- :35 — `lhs.peer.isEqual(rhs.peer)``lhs.peer == rhs.peer` (EnginePeer is `Equatable`; was Peer-protocol method).
- :42 — `item()` closure params `(Peer) -> Void``(EnginePeer) -> Void` and `(Peer, ASDisplayNode, ContextGesture?) -> Void``(EnginePeer, ASDisplayNode, ContextGesture?) -> Void`.
- :44 — `peer: EnginePeer(self.peer)``peer: self.peer` (drop wrap; `ItemListPeerItem.peer:` already takes `EnginePeer`).
- :54 — `preparedTransition()` closure params, same migration as :42.
- :232 — `GroupsInCommonListEntry(... peer: peer)``peer: EnginePeer(peer)` (boundary lift; `peer.peer` source is `RenderedPeer.peer: Peer?`).
- :236 — `self?.chatControllerInteraction.openPeer(EnginePeer(peer), …)``self?.chatControllerInteraction.openPeer(peer, …)` (drop wrap; `peer` is now `EnginePeer` from migrated closure param).
- :238 — `self?.openPeerContextAction(false, peer, node, gesture)``self?.openPeerContextAction(false, peer._asPeer(), node, gesture)` (internal bridge to still-Peer-typed stored field at PIGCP:68/109).
**Net bridge accounting:**
- DROPs (2): 1× `EnginePeer(...)` wrap (PIGCP:44 → ItemListPeerItem), 1× `EnginePeer(...)` wrap (PIGCP:236 → chatControllerInteraction.openPeer).
- ADDs (2): 1× boundary lift `EnginePeer(peer)` (PIGCP:232; `RenderedPeer.peer: Peer?` → struct field; correct/permanent), 1× internal `_asPeer()` bridge (PIGCP:238) to the still-`Peer`-typed stored `openPeerContextAction` field.
- Net internal bridges: **1**. Boundary lifts: **+1**.
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.454s; PeerInfoScreen + TelegramUI recompiled. Continues the first-pass-clean streak (waves 42, 45, 46, 48, 49, 50, 51 — 7 of last 9 waves first-pass-clean; waves 44 and 47 each took 2 iterations).
**Commit:** `f2b67a1b54` (7 edits in 1 file).
**Lessons:**
- **Narrow-wave shape works for struct-field migrations even when the field flows into closure params.** The internal closures (`item()`, `preparedTransition()`) can be migrated independently from the public init's stored closure field, by sandwiching the still-Postbox-typed boundary at the closure that captures the stored field. This adds one `_asPeer()` bridge at the boundary but keeps the wave atomic and first-pass-clean.
- **Boundary-lift accounting clarifies wave value.** Net internal-bridge count (1) is the correct progress metric; boundary lifts (+1) at `RenderedPeer.peer` are permanent until `RenderedPeer → EngineRenderedPeer` lands. The wave is a real ratchet step even with net-zero raw bridge count.
- **Memory-stored field name was wrong (memory said `PeerEntry`, actual was `GroupsInCommonListEntry`).** Verify struct identifiers at plan time. Doesn't affect the wave but should keep memory accurate for future planning.
---
## Wave 52 outcome (2026-04-25)
`PeerInfoPaneContainerNode.openPeerContextAction` closure parameter `Peer → EnginePeer` cascade across `PeerInfoPaneContainerNode` (PIPC), `PeerInfoGroupsInCommonPaneNode` (PIGCP), `PeerInfoRecommendedPeersPaneNode` (PIRP), and `PeerInfoScreen.swift` (PIS) — closes the wave-51 PIGCP:238 `_asPeer()` bridge and unifies the closure type across both sibling panes. 4 files, 8 type-site edits + 5 drops, **first-pass-clean**.
**Edits:**
- `PeerInfoPaneContainerNode.swift` (2): :411 init param closure type, :640 stored field closure type — both `(Bool, Peer, …) → (Bool, EnginePeer, …)`.
- `PeerInfoGroupsInCommonPaneNode.swift` (3): :68 stored field, :109 init param, :238 dropped `peer._asPeer()` (the wave-51 bridge — closed in this wave).
- `PeerInfoRecommendedPeersPaneNode.swift` (5): :68 inner item closure, :88 dropped `peer._asPeer()`, :94 `preparedTransition` closure, :119 stored field, :155 init param.
- `PeerInfoScreen.swift` (3): :1331 dropped `EnginePeer(peer)` wrap (`chatInterfaceInteraction.openPeer`), :1340 dropped `EnginePeer(peer)` wrap (`joinChannel(peer:)`), :1348 dropped `EnginePeer(peer)` wrap (second `chatInterfaceInteraction.openPeer`).
**Net bridge accounting:**
- DROPs (5): 2× internal `_asPeer()` (PIGCP:238 + PIRP:88), 3× `EnginePeer(peer)` wrap (PIS:1331 / :1340 / :1348).
- ADDs (0). The four forwarding closures (PIPC:10441045, PIRP:273274 / :303304, PIS:1319) all use parameter type inference, so the closure-type cascade ripples through automatically without explicit edits at those sites.
- Net internal bridges: **5**. No boundary lifts (the migration unifies closure types end-to-end inside the module; receiving APIs already accepted `EnginePeer`).
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel build 29.52s. Streak continues — 8 of last 10 waves first-pass-clean (42, 45, 46, 48, 49, 50, 51, 52; waves 44 and 47 each took 2 iterations).
**Commit:** `c86aa1aba3` (4 files, 13 insertions, 13 deletions).
**Lessons:**
- **Closure-type cascade as a wave-shape variant of struct-field migration.** When the migrating type is a closure parameter (not a struct field), Swift's parameter-type inference at every forwarding-closure site does the cascade automatically — no explicit edits needed at type-inferred sites. Wave 52 had 5 inferred-cascade sites (PIPC:1044, PIRP:273/303, PIS:1319) and only 8 explicit type-site edits (init params + stored fields). Compare to wave 50 (struct-field cascade with 19 explicit edits).
- **Pre-flight receiving-API verification is load-bearing for closure-type migrations.** Wave 52's plan verified `chatControllerInteraction.openPeer: (EnginePeer, …)` and `joinChannel(peer: EnginePeer)` BEFORE writing the wave, ensuring the 3 PIS body wraps would drop without compensating ADDs. If either API still took raw `Peer`, the migration would have re-introduced wraps inside the closure body, defeating the wave's purpose.
- **The wave-51 deferral was the correct call.** Wave 51 explicitly noted `openPeerContextAction: (Bool, Peer, …)` would NOT migrate within that wave's scope. Wave 52 closed it cleanly with a 4-file unit. This validates the wave-47 lesson: "memory-stored wave-N+1 candidate notes are load-bearing plan input." The deferral pre-classified the candidate, and wave 52 picked it up directly.
- **Bundled implementer dispatch with Haiku confirmed for closure-type cascades.** Wave 50 established Haiku-sufficiency for mechanical wave-shape edits (19 edits). Wave 52 reproduces with 13 edits — implementer applied all edits byte-perfectly first attempt, all greps passed, build first-pass-clean. Total cost ~56k implementer tokens.
---
## Wave 53 outcome (2026-04-25)
`PeerInfoScreenData.chatPeer: Peer? → EnginePeer?` field migration with deliberately narrow scope (defers `ClearPeerHistory.init.chatPeer: Peer` and `openClearHistory.chatPeer: Peer` to a future wave). 3 files / 14 insertions / 14 deletions / **first-pass-clean** Bazel ~29.5s.
**Edits:**
`PeerInfoData.swift` (PSD, 6 edits):
- :387 — field decl `let chatPeer: Peer? → let chatPeer: EnginePeer?`.
- :443 — init param `chatPeer: Peer? → chatPeer: EnginePeer?`.
- :1028, :1621, :1868, :2206 — boundary lifts at PSD-internal init call sites: `chatPeer: peer → chatPeer: peer.flatMap(EnginePeer.init)` (1028), `chatPeer: peerView.peers[peerId/groupId] → chatPeer: peerView.peers[...].flatMap(EnginePeer.init)` (1621/1868/2206). Each matches the sibling `peer:` line on the same construction.
`PeerInfoScreenOpenChat.swift` (PISOC, 2 edits):
- :25 — `chatLocation: .peer(EnginePeer(peer)) → chatLocation: .peer(peer)` (drop wrap).
- :89 — same pattern (drop wrap).
`PeerInfoScreenPerformButtonAction.swift` (PISPBA, 6 edits):
- :428 — `if let secretChat = chatPeer as? TelegramSecretChat → if case let .secretChat(secretChat) = chatPeer`.
- :431 — `} else if let group = chatPeer as? TelegramGroup → } else if case let .legacyGroup(group) = chatPeer`.
- :435 — `} else if let user = chatPeer as? TelegramUser → } else if case let .user(user) = chatPeer`.
- :439 — `} else if let channel = chatPeer as? TelegramChannel → } else if case let .channel(channel) = chatPeer`.
- :463 — `if let channel = chatPeer as? TelegramChannel → if case let .channel(channel) = chatPeer` (separate `if`, not in the else-if chain).
- :851 — `chatPeer: chatPeer → chatPeer: chatPeer._asPeer()` (1× ADD `_asPeer()` boundary bridge for unmigrated `ClearPeerHistory.init.chatPeer: Peer`).
**Net bridge accounting:**
- DROPs (2): `EnginePeer(peer)` wrap at PISOC:25 + PISOC:89.
- ADDs (1): `_asPeer()` at PISPBA:851 (boundary with unmigrated `ClearPeerHistory.init`).
- Boundary lifts (4): PSD:1028, :1621, :1868, :2206 — `Peer? → EnginePeer?` at the data-flow boundary into the migrated struct field. Permanent until upstream `RenderedPeer.peer / peerView.peers` migrate.
- Net internal bridges: **1**.
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel ~29.5s. Streak: 9 of last 11 waves first-pass-clean (42, 45, 46, 48, 49, 50, 51, 52, 53; waves 44 and 47 each took 2 iterations).
**Commit:** `438b4d7f46` (3 files, 14/14 line changes).
**Lessons:**
- **Narrow-scope is the right wave shape when bundling produces net-negative accounting.** Bundling wave 53 with `ClearPeerHistory.init` + `openClearHistory` migrations would have dropped 4 `EnginePeer(chatPeer)` wraps in `openClearHistory` body BUT added 46 `EnginePeer(...)` boundary lifts at PISPBA call sites (where `channel`/`group`/`user` Peer locals get passed). Pre-flight call-site classification turned this from "tempting bundle" into "net-negative bundle" — the deferral is correct.
- **`as? Telegram*` cluster conversion is mechanically safe even with `else if` chains.** 5 conversions in the same `.more` case body, all sharing the `chatPeer` scrutinee. Each `case let .x(y) = chatPeer` has the same scope semantics as the original `let y = chatPeer as? X``y` shadows nothing in the surrounding scope, control flow is preserved, and `else if` chains over EnginePeer's 4-case enum are equivalent to the original sequential `as?` casts.
- **Boundary-lift uniformity check.** Each `chatPeer:` PSD construction site already had a sibling `peer:` line one above with `peer.flatMap(EnginePeer.init)` or `peerView.peers[...].flatMap(EnginePeer.init)`. The wave-53 edits are byte-identical to the sibling pattern. Pre-flight verification ("does the sibling line use the same construction?") is the simplest signal that boundary lifts are idiomatic for the surrounding code.
- **Memory's "30+ sites / multi-iter" overcount.** Memory's wave-53 estimate captured the BUNDLED scope; narrow scope reduces to 14 sites / 1-iteration. Future memory updates should distinguish "bundled candidate site count" from "narrow candidate site count" so wave-N+1 budgets aren't anchored too high.
---
## Wave 54 outcome (2026-04-25)
`ClearPeerHistory.init.chatPeer` + `openClearHistory.chatPeer` `Peer → EnginePeer` bundled method-signature migration. Closes wave-53's deferred sibling. 2 files / 16 insertions / 16 deletions / **first-pass-clean** Bazel 31.484s. Commit `e3da090a7f`.
**Edits:**
`PeerInfoScreen.swift` (PIS, 10 edits):
- :3213 — `openClearHistory(... chatPeer: Peer) → ... chatPeer: EnginePeer)`.
- :3230, :3232, :3251, :3269 — drop 4 internal display-call wraps `EnginePeer(chatPeer).compactDisplayTitle → chatPeer.compactDisplayTitle` (the hot-path ratchet target).
- :7416 — `ClearPeerHistory.init(... chatPeer: Peer, cachedData:) → ... chatPeer: EnginePeer, cachedData:`.
- :7421 — `} else if chatPeer is TelegramSecretChat { → } else if case .secretChat = chatPeer {` (no-bind pattern; case body uses no fields from the secretChat).
- :7425 — `} else if let group = chatPeer as? TelegramGroup { → } else if case let .legacyGroup(group) = chatPeer {`.
- :7436 — `} else if let channel = chatPeer as? TelegramChannel { → } else if case let .channel(channel) = chatPeer {`.
- :7464 — `if let user = chatPeer as? TelegramUser, user.botInfo != nil { → if case let .user(user) = chatPeer, user.botInfo != nil {`.
`PeerInfoScreenPerformButtonAction.swift` (PISPBA, 6 edits):
- :851 — DROP wave-53 ADD: `chatPeer: chatPeer._asPeer() → chatPeer: chatPeer`.
- :857 — boundary lift: `chatPeer: user → chatPeer: EnginePeer(user)` (TelegramUser local from `case let .user(user)` upstream).
- :1067, :1073 — `chatPeer: channel → chatPeer: EnginePeer(channel)` (TelegramChannel locals).
- :1234, :1240 — `chatPeer: group → chatPeer: EnginePeer(group)` (TelegramGroup locals).
**Net bridge accounting:**
- DROPs (5): 4 internal `EnginePeer(chatPeer).compactDisplayTitle` wraps in PIS:openClearHistory body + 1 `_asPeer()` bridge at PISPBA:851 (the wave-53 ADD).
- ADDs (5): 5 boundary `EnginePeer(...)` lifts at PISPBA call sites.
- Conversions (4): `is`/`as?``case let` on PIS:7421/7425/7436/7464.
- Type-site (2): signature changes on PIS:3213 and PIS:7416.
- Net internal bridges: **0 raw count** — but the **ratchet kills 4 internal display-call wraps in the hot path** (PIS:3230/3232/3251/3269); only call-site boundary lifts remain, and those are permanent until upstream PISPBA sites get further migrated (e.g., to flow EnginePeer locals end-to-end).
**Build outcome:** 1 iteration (first-pass-clean). Full-project Bazel 31.484s. Streak: 10 of last 12 waves first-pass-clean (42, 45, 46, 48, 49, 50, 51, 52, 53, 54; waves 44 and 47 each took 2 iterations).
**Lessons:**
- **Bundled deferral closure cleanly inverts the wave-53 narrow-scope decision.** Wave 53 deferred this bundle because the call-site classification at that time was "5 boundary lifts vs. 4 wrap drops = net-negative" — but the analysis missed that closing wave-53's 1 ADD is also a drop. The actual full accounting at wave-54 time: 5 drops (4 wraps + wave-53 ADD) vs. 5 lifts (1 user + 2 channel + 2 group) = 0 raw count, with ratchet benefit of 4 hot-path wraps killed. Wave-by-wave deferral followed by closure is the right shape when the bundle's first half is independently valuable (wave 53 dropped 2 PISOC wraps and migrated the field type on its own merit).
- **`case .secretChat = chatPeer` (no-bind pattern) compiles cleanly under `-warnings-as-errors`.** No "unused binding" warning because there is no binding. The original `chatPeer is TelegramSecretChat` form is structurally similar — both are predicate checks with no name to discard.
- **`peer:` parameter unused in body is fine for `-warnings-as-errors`.** `openClearHistory(... peer: Peer, ...)` body never references `peer`; this passed wave 53 and continues to pass wave 54. Function-parameter unused warnings are not enabled in this codebase's compile flags. Confirms a wave-53 implicit assumption.
- **Bundled signature migration with mechanical `as?` cluster + small call-site count = 1-iteration target.** 2 files, 16 edits, 4 case-let conversions, 5 boundary lifts, 1 wave-53-ADD drop, 4 hot-path wrap drops. Full-project Bazel completed in one shot.
---
## Wave 55 outcome (2026-04-25)
`PeerInfoScreenViewControllerNode.deletePeerChat(peer:)` private-method first-arg `Peer → EnginePeer`. 1 file / 4 edits / **first-pass-clean** Bazel 29.284s. Commit `4818a0f090`.
**Edits:** all in `PeerInfoScreen.swift`:
- :4502 — drop `_asPeer()` at `openDeletePeer` ActionSheet button: `self?.deletePeerChat(peer: peer._asPeer(), globally: true) → self?.deletePeerChat(peer: peer, globally: true)`.
- :4556 — same drop at `openLeavePeer` TextAlertAction.
- :4564 — definition signature `peer: Peer → peer: EnginePeer`.
- :4573 — drop internal wrap inside body: `EngineRenderedPeer(peer: EnginePeer(peer)) → EngineRenderedPeer(peer: peer)` passed to `chatListController.maybeAskForPeerChatRemoval`.
**Net:** 3 drops, 0 adds. Both call sites' `peer` is `EnginePeer` (sourced from `engine.data.get(...Item.Peer.Peer(id:))` returning `Signal<EnginePeer?, NoError>`). Hot-path bridges eliminated end-to-end from delete/leave entry points down to `maybeAskForPeerChatRemoval`. Streak: 11 of last 13 first-pass-clean.
**Lessons:**
- **Single-file private-method migration with both call sites already EnginePeer = pure-drop wave shape.** The `_asPeer()` bridge at the call site exists *because* the receiving method was Peer-typed; once both endpoints are migrated, the bridge becomes deletable in the same wave. Pre-flight grep for `<method>(.*\._asPeer\(\))` patterns surfaces this shape immediately.
---
## Wave 56 outcome (2026-04-25)
`PeerInfoInteraction.openPeerInfo` closure-type and `PeerInfoScreenViewControllerNode.openPeerInfo` private-method first-arg `Peer → EnginePeer` cascade. 3 files / 7 edits / **first-pass-clean** Bazel 29.074s. Commit `31433fc1d4`.
**Edits:**
`PeerInfoInteraction.swift` (PII, 2 edits):
- :45 — `let openPeerInfo: (Peer, Bool) -> Void → (EnginePeer, Bool) -> Void`.
- :123 — same closure type in init param.
`PeerInfoScreen.swift` (PIS, 2 edits):
- :4304 — `private func openPeerInfo(peer: Peer, ...) → ... peer: EnginePeer`.
- :4306 — drop internal `EnginePeer(peer)` wrap (passed to `makePeerInfoController` which already takes EnginePeer post-wave-39).
- :1482 — boundary lift: `strongSelf.openPeerInfo(peer: member.peer, ...) → ... peer: EnginePeer(member.peer)` (`PeerInfoMember.peer` is still `Peer`, depends on RenderedPeer foundational migration).
`PeerInfoProfileItems.swift` (PIPI, 2 edits):
- :524 — drop `_asPeer()` bridge: `interaction.openPeerInfo(managedByBot._asPeer(), false) → interaction.openPeerInfo(managedByBot, false)` (`managedByBot` is EnginePeer? from PSD field).
- :860 — boundary lift: `interaction.openPeerInfo(member.peer, true) → interaction.openPeerInfo(EnginePeer(member.peer), true)`.
**Net:** 2 drops (PIS:4306 wrap + PIPI:524 _asPeer bridge), 2 boundary lifts (PIS:1482 + PIPI:860 at PeerInfoMember.peer source sites). Net raw count = 0 but 1 hot-path bridge eliminated. The 2 boundary lifts will close when `PeerInfoMember.peer` migrates (depends on RenderedPeer foundational session). Streak: 12 of last 14.
**Lessons:**
- **PIS:520-521 lambda forwarding `peer, isMember in self?.openPeerInfo(...)` needs no edit when the closure-field type migrates.** Swift parameter-type inference cascades from the migrated closure-field type to the lambda's parameter, and the lambda body forwards an EnginePeer to the migrated method. This is the same lesson as wave 52's PIPC.openPeerContextAction cascade — when the lambda's only role is to forward the parameter, the type re-bind is invisible at the lambda site.
- **Net-zero migrations are still worth doing when they consolidate a hot path.** Even though wave 56 has 2 drops and 2 adds, the 2 adds are at sources that will likely migrate later (PeerInfoMember.peer foundational), and the 2 drops include a hot-path `_asPeer()` ratchet at PIPI:524. The migration also makes the chain (`interaction.openPeerInfo → self.openPeerInfo → makePeerInfoController`) cleanly EnginePeer end-to-end, so future migrations can reason about it without bridge-hopping.
---
## Wave 57 outcome (2026-04-25)
Add `EnginePeer.isCopyProtectionEnabled` forwarding property in TelegramCore + drop 4 consumer-side `_asPeer().isCopyProtectionEnabled` bridges. 5 files / 5 edits / **first-pass-clean** Bazel 237.868s (cascade compile from TelegramCore touch). Commit `a5fc9fcf0e`.
**Edits:**
`TelegramCore/Sources/TelegramEngine/Peers/Peer.swift` (1 edit, 1 addition):
- Appended `var isCopyProtectionEnabled: Bool { return self._asPeer().isCopyProtectionEnabled }` to the existing `public extension EnginePeer { ... }` block (sibling to isDeleted, isScam, isVerified, isPremium).
Consumer drops (4 edits across 4 files):
- `PeerInfoEditingAvatarNode.swift:166``peer._asPeer().isCopyProtectionEnabled → peer.isCopyProtectionEnabled` (NativeVideoContent.captureProtected).
- `PeerInfoAvatarTransformContainerNode.swift:406` — same.
- `PeerInfoData.swift:2259``peerInfoIsCopyProtected(data:)` body.
- `PeerAvatarGalleryUI/Sources/AvatarGalleryItemFooterContentNode.swift:128``canShare = !peer.isCopyProtectionEnabled` (peer from `case let .image(_, _, _, _, peer, _, _, _, _, _, _, _) = entry`, which is `EnginePeer?`).
**Net:** 4 internal `_asPeer()` bridge drops; 0 adds. Other `_asPeer()` calls at adjacent lines (e.g., `PeerReference(peer._asPeer())` at PIED:159 / PIATCN:404) remain — `PeerReference.init` still takes raw Postbox Peer.
**Cascade compile:** full-project Bazel 237.868s (vs. ~30s typical) due to TelegramCore touch triggering downstream module rebuilds. Behavior parity verified by build success.
**Lessons:**
- **EnginePeer forwarding-extension addition is a high-leverage wave shape when ≥3 consumer sites use the same `_asPeer().<prop>` pattern.** Cost: 1 line in TelegramCore. Benefit: drops N consumer-side bridges (4 in this wave) and unlocks future call-site simplifications that pattern-match on the property. The wave-26 lesson "EnginePeer forwarding audit is load-bearing for multi-property methods" extends to "single-property forwarding is a wave shape on its own when the access-count threshold is met."
- **TelegramCore touches incur cascade-recompile cost (~210s extra wall-clock).** Worth budgeting; not a blocker. Future TelegramCore-touching waves should weight cascade time as part of wave-shape planning.
- **`EnginePeer` cases (.user/.legacyGroup/.channel/.secretChat) all preserve the underlying Postbox Peer's flag access via `_asPeer()`** — the forwarding implementation is a one-line wrapper. No case-by-case logic needed for properties that already exist on `Peer` protocol's extension. Rule of thumb: any `var <prop>` on `extension Peer` in PeerUtils.swift can be forwarded mechanically.
---
## Wave 58 outcome (2026-04-25)
`AccountContext.openAddPeerMembers` + `presentAddMembersImpl` cross-module `groupPeer: Peer → EnginePeer` migration. 6 files / 9 edits / **2 build iterations**. Commit `261c086c15`. Bazel 180.751s (iter 1, failure with 1 error) + 29.946s (iter 2, clean) = ~210s wall-clock.
**Edits:**
Protocol + impl (2 edits, 2 files):
- `AccountContext.swift:1456` — protocol method `groupPeer: Peer → groupPeer: EnginePeer`.
- `SharedAccountContext.swift:2351` — implementation signature.
`PresentAddMembers.swift` (4 edits, 1 file):
- :14 — public function signature.
- :38 — `if let group = groupPeer as? TelegramGroup → if case let .legacyGroup(group) = groupPeer`.
- :47 — `} else if let channel = groupPeer as? TelegramChannel, ... → } else if case let .channel(channel) = groupPeer, ...`.
- :210 — **iter-2 fix:** drop now-redundant `EnginePeer(groupPeer)` wrap inside body — `peer: EnginePeer(groupPeer) → peer: groupPeer` (after parameter is EnginePeer, wrapping it again doesn't compile).
Call sites (3 edits, 3 files):
- `PeerInfoScreen.swift:4606` — drop `_asPeer()`: `groupPeer: groupPeer._asPeer() → groupPeer: groupPeer` (`groupPeer = data.peer` is EnginePeer? post-wave-42).
- `ChatListUI/Sources/ChatListController.swift:3815` — drop `_asPeer()`: `groupPeer: peer._asPeer() → groupPeer: peer` (`peer` from `engine.data.get(...Item.Peer.Peer(id:))` is EnginePeer).
- `TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift:4006` — boundary lift: `groupPeer: peer → groupPeer: EnginePeer(peer)` (`peer` from `renderedPeer?.peer` is Postbox Peer; needed because `is TelegramGroup || is TelegramChannel` pre-check still runs against the Peer-typed local).
**Net:** 3 drops (2 `_asPeer()` at PIS:4606 + CLC:3815, 1 redundant `EnginePeer()` wrap at PAM:210), 1 boundary lift (CCLDN:4006), 2 case-let conversions in PAM body. Net internal-bridge progress: **2**.
**Iter-2 lesson — pre-flight scope must include "internal wraps inside the migrated function's body."** Pre-flight grep covered:
- `<func>(...<arg>:` call sites ✓
- `<arg> as? Telegram*` casts ✓
But missed:
- `EnginePeer(<arg>)` wraps INSIDE the migrated body — once the parameter type changes, those become invalid.
Adding `<arg>\b` (the parameter name) to the pre-flight grep would have caught the PAM:210 site. Cheap rule going forward: grep the body for `EnginePeer\(<paramname>\)` before declaring inventory complete.
**Lessons:**
- **Cross-module migration with protocol method = uniform 2-iter target unless body is small.** `AccountContext` protocol method touches 3 callers + 1 impl + 1 forwarding free function with internal body. Each axis can hide one missed reference. Wave 58 hit one such miss (PAM:210); this is typical. Budget 2 iters for protocol-method migrations even when all axes look complete in inventory.
- **Boundary-lift planning is correct when the call-site's source is foundational.** CCLDN:4006 boundary lift is permanent until `RenderedPeer.peer` migrates. Adding it now is the right shape — defers the foundational migration without inflating the wave to that scope.
- **Pure cross-module migrations don't always cascade-recompile through TelegramCore.** Wave 58 touched TelegramUI consumers + AccountContext protocol but NOT TelegramCore; iter-2 rebuild was 29.9s (typical). Compare to wave 57's 237.9s when TelegramCore was touched.
---
## Wave 59 outcome (2026-04-25)
`_asPeer() as? TelegramX` micro-cluster migration. 4 files / 7 edits / **first-pass-clean** Bazel 25.833s. Commit `6ca4058ae8`.
**Edits:**
- `PeerInfoHeaderEditingContentNode.swift:115``(peer?._asPeer() as? TelegramUser)?.lastName ?? ""``if case let .user(user) = peer { updateText = user.lastName ?? "" } else { updateText = "" }` (preserves `?? ""` fallback semantics on cast-failure path).
- `StarsTransactionsScreen.swift:1288``if let channel = subscription.peer._asPeer() as? TelegramChannel { ... }``if case let .channel(channel) = subscription.peer { ... }`.
- `StarsTransactionScreen.swift:280``if let creationDate = (subscription.peer._asPeer() as? TelegramChannel)?.creationDate, creationDate > 0 {``if case let .channel(channel) = subscription.peer, channel.creationDate > 0 { additionalDate = channel.creationDate ... }`. (TelegramChannel.creationDate is non-optional Int32.)
- `ChatListSearchContainerNode.swift:927``if let user = message.author?._asPeer() as? TelegramUser { ... }``if case let .user(user) = message.author { ... }` (message.author is EnginePeer?).
**Net:** 4 internal `_asPeer() as? TelegramX` cast bridges eliminated; 0 adds. PIHEC site adds explicit `else { updateText = "" }` to preserve original cast-failure fallback.
**Lessons:**
- **`_asPeer()` is a tag for "this value is EnginePeer" by construction.** `_asPeer()` only exists on `EnginePeer`; calling it on any other type (including `Peer` protocol) doesn't compile. So every consumer-side `X._asPeer()` is provably an EnginePeer source — no need to verify per-site. This invariant makes bulk `_asPeer()` cluster migrations safe.
- **Preserve cast-failure-path semantics when converting `as?` clusters.** Original `(X as? TelegramUser)?.lastName ?? ""` returns `""` if X is nil OR not TelegramUser OR lastName is nil. Naive `if case let .user(user) = X { updateText = user.lastName ?? "" }` only sets updateText for the .user case, leaving it nil for other cases. Add explicit `else` branch when the original `??` fallback is load-bearing.
---
## Wave 60 outcome (2026-04-25)
Add `PeerReference.init?(_ peer: EnginePeer)` convenience init in TelegramCore + drop **49** consumer-side `PeerReference(X._asPeer())` bridges across 29 files / 53 insertions / 49 deletions. **First-pass-clean** Bazel 238.803s (cascade compile). Commit `b6cc2bfbd1`.
**TelegramCore extension addition:**
`submodules/TelegramCore/Sources/ApiUtils/ApiUtils.swift` — appended `init?(_ peer: EnginePeer) { self.init(peer._asPeer()) }` to the existing `public extension PeerReference` block.
**Consumer drops:** 49 sites where `PeerReference(X._asPeer())` becomes `PeerReference(X)`. 12 distinct expression patterns: `$0`, `author`, `bot.peer`, `component.peer`, `component.slice.effectivePeer`, `component.slice.peer`, `item.peer`, `participantPeer`, `peer`, `peerValue`, `primary.1`, `self.peer`. All EnginePeer-typed by the `_asPeer()` invariant.
**Execution method:** Python regex replacement (`re.sub` with pattern `PeerReference\(([^)(]+)\._asPeer\(\)\)``PeerReference(\1)`). Non-`)`-non-`(` capture group ensures the match is the smallest expression before the trailing `._asPeer())`. Tested against all 12 distinct expression patterns including chained property accesses and closure-arg captures.
**Net:** 49 internal `_asPeer()` bridge drops; 0 adds. Largest single-wave consumer-drop count to date.
**Lessons:**
- **The wave-57 forwarding-extension pattern scales to bulk init/method addition.** Convenience init/method overload in TelegramCore (1 line) drops N consumer bridges (49 in this case). Cost: 1 line + bulk consumer migration. Benefit: O(N) bridges dropped. Threshold for triggering this shape is ≥5 consumer sites (anything fewer is per-site case-by-case).
- **Python regex with `[^)(]+` capture is the safe automation primitive for migrating `Wrapper(X._asPeer())` patterns.** Greedy `.*` captures cause over-match. Non-greedy `.*?` is fragile in BSD sed. The `[^)(]+` capture explicitly avoids both `(` and `)` inside the captured expression, which works for chained property accesses (`a.b.c`), closure args (`$0`), and tuple-indexed accesses (`primary.1`). 49 sites all replaced safely.
- **The `_asPeer()` invariant guarantees migration safety.** Because `_asPeer()` only compiles on EnginePeer, no per-site type verification is needed for `Wrapper(X._asPeer()) → Wrapper(X)` migrations once the EnginePeer-accepting overload is added.
---
## Wave 61 outcome (2026-04-25)
Drop 6 `_asPeer().X` consumer-side bridges where X is already on the EnginePeer forwarding extension. 3 files / 6 edits / **first-pass-clean** Bazel 19.351s. Commit `5d497cc5e9`.
**Edits:**
- `ChannelVisibilityController.swift:1478``peer?._asPeer().usernames``peer?.usernames`.
- `PeerInfoCoverComponent.swift:74``peer._asPeer().profileColor``peer.profileColor`.
- `PeerInfoCoverComponent.swift:82``peer._asPeer().nameColor``peer.nameColor`.
- `StoryItemSetContainerComponent.swift:6700/6956/7274``component.slice.effectivePeer._asPeer().usernames``component.slice.effectivePeer.usernames` (replace_all=true; 3 sites).
**Net:** 6 internal `_asPeer()` bridge drops; 0 adds. All 6 sites used properties (`usernames`, `profileColor`, `nameColor`) already present on `public extension EnginePeer` block in TelegramCore — no new TelegramCore touch needed (no cascade compile).
**Skipped:** `peer._asPeer().indexName.indexTokens` at `ChannelDiscussionGroupSearchContainerNode.swift:157``EnginePeer.indexName` returns wrapper enum `EnginePeer.IndexName` without `.indexTokens`. Needs a separate IndexName extension or consumer refactor. Defer.
---
## Wave 62 outcome (2026-04-25)
Add 4 EnginePeer forwarding entries (isMonoForum, associatedPeerId, hasCustomNameColor, hasSensitiveContent) + drop 5 consumer-side `_asPeer().X` bridges. 6 files / 9 edits / **first-pass-clean** Bazel 237.769s (cascade compile). Commit `cdce0dba01`.
**TelegramCore extension additions** (Peer.swift, 4 entries — 16 lines):
```swift
var isMonoForum: Bool {
return self._asPeer().isMonoForum
}
var associatedPeerId: Id? {
return self._asPeer().associatedPeerId
}
var hasCustomNameColor: Bool {
return self._asPeer().hasCustomNameColor
}
func hasSensitiveContent(platform: String) -> Bool {
return self._asPeer().hasSensitiveContent(platform: platform)
}
```
All four forwarding implementations live in `submodules/TelegramCore/Sources/Utils/PeerUtils.swift` on the `extension Peer { ... }` block. The `Id` typealias on EnginePeer is `PeerId`, so `associatedPeerId: Id?` is type-equivalent to the underlying `Peer.associatedPeerId: PeerId?`.
**Consumer drops** (5 sites):
- `ThemeSettingsController.swift:428``accountPeer._asPeer().hasCustomNameColor``accountPeer.hasCustomNameColor`.
- `AgeVerificationScreen.swift:31``peer._asPeer().hasSensitiveContent(platform: "ios")``peer.hasSensitiveContent(platform: "ios")`.
- `TextProcessingScreen.swift:1281``peer._asPeer().isMonoForum``peer.isMonoForum`.
- `ShareSearchContainerNode.swift:478``mainPeer._asPeer().associatedPeerId``mainPeer.associatedPeerId`.
- `ChatListSearchListPaneNode.swift:153``maybeChatPeer._asPeer().associatedPeerId``maybeChatPeer.associatedPeerId`.
**Net:** 5 internal bridge drops; 0 adds.
**Lessons:**
- **Bundle multi-property forwarding additions in a single TelegramCore touch.** When 4 properties each unblock 1-2 consumer drops, bundling the additions into one wave amortizes the cascade-recompile cost (~210s) across 5 drops instead of paying 4× across 4 separate waves.
---
## Wave 63 outcome (2026-04-25)
`resolvedAreStoriesMuted(peer:)` Peer → EnginePeer cross-module function-signature migration. 5 files / 11 edits / **first-pass-clean** Bazel 239.818s (cascade compile). Commit `499edc0ddb`.
**TelegramCore signature change** (`ChangePeerNotificationSettings.swift`):
- :65 — `peer: Peer → peer: EnginePeer`. Body uses only `peer.id` (works on EnginePeer).
- :117 — internal call site boundary lift: `peer: peer → peer: EnginePeer(peer)` (peer source is `transaction.getPeer(peerId)` returning Postbox Peer).
**Consumer drops** (9 sites, 4 files):
- `ContactContextMenus.swift:50` — drop `_asPeer()`.
- `ChatListController.swift:3317` — drop `_asPeer()`.
- `StoryItemSetContainerComponent.swift:7224` — drop `_asPeer()` on `component.slice.effectivePeer`.
- `StoryChatContent.swift` — 6 sites (lines 210, 212, 1287, 1599, 2490, 2492); pattern `peer: peer._asPeer(),``peer: peer,` via `replace_all=true`.
**Net:** 9 drops, 1 boundary lift = **8 internal bridges**.
**Lessons:**
- **Cross-module function-signature migration is a uniform 1-iter target when (a) the function body uses only EnginePeer-compatible properties and (b) all call sites have EnginePeer-typed sources via `_asPeer()`.** Pre-flight grep `<func>.*_asPeer` enumerates all consumer-side bridges; each drop is mechanical. Internal call sites in TelegramCore that pass Postbox Peer-typed locals get a boundary lift.
- **`replace_all=true` shines when the same parameter pattern repeats in a single file.** StoryChatContent had 6 identical `peer: peer._asPeer(),` patterns — one Edit call with `replace_all=true` replaced all of them. Saves 5 round-trips.
---
## Wave 64 outcome (2026-04-25)
`RenderedPeer.convenience init(peer: EnginePeer)` in TelegramCore + 5 consumer drops. 3 files / 6 edits / **first-pass-clean** Bazel 237.725s (cascade compile). Commit `109c2fe172`. Wave-shape: foundational TelegramCore extension addition (wave-57 pattern). Original `init(peer: Peer)` retained as designated init; Swift overload resolution selects the EnginePeer init only when the argument is EnginePeer-typed. Sites: PeerInfoSettingsItems:131 + ChatListSearchListPaneNode:4173/4213/4336/4371.
## Wave 65 outcome (2026-04-25)
`MergedAvatarsNode.update(peers: [Peer]) → [EnginePeer]` + private `PeerAvatarReference.init(peer:)` cascade. 6 files / 9 edits / **3 iter**. Commit `37c680c86c`. Iter-2 missed `groupsInCommon: [Peer]` field at ChatUserInfoItem; iter-3 missed `recentVoterPeers: [Peer]` and `avatarPeers: [Peer]` at ChatMessagePollBubbleContentNode. Lesson: **migrating a public function's array-of-Peer parameter forces a transitive inventory of *all* `[Peer]` field declarations across consumer modules**. Pre-flight grep should include `\[Peer\]\s*=\s*\[\]` and similar local/field decls.
## Wave 66 outcome (2026-04-25)
`VoiceChatJoinScreen.setPeer` + `VoiceChatPreviewContentNode.init` chain Peer→EnginePeer. Single file / 5 edits / first-pass-clean. Commit `148aa53f3f`. Net **3** internal bridges. Wave-55 single-file pure-drop pattern.
## Wave 67 outcome (2026-04-25)
`_internal_storedMessageFromSearchPeer` signature Peer→EnginePeer + return type Signal<Peer>→Signal<EnginePeer>. 3 files / 5 edits / first-pass-clean Bazel 226.884s. Commit `3732fb66b6`. Drops `peer._asPeer()` and `|> map { EnginePeer(result) }` in `ensurePeerIsLocallyAvailable`. Net **1** with 2 internal boundary lifts inside TelegramCore body.
## Wave 68 outcome (2026-04-25)
`SelectivePrivacyPeer.convenience init(peer: EnginePeer, participantCount:)` + 3 consumer drops. 4 files / 4 edits / first-pass-clean (initial 221s after TelegramCore touch + 17s consumer-only rebuild). Commit `73811a4e5d`. Original `init(peer: Peer)` and stored `peer: Peer` field unchanged — full migration of the field deferred (69 references repo-wide).
## Wave 69 outcome (2026-04-25)
`_internal_storedMessageFromSearchPeers` (plural) Peer→EnginePeer. Closes wave-67 sibling. 2 files / 3 edits / first-pass-clean Bazel 225.143s. Commit `42252eb9fd`. Drops `peers.map { $0._asPeer() }` in `ensurePeersAreLocallyAvailable`.
## Wave 70 outcome (2026-04-25)
`StatsMessageItem.peer` + `ChannelStatsController .post` enum-case payload + `MessageStatsController` local var Peer→EnginePeer cascade. 3 files / 9 edits / **3 iter**. Commit `f14dfe2273`. Iter-2 missed `entries.append(.post)` building sites at ChannelStatsController:1429/1433. Iter-3 missed `arePeersEqual(lhsPeer, rhsPeer)` Equatable comparison at L639 — replaced with `lhsPeer == rhsPeer` since EnginePeer is Equatable. **Pre-flight rule: when migrating an enum-case payload from Peer to EnginePeer, grep the same enum's `==` Equatable conformance for `arePeersEqual` calls.** Net **5** internal bridges.
## Wave 71 outcome (2026-04-25)
`peerInfoControllerImpl` signature Peer→EnginePeer + drop `_asPeer()` shadow at SAC:1938. 1 file / 6 edits / first-pass-clean Bazel 24.582s. Commit `1650bc1521`. Wave-shape: single-file private-function with shadow-assignment pattern (`let peer = peer._asPeer()` shadowing the EnginePeer parameter to a Peer local). Mechanical conversion of body's 4 `as?`/`is` checks. Cheap rebuild because no public API change.
## Wave 72 outcome (2026-04-25)
`canSendMessagesToPeer` body cleanup: drop `_asPeer()` shadow + restructure `as?` casts as exhaustive switch on EnginePeer cases. 1 file / 1 edit / first-pass-clean Bazel 25.792s. Commit `98e7158b7a`. Wave-shape: TelegramCore-internal body cleanup. Public API was already EnginePeer (wave 38) — this wave only refactors the implementation. Cheap rebuild despite TelegramCore touch.
## Wave 73 outcome (2026-04-25)
`ChatQrCodeScreenImpl.Subject.peer` enum-payload Peer→EnginePeer + `QrContentNode.peer` field migration + drop `_asPeer()` shadow at SAC:2731. 2 files / 8 edits / **2 iter**. Commit `0faf4a0336`. Iter-2 missed two additional `peer as? TelegramX` casts at QrContentNode body L1742/L1744; also fixed an over-aggressive `replace_all=true` that caught a different `peer: EnginePeer(peer)` site at MessageContentNode L2184 where the local `peer` is Postbox-typed (boundary lift restored).
**Lesson reinforced: `replace_all=true` on a parametric wrapping pattern (`peer: EnginePeer(peer)`) must verify ALL matches share the same scope/source.** When the same call appears in multiple scopes with different local-variable types, `replace_all` propagates the wrong intent. Mitigation: grep first to verify unique-scope assumption, or fall back to per-site Edit when unsure.
---
## 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,127 @@
# Wave 50 — `enclosingPeer` Peer? → EnginePeer?
**Date:** 2026-04-25
**Pattern:** struct-field + stored-form `Peer?``EnginePeer?` (wave-47/48 shape).
**Module:** `submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/` only — no public-API leaks.
## Goal
Migrate the PeerInfo members chain's `enclosingPeer` field from raw Postbox `Peer?` to `EnginePeer?`. Drops 2 `_asPeer()` demotions, 1 `EnginePeer(...)` wrap, 1 `flatMap(EnginePeer.init)` simplification, and 1 PSPB boundary `_asPeer()` lift. Closes the wave-48-pattern internal-demotion-and-external-re-promotion ratchet at PIMP:354363 (engine.data subscription returns `EnginePeer?`, currently demoted to `Peer?` at the storage boundary).
## Type changes
| File | Site | Before | After |
|---|---|---|---|
| `PeerInfoScreenMemberItem.swift:23` | stored `let enclosingPeer` | `Peer?` | `EnginePeer?` |
| `PeerInfoScreenMemberItem.swift:34` | init param | `Peer?` | `EnginePeer?` |
| `PeerInfoMembersPane.swift:92` | `func item(... enclosingPeer:)` | `Peer` | `EnginePeer` |
| `PeerInfoMembersPane.swift:271` | `func preparedTransition(... enclosingPeer:)` | `Peer` | `EnginePeer` |
| `PeerInfoMembersPane.swift:293` | `private var enclosingPeer` | `Peer?` | `EnginePeer?` |
| `PeerInfoMembersPane.swift:442` | `func updateState(enclosingPeer:)` | `Peer` | `EnginePeer` |
`PeerInfoScreenMemberItem` and `PeerInfoMembersPaneNode` are local to the module — no cross-module signature ripple.
## Edit patterns
### A. Conditional cast → case-let (wave-41/45 idiom)
| File:Line | Before | After |
|---|---|---|
| PSMI:152 | `if let channel = item.enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank)` | `if case let .channel(channel) = item.enclosingPeer, channel.hasPermission(.editRank)` |
| PSMI:154 | `else if let group = item.enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank)` | `else if case let .legacyGroup(group) = item.enclosingPeer, !group.hasBannedPermission(.banEditRank)` |
| PIMP:113 | `if let channel = enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank)` | `if case let .channel(channel) = enclosingPeer, channel.hasPermission(.editRank)` |
| PIMP:115 | `else if let group = enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank)` | `else if case let .legacyGroup(group) = enclosingPeer, !group.hasBannedPermission(.banEditRank)` |
The `case let` pattern binds `channel: TelegramChannel` / `group: TelegramGroup` directly — `.hasPermission(.editRank)` and `.hasBannedPermission(.banEditRank)` are class methods on the bound concrete types. No `_asPeer()` bridge needed.
### B. `is`-check → `case` (wave-41 always-false-warning fix)
| File:Line | Before | After |
|---|---|---|
| PSMI:181 | `if actions.contains(.promote) && item.enclosingPeer is TelegramChannel` | `if actions.contains(.promote), case .channel = item.enclosingPeer` |
| PSMI:187 | `if item.enclosingPeer is TelegramChannel` | `if case .channel = item.enclosingPeer` |
| PIMP:142 | `if actions.contains(.promote) && enclosingPeer is TelegramChannel` | `if actions.contains(.promote), case .channel = enclosingPeer` |
| PIMP:148 | `if enclosingPeer is TelegramChannel` | `if case .channel = enclosingPeer` |
PIMP:113/115/142/148 are inside `func item(... enclosingPeer: EnginePeer ...)`, so `enclosingPeer` is non-optional inside that body; PSMI sites are against `item.enclosingPeer: EnginePeer?`. `case let .channel(channel)` and `case .channel` both compile cleanly against optional and non-optional EnginePeer.
### C. Drop wraps / unwraps
| File:Line | Before | After |
|---|---|---|
| PSMI:178 | `peer: item.enclosingPeer.flatMap(EnginePeer.init)` | `peer: item.enclosingPeer` |
| PIMP:139 | `peer: EnginePeer(enclosingPeer)` | `peer: enclosingPeer` |
| PIMP:361 | `strongSelf.enclosingPeer = enclosingPeer._asPeer()` | `strongSelf.enclosingPeer = enclosingPeer` |
| PIMP:363 | `updateState(enclosingPeer: enclosingPeer._asPeer(), state: state, presentationData: presentationData)` | `updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData)` |
| PSPB:852 | `enclosingPeer: peer._asPeer()` | `enclosingPeer: peer` |
### D. No-op call sites (type flows through transparently)
- `PeerInfoSettingsItems.swift:132``enclosingPeer: nil` (nil literal works for any optional)
- `PeerInfoMembersPane.swift:275/276` — pass-through `enclosingPeer: enclosingPeer`
- `PeerInfoMembersPane.swift:437/438``if let enclosingPeer = self.enclosingPeer ... self.updateState(enclosingPeer: enclosingPeer, ...)` (both stored-form and `updateState` param shift to EnginePeer; type carries through)
- `PeerInfoMembersPane.swift:451` — pass-through
- `PeerInfoMembersPane.swift:485``self.enclosingPeer = enclosingPeer` (param and stored-form both EnginePeer)
- `PeerInfoScreenOpenMember.swift` — uses `self.data?.peer` (already `EnginePeer?` post-wave-42), unrelated to this migration
**Total edits:** 19 across 3 files (PSMI, PIMP, PSPB) — 6 type-change edits in the table at the top of this spec + 4 (Pattern A) + 4 (Pattern B) + 5 (Pattern C).
## Risk register
| Risk | Mitigation |
|---|---|
| `case .channel = item.enclosingPeer` against `EnginePeer?` semantics | Wave-45 lesson confirms `case let .x(y) = peer` compiles cleanly against `EnginePeer?`. Matches `.some(.channel)`, rejects `nil` and other cases — equivalent to `is TelegramChannel` semantics. |
| `if actions.contains(.promote), case .channel = ...` mixed boolean + pattern condition | Standard Swift if-case syntax (introduced in wave 41 idiom for this codebase). |
| Hidden Peer-only property access on bare `enclosingPeer` | Pre-flight grep complete: only access patterns are `.id` (EnginePeer has it), and cast-bound `channel.hasPermission` / `group.hasBannedPermission`. No `_asPeer()` bridges expected. |
| Closure capture aliases (wave-47 lesson) | Pre-flight grep covered `strongSelf.enclosingPeer` (PIMP:361) and `self.enclosingPeer` (PIMP:437/485). |
| `enclosingPeer: nil` literal at PSI:132 | `nil` is valid for any optional — no edit. |
| `availableActionsForMemberOfPeer` signature compatibility | Confirmed `EnginePeer?` at `PeerInfoData.swift:2314`. Both PSMI:178 and PIMP:139 are pure simplifications. |
| Always-false `is` check warning under `-warnings-as-errors` | Wave-41 lesson — handled by Pattern B. |
## Wave shape
**Classification:** cross-file private struct-field migration with stored-form ratchet (wave-47 taxonomy: "cross-file private").
**Iteration budget:** 12 (target first-pass-clean per wave 48/49 streak).
**Subagent dispatch:** not needed — 17 edits / 3 files is single-implementer scope.
## Verification
### Build
```sh
source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \
--cacheDir ~/telegram-bazel-cache \
build \
--configurationPath build-system/appstore-configuration.json \
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
--gitCodesigningType development --gitCodesigningUseCurrent \
--buildNumber=1 --configuration=debug_sim_arm64 --continueOnError
```
### Post-edit residue grep (expect empty)
```sh
grep -rnE "enclosingPeer\._asPeer|EnginePeer\(enclosingPeer\)|enclosingPeer\.flatMap\(EnginePeer" \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/
grep -rnE "enclosingPeer.*as\? TelegramChannel|enclosingPeer.*as\? TelegramGroup|enclosingPeer is TelegramChannel" \
submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/
```
## Net delta projection
- **Internal bridges:** 5 (2× `_asPeer()` at PIMP:361/363, 1× `EnginePeer(...)` at PIMP:139, 1× `flatMap(EnginePeer.init)` at PSMI:178, 1× boundary `_asPeer()` at PSPB:852).
- **Boundary lifts:** 0 net new — the source pipeline (engine.data subscription at PIMP:354) already yields `EnginePeer?`. Migration just removes the demote-then-promote dance.
- **ADD wraps:** 0 expected (no Peer-only property accesses on bare `enclosingPeer`).
## Out of scope
- `PeerInfoScreenData.chatPeer: Peer?` — large cascade (PSPB `as? TelegramX` × 5, ClearPeerHistory cascade, openClearHistory wraps × 4, PSOC × 2). Memory's wave-50 candidate Option 3, deferred for a multi-iteration wave.
- `PeerInfoGroupsInCommonPaneNode.PeerEntry.peer: Peer` — separate single-file migration, not bundled (wave-49 source-of-truth-coherence rule: unrelated chains stay in their own waves). Candidate for wave 51.
- `RenderedPeer → EngineRenderedPeer` foundational refactor — dedicated session.
## Memory file update
After landing, update `project_postbox_refactor_next_wave.md`:
- Move wave 50 outcome into the recent-waves list.
- Promote wave 51 candidate (`PeerInfoGroupsInCommonPaneNode.PeerEntry.peer` likely; otherwise re-scan the module with the standard grep).

View file

@ -111,18 +111,18 @@ public enum TextLinkItem: Equatable {
public final class AccountWithInfo: Equatable {
public let account: Account
public let peer: Peer
public init(account: Account, peer: Peer) {
public let peer: EnginePeer
public init(account: Account, peer: EnginePeer) {
self.account = account
self.peer = peer
}
public static func ==(lhs: AccountWithInfo, rhs: AccountWithInfo) -> Bool {
if lhs.account !== rhs.account {
return false
}
if !arePeersEqual(lhs.peer, rhs.peer) {
if lhs.peer != rhs.peer {
return false
}
return true
@ -1453,7 +1453,7 @@ public protocol SharedAccountContext: AnyObject {
completion: @escaping (UIImage?) -> Void,
completedWithUploadingImage: @escaping (UIImage, Signal<PeerInfoAvatarUploadStatus, NoError>) -> UIView?
)
func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, parentController: ViewController, groupPeer: Peer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable)
func openAddPeerMembers(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, parentController: ViewController, groupPeer: EnginePeer, selectAddMemberDisposable: MetaDisposable, addMemberDisposable: MetaDisposable)
func makeInstantPageController(context: AccountContext, message: Message, sourcePeerType: MediaAutoDownloadPeerType?) -> ViewController?
func makeInstantPageController(context: AccountContext, webPage: TelegramMediaWebpage, anchor: String?, sourceLocation: InstantPageSourceLocation) -> ViewController
func openChatWallpaper(context: AccountContext, message: Message, present: @escaping (ViewController, Any?) -> Void)

View file

@ -91,7 +91,7 @@ private final class ContentNode: ASDisplayNode {
self.addSubnode(self.clippedNode)
if let peer = peer {
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.lightGray.cgColor)
@ -328,7 +328,7 @@ public final class AnimatedAvatarSetView: UIView {
self.addSubview(self.clippedView)
if let peer = peer {
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
if let representation = peer.smallProfileImage, let signal = peerAvatarImage(account: context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: synchronousLoad) {
let image = generateImage(size, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.lightGray.cgColor)

View file

@ -1718,14 +1718,14 @@ public class AttachmentController: ViewController, MinimizableController {
|> deliverOnMainQueue).startStandalone(next: { bots in
for bot in bots {
for (name, file) in bot.icons {
if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) {
if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer) {
if case .placeholder = name {
let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())
if !FileManager.default.fileExists(atPath: path) {
let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in
let accountResource = context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true)
let fetchedFullSize = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource))
let fetchedFullSize = context.engine.resources.fetch(reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource), userLocation: .other, userContentType: MediaResourceUserContentType(file: file))
let fetchedFullSizeDisposable = fetchedFullSize.start()
let fullSizeDisposable = accountResource.start()

View file

@ -302,7 +302,7 @@ private final class AttachButtonComponent: CombinedComponent {
)
} else {
var fileReference: FileMediaReference?
if let peer = botPeer.flatMap({ PeerReference($0._asPeer())}), let imageFile = imageFile {
if let peer = botPeer.flatMap({ PeerReference($0)}), let imageFile = imageFile {
fileReference = .attachBot(peer: peer, media: imageFile)
}
@ -2065,7 +2065,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
if case let .app(bot) = type {
for (name, file) in bot.icons {
if [.default, .iOSAnimated, .iOSSettingsStatic, .placeholder].contains(name) {
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer._asPeer()) {
if self.iconDisposables[file.fileId] == nil, let peer = PeerReference(bot.peer) {
if case .placeholder = name {
let account = self.context.account
let path = account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation())

View file

@ -643,7 +643,7 @@ public final class AvatarNode: ASDisplayNode {
let parameters: AvatarNodeParameters
if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) {
if let peer = peer, let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) {
self.contents = nil
self.displaySuspended = true
self.imageReady.set(self.imageNode.contentReady)
@ -763,7 +763,7 @@ public final class AvatarNode: ASDisplayNode {
self.imageNode.view.mask = nil
}
if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer._asPeer()) {
if let imageCache = genericContext.imageCache as? DirectMediaImageCache, let peer, let smallProfileImage = peer.smallProfileImage, let peerReference = PeerReference(peer) {
if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: peer.profileImageRepresentations.first?.immediateThumbnailData, size: Int(displayDimensions.width * UIScreenScale), synchronous: synchronousLoad) {
if let image = result.image {
self.imageNode.contents = image.cgImage
@ -852,7 +852,7 @@ public final class AvatarNode: ASDisplayNode {
let account = account ?? genericContext.account
if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) {
if let peer = peer, let signal = peerAvatarImage(account: account, peerReference: PeerReference(peer), authorOfMessage: authorOfMessage, representation: representation, displayDimensions: displayDimensions, clipStyle: clipStyle, emptyColor: emptyColor, synchronousLoad: synchronousLoad, provideUnrounded: storeUnrounded, cutoutRect: cutoutRect) {
self.contents = nil
self.displaySuspended = true
self.imageReady.set(self.imageNode.contentReady)

View file

@ -126,8 +126,8 @@ public func peerAvatarCompleteImage(postbox: Postbox, network: Network, peer: En
thumbnailRepresentation = peer.profileImageRepresentations.first
}
if let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: thumbnailRepresentation, displayDimensions: size, clipStyle: clipStyle, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) {
if fullSize, let fullSizeSignal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) {
if let signal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer), authorOfMessage: nil, representation: thumbnailRepresentation, displayDimensions: size, clipStyle: clipStyle, blurred: blurred, inset: 0.0, emptyColor: nil, synchronousLoad: fullSize) {
if fullSize, let fullSizeSignal = peerAvatarImage(postbox: postbox, network: network, peerReference: PeerReference(peer), authorOfMessage: nil, representation: peer.profileImageRepresentations.last, displayDimensions: size, emptyColor: nil, synchronousLoad: true) {
iconSignal = combineLatest(.single(nil) |> then(signal), .single(nil) |> then(fullSizeSignal))
|> mapToSignal { thumbnailImage, fullSizeImage -> Signal<UIImage?, NoError> in
if let fullSizeImage = fullSizeImage {

View file

@ -206,7 +206,7 @@ public final class AvatarVideoNode: ASDisplayNode {
self.internalSize = size
if let markup = photo.emojiMarkup {
self.update(markup: markup, size: size, useAnimationNode: false)
} else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer._asPeer()) {
} else if let video = smallestVideoRepresentation(photo.videoRepresentations), let peerReference = PeerReference(peer) {
self.backgroundNode.image = nil
let videoId = photo.id?.id ?? peer.id.id._internalGetInt64Value()
@ -217,7 +217,7 @@ public final class AvatarVideoNode: ASDisplayNode {
self.videoContent = videoContent
self.videoFileDisposable?.dispose()
self.videoFileDisposable = fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .peer(peer.id), userContentType: .avatar, reference: videoFileReference.resourceReference(videoFileReference.media.resource)).startStrict()
self.videoFileDisposable = self.context.engine.resources.fetch(reference: videoFileReference.resourceReference(videoFileReference.media.resource), userLocation: .peer(peer.id), userContentType: .avatar).startStrict()
}
}
}

View file

@ -140,7 +140,7 @@ private final class MarkdownConversionContext {
)
case let .data(data, dimensions):
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max), size: Int64(data.count), isSecretRelated: false)
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
self.context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data)
let mediaId = self.nextMediaId(namespace: Namespaces.Media.LocalImage)
self.media[mediaId] = TelegramMediaImage(
@ -185,7 +185,7 @@ func markdownWebpage(context: AccountContext, file: FileMediaReference) -> (webP
guard #available(iOS 15.0, *) else {
return nil
}
guard let path = context.account.postbox.mediaBox.completedResourcePath(file.media.resource) else {
guard let path = context.engine.resources.completedResourcePath(id: EngineMediaResource.Id(file.media.resource.id)) else {
return nil
}
let fileURL = URL(fileURLWithPath: path)

View file

@ -472,7 +472,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
var entry: FetchManagerEntrySummary
var isRemoved: Bool = false
var statusDisposable: Disposable?
var status: MediaResourceStatus?
var status: EngineMediaResource.FetchStatus?
init(entry: FetchManagerEntrySummary) {
self.entry = entry
@ -522,7 +522,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
if context.statusDisposable == nil {
context.statusDisposable = (engine.account.postbox.mediaBox.resourceStatus(context.entry.resourceReference.resource)
context.statusDisposable = (engine.resources.status(resource: EngineMediaResource(context.entry.resourceReference.resource))
|> deliverOn(self.queue)).startStrict(next: { [weak self, weak context] status in
guard let strongSelf = self, let context = context else {
return
@ -3314,7 +3314,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
})
})))
let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers)
let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers)
items.append(.action(ContextMenuActionItem(text: isMuted ? self.presentationData.strings.StoryFeed_ContextNotifyOn : self.presentationData.strings.StoryFeed_ContextNotifyOff, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] _, f in
@ -3812,7 +3812,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
let selectAddMemberDisposable = MetaDisposable()
let addMemberDisposable = MetaDisposable()
context.sharedContext.openAddPeerMembers(context: context, updatedPresentationData: nil, parentController: sourceController, groupPeer: peer._asPeer(), selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable)
context.sharedContext.openAddPeerMembers(context: context, updatedPresentationData: nil, parentController: sourceController, groupPeer: peer, selectAddMemberDisposable: selectAddMemberDisposable, addMemberDisposable: addMemberDisposable)
})
})))
}

View file

@ -924,7 +924,7 @@ public final class ChatListSearchContainerNode: SearchDisplayControllerContentNo
}
var type: PeerType = .group
for message in messages {
if let user = message.author?._asPeer() as? TelegramUser {
if case let .user(user) = message.author {
if user.botInfo != nil && !user.id.isVerificationCodes {
type = .bot
} else {

View file

@ -150,7 +150,7 @@ private enum ChatListRecentEntry: Comparable, Identifiable {
let primaryPeer: EnginePeer
var chatPeer: EnginePeer?
let maybeChatPeer = EnginePeer(peer.peer.peers[peer.peer.peerId]!)
if case .secretChat = maybeChatPeer, let associatedPeerId = maybeChatPeer._asPeer().associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] {
if case .secretChat = maybeChatPeer, let associatedPeerId = maybeChatPeer.associatedPeerId, let associatedPeer = peer.peer.peers[associatedPeerId] {
primaryPeer = EnginePeer(associatedPeer)
chatPeer = maybeChatPeer
} else if case .channel = maybeChatPeer, let mainChannel = peer.peer.chatOrMonoforumMainPeer {
@ -4170,7 +4170,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
result.append(.peer(
index: result.count,
peer: RecentlySearchedPeer(
peer: RenderedPeer(peer: peer._asPeer()),
peer: RenderedPeer(peer: peer),
presence: nil,
notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }),
unreadCount: unreadCount,
@ -4210,7 +4210,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
result.append(.peer(
index: result.count,
peer: RecentlySearchedPeer(
peer: RenderedPeer(peer: peer._asPeer()),
peer: RenderedPeer(peer: peer),
presence: nil,
notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }),
unreadCount: 0,
@ -4333,7 +4333,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
result.append(.peer(
index: result.count,
peer: RecentlySearchedPeer(
peer: RenderedPeer(peer: peer._asPeer()),
peer: RenderedPeer(peer: peer),
presence: nil,
notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }),
unreadCount: unreadCount,
@ -4368,7 +4368,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
result.append(.peer(
index: result.count,
peer: RecentlySearchedPeer(
peer: RenderedPeer(peer: peer._asPeer()),
peer: RenderedPeer(peer: peer),
presence: nil,
notificationSettings: peerNotificationSettings.flatMap({ $0._asNotificationSettings() }),
unreadCount: 0,

View file

@ -18,7 +18,7 @@ public let sharedReactionStaticImage = Queue(name: "SharedReactionStaticImage",
public func reactionStaticImage(context: AccountContext, animation: TelegramMediaFile, pixelSize: CGSize, queue: Queue) -> Signal<EngineMediaResource.ResourceData, NoError> {
return context.engine.resources.custom(id: "\(animation.resource.id.stringRepresentation):reaction-static-\(pixelSize.width)x\(pixelSize.height)-v10", fetch: EngineMediaResource.Fetch {
return Signal { subscriber in
let fetchDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: MediaResourceReference.standalone(resource: animation.resource)).start()
let fetchDisposable = context.engine.resources.fetch(reference: MediaResourceReference.standalone(resource: animation.resource), userLocation: .other, userContentType: .image).start()
var customColor: UIColor?
if animation.isCustomTemplateEmoji {

View file

@ -47,7 +47,7 @@ func contactContextMenuItems(context: AccountContext, peerId: EnginePeer.Id, con
})
})))
let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer._asPeer(), peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers)
let isMuted = resolvedAreStoriesMuted(globalSettings: globalSettings._asGlobalNotificationSettings(), peer: peer, peerSettings: notificationSettings._asNotificationSettings(), topSearchPeers: topSearchPeers)
items.append(.action(ContextMenuActionItem(text: isMuted ? strings.StoryFeed_ContextNotifyOn : strings.StoryFeed_ContextNotifyOff, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor)

View file

@ -183,7 +183,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
self.maybeLoadContent()
self.setupStatus(context: context, resource: fileReference.media.resource)
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file).start())
}
}
@ -388,7 +388,7 @@ class ChatDocumentGalleryItemNode: ZoomableContentGalleryItemNode, WKNavigationD
case .Fetching:
context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(fileReference.media.resource.id))
case .Remote:
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file).start())
default:
break
}

View file

@ -322,7 +322,7 @@ class ChatExternalFileGalleryItemNode: GalleryItemNode {
case .Fetching:
context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(fileReference.media.resource.id))
case .Remote:
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: (self.message?.id.peerId).flatMap(MediaResourceUserLocation.peer) ?? .other, userContentType: .file).start())
default:
break
}

View file

@ -507,7 +507,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
self.zoomableContent = (largestSize.dimensions.cgSize, self.imageNode)
self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: imageReference.resourceReference(largestSize.resource)).start())
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: imageReference.resourceReference(largestSize.resource), userLocation: userLocation, userContentType: .image).start())
self.setupStatus(resource: largestSize.resource)
} else {
self._ready.set(.single(Void()))
@ -932,7 +932,7 @@ final class ChatImageGalleryItemNode: ZoomableContentGalleryItemNode {
}
self._rightBarButtonItems.set(.single(barButtonItems))
self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: fileReference.resourceReference(fileReference.media.resource)).start())
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: userLocation, userContentType: .image).start())
} else {
let _ = (chatMessageFileDatas(account: context.account, userLocation: userLocation, fileReference: fileReference, progressive: false, fetched: true)
|> mapToSignal { value -> Signal<UIImage?, NoError> in

View file

@ -3,7 +3,6 @@ import CoreLocation
import SwiftSignalKit
import StoreKit
import TelegramCore
import Postbox
import TelegramStringFormatting
import TelegramUIPreferences
import PersistentStringHash
@ -623,9 +622,9 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
}
let id = Int64.random(in: Int64.min ... Int64.max)
let fileResource = LocalFileMediaResource(fileId: id, size: Int64(receiptData.count), isSecretRelated: false)
engine.account.postbox.mediaBox.storeResourceData(fileResource.id, data: receiptData)
engine.resources.storeResourceData(id: EngineMediaResource.Id(fileResource.id), data: receiptData)
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: [])
let file = TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: id), partialReference: nil, resource: fileResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "application/text", size: Int64(receiptData.count), attributes: [.FileName(fileName: "Receipt.dat")], alternativeRepresentations: [])
let message: EnqueueMessage = .message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: file), threadId: nil, replyToMessageId: nil, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])
let _ = enqueueMessages(account: engine.account, peerId: engine.account.peerId, messages: [message]).start()

View file

@ -182,7 +182,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {
})
} else {
self.imageNode.setSignal(chatMessagePhoto(postbox: self.context.account.postbox, userLocation: userLocation, photoReference: imageReference), dispatchOnDisplayLink: false)
self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: imageReference.resourceReference(largestSize.resource)).start())
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: imageReference.resourceReference(largestSize.resource), userLocation: userLocation, userContentType: .image).start())
}
} else {
self._ready.set(.single(Void()))
@ -338,7 +338,7 @@ final class InstantImageGalleryItemNode: ZoomableContentGalleryItemNode {
if let (context, media) = self.contextAndMedia, let fileReference = media.concrete(TelegramMediaFile.self) {
if isVisible {
self.fetchDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: self.userLocation ?? .other, userContentType: .file, reference: fileReference.resourceReference(fileReference.media.resource)).start())
self.fetchDisposable.set(context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: self.userLocation ?? .other, userContentType: .file).start())
} else {
self.fetchDisposable.set(nil)
}

View file

@ -68,7 +68,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
self.addSubnode(self.videoNode)
if case let .file(file) = media.media {
self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start())
self.fetchedDisposable.set(context.engine.resources.fetch(reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource), userLocation: userLocation, userContentType: .video).start())
self.statusDisposable.set((context.engine.resources.status(resource: EngineMediaResource(file.resource)) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {

View file

@ -227,7 +227,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable
}
let primaryLanguageByCountry = configuration.nativeLanguageByCountry
return .single(SecureIdEncryptedFormData(form: form, primaryLanguageByCountry: primaryLanguageByCountry, accountPeer: accountPeer._asPeer(), servicePeer: servicePeer._asPeer()))
return .single(SecureIdEncryptedFormData(form: form, primaryLanguageByCountry: primaryLanguageByCountry, accountPeer: accountPeer, servicePeer: servicePeer))
}
}
|> deliverOnMainQueue).start(next: { [weak self] formData in
@ -268,7 +268,7 @@ public final class SecureIdAuthController: ViewController, StandalonePresentable
case .form:
break
case var .list(list):
list.accountPeer = accountPeer._asPeer()
list.accountPeer = accountPeer
list.primaryLanguageByCountry = primaryLanguageByCountry
list.encryptedValues = values
return .list(list)

View file

@ -303,7 +303,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
current.updateValues(formData.values)
contentNode = current
} else {
let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peer: EnginePeer(encryptedFormData.servicePeer), privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, openField: { [weak self] field in
let current = SecureIdAuthFormContentNode(theme: self.presentationData.theme, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder, peer: encryptedFormData.servicePeer, privacyPolicyUrl: encryptedFormData.form.termsUrl, form: formData, primaryLanguageByCountry: encryptedFormData.primaryLanguageByCountry, openField: { [weak self] field in
if let strongSelf = self {
switch field {
case .identity, .address:
@ -684,7 +684,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
var currentValue: SecureIdValueWithContext?
switch type {
case .phone:
if let peer = form.encryptedFormData?.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
if case let .user(peer)? = form.encryptedFormData?.accountPeer, let phone = peer.phone, !phone.isEmpty {
immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone))
}
currentValue = findValue(formData.values, key: .phone)?.1
@ -936,7 +936,7 @@ final class SecureIdAuthControllerNode: ViewControllerTracingNode {
deleteField(.phone)
} else {
var immediatelyAvailableValue: SecureIdValue?
if let peer = list.accountPeer as? TelegramUser, let phone = peer.phone, !phone.isEmpty {
if case let .user(peer)? = list.accountPeer, let phone = peer.phone, !phone.isEmpty {
immediatelyAvailableValue = .phone(SecureIdPhoneValue(phone: phone))
}
self.interaction.push(SecureIdPlaintextFormController(context: self.context, secureIdContext: secureIdContext, type: .phone, immediatelyAvailableValue: immediatelyAvailableValue, updatedValue: { value in

View file

@ -6,8 +6,8 @@ import TelegramCore
struct SecureIdEncryptedFormData {
let form: EncryptedSecureIdForm
let primaryLanguageByCountry: [String: String]
let accountPeer: Peer
let servicePeer: Peer
let accountPeer: EnginePeer
let servicePeer: EnginePeer
}
enum SecureIdAuthPasswordChallengeState {
@ -64,7 +64,7 @@ struct SecureIdAuthControllerFormState: Equatable {
}
struct SecureIdAuthControllerListState: Equatable {
var accountPeer: Peer?
var accountPeer: EnginePeer?
var twoStepEmail: String?
var verificationState: SecureIdAuthControllerVerificationState?
var encryptedValues: EncryptedAllSecureIdValues?
@ -73,7 +73,7 @@ struct SecureIdAuthControllerListState: Equatable {
var removingValues: Bool = false
static func ==(lhs: SecureIdAuthControllerListState, rhs: SecureIdAuthControllerListState) -> Bool {
if !arePeersEqual(lhs.accountPeer, rhs.accountPeer) {
if lhs.accountPeer != rhs.accountPeer {
return false
}
if let lhsTwoStepEmail = lhs.twoStepEmail, let rhsTwoStepEmail = rhs.twoStepEmail, lhsTwoStepEmail != rhsTwoStepEmail {

View file

@ -52,8 +52,8 @@ final class SecureIdAuthHeaderNode: ASDisplayNode {
func updateState(formData: SecureIdEncryptedFormData?, verificationState: SecureIdAuthControllerVerificationState) {
if let formData = formData {
self.serviceAvatarNode.setPeer(context: self.context, theme: self.theme, peer: EnginePeer(formData.servicePeer))
let titleData = self.strings.Passport_RequestHeader(EnginePeer(formData.servicePeer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder))
self.serviceAvatarNode.setPeer(context: self.context, theme: self.theme, peer: formData.servicePeer)
let titleData = self.strings.Passport_RequestHeader(formData.servicePeer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder))
let titleString = NSMutableAttributedString()
titleString.append(NSAttributedString(string: titleData.string, font: textFont, textColor: self.theme.list.freeTextColor))

View file

@ -233,11 +233,11 @@ public func normalizeEntries(_ entries: [AvatarGalleryEntry]) -> [AvatarGalleryE
public func initialAvatarGalleryEntries(account: Account, engine: TelegramEngine, peer: EnginePeer) -> Signal<[AvatarGalleryEntry]?, NoError> {
var initialEntries: [AvatarGalleryEntry] = []
if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer._asPeer()) {
if !peer.profileImageRepresentations.isEmpty, let peerReference = PeerReference(peer) {
initialEntries.append(.topImage(peer.profileImageRepresentations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource)) }), [], peer, nil, nil, nil))
}
guard let peerReference = PeerReference(peer._asPeer()) else {
guard let peerReference = PeerReference(peer) else {
return .single(initialEntries)
}
switch peer {
@ -283,7 +283,7 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account
var result: [AvatarGalleryEntry] = []
if photos.isEmpty {
result = initialEntries
} else if let peerReference = PeerReference(peer._asPeer()) {
} else if let peerReference = PeerReference(peer) {
var index: Int32 = 0
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) {
var initialMediaIds = Set<EngineMedia.Id>()
@ -339,13 +339,13 @@ public func fetchedAvatarGalleryEntries(engine: TelegramEngine, account: Account
let initialEntries = [firstEntry]
if photos.isEmpty {
result = initialEntries
if let lastEntry, let firstEntry = result.first, firstEntry.videoRepresentations.isEmpty, !lastEntry.videoRepresentations.isEmpty, avatarGalleryEntryMatchesImage(firstEntry, lastEntry), let peerReference = PeerReference(peer._asPeer()) {
if let lastEntry, let firstEntry = result.first, firstEntry.videoRepresentations.isEmpty, !lastEntry.videoRepresentations.isEmpty, avatarGalleryEntryMatchesImage(firstEntry, lastEntry), let peerReference = PeerReference(peer) {
let videoRepresentations = lastEntry.videoRepresentations.map {
VideoRepresentationWithReference(representation: $0, reference: MediaResourceReference.avatar(peer: peerReference, resource: $0.resource))
}
result[0] = avatarGalleryEntryWithVideoRepresentations(firstEntry, videoRepresentations: videoRepresentations, immediateThumbnailData: firstEntry.immediateThumbnailData ?? lastEntry.immediateThumbnailData)
}
} else if let peerReference = PeerReference(peer._asPeer()) {
} else if let peerReference = PeerReference(peer) {
var index: Int32 = 0
if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(peer.id.namespace) {
@ -846,7 +846,7 @@ public class AvatarGalleryController: ViewController, StandalonePresentableContr
} else {
}
case let .image(_, reference, _, _, _, _, _, _, _, _, _, _):
if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer._asPeer()) {
if self.peer.id == self.context.account.peerId, let peerReference = PeerReference(self.peer) {
if let reference = reference {
let _ = (self.context.engine.accountData.updatePeerPhotoExisting(reference: reference)
|> deliverOnMainQueue).start(next: { [weak self] photo in

View file

@ -125,7 +125,7 @@ final class AvatarGalleryItemFooterContentNode: GalleryFooterContentNode {
}
if let peer = peer {
canShare = !peer._asPeer().isCopyProtectionEnabled
canShare = !peer.isCopyProtectionEnabled
}
default:
break

View file

@ -185,7 +185,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
if let strongSelf = self, let entry = strongSelf.entry, !entry.representations.isEmpty {
let subject: ShareControllerSubject
var actionCompletionText: String?
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) {
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(peer) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
subject = .media(videoFileReference.abstract, nil)
actionCompletionText = strongSelf.presentationData.strings.Gallery_VideoSaved
@ -260,7 +260,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
self.zoomableContent = (largestSize.dimensions.cgSize, self.contentNode)
if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) {
self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference).start())
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: representations[largestIndex].reference, userLocation: .other, userContentType: .image).start())
}
var id: Int64
@ -274,7 +274,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
id = id &+ resource.photoId
}
}
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) {
if let video = entry.videoRepresentations.last, let peerReference = PeerReference(self.peer) {
if video != previousVideoRepresentations?.last {
let mediaManager = self.context.sharedContext.mediaManager
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: entry.immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
@ -610,7 +610,7 @@ final class PeerAvatarImageGalleryItemNode: ZoomableContentGalleryItemNode {
}
if let largestIndex = representations.firstIndex(where: { $0.representation == largestSize }) {
self.fetchDisposable.set(fetchedMediaResource(mediaBox: self.context.account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference).start())
self.fetchDisposable.set(self.context.engine.resources.fetch(reference: representations[largestIndex].reference, userLocation: .other, userContentType: .image).start())
}
default:
break

View file

@ -534,7 +534,7 @@ public final class PeerInfoAvatarListItemNode: ASDisplayNode {
self.didSetReady = true
self.isReady.set(.single(true))
}
} else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer._asPeer()) {
} else if let video = videoRepresentations.last, let peerReference = PeerReference(self.peer) {
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
let videoContent = NativeVideoContent(id: .profileVideo(id, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: fullSizeOnly, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, storeAfterDownload: nil)

View file

@ -1475,7 +1475,7 @@ public func channelVisibilityController(context: AccountContext, updatedPresenta
let (limits, premiumLimits) = data
let isPremium = accountPeer?.isPremium ?? false
let hasAdditionalUsernames = (peer?._asPeer().usernames.firstIndex(where: { !$0.flags.contains(.isEditable) }) ?? nil) != nil
let hasAdditionalUsernames = (peer?.usernames.firstIndex(where: { !$0.flags.contains(.isEditable) }) ?? nil) != nil
if let peers = peers {
let count = Int32(peers.count)

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AppBundle
@ -239,11 +238,11 @@ public final class ReactionNode: ASDisplayNode, ReactionItemNode {
strongSelf.animateInAnimationNode = nil
}
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.appearAnimation._parse().resource)).start()
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.stillAnimation._parse().resource)).start()
self.fetchStickerDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: item.listAnimation._parse().resource)).start()
self.fetchStickerDisposable = context.engine.resources.fetch(reference: .standalone(resource: item.appearAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start()
self.fetchStickerDisposable = context.engine.resources.fetch(reference: .standalone(resource: item.stillAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start()
self.fetchStickerDisposable = context.engine.resources.fetch(reference: .standalone(resource: item.listAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start()
if let applicationAnimation = item.applicationAnimation {
self.fetchFullAnimationDisposable = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: .standalone(resource: applicationAnimation._parse().resource)).start()
self.fetchFullAnimationDisposable = context.engine.resources.fetch(reference: .standalone(resource: applicationAnimation._parse().resource), userLocation: .other, userContentType: .sticker).start()
}
if self.isLocked {

View file

@ -794,7 +794,7 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
for (key, value) in list.settings {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
if value.storySettings != defaultStorySettings {
stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
stories[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
switch value.muteState {
@ -805,24 +805,24 @@ public func notificationsAndSoundsController(context: AccountContext, exceptions
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(peer) = peer, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer))
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(peer) = peer, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer))
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}

View file

@ -18,9 +18,9 @@ private final class BlockedPeersControllerArguments {
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
let addPeer: () -> Void
let removePeer: (PeerId) -> Void
let openPeer: (Peer) -> Void
init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (Peer) -> Void) {
let openPeer: (EnginePeer) -> Void
init(context: AccountContext, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void, addPeer: @escaping () -> Void, removePeer: @escaping (PeerId) -> Void, openPeer: @escaping (EnginePeer) -> Void) {
self.context = context
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
self.addPeer = addPeer
@ -41,7 +41,7 @@ private enum BlockedPeersEntryStableId: Hashable {
private enum BlockedPeersEntry: ItemListNodeEntry {
case add(PresentationTheme, String)
case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, ItemListPeerItemEditing, Bool)
case peerItem(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, EnginePeer, ItemListPeerItemEditing, Bool)
var section: ItemListSectionId {
switch self {
@ -86,7 +86,7 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
if lhsNameOrder != rhsNameOrder {
return false
}
if !lhsPeer.isEqual(rhsPeer) {
if lhsPeer != rhsPeer {
return false
}
if lhsEditing != rhsEditing {
@ -132,7 +132,7 @@ private enum BlockedPeersEntry: ItemListNodeEntry {
arguments.removePeer(peer.id)
})])
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: EnginePeer(peer), presence: nil, text: .none, label: .none, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
return ItemListPeerItem(presentationData: presentationData, systemStyle: .glass, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, context: arguments.context, peer: peer, presence: nil, text: .none, label: .none, editing: editing, revealOptions: revealOptions, switchValue: nil, enabled: enabled, selectable: true, sectionId: self.section, action: {
arguments.openPeer(peer)
}, setPeerIdWithRevealedOptions: { previousId, id in
arguments.setPeerIdWithRevealedOptions(previousId, id)
@ -195,7 +195,7 @@ private func blockedPeersControllerEntries(presentationData: PresentationData, s
var index: Int32 = 0
for peer in blockedPeersState.peers {
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, peer.peer!, ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.peerId == state.peerIdWithRevealedOptions), state.removingPeerId != peer.peerId))
entries.append(.peerItem(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, EnginePeer(peer.peer!), ItemListPeerItemEditing(editable: true, editing: state.editing, revealed: peer.peerId == state.peerIdWithRevealedOptions), state.removingPeerId != peer.peerId))
index += 1
}
}
@ -267,7 +267,7 @@ public func blockedPeersController(context: AccountContext, blockedPeersContext:
}
}))
}, openPeer: { peer in
if let controller = context.sharedContext.makePeerInfoController(context: context, updatedPresentationData: nil, peer: EnginePeer(peer), 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)
}
})

View file

@ -340,7 +340,7 @@ public func incomingMessagePrivacyScreen(context: AccountContext, value: GlobalP
}
}
updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount)
updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer, participantCount: participantCount)
}
}
return updatedPeers

View file

@ -3,7 +3,6 @@ import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -36,7 +35,7 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem {
let dateTimeFormat: PresentationDateTimeFormat
let nameDisplayOrder: PresentationPersonNameOrder
let website: WebAuthorization
let peer: Peer?
let peer: EnginePeer?
let enabled: Bool
let editing: Bool
let revealed: Bool
@ -45,7 +44,7 @@ final class ItemListWebsiteItem: ListViewItem, ItemListItem {
let removeSession: (Int64) -> Void
let action: (() -> Void)?
init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) {
init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool, sectionId: ItemListSectionId, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, action: (() -> Void)?) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
@ -229,8 +228,8 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
let rightInset: CGFloat = params.rightInset
if let user = item.peer as? TelegramUser {
titleAttributedString = NSAttributedString(string: EnginePeer(user).displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder), font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
if let peer = item.peer, case .user = peer {
titleAttributedString = NSAttributedString(string: peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.nameDisplayOrder), font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
}
var appString = ""
@ -332,7 +331,7 @@ class ItemListWebsiteItemNode: ItemListRevealOptionsItemNode {
}
if let peer = item.peer {
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(peer), authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false)
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, authorOfMessage: nil, overrideImage: nil, emptyColor: nil, clipStyle: .none, synchronousLoad: false)
}
let revealOffset = strongSelf.revealOffset

View file

@ -21,7 +21,7 @@ private final class RecentSessionsControllerArguments {
let terminateOtherSessions: () -> Void
let openSession: (RecentAccountSession) -> Void
let openWebSession: (WebAuthorization, Peer?) -> Void
let openWebSession: (WebAuthorization, EnginePeer?) -> Void
let removeWebSession: (Int64) -> Void
let terminateAllWebSessions: () -> Void
@ -34,7 +34,7 @@ private final class RecentSessionsControllerArguments {
let openDesktopLink: () -> Void
let openWebLink: () -> Void
init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openWebSession: @escaping (WebAuthorization, Peer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) {
init(context: AccountContext, setSessionIdWithRevealedOptions: @escaping (Int64?, Int64?) -> Void, removeSession: @escaping (Int64) -> Void, terminateOtherSessions: @escaping () -> Void, openSession: @escaping (RecentAccountSession) -> Void, openWebSession: @escaping (WebAuthorization, EnginePeer?) -> Void, removeWebSession: @escaping (Int64) -> Void, terminateAllWebSessions: @escaping () -> Void, addDevice: @escaping () -> Void, openOtherAppsUrl: @escaping () -> Void, setupAuthorizationTTL: @escaping () -> Void, openDesktopLink: @escaping () -> Void, openWebLink: @escaping () -> Void) {
self.context = context
self.setSessionIdWithRevealedOptions = setSessionIdWithRevealedOptions
self.removeSession = removeSession
@ -117,7 +117,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
case otherSessionsHeader(SortIndex, String)
case addDevice(SortIndex, String)
case session(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, session: RecentAccountSession, enabled: Bool, editing: Bool, revealed: Bool)
case website(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: Peer?, enabled: Bool, editing: Bool, revealed: Bool)
case website(index: Int32, sortIndex: SortIndex, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, website: WebAuthorization, peer: EnginePeer?, enabled: Bool, editing: Bool, revealed: Bool)
case devicesInfo(SortIndex, String)
case ttlHeader(SortIndex, String)
case ttlTimeout(SortIndex, String, String)
@ -296,7 +296,7 @@ private enum RecentSessionsEntry: ItemListNodeEntry {
return false
}
case let .website(lhsIndex, lhsSortIndex, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsWebsite, lhsPeer, lhsEnabled, lhsEditing, lhsRevealed):
if case let .website(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, arePeersEqual(lhsPeer, rhsPeer), lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed {
if case let .website(rhsIndex, rhsSortIndex, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsWebsite, rhsPeer, rhsEnabled, rhsEditing, rhsRevealed) = rhs, lhsIndex == rhsIndex, lhsSortIndex == rhsSortIndex, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsNameOrder == rhsNameOrder, lhsWebsite == rhsWebsite, lhsPeer == rhsPeer, lhsEnabled == rhsEnabled, lhsEditing == rhsEditing, lhsRevealed == rhsRevealed {
return true
} else {
return false
@ -562,7 +562,7 @@ private func recentSessionsControllerEntries(presentationData: PresentationData,
let website = websites[i]
if !existingSessionIds.contains(website.hash) {
existingSessionIds.insert(website.hash)
entries.append(.website(index: Int32(i), sortIndex: SortIndex(section: 1, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, website: website, peer: peers[website.botId], enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash))
entries.append(.website(index: Int32(i), sortIndex: SortIndex(section: 1, item: i), strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, website: website, peer: peers[website.botId].flatMap(EnginePeer.init), enabled: state.removingSessionId != website.hash && !state.terminatingOtherSessions, editing: state.editing, revealed: state.sessionIdWithRevealedOptions == website.hash))
}
}
}
@ -727,7 +727,7 @@ public func recentSessionsController(context: AccountContext, activeSessionsCont
})
presentControllerImpl?(controller, nil)
}, openWebSession: { session, peer in
let controller = RecentSessionScreen(context: context, subject: .website(session, peer.flatMap(EnginePeer.init)), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in
let controller = RecentSessionScreen(context: context, subject: .website(session, peer), updateAcceptSecretChats: { _ in }, updateAcceptIncomingCalls: { _ in }, remove: { completion in
removeWebSessionImpl(session.hash)
completion()
})

View file

@ -1478,7 +1478,7 @@ public func selectivePrivacySettingsController(
}
}
updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount)
updatedPeers[peer.id] = SelectivePrivacyPeer(peer: peer, participantCount: participantCount)
}
}
return updatedPeers

View file

@ -475,7 +475,7 @@ public func selectivePrivacyPeersController(context: AccountContext, title: Stri
}
}
updatedPeers.append(SelectivePrivacyPeer(peer: peer._asPeer(), participantCount: participantCount))
updatedPeers.append(SelectivePrivacyPeer(peer: peer, participantCount: participantCount))
}
}
return updatedPeers

View file

@ -1551,7 +1551,7 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
for (key, value) in list.settings {
if let peer = list.peers[key], !peer.debugDisplayTitle.isEmpty, peer.id != context.account.peerId {
if value.storySettings != defaultStorySettings {
stories[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
stories[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
switch value.muteState {
@ -1562,24 +1562,24 @@ private func notificationSearchableItems(context: AccountContext, settings: Glob
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(peer) = peer, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer))
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}
default:
switch key.namespace {
case Namespaces.Peer.CloudUser:
users[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
users[key] = NotificationExceptionWrapper(settings: value, peer: peer)
default:
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
if case let .channel(peer) = peer, case .broadcast = peer.info {
channels[key] = NotificationExceptionWrapper(settings: value, peer: .channel(peer))
} else {
groups[key] = NotificationExceptionWrapper(settings: value, peer: EnginePeer(peer))
groups[key] = NotificationExceptionWrapper(settings: value, peer: peer)
}
}
}

View file

@ -3,7 +3,6 @@ import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -1230,7 +1229,7 @@ public func themePickerController(context: AccountContext, focusOnItemTag: Theme
wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings)
|> mapToSignal { cachedWallpaper in
if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper {
let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start()
let _ = context.engine.resources.fetch(reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource), userLocation: .other, userContentType: .other).start()
return .single(wallpaper)

View file

@ -276,7 +276,7 @@ private final class ThemeGridThemeItemIconNode : ASDisplayNode {
animatedStickerNode.autoplay = true
animatedStickerNode.visibility = true
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start())
self.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .other).start())
let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize)

View file

@ -3,7 +3,6 @@ import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -425,7 +424,7 @@ private func themeSettingsControllerEntries(
var authorName = presentationData.strings.Appearance_PreviewReplyAuthor
if let accountPeer {
nameColor = accountPeer.nameColor ?? .preset(.blue)
if accountPeer._asPeer().hasCustomNameColor {
if accountPeer.hasCustomNameColor {
authorName = accountPeer.displayTitle(strings: strings, displayOrder: presentationData.nameDisplayOrder)
}
profileColor = accountPeer.effectiveProfileColor
@ -1323,7 +1322,7 @@ public func themeSettingsController(context: AccountContext, focusOnItemTag: The
wallpaperSignal = cachedWallpaper(account: context.account, slug: file.slug, settings: colorWallpaper.settings)
|> mapToSignal { cachedWallpaper in
if let wallpaper = cachedWallpaper?.wallpaper, case let .file(file) = wallpaper {
let _ = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource)).start()
let _ = context.engine.resources.fetch(reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource), userLocation: .other, userContentType: .other).start()
return .single(wallpaper)

View file

@ -299,18 +299,18 @@ public final class ShareControllerAppEnvironment: ShareControllerEnvironment {
public final class ShareControllerSwitchableAccount: Equatable {
public let account: ShareControllerAccountContext
public let peer: Peer
public init(account: ShareControllerAccountContext, peer: Peer) {
public let peer: EnginePeer
public init(account: ShareControllerAccountContext, peer: EnginePeer) {
self.account = account
self.peer = peer
}
public static func ==(lhs: ShareControllerSwitchableAccount, rhs: ShareControllerSwitchableAccount) -> Bool {
if lhs.account !== rhs.account {
return false
}
if !arePeersEqual(lhs.peer, rhs.peer) {
if lhs.peer != rhs.peer {
return false
}
return true
@ -1147,8 +1147,8 @@ public final class ShareController: ViewController {
accountPeerId: info.account.accountPeerId,
stateManager: info.account.stateManager,
contentSettings: info.account.contentSettings,
peer: EnginePeer(info.peer),
title: EnginePeer(info.peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder),
peer: info.peer,
title: info.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder),
isSelected: info.account.accountId == strongSelf.currentContext.accountId,
strings: presentationData.strings,
theme: presentationData.theme,

View file

@ -232,7 +232,7 @@ final class SharePeersContainerNode: ASDisplayNode, ShareContentContainerNode {
network: context.stateManager.network,
contentSettings: context.contentSettings,
theme: theme,
peer: EnginePeer(info.peer),
peer: info.peer,
emptyColor: nil,
synchronousLoad: false
)

View file

@ -475,7 +475,7 @@ final class ShareSearchContainerNode: ASDisplayNode, ShareContentContainerNode {
var index = 0
for (peer, requiresPremiumForMessaging) in recentPeerList {
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))
recentItemList.append(.peer(index: index, theme: theme, peer: mainPeer, associatedPeer: mainPeer.associatedPeerId.flatMap { peer.peers[$0] }, presence: nil, requiresPremiumForMessaging: requiresPremiumForMessaging, requiresStars: nil, strings: strings))
index += 1
}
}

View file

@ -205,7 +205,7 @@ private enum StatsEntry: ItemListNodeEntry {
case instantPageInteractionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case postsTitle(PresentationTheme, String)
case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Peer, StatsPostItem, ChannelStatsPostInteractions)
case post(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, EnginePeer, StatsPostItem, ChannelStatsPostInteractions)
case boostLevel(PresentationTheme, Int32, Int32, CGFloat)
@ -636,7 +636,7 @@ private enum StatsEntry: ItemListNodeEntry {
return false
}
case let .post(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsPeer, lhsPost, lhsInteractions):
if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPost, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, arePeersEqual(lhsPeer, rhsPeer), lhsPost == rhsPost, lhsInteractions == rhsInteractions {
if case let .post(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsPeer, rhsPost, rhsInteractions) = rhs, lhsIndex == rhsIndex, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsPeer == rhsPeer, lhsPost == rhsPost, lhsInteractions == rhsInteractions {
return true
} else {
return false
@ -960,7 +960,7 @@ private enum StatsEntry: ItemListNodeEntry {
}, sectionId: self.section, style: .blocks)
case let .post(_, _, _, _, peer, post, interactions):
return StatsMessageItem(context: arguments.context, presentationData: presentationData, peer: peer, item: post, views: interactions.views, reactions: interactions.reactions, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
arguments.openPostStats(EnginePeer(peer), post)
arguments.openPostStats(peer, post)
}, openStory: { sourceView in
if case let .story(_, story) = post {
arguments.openStory(story, sourceView)
@ -1426,11 +1426,11 @@ private func statsEntries(
switch post {
case let .message(message):
if let interactions = interactions[.message(id: message.id)] {
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions))
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, post, interactions))
}
case let .story(_, story):
if let interactions = interactions[.story(peerId: peer.id, id: story.id)] {
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer._asPeer(), post, interactions))
entries.append(.post(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, peer, post, interactions))
}
}
index += 1

View file

@ -198,10 +198,10 @@ private enum StatsEntry: ItemListNodeEntry {
var reactions: Int32 = 0
var isStory = false
let peer: Peer
let peer: EnginePeer
switch item {
case let .message(message):
peer = message.peers[message.id.peerId]!
peer = EnginePeer(message.peers[message.id.peerId]!)
for attribute in message.attributes {
if let viewsAttribute = attribute as? ViewCountMessageAttribute {
views = Int32(viewsAttribute.count)
@ -214,7 +214,7 @@ private enum StatsEntry: ItemListNodeEntry {
}
}
case let .story(peerValue, story):
peer = peerValue._asPeer()
peer = peerValue
views = Int32(story.views?.seenCount ?? 0)
forwards = Int32(story.views?.forwardCount ?? 0)
reactions = Int32(story.views?.reactedCount ?? 0)

View file

@ -19,7 +19,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem {
let context: AccountContext
let presentationData: ItemListPresentationData
let systemStyle: ItemListSystemStyle
let peer: Peer
let peer: EnginePeer
let item: StatsPostItem
let views: Int32
let reactions: Int32
@ -31,7 +31,7 @@ public class StatsMessageItem: ListViewItem, ItemListItem {
let openStory: (UIView) -> Void
let contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?
init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .glass, peer: Peer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, isPeer: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, openStory: @escaping (UIView) -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
init(context: AccountContext, presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .glass, peer: EnginePeer, item: StatsPostItem, views: Int32, reactions: Int32, forwards: Int32, isPeer: Bool = false, sectionId: ItemListSectionId, style: ItemListStyle, action: (() -> Void)?, openStory: @escaping (UIView) -> Void, contextAction: ((ASDisplayNode, ContextGesture?) -> Void)?) {
self.context = context
self.presentationData = presentationData
self.systemStyle = systemStyle
@ -351,7 +351,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
}
if item.isPeer {
text = EnginePeer(item.peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
text = item.peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
} else {
text = foldLineBreaks(text)
}
@ -476,7 +476,7 @@ final class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.offsetContainerNode.addSubnode(avatarNode)
strongSelf.avatarNode = avatarNode
}
avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: EnginePeer(item.peer))
avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: item.peer)
if case .story = item.item {
contentImageInset += 3.0

View file

@ -626,7 +626,7 @@ final class CallControllerNodeV2: ViewControllerTracingNode, CallControllerNodeP
self.peerAvatarDisposable?.dispose()
let size = CGSize(width: 128.0, height: 128.0)
if let representation = peer.largeProfileImage, let signal = peerAvatarImage(account: self.call.context.account, peerReference: PeerReference(peer._asPeer()), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: self.callScreenState?.avatarImage == nil) {
if let representation = peer.largeProfileImage, let signal = peerAvatarImage(account: self.call.context.account, peerReference: PeerReference(peer), authorOfMessage: nil, representation: representation, displayDimensions: size, synchronousLoad: self.callScreenState?.avatarImage == nil) {
self.peerAvatarDisposable = (signal
|> deliverOnMainQueue).startStrict(next: { [weak self] imageVersions in
guard let self else {

View file

@ -350,7 +350,7 @@ final class VideoChatParticipantVideoComponent: Component {
if self.blurredAvatarDisposable == nil {
//TODO:release synchronous
if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer._asPeer()) {
if let participantPeer = component.participant.peer, let imageCache = component.call.accountContext.imageCache as? DirectMediaImageCache, let peerReference = PeerReference(participantPeer) {
if let result = imageCache.getAvatarImage(peer: peerReference, resource: MediaResourceReference.avatar(peer: peerReference, resource: smallProfileImage.resource), immediateThumbnail: participantPeer.profileImageRepresentations.first?.immediateThumbnailData, size: 64, synchronous: false) {
if let image = result.image {
blurredAvatarView.image = blurredAvatarImage(image)

View file

@ -551,7 +551,7 @@ extension VideoChatScreenComponent.View {
let peerId = callState.myPeerId
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
currentCall.accountContext.account.postbox.mediaBox.storeResourceData(resource.id, data: data)
currentCall.accountContext.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
self.currentUpdatingAvatar = (representation, 0.0)
@ -592,7 +592,7 @@ extension VideoChatScreenComponent.View {
let peerId = callState.myPeerId
let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
currentCall.accountContext.account.postbox.mediaBox.storeResourceData(photoResource.id, data: data)
currentCall.accountContext.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data)
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)
self.currentUpdatingAvatar = (representation, 0.0)

View file

@ -139,7 +139,7 @@ public final class VoiceChatJoinScreen: ViewController {
strongSelf.dismiss()
strongSelf.join(activeCall)
} else {
strongSelf.controllerNode.setPeer(call: activeCall, peer: peer._asPeer(), title: call.info.title, memberCount: call.info.participantCount, isStream: call.info.isStream)
strongSelf.controllerNode.setPeer(call: activeCall, peer: peer, title: call.info.title, memberCount: call.info.participantCount, isStream: call.info.isStream)
}
} else {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
@ -622,7 +622,7 @@ public final class VoiceChatJoinScreen: ViewController {
}))
}
func setPeer(call: CachedChannelData.ActiveCall, peer: Peer, title: String?, memberCount: Int, isStream: Bool) {
func setPeer(call: CachedChannelData.ActiveCall, peer: EnginePeer, title: String?, memberCount: Int, isStream: Bool) {
self.call = call
let transition = ContainedViewLayoutTransition.animated(duration: 0.22, curve: .easeInOut)
@ -647,7 +647,7 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod
private let titleNode: ImmediateTextNode
private let countNode: ImmediateTextNode
init(context: AccountContext, peer: Peer, title: String?, memberCount: Int, isStream: Bool, theme: PresentationTheme, strings: PresentationStrings, displayOrder: PresentationPersonNameOrder) {
init(context: AccountContext, peer: EnginePeer, title: String?, memberCount: Int, isStream: Bool, theme: PresentationTheme, strings: PresentationStrings, displayOrder: PresentationPersonNameOrder) {
self.avatarNode = AvatarNode(font: avatarFont)
self.titleNode = ImmediateTextNode()
self.titleNode.maximumNumberOfLines = 4
@ -659,10 +659,10 @@ final class VoiceChatPreviewContentNode: ASDisplayNode, ShareContentContainerNod
super.init()
self.addSubnode(self.avatarNode)
self.avatarNode.setPeer(context: context, theme: theme, peer: EnginePeer(peer), emptyColor: theme.list.mediaPlaceholderColor)
self.avatarNode.setPeer(context: context, theme: theme, peer: peer, emptyColor: theme.list.mediaPlaceholderColor)
self.addSubnode(self.titleNode)
self.titleNode.attributedText = NSAttributedString(string: title ?? EnginePeer(peer).displayTitle(strings: strings, displayOrder: displayOrder), font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor)
self.titleNode.attributedText = NSAttributedString(string: title ?? peer.displayTitle(strings: strings, displayOrder: displayOrder), font: Font.semibold(16.0), textColor: theme.actionSheet.primaryTextColor)
self.addSubnode(self.countNode)

View file

@ -14,6 +14,10 @@ public extension PeerReference {
return PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(id))
}
}
init?(_ peer: EnginePeer) {
self.init(peer._asPeer())
}
}
extension PeerReference {

View file

@ -11,6 +11,10 @@ public final class SelectivePrivacyPeer: Equatable {
self.peer = peer
self.participantCount = participantCount
}
public convenience init(peer: EnginePeer, participantCount: Int32?) {
self.init(peer: peer._asPeer(), participantCount: participantCount)
}
public static func ==(lhs: SelectivePrivacyPeer, rhs: SelectivePrivacyPeer) -> Bool {
if !lhs.peer.isEqual(rhs.peer) {

View file

@ -62,7 +62,7 @@ func _internal_togglePeerMuted(account: Account, peerId: PeerId, threadId: Int64
}
}
public func resolvedAreStoriesMuted(globalSettings: GlobalNotificationSettingsSet, peer: Peer, peerSettings: TelegramPeerNotificationSettings?, topSearchPeers: [PeerId]) -> Bool {
public func resolvedAreStoriesMuted(globalSettings: GlobalNotificationSettingsSet, peer: EnginePeer, peerSettings: TelegramPeerNotificationSettings?, topSearchPeers: [PeerId]) -> Bool {
let defaultIsMuted: Bool
switch globalSettings.privateChats.storySettings.mute {
case .muted:
@ -114,7 +114,7 @@ func _internal_togglePeerStoriesMuted(account: Account, peerId: PeerId) -> Signa
case .default:
let globalNotificationSettings = transaction.getPreferencesEntry(key: PreferencesKeys.globalNotifications)?.get(GlobalNotificationSettings.self) ?? GlobalNotificationSettings.defaultSettings
if resolvedAreStoriesMuted(globalSettings: globalNotificationSettings.effective, peer: peer, peerSettings: previousSettings, topSearchPeers: topSearchPeers) {
if resolvedAreStoriesMuted(globalSettings: globalNotificationSettings.effective, peer: EnginePeer(peer), peerSettings: previousSettings, topSearchPeers: topSearchPeers) {
storySettings.mute = .unmuted
} else {
storySettings.mute = .muted

View file

@ -5,17 +5,17 @@ import Postbox
import TelegramApi
public struct InactiveChannel : Equatable {
public let peer: Peer
public let peer: EnginePeer
public let lastActivityDate: Int32
public let participantsCount: Int32?
init(peer: Peer, lastActivityDate: Int32, participantsCount: Int32?) {
init(peer: EnginePeer, lastActivityDate: Int32, participantsCount: Int32?) {
self.peer = peer
self.lastActivityDate = lastActivityDate
self.participantsCount = participantsCount
}
public static func ==(lhs: InactiveChannel, rhs: InactiveChannel) -> Bool {
return lhs.peer.isEqual(rhs.peer) && lhs.lastActivityDate == rhs.lastActivityDate && lhs.participantsCount == rhs.participantsCount
return lhs.peer == rhs.peer && lhs.lastActivityDate == rhs.lastActivityDate && lhs.participantsCount == rhs.participantsCount
}
}
@ -43,7 +43,7 @@ func _internal_inactiveChannelList(network: Network) -> Signal<[InactiveChannel]
}
var inactive: [InactiveChannel] = []
for (i, channel) in channels.enumerated() {
inactive.append(InactiveChannel(peer: channel, lastActivityDate: i < dates.count ? dates[i] : 0, participantsCount: participantsCounts[channel.id]))
inactive.append(InactiveChannel(peer: EnginePeer(channel), lastActivityDate: i < dates.count ? dates[i] : 0, participantsCount: participantsCounts[channel.id]))
}
return inactive
}

View file

@ -5,14 +5,14 @@ import TelegramApi
public final class NotificationExceptionsList: Equatable {
public let peers: [PeerId: Peer]
public let peers: [PeerId: EnginePeer]
public let settings: [PeerId: TelegramPeerNotificationSettings]
public init(peers: [PeerId: Peer], settings: [PeerId: TelegramPeerNotificationSettings]) {
public init(peers: [PeerId: EnginePeer], settings: [PeerId: TelegramPeerNotificationSettings]) {
self.peers = peers
self.settings = settings
}
public static func ==(lhs: NotificationExceptionsList, rhs: NotificationExceptionsList) -> Bool {
return lhs === rhs
}
@ -41,10 +41,10 @@ func _internal_notificationExceptionsList(accountPeerId: PeerId, postbox: Postbo
let parsedPeers = AccumulatedPeers(transaction: transaction, chats: chats, users: users)
updatePeers(transaction: transaction, accountPeerId: accountPeerId,peers: parsedPeers)
var peers: [PeerId: Peer] = [:]
var peers: [PeerId: EnginePeer] = [:]
for id in parsedPeers.allIds {
if let peer = transaction.getPeer(id) {
peers[peer.id] = peer
peers[peer.id] = EnginePeer(peer)
}
}

View file

@ -558,6 +558,26 @@ public extension EnginePeer {
var profileBackgroundEmojiId: Int64? {
return self._asPeer().profileBackgroundEmojiId
}
var isCopyProtectionEnabled: Bool {
return self._asPeer().isCopyProtectionEnabled
}
var isMonoForum: Bool {
return self._asPeer().isMonoForum
}
var associatedPeerId: Id? {
return self._asPeer().associatedPeerId
}
var hasCustomNameColor: Bool {
return self._asPeer().hasCustomNameColor
}
func hasSensitiveContent(platform: String) -> Bool {
return self._asPeer().hasSensitiveContent(platform: platform)
}
}
public extension EnginePeer {

View file

@ -1233,14 +1233,11 @@ public extension TelegramEngine {
}
public func ensurePeerIsLocallyAvailable(peer: EnginePeer) -> Signal<EnginePeer, NoError> {
return _internal_storedMessageFromSearchPeer(postbox: self.account.postbox, peer: peer._asPeer())
|> map { result -> EnginePeer in
return EnginePeer(result)
}
return _internal_storedMessageFromSearchPeer(postbox: self.account.postbox, peer: peer)
}
public func ensurePeersAreLocallyAvailable(peers: [EnginePeer]) -> Signal<Never, NoError> {
return _internal_storedMessageFromSearchPeers(account: self.account, peers: peers.map { $0._asPeer() })
return _internal_storedMessageFromSearchPeers(account: self.account, peers: peers)
}
public func mostRecentSecretChat(id: EnginePeer.Id) -> Signal<EnginePeer.Id?, NoError> {

View file

@ -7,16 +7,15 @@ private final class LinkHelperClass: NSObject {
}
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 {
return !peer.isDeleted
} else if let peer = peer as? TelegramSecretChat {
return peer.embeddedState == .active
} else if let peer = peer as? TelegramChannel {
return peer.hasPermission(.sendSomething, ignoreDefault: ignoreDefault)
} else {
if case let .user(user) = peer, user.addressName == "replies" {
return false
}
switch peer {
case .user, .legacyGroup:
return !peer.isDeleted
case let .secretChat(secretChat):
return secretChat.embeddedState == .active
case let .channel(channel):
return channel.hasPermission(.sendSomething, ignoreDefault: ignoreDefault)
}
}

View file

@ -491,6 +491,10 @@ public func peerViewMonoforumMainPeer(_ view: PeerView) -> Peer? {
}
public extension RenderedPeer {
convenience init(peer: EnginePeer) {
self.init(peer: peer._asPeer())
}
convenience init(message: Message) {
var peers = SimpleDictionary<PeerId, Peer>()
let peerId = message.id.peerId

View file

@ -2,16 +2,16 @@ import Foundation
import Postbox
import SwiftSignalKit
public func _internal_storedMessageFromSearchPeer(postbox: Postbox, peer: Peer) -> Signal<Peer, NoError> {
return postbox.transaction { transaction -> Peer in
public func _internal_storedMessageFromSearchPeer(postbox: Postbox, peer: EnginePeer) -> Signal<EnginePeer, NoError> {
return postbox.transaction { transaction -> EnginePeer in
if transaction.getPeer(peer.id) == nil {
updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updatedPeer in
updatePeersCustom(transaction: transaction, peers: [peer._asPeer()], update: { _, updatedPeer in
return updatedPeer
})
}
if let group = transaction.getPeer(peer.id) as? TelegramGroup, let migrationReference = group.migrationReference {
if let migrationPeer = transaction.getPeer(migrationReference.peerId) {
return migrationPeer
return EnginePeer(migrationPeer)
} else {
return peer
}
@ -20,11 +20,11 @@ public func _internal_storedMessageFromSearchPeer(postbox: Postbox, peer: Peer)
}
}
func _internal_storedMessageFromSearchPeers(account: Account, peers: [Peer]) -> Signal<Never, NoError> {
func _internal_storedMessageFromSearchPeers(account: Account, peers: [EnginePeer]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Void in
for peer in peers {
if transaction.getPeer(peer.id) == nil {
updatePeersCustom(transaction: transaction, peers: [peer], update: { _, updatedPeer in
updatePeersCustom(transaction: transaction, peers: [peer._asPeer()], update: { _, updatedPeer in
return updatedPeer
})
}

View file

@ -366,7 +366,7 @@ final class ChatAdPanelNode: ASDisplayNode {
if fileReference.media.isAnimatedSticker {
let dimensions = fileReference.media.dimensions ?? PixelDimensions(width: 512, height: 512)
updateImageSignal = chatMessageAnimatedSticker(postbox: context.account.postbox, userLocation: .peer(message.id.peerId), file: fileReference.media, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 160.0, height: 160.0)))
updatedFetchMediaSignal = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media), reference: fileReference.resourceReference(fileReference.media.resource))
updatedFetchMediaSignal = context.engine.resources.fetch(reference: fileReference.resourceReference(fileReference.media.resource), userLocation: .peer(message.id.peerId), userContentType: MediaResourceUserContentType(file: fileReference.media))
} else if fileReference.media.isVideo || fileReference.media.isAnimated {
updateImageSignal = chatMessageVideoThumbnail(account: context.account, userLocation: .peer(message.id.peerId), fileReference: fileReference, blurred: false)
} else if let iconImageRepresentation = smallestImageRepresentation(fileReference.media.previewRepresentations) {

View file

@ -629,7 +629,7 @@ public func makeAttachmentFileControllerImpl(
if let file = message.media.first(where: { $0 is TelegramMediaFile }) as? TelegramMediaFile {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { peer in
guard let peer, let peerReference = PeerReference(peer._asPeer()) else {
guard let peer, let peerReference = PeerReference(peer) else {
return
}
send([.savedMusic(peer: peerReference, media: file)], false, nil, nil)
@ -981,7 +981,7 @@ public func makeAttachmentFileControllerImpl(
return .single(result)
}
).start(next: { peer, remoteMessages in
guard let peer, let peerReference = PeerReference(peer._asPeer()) else {
guard let peer, let peerReference = PeerReference(peer) else {
return
}
var messageMap: [MessageId: Message] = [:]

View file

@ -1115,7 +1115,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode {
inlineMedia.setSignal(updateInlineImageSignal)
}
case let .peerAvatar(peer):
if let peerReference = PeerReference(peer._asPeer()) {
if let peerReference = PeerReference(peer) {
if let signal = peerAvatarImage(account: context.account, peerReference: peerReference, authorOfMessage: nil, representation: peer.largeProfileImage, displayDimensions: inlineMediaSize, clipStyle: .none, blurred: false, inset: 0.0, emptyColor: mainColor.withMultipliedAlpha(0.1), synchronousLoad: synchronousLoads, provideUnrounded: false) {
let updateInlineImageSignal = signal |> map { images -> (TransformImageArguments) -> DrawingContext? in
let image = images?.0

View file

@ -703,7 +703,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
if let updatedFile = updatedFile, updatedMedia {
if let resource = updatedFile.previewRepresentations.first?.resource {
strongSelf.fetchedThumbnailDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .peer(item.message.id.peerId), userContentType: .video, reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource)).startStrict())
strongSelf.fetchedThumbnailDisposable.set(item.context.engine.resources.fetch(reference: FileMediaReference.message(message: MessageReference(item.message), media: updatedFile).resourceReference(resource), userLocation: .peer(item.message.id.peerId), userContentType: .video).startStrict())
} else {
strongSelf.fetchedThumbnailDisposable.set(nil)
}

View file

@ -1471,7 +1471,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}, cancel: {
if file.isAnimated {
context.account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource)
context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(file.resource.id))
} else {
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file)
}
@ -1706,7 +1706,7 @@ public final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTr
}
}, cancel: {
if file.isAnimated {
context.account.postbox.mediaBox.cancelInteractiveResourceFetch(file.resource)
context.engine.resources.cancelInteractiveResourceFetch(id: EngineMediaResource.Id(file.resource.id))
} else {
messageMediaFileCancelInteractiveFetch(context: context, messageId: message.id, file: file)
}

View file

@ -777,7 +777,7 @@ private final class ChannelItemComponent: Component {
self.mergedAvatarsNode = mergedAvatarsNode
}
mergedAvatarsNode.update(context: component.context, peers: component.peers.map { $0._asPeer() }, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0)
mergedAvatarsNode.update(context: component.context, peers: component.peers, synchronousLoad: false, imageSize: 60.0, imageSpacing: 10.0, borderWidth: 2.0, avatarFontSize: 26.0)
let avatarsSize = CGSize(width: avatarSize.width + 20.0, height: avatarSize.height)
mergedAvatarsNode.updateLayout(size: avatarsSize)
mergedAvatarsNode.frame = CGRect(origin: CGPoint(x: avatarFrame.midX - avatarsSize.width / 2.0, y: avatarFrame.minY), size: avatarsSize)

View file

@ -854,9 +854,9 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
// }
let rightInset: CGFloat = 10.0 + mediaInset
let recentVoterPeers: [Peer]
let recentVoterPeers: [EnginePeer]
if let optionResult {
recentVoterPeers = optionResult.recentVoterPeerIds.compactMap { message.peers[$0] }
recentVoterPeers = optionResult.recentVoterPeerIds.compactMap { message.peers[$0] }.map(EnginePeer.init)
} else {
recentVoterPeers = []
}
@ -2567,11 +2567,11 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: textInsets))
let typeText: String
var avatarPeers: [Peer] = []
var avatarPeers: [EnginePeer] = []
if let poll = poll {
for peerId in poll.results.recentVoters {
if let peer = item.message.peers[peerId] {
avatarPeers.append(peer)
avatarPeers.append(EnginePeer(peer))
}
}
}

View file

@ -519,7 +519,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
animatedStickerNode.autoplay = true
animatedStickerNode.visibility = strongSelf.visibilityStatus
strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).startStrict())
strongSelf.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .sticker).startStrict())
let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize)
@ -571,7 +571,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
public static let themeCrossfadeDelay: Double = 0.05
public enum Subject {
case peer(peer: Peer, threadId: Int64?, temporary: Bool)
case peer(peer: EnginePeer, threadId: Int64?, temporary: Bool)
case messages([Message])
public var fileName: String {
@ -580,7 +580,7 @@ public final class ChatQrCodeScreenImpl: ViewController, ChatQrCodeScreen {
var result: String
if let addressName = peer.addressName, !addressName.isEmpty {
result = "t_me-\(peer.addressName ?? "")"
} else if let peer = peer as? TelegramUser {
} else if case let .user(peer) = peer {
result = "t_me-\(peer.phone ?? "")"
} else {
result = "t_me-\(Int32.random(in: 0 ..< Int32.max))"
@ -1576,7 +1576,7 @@ private protocol ContentNode: ASDisplayNode {
private class QrContentNode: ASDisplayNode, ContentNode {
private let context: AccountContext
private let peer: Peer
private let peer: EnginePeer
private let threadId: Int64?
private let isStatic: Bool
private let temporary: Bool
@ -1618,7 +1618,7 @@ private class QrContentNode: ASDisplayNode, ContentNode {
private var tokenUpdated = false
var requestNextToken: () -> Void = {}
init(context: AccountContext, peer: Peer, threadId: Int64?, isStatic: Bool = false, temporary: Bool) {
init(context: AccountContext, peer: EnginePeer, threadId: Int64?, isStatic: Bool = false, temporary: Bool) {
self.context = context
self.peer = peer
self.threadId = threadId
@ -1706,7 +1706,7 @@ private class QrContentNode: ASDisplayNode, ContentNode {
self.avatarNode = ImageNode()
self.avatarNode.displaysAsynchronously = false
self.avatarNode.setSignal(peerAvatarCompleteImage(account: context.account, peer: EnginePeer(peer), size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true))
self.avatarNode.setSignal(peerAvatarCompleteImage(account: context.account, peer: peer, size: CGSize(width: 180.0, height: 180.0), font: avatarPlaceholderFont(size: 78.0), fullSize: true))
super.init()
@ -1739,9 +1739,9 @@ private class QrContentNode: ASDisplayNode, ContentNode {
var codeLink: String
if let addressName = peer.addressName, !addressName.isEmpty {
codeLink = "https://t.me/\(peer.addressName ?? "")"
} else if let peer = peer as? TelegramUser {
} else if case let .user(peer) = peer {
codeLink = "https://t.me/+\(peer.phone ?? "")"
} else if let _ = peer as? TelegramChannel {
} else if case .channel = peer {
codeLink = "https://t.me/c/\(peer.id.id._internalGetInt64Value())"
} else {
codeLink = ""

View file

@ -20,7 +20,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}
private let context: AccountContext
private let peer: Peer
private let peer: EnginePeer
private let initialAdminPeerId: PeerId?
let starsState: StarsRevenueStats?
@ -39,7 +39,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
private var adminsDisposable: Disposable?
public init(context: AccountContext, peer: Peer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) {
public init(context: AccountContext, peer: EnginePeer, adminPeerId: PeerId?, starsState: StarsRevenueStats?) {
self.context = context
self.peer = peer
self.initialAdminPeerId = adminPeerId
@ -197,7 +197,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
let rightBarButton = ChatNavigationButton(action: .search(hasTags: false), buttonItem: UIBarButtonItem(image: PresentationResourcesRootController.navigationCompactSearchIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.activateSearch)))
self.rightBarButton = rightBarButton
self.titleView.title = CounterControllerTitle(title: EnginePeer(peer).compactDisplayTitle, counter: self.presentationData.strings.Channel_AdminLog_TitleAllEvents)
self.titleView.title = CounterControllerTitle(title: peer.compactDisplayTitle, counter: self.presentationData.strings.Channel_AdminLog_TitleAllEvents)
let chatTheme = self.context.account.postbox.peerView(id: peer.id)
|> map { view -> ChatTheme? in
@ -274,7 +274,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}
override public func loadDisplayNode() {
self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer, presentationData: self.presentationData, pushController: { [weak self] c in
self.displayNode = ChatRecentActionsControllerNode(context: self.context, controller: self, peer: self.peer._asPeer(), presentationData: self.presentationData, pushController: { [weak self] c in
(self?.navigationController as? NavigationController)?.pushViewController(c)
}, presentController: { [weak self] c, t, a in
self?.present(c, in: t, with: a, blockInteraction: true)
@ -361,7 +361,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}
let controller = RecentActionsSettingsSheet(
context: self.context,
peer: EnginePeer(self.peer),
peer: self.peer,
adminPeers: adminPeers,
initialValue: RecentActionsSettingsSheet.Value(
events: self.controllerNode.filter.events,
@ -380,7 +380,7 @@ public final class ChatRecentActionsController: TelegramBaseController {
}
private func updateTitle() {
let title = EnginePeer(self.peer).compactDisplayTitle
let title = self.peer.compactDisplayTitle
let subtitle: String
if self.controllerNode.filter.isEmpty {
subtitle = self.presentationData.strings.Channel_AdminLog_TitleAllEvents

View file

@ -114,7 +114,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
private var groupsInCommonContext: GroupsInCommonContext?
private var groupsInCommonDisposable: Disposable?
private var groupsInCommon: [Peer] = []
private var groupsInCommon: [EnginePeer] = []
private let disclaimerTextNode: TextNodeWithEntities
@ -436,7 +436,7 @@ public final class ChatUserInfoItemNode: ListViewItemNode, ASGestureRecognizerDe
guard let self, let item = self.item else {
return
}
self.groupsInCommon = Array(state.peers.compactMap { $0.peer }.prefix(3))
self.groupsInCommon = Array(state.peers.compactMap { $0.peer }.prefix(3)).map(EnginePeer.init)
self.groupsAvatarsNode.update(context: item.context, peers: self.groupsInCommon, synchronousLoad: true, imageSize: avatarImageSize, imageSpacing: avatarSpacing, borderWidth: avatarBorder)
})
}

View file

@ -24,7 +24,7 @@ private enum PeerAvatarReference: Equatable {
}
private extension PeerAvatarReference {
init(peer: Peer) {
init(peer: EnginePeer) {
if let photo = peer.smallProfileImage, let peerReference = PeerReference(peer) {
self = .image(peerReference, photo)
} else {
@ -97,7 +97,7 @@ public final class MergedAvatarsNode: ASDisplayNode {
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
}
public func update(context: AccountContext, peers: [Peer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) {
public func update(context: AccountContext, peers: [EnginePeer], synchronousLoad: Bool, imageSize: CGFloat, imageSpacing: CGFloat, borderWidth: CGFloat, avatarFontSize: CGFloat = 8.0) {
self.imageSize = imageSize
self.imageSpacing = imageSpacing
self.borderWidthValue = borderWidth

View file

@ -358,7 +358,7 @@ final class ChatListNoticeItemNode: ItemListRevealOptionsItemNode {
strongSelf.avatarsNode = avatarsNode
}
let avatarSize = CGSize(width: 30.0, height: 30.0)
avatarsNode.update(context: item.context, peers: avatarPeers.map { $0._asPeer() }, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0)
avatarsNode.update(context: item.context, peers: avatarPeers, synchronousLoad: false, imageSize: avatarSize.width, imageSpacing: 16.0, borderWidth: 2.0 - UIScreenPixel, avatarFontSize: 10.0)
let avatarsSize = CGSize(width: avatarSize.width + 16.0 * CGFloat(avatarPeers.count - 1), height: avatarSize.height)
avatarsNode.updateLayout(size: avatarsSize)
avatarsNode.frame = CGRect(origin: CGPoint(x: sideInset - 6.0, y: floor((layout.size.height - avatarsSize.height) / 2.0)), size: avatarsSize)

View file

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
@ -500,7 +499,7 @@ private final class ThemeSettingsThemeItemIconNode : ListViewItemNode {
animatedStickerNode.autoplay = true
animatedStickerNode.visibility = strongSelf.visibilityStatus
strongSelf.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: item.context.account.postbox.mediaBox, userLocation: .other, userContentType: .sticker, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).startStrict())
strongSelf.stickerFetchedDisposable.set(item.context.engine.resources.fetch(reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource), userLocation: .other, userContentType: .sticker).startStrict())
let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
strongSelf.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize)

View file

@ -73,7 +73,7 @@ public func ageVerificationAvailability(context: AccountContext) -> Signal<AgeVe
|> take(1)
|> mapToSignal { maybeFileAndMessage -> Signal<AgeVerificationAvailability, NoError> in
if let (file, message) = maybeFileAndMessage {
let fetchedData = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .file, reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource))
let fetchedData = context.engine.resources.fetch(reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource), userLocation: .other, userContentType: .file)
enum FetchStatus {
case completed(String)

View file

@ -28,7 +28,7 @@ public func requireAgeVerification(context: AccountContext) -> Bool {
}
public func requireAgeVerification(context: AccountContext, peer: EnginePeer) -> Signal<Bool, NoError> {
if requireAgeVerification(context: context), peer._asPeer().hasSensitiveContent(platform: "ios") {
if requireAgeVerification(context: context), peer.hasSensitiveContent(platform: "ios") {
return context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.ContentSettings())
|> map { contentSettings in
if !contentSettings.ignoreContentRestrictionReasons.contains("sensitive") {

View file

@ -65,7 +65,7 @@ public func cutoutAvailability(context: AccountContext) -> Signal<CutoutAvailabi
|> take(1)
|> mapToSignal { maybeFileAndMessage -> Signal<CutoutAvailability, NoError> in
if let (file, message) = maybeFileAndMessage {
let fetchedData = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .file, reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource))
let fetchedData = context.engine.resources.fetch(reference: FileMediaReference.message(message: MessageReference(message._asMessage()), media: file).resourceReference(file.resource), userLocation: .other, userContentType: .file)
enum FetchStatus {
case completed(String)

View file

@ -24,7 +24,7 @@ public extension MediaEditorScreenImpl {
willDismiss: @escaping () -> Void = {},
update: @escaping (Disposable?) -> Void
) -> MediaEditorScreenImpl? {
guard let peerReference = PeerReference(peer._asPeer()) else {
guard let peerReference = PeerReference(peer) else {
return nil
}
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>

View file

@ -71,7 +71,7 @@ public final class PeerInfoCoverComponent: Component {
func colors(context: AccountContext, isDark: Bool) -> (UIColor, UIColor)? {
switch self {
case let .peer(peer):
if let colors = peer._asPeer().profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: isDark) }) {
if let colors = peer.profileColor.flatMap({ context.peerNameColors.getProfile($0, dark: isDark) }) {
let backgroundColor = colors.main
let secondaryBackgroundColor = colors.secondary ?? colors.main
return (backgroundColor, secondaryBackgroundColor)
@ -79,7 +79,7 @@ public final class PeerInfoCoverComponent: Component {
return nil
}
case let .managedBot(peer):
if let color = peer._asPeer().nameColor {
if let color = peer.nameColor {
let colors = calculateAvatarColors(context: context, explicitColorIndex: nil, peerId: peer.id, nameColor: color, icon: .none, theme: nil)
if colors.count == 2 {
return (colors[0], colors[1])

View file

@ -20,7 +20,7 @@ enum PeerInfoScreenMemberItemAction {
final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
let id: AnyHashable
let context: ItemListPeerItem.Context
let enclosingPeer: Peer?
let enclosingPeer: EnginePeer?
let member: PeerInfoMember
let badge: String?
let isAccount: Bool
@ -31,7 +31,7 @@ final class PeerInfoScreenMemberItem: PeerInfoScreenItem {
init(
id: AnyHashable,
context: ItemListPeerItem.Context,
enclosingPeer: Peer?,
enclosingPeer: EnginePeer?,
member: PeerInfoMember,
badge: String? = nil,
isAccount: Bool,
@ -149,9 +149,9 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
case .member:
var canEditRank = false
if item.member.id == item.context.accountPeerId {
if let channel = item.enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) {
if case let .channel(channel) = item.enclosingPeer, channel.hasPermission(.editRank) {
canEditRank = true
} else if let group = item.enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) {
} else if case let .legacyGroup(group) = item.enclosingPeer, !group.hasBannedPermission(.banEditRank) {
canEditRank = true
}
}
@ -175,16 +175,16 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
break
}
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer.flatMap(EnginePeer.init), member: item.member)
let actions = availableActionsForMemberOfPeer(accountPeerId: item.context.accountPeerId, peer: item.enclosingPeer, member: item.member)
var options: [ItemListPeerItemRevealOption] = []
if actions.contains(.promote) && item.enclosingPeer is TelegramChannel {
if actions.contains(.promote), case .channel = item.enclosingPeer {
options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: {
item.action?(.promote)
}))
}
if actions.contains(.restrict) {
if item.enclosingPeer is TelegramChannel {
if case .channel = item.enclosingPeer {
options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: {
item.action?(.restrict)
}))
@ -220,7 +220,7 @@ private final class PeerInfoScreenMemberItemNode: PeerInfoScreenItemNode {
itemText = .presence
}
let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: EnginePeer(item.member.peer), height: itemHeight, presence: item.member.presence.flatMap(EnginePeer.Presence.init), text: itemText, label: itemLabel, editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, animateFirstAvatarTransition: !item.isAccount, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
let peerItem = ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), systemStyle: .glass, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: item.context, peer: item.member.peer, height: itemHeight, presence: item.member.presence.flatMap(EnginePeer.Presence.init), text: itemText, label: itemLabel, editing: ItemListPeerItemEditing(editable: !options.isEmpty, editing: false, revealed: nil), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: false, animateFirstAvatarTransition: !item.isAccount, sectionId: 0, action: nil, setPeerIdWithRevealedOptions: { lhs, rhs in
}, removePeer: { _ in

View file

@ -25,23 +25,23 @@ private struct GroupsInCommonListTransaction {
private struct GroupsInCommonListEntry: Comparable, Identifiable {
var index: Int
var peer: Peer
var peer: EnginePeer
var stableId: PeerId {
return self.peer.id
}
static func ==(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool {
return lhs.peer.isEqual(rhs.peer)
return lhs.peer == rhs.peer
}
static func <(lhs: GroupsInCommonListEntry, rhs: GroupsInCommonListEntry) -> Bool {
return lhs.index < rhs.index
}
func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
func item(context: AccountContext, presentationData: PresentationData, openPeer: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
let peer = self.peer
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: EnginePeer(self.peer), presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
return ItemListPeerItem(presentationData: ItemListPresentationData(presentationData), dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, context: context, peer: self.peer, presence: nil, text: .none, label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: 0, action: {
openPeer(peer)
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
@ -51,7 +51,7 @@ private struct GroupsInCommonListEntry: Comparable, Identifiable {
}
}
private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (Peer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction {
private func preparedTransition(from fromEntries: [GroupsInCommonListEntry], to toEntries: [GroupsInCommonListEntry], context: AccountContext, presentationData: PresentationData, openPeer: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> GroupsInCommonListTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
@ -65,7 +65,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let peerId: PeerId
private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
private let openPeerContextAction: (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void
private let groupsInCommonContext: GroupsInCommonContext
weak var parentController: ViewController?
@ -106,7 +106,7 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void, groupsInCommonContext: GroupsInCommonContext) {
self.context = context
self.peerId = peerId
self.chatControllerInteraction = chatControllerInteraction
@ -229,11 +229,11 @@ final class PeerInfoGroupsInCommonPaneNode: ASDisplayNode, PeerInfoPaneNode {
var entries: [GroupsInCommonListEntry] = []
for peer in state.peers {
if let peer = peer.peer {
entries.append(GroupsInCommonListEntry(index: entries.count, peer: peer))
entries.append(GroupsInCommonListEntry(index: entries.count, peer: EnginePeer(peer)))
}
}
let transaction = preparedTransition(from: self.currentEntries, to: entries, context: self.context, presentationData: presentationData, openPeer: { [weak self] peer in
self?.chatControllerInteraction.openPeer(EnginePeer(peer), .default, nil, .default)
self?.chatControllerInteraction.openPeer(peer, .default, nil, .default)
}, openPeerContextAction: { [weak self] peer, node, gesture in
self?.openPeerContextAction(false, peer, node, gesture)
})

View file

@ -89,7 +89,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
}
}
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem {
func item(context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> ListViewItem {
switch self {
case let .addMember(_, text):
return ItemListPeerActionItem(presentationData: ItemListPresentationData(presentationData), icon: PresentationResourcesItemList.addPersonIcon(presentationData.theme), title: text, alwaysPlain: true, sectionId: 0, height: .compactPeerList, color: .accent, editing: false, action: {
@ -110,9 +110,9 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
case .member:
var canEditRank = false
if member.id == context.account.peerId {
if let channel = enclosingPeer as? TelegramChannel, channel.hasPermission(.editRank) {
if case let .channel(channel) = enclosingPeer, channel.hasPermission(.editRank) {
canEditRank = true
} else if let group = enclosingPeer as? TelegramGroup, !group.hasBannedPermission(.banEditRank) {
} else if case let .legacyGroup(group) = enclosingPeer, !group.hasBannedPermission(.banEditRank) {
canEditRank = true
}
}
@ -136,16 +136,16 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
break
}
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: EnginePeer(enclosingPeer), member: member)
let actions = availableActionsForMemberOfPeer(accountPeerId: context.account.peerId, peer: enclosingPeer, member: member)
var options: [ItemListPeerItemRevealOption] = []
if actions.contains(.promote) && enclosingPeer is TelegramChannel {
if actions.contains(.promote), case .channel = enclosingPeer {
options.append(ItemListPeerItemRevealOption(type: .neutral, title: presentationData.strings.GroupInfo_ActionPromote, action: {
action(member, .promote)
}))
}
if actions.contains(.restrict) {
if enclosingPeer is TelegramChannel {
if case .channel = enclosingPeer {
options.append(ItemListPeerItemRevealOption(type: .warning, title: presentationData.strings.GroupInfo_ActionRestrict, action: {
action(member, .restrict)
}))
@ -165,7 +165,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
}
var status: ContactsPeerItemStatus = .presence(presence, presentationData.dateTimeFormat)
if let user = member.peer as? TelegramUser, let botInfo = user.botInfo {
if case let .user(user) = member.peer, let botInfo = user.botInfo {
let botStatus: String
if botInfo.flags.contains(.hasAccessToChatHistory) {
botStatus = presentationData.strings.Bot_GroupStatusReadsHistory
@ -188,7 +188,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
displayOrder: presentationData.nameDisplayOrder,
context: context,
peerMode: .memberList,
peer: .peer(peer: EnginePeer(member.peer), chatPeer: EnginePeer(member.peer)),
peer: .peer(peer: member.peer, chatPeer: member.peer),
status: status,
rightLabelText: label.flatMap { .init(text: $0, color: labelColor, hasBackground: labelBackground) },
enabled: true,
@ -268,7 +268,7 @@ private enum PeerMembersListEntry: Comparable, Identifiable {
}
}
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: Peer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction {
private func preparedTransition(from fromEntries: [PeerMembersListEntry], to toEntries: [PeerMembersListEntry], context: AccountContext, presentationData: PresentationData, enclosingPeer: EnginePeer, addMemberAction: @escaping () -> Void, action: @escaping (PeerInfoMember, PeerMembersListAction) -> Void, contextAction: ((PeerInfoMember, ASDisplayNode, ContextGesture?) -> Void)?) -> PeerMembersListTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
@ -290,7 +290,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let listMaskView: UIImageView
private let listNode: ListView
private var currentEntries: [PeerMembersListEntry] = []
private var enclosingPeer: Peer?
private var enclosingPeer: EnginePeer?
private var currentState: PeerInfoMembersState?
private var canLoadMore: Bool = false
private var enqueuedTransactions: [PeerMembersListTransaction] = []
@ -358,9 +358,9 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
return
}
strongSelf.enclosingPeer = enclosingPeer._asPeer()
strongSelf.enclosingPeer = enclosingPeer
strongSelf.currentState = state
strongSelf.updateState(enclosingPeer: enclosingPeer._asPeer(), state: state, presentationData: presentationData)
strongSelf.updateState(enclosingPeer: enclosingPeer, state: state, presentationData: presentationData)
})
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
@ -439,7 +439,7 @@ final class PeerInfoMembersPaneNode: ASDisplayNode, PeerInfoPaneNode {
}
}
private func updateState(enclosingPeer: Peer, state: PeerInfoMembersState, presentationData: PresentationData) {
private func updateState(enclosingPeer: EnginePeer, state: PeerInfoMembersState, presentationData: PresentationData) {
var entries: [PeerMembersListEntry] = []
if state.canAddMembers {
entries.append(.addMember(presentationData.theme, presentationData.strings.GroupInfo_AddParticipant))

View file

@ -65,7 +65,7 @@ private enum RecommendedPeersListEntry: Comparable, Identifiable {
}
}
func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
func item(context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> ListViewItem {
switch self {
case let .peer(_, _, peer, subscribers):
let text: ItemListPeerItemText
@ -85,13 +85,13 @@ private enum RecommendedPeersListEntry: Comparable, Identifiable {
}, setPeerIdWithRevealedOptions: { _, _ in
}, removePeer: { _ in
}, contextAction: { node, gesture in
openPeerContextAction(peer._asPeer(), node, gesture)
openPeerContextAction(peer, node, gesture)
}, hasTopStripe: false, noInsets: true, noCorners: true, style: .plain, disableInteractiveTransitionIfNecessary: true)
}
}
}
private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (Peer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction {
private func preparedTransition(from fromEntries: [RecommendedPeersListEntry], to toEntries: [RecommendedPeersListEntry], context: AccountContext, presentationData: PresentationData, action: @escaping (EnginePeer) -> Void, openPeerContextAction: @escaping (EnginePeer, ASDisplayNode, ContextGesture?) -> Void) -> RecommendedPeersListTransaction {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
@ -116,7 +116,7 @@ extension RecommendedBots: RecommendedPeers {
final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode {
private let context: AccountContext
private let chatControllerInteraction: ChatControllerInteraction
private let openPeerContextAction: (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void
private let openPeerContextAction: (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void
weak var parentController: ViewController?
@ -152,7 +152,7 @@ final class PeerInfoRecommendedPeersPaneNode: ASDisplayNode, PeerInfoPaneNode {
private var disposable: Disposable?
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void) {
init(context: AccountContext, peerId: PeerId, chatControllerInteraction: ChatControllerInteraction, openPeerContextAction: @escaping (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void) {
self.context = context
self.chatControllerInteraction = chatControllerInteraction
self.openPeerContextAction = openPeerContextAction

View file

@ -23,7 +23,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
let isReady = Promise<Bool>()
var arguments: (Peer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)?
var arguments: (EnginePeer?, Int64?, EngineMessageHistoryThread.Info?, PresentationTheme, CGFloat, Bool)?
var item: PeerInfoAvatarListItem?
var itemsUpdated: (([PeerInfoAvatarListItem]) -> Void)?
@ -128,7 +128,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
}
}
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: EnginePeer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded)
self.maskNode.isForum = isForum
self.pinchSourceNode.update(size: size, transition: transition)

View file

@ -101,7 +101,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
self.playbackStartDisposable.dispose()
}
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme, peer: Peer?) {
func updateStoryView(transition: ContainedViewLayoutTransition, theme: PresentationTheme, peer: EnginePeer?) {
var colors = AvatarNode.Colors(theme: theme)
let regularNavigationContentsSecondaryColor: UIColor
@ -160,7 +160,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
}
var isForum = false
if let peer, let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
if let peer, case let .channel(channel) = peer, channel.isForumOrMonoForum {
isForum = true
}
@ -210,7 +210,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
}
private struct Params {
let peer: Peer?
let peer: EnginePeer?
let threadId: Int64?
let threadInfo: EngineMessageHistoryThread.Info?
let item: PeerInfoAvatarListItem?
@ -219,7 +219,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
let isExpanded: Bool
let isSettings: Bool
init(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) {
init(peer: EnginePeer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) {
self.peer = peer
self.threadId = threadId
self.threadInfo = threadInfo
@ -251,7 +251,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
)
}
func update(peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) {
func update(peer: EnginePeer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, item: PeerInfoAvatarListItem?, theme: PresentationTheme, avatarSize: CGFloat, isExpanded: Bool, isSettings: Bool) {
self.params = Params(peer: peer, threadId: threadId, threadInfo: threadInfo, item: item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: isSettings)
if let peer = peer {
@ -282,7 +282,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
}
self.avatarNode.imageNode.animateFirstTransition = !isSettings
self.avatarNode.setPeer(context: self.context, theme: theme, peer: EnginePeer(peer), overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true)
self.avatarNode.setPeer(context: self.context, theme: theme, peer: peer, overrideImage: overrideImage, clipStyle: .none, synchronousLoad: self.isFirstAvatarLoading, displayDimensions: CGSize(width: avatarSize, height: avatarSize), storeUnrounded: true)
if let threadInfo = threadInfo {
self.avatarNode.isHidden = true
@ -327,7 +327,7 @@ final class PeerInfoAvatarTransformContainerNode: ASDisplayNode {
var isForum = false
let avatarCornerRadius: CGFloat
if let channel = peer as? TelegramChannel, channel.isForumOrMonoForum {
if case let .channel(channel) = peer, channel.isForumOrMonoForum {
avatarCornerRadius = floor(avatarSize * 0.25)
isForum = true
} else {

View file

@ -384,8 +384,8 @@ final class PeerInfoPersonalChannelData: Equatable {
final class PeerInfoScreenData {
let peer: EnginePeer?
let chatPeer: Peer?
let savedMessagesPeer: Peer?
let chatPeer: EnginePeer?
let savedMessagesPeer: EnginePeer?
let cachedData: CachedPeerData?
let status: PeerInfoStatusData?
let peerNotificationSettings: TelegramPeerNotificationSettings?
@ -393,8 +393,8 @@ final class PeerInfoScreenData {
let globalNotificationSettings: EngineGlobalNotificationSettings?
let availablePanes: [PeerInfoPaneKey]
let groupsInCommon: GroupsInCommonContext?
let linkedDiscussionPeer: Peer?
let linkedMonoforumPeer: Peer?
let linkedDiscussionPeer: EnginePeer?
let linkedMonoforumPeer: EnginePeer?
let members: PeerInfoMembersData?
let storyListContext: StoryListContext?
let storyArchiveListContext: StoryListContext?
@ -440,8 +440,8 @@ final class PeerInfoScreenData {
init(
peer: EnginePeer?,
chatPeer: Peer?,
savedMessagesPeer: Peer?,
chatPeer: EnginePeer?,
savedMessagesPeer: EnginePeer?,
cachedData: CachedPeerData?,
status: PeerInfoStatusData?,
peerNotificationSettings: TelegramPeerNotificationSettings?,
@ -450,8 +450,8 @@ final class PeerInfoScreenData {
isContact: Bool,
availablePanes: [PeerInfoPaneKey],
groupsInCommon: GroupsInCommonContext?,
linkedDiscussionPeer: Peer?,
linkedMonoforumPeer: Peer?,
linkedDiscussionPeer: EnginePeer?,
linkedMonoforumPeer: EnginePeer?,
members: PeerInfoMembersData?,
storyListContext: StoryListContext?,
storyArchiveListContext: StoryListContext?,
@ -1025,7 +1025,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id,
return PeerInfoScreenData(
peer: peer.flatMap(EnginePeer.init),
chatPeer: peer,
chatPeer: peer.flatMap(EnginePeer.init),
savedMessagesPeer: nil,
cachedData: peerView.cachedData,
status: nil,
@ -1618,8 +1618,8 @@ func peerInfoScreenData(
return PeerInfoScreenData(
peer: peer.flatMap(EnginePeer.init),
chatPeer: peerView.peers[peerId],
savedMessagesPeer: savedMessagesPeer?._asPeer(),
chatPeer: peerView.peers[peerId].flatMap(EnginePeer.init),
savedMessagesPeer: savedMessagesPeer,
cachedData: peerView.cachedData,
status: status,
peerNotificationSettings: peerView.notificationSettings as? TelegramPeerNotificationSettings,
@ -1833,14 +1833,14 @@ func peerInfoScreenData(
}
}
var discussionPeer: Peer?
var discussionPeer: EnginePeer?
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
discussionPeer = peer
discussionPeer = EnginePeer(peer)
}
var monoforumPeer: Peer?
var monoforumPeer: EnginePeer?
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId {
monoforumPeer = peerView.peers[linkedMonoforumId]
monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)
}
var canManageInvitations = false
@ -1865,7 +1865,7 @@ func peerInfoScreenData(
return PeerInfoScreenData(
peer: peerView.peers[peerId].flatMap(EnginePeer.init),
chatPeer: peerView.peers[peerId],
chatPeer: peerView.peers[peerId].flatMap(EnginePeer.init),
savedMessagesPeer: nil,
cachedData: peerView.cachedData,
status: status,
@ -2128,14 +2128,14 @@ func peerInfoScreenData(
starsRevenueContextAndState
)
|> mapToSignal { peerView, availablePanes, globalNotificationSettings, status, membersData, currentInvitationsContext, invitations, currentRequestsContext, requests, hasStories, threadData, preferencesView, accountIsPremium, hasSavedMessages, hasSavedMessagesChats, hasSavedMessageTags, isPremiumRequiredForStoryPosting, starsRevenueContextAndState -> Signal<PeerInfoScreenData, NoError> in
var discussionPeer: Peer?
var discussionPeer: EnginePeer?
if case let .known(maybeLinkedDiscussionPeerId) = (peerView.cachedData as? CachedChannelData)?.linkedDiscussionPeerId, let linkedDiscussionPeerId = maybeLinkedDiscussionPeerId, let peer = peerView.peers[linkedDiscussionPeerId] {
discussionPeer = peer
discussionPeer = EnginePeer(peer)
}
var monoforumPeer: Peer?
var monoforumPeer: EnginePeer?
if let channel = peerViewMainPeer(peerView) as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasMonoforum), let linkedMonoforumId = channel.linkedMonoforumId {
monoforumPeer = peerView.peers[linkedMonoforumId]
monoforumPeer = peerView.peers[linkedMonoforumId].flatMap(EnginePeer.init)
}
var availablePanes = availablePanes
@ -2203,7 +2203,7 @@ func peerInfoScreenData(
return .single(PeerInfoScreenData(
peer: peerView.peers[groupId].flatMap(EnginePeer.init),
chatPeer: peerView.peers[groupId],
chatPeer: peerView.peers[groupId].flatMap(EnginePeer.init),
savedMessagesPeer: nil,
cachedData: peerView.cachedData,
status: status,
@ -2256,7 +2256,7 @@ 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._asPeer().isCopyProtectionEnabled {
} else if let peer = data.peer, peer.isCopyProtectionEnabled {
isCopyProtected = true
}
return isCopyProtected

View file

@ -156,14 +156,14 @@ final class PeerInfoEditingAvatarNode: ASDisplayNode {
}
markupNode.update(markup: markup, size: CGSize(width: 320.0, height: 320.0))
markupNode.updateVisibility(true)
} else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer._asPeer()) {
} else if threadData == nil, let video = videoRepresentations.last, let peerReference = PeerReference(peer) {
if let markupNode = self.markupNode {
self.markupNode = nil
markupNode.removeFromSupernode()
}
let videoFileReference = FileMediaReference.avatarList(peer: peerReference, media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: 0), partialReference: nil, resource: video.representation.resource, previewRepresentations: representations.map { $0.representation }, videoThumbnails: [], immediateThumbnailData: immediateThumbnailData, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: video.representation.dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []))
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer._asPeer().isCopyProtectionEnabled, storeAfterDownload: nil)
let videoContent = NativeVideoContent(id: .profileVideo(videoId, nil), userLocation: .other, fileReference: videoFileReference, streamVideo: isMediaStreamable(resource: video.representation.resource) ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, useLargeThumbnail: true, autoFetchFullSizeThumbnail: true, startTimestamp: video.representation.startTimestamp, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .clear, captureProtected: peer.isCopyProtectionEnabled, storeAfterDownload: nil)
if videoContent.id != self.videoContent?.id {
self.videoNode?.removeFromSupernode()

View file

@ -112,7 +112,11 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
}
}
case .lastName:
updateText = (peer?._asPeer() as? TelegramUser)?.lastName ?? ""
if case let .user(user) = peer {
updateText = user.lastName ?? ""
} else {
updateText = ""
}
case .title:
updateText = peer?.debugDisplayTitle ?? ""
case .description:

View file

@ -89,7 +89,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
private weak var controller: PeerInfoScreenImpl?
private var presentationData: PresentationData?
private var state: PeerInfoState?
private var peer: Peer?
private var peer: EnginePeer?
private var threadData: MessageHistoryThreadData?
private var isSearching: Bool = false
private var avatarSize: CGFloat?
@ -360,7 +360,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
guard let strongSelf = self, let state = strongSelf.state, let peer = strongSelf.peer, let presentationData = strongSelf.presentationData, let avatarSize = strongSelf.avatarSize else {
return
}
strongSelf.editingContentNode.avatarNode.update(peer: EnginePeer(peer), threadData: strongSelf.threadData, chatLocation: chatLocation, item: strongSelf.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
strongSelf.editingContentNode.avatarNode.update(peer: peer, threadData: strongSelf.threadData, chatLocation: chatLocation, item: strongSelf.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
}
self.avatarListNode.animateOverlaysFadeIn = { [weak self] in
@ -518,7 +518,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
self.state = state
self.peer = peer?._asPeer()
self.peer = peer
self.threadData = threadData
self.isSearching = isSearching
self.avatarListNode.listContainerNode.peer = peer
@ -1812,7 +1812,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
})
}
self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer?._asPeer(), isForum: isForum, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition)
self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, isForum: isForum, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition)
self.editingContentNode.avatarNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
self.avatarOverlayNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
if additive {
@ -2051,7 +2051,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
}
self.controller?.push(ProfileLevelInfoScreen(
context: self.context,
peer: EnginePeer(peer),
peer: peer,
starRating: currentStarRating,
pendingStarRating: self.currentPendingStarRating,
customTheme: self.presentationData?.theme

View file

@ -42,7 +42,7 @@ final class PeerInfoInteraction {
let openPermissions: () -> Void
let openLocation: () -> Void
let editingOpenSetupLocation: () -> Void
let openPeerInfo: (Peer, Bool) -> Void
let openPeerInfo: (EnginePeer, Bool) -> Void
let performMemberAction: (PeerInfoMember, PeerInfoMemberAction) -> Void
let openPeerInfoContextMenu: (PeerInfoContextSubject, ASDisplayNode, CGRect?) -> Void
let performBioLinkAction: (TextLinkItemActionType, TextLinkItem) -> Void
@ -120,7 +120,7 @@ final class PeerInfoInteraction {
openPermissions: @escaping () -> Void,
openLocation: @escaping () -> Void,
editingOpenSetupLocation: @escaping () -> Void,
openPeerInfo: @escaping (Peer, Bool) -> Void,
openPeerInfo: @escaping (EnginePeer, Bool) -> Void,
performMemberAction: @escaping (PeerInfoMember, PeerInfoMemberAction) -> Void,
openPeerInfoContextMenu: @escaping (PeerInfoContextSubject, ASDisplayNode, CGRect?) -> Void,
performBioLinkAction: @escaping (TextLinkItemActionType, TextLinkItem) -> Void,

View file

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

View file

@ -408,7 +408,7 @@ private final class PeerInfoPendingPane {
updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?,
chatControllerInteraction: ChatControllerInteraction,
data: PeerInfoScreenData,
openPeerContextAction: @escaping (Bool, Peer, ASDisplayNode, ContextGesture?) -> Void,
openPeerContextAction: @escaping (Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void,
openAddMemberAction: @escaping () -> Void,
requestPerformPeerMemberAction: @escaping (PeerInfoMember, PeerMembersListAction) -> Void,
peerId: PeerId,
@ -637,7 +637,7 @@ final class PeerInfoPaneContainerNode: ASDisplayNode, ASGestureRecognizerDelegat
var selectionPanelNode: PeerInfoSelectionPanelNode?
var chatControllerInteraction: ChatControllerInteraction?
var openPeerContextAction: ((Bool, Peer, ASDisplayNode, ContextGesture?) -> Void)?
var openPeerContextAction: ((Bool, EnginePeer, ASDisplayNode, ContextGesture?) -> Void)?
var openAddMemberAction: (() -> Void)?
var requestPerformPeerMemberAction: ((PeerInfoMember, PeerMembersListAction) -> Void)?

View file

@ -521,7 +521,7 @@ func infoItems(
if let managedByBot = data.managedByBot {
items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: ItemBotAddToChatInfo, icon: .managedBot, text: presentationData.strings.PeerInfo_ManagedBotFooter(managedByBot.compactDisplayTitle).string, linkAction: { _ in
interaction.openPeerInfo(managedByBot._asPeer(), false)
interaction.openPeerInfo(managedByBot, false)
}))
} else {
items[currentPeerInfoSection]!.append(PeerInfoScreenCommentItem(id: ItemBotAddToChatInfo, text: presentationData.strings.Bot_AddToChatInfo))
@ -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._asPeer(), member: member, isAccount: false, action: isAccountPeer ? { _ in
items[.peerMembers]!.append(PeerInfoScreenMemberItem(id: member.id, context: .account(context), enclosingPeer: peer, 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)
@ -1099,7 +1099,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
if let addressName = peer.addressName, !addressName.isEmpty {
discussionGroupTitle = "@\(addressName)"
} else {
discussionGroupTitle = EnginePeer(peer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
discussionGroupTitle = peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
} else {
discussionGroupTitle = presentationData.strings.Channel_DiscussionGroupAdd
@ -1194,7 +1194,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
if isCreator || (channel.adminRights?.rights.contains(.canChangeInfo) == true) {
let labelString: NSAttributedString
if channel.linkedMonoforumId != nil {
if let monoforumPeer = data.linkedMonoforumPeer as? TelegramChannel {
if case let .channel(monoforumPeer) = data.linkedMonoforumPeer {
if let sendPaidMessageStars = monoforumPeer.sendPaidMessageStars {
let formattedLabel = formatStarsAmountText(sendPaidMessageStars, dateTimeFormat: presentationData.dateTimeFormat)
let smallLabelFont = Font.regular(floor(presentationData.listsFontSize.itemListBaseFontSize / 17.0 * 13.0))
@ -1406,7 +1406,7 @@ func editingItems(data: PeerInfoScreenData?, boostStatus: ChannelBoostStatus?, s
if let addressName = linkedDiscussionPeer.addressName, !addressName.isEmpty {
peerTitle = "@\(addressName)"
} else {
peerTitle = EnginePeer(linkedDiscussionPeer).displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
peerTitle = linkedDiscussionPeer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
}
items[.peerDataSettings]!.append(PeerInfoScreenDisclosureItem(id: ItemLinkedChannel, label: .text(peerTitle), text: presentationData.strings.Group_LinkedChannel, icon: PresentationResourcesSettings.channels, action: {

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